Masquer les arguments à programmer sans code source


15

J'ai besoin de cacher des arguments sensibles à un programme que j'exécute, mais je n'ai pas accès au code source. J'exécute également cela sur un serveur partagé, donc je ne peux pas utiliser quelque chose comme hidepidparce que je n'ai pas les privilèges sudo.

Voici quelques choses que j'ai essayées:

  • export SECRET=[my arguments], suivi d'un appel à ./program $SECRET, mais cela ne semble pas aider.

  • ./program `cat secret.txt`secret.txtcontient mes arguments, mais le tout ps- puissant est capable de flairer mes secrets.

Existe-t-il un autre moyen de masquer mes arguments qui n'implique aucune intervention de l'administrateur?


Quel est ce programme particulier? Si c'est une commande habituelle, vous devez dire (et il pourrait y avoir une autre approche) de laquelle il s'agit
Basile Starynkevitch

14
Donc, vous comprenez ce qui se passe, les choses que vous avez essayées n'ont aucune chance de fonctionner car le shell est responsable de l'expansion des variables d'environnement et de la substitution de commandes avant d' appeler le programme. psne fait rien de magique pour "flairer vos secrets". Quoi qu'il en soit, les programmes raisonnablement écrits devraient plutôt offrir une option de ligne de commande pour lire un secret à partir d'un fichier spécifié ou de stdin au lieu de le prendre directement comme argument.
jamesdlin

J'exécute un programme de simulation météorologique écrit par une entreprise privée. Ils ne partagent pas leur code source, et leur documentation ne fournit aucun moyen de partager un secret à partir d'un fichier. Peut-être pas d'options ici
MS

Réponses:


25

Comme expliqué ici , Linux place les arguments d'un programme dans l'espace de données du programme et garde un pointeur sur le début de cette zone. C'est ce qui est utilisé parps et ainsi de suite pour rechercher et afficher les arguments du programme.

Étant donné que les données se trouvent dans l'espace du programme, il peut les manipuler. Faire cela sans changer le programme lui-même implique de charger un shim avec une main()fonction qui sera appelée avant la vraie main du programme. Cette cale peut copier les vrais arguments dans un nouvel espace, puis écraser les arguments d'origine de sorte queps ne voir que nuls.

Le code C suivant le fait.

/* /unix//a/403918/119298
 * capture calls to a routine and replace with your code
 * gcc -Wall -O2 -fpic -shared -ldl -o shim_main.so shim_main.c
 * LD_PRELOAD=/.../shim_main.so theprogram theargs...
 */
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <dlfcn.h>

typedef int (*pfi)(int, char **, char **);
static pfi real_main;

/* copy argv to new location */
char **copyargs(int argc, char** argv){
    char **newargv = malloc((argc+1)*sizeof(*argv));
    char *from,*to;
    int i,len;

    for(i = 0; i<argc; i++){
        from = argv[i];
        len = strlen(from)+1;
        to = malloc(len);
        memcpy(to,from,len);
        memset(from,'\0',len);    /* zap old argv space */
        newargv[i] = to;
        argv[i] = 0;
    }
    newargv[argc] = 0;
    return newargv;
}

static int mymain(int argc, char** argv, char** env) {
    fprintf(stderr, "main argc %d\n", argc);
    return real_main(argc, copyargs(argc,argv), env);
}

int __libc_start_main(pfi main, int argc,
                      char **ubp_av, void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void), void (*stack_end)){
    static int (*real___libc_start_main)() = NULL;

    if (!real___libc_start_main) {
        char *error;
        real___libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if ((error = dlerror()) != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(1);
        }
    }
    real_main = main;
    return real___libc_start_main(mymain, argc, ubp_av, init, fini,
            rtld_fini, stack_end);
}

Il n'est pas possible d'intervenir main(), mais vous pouvez intervenir sur la fonction de bibliothèque C standard __libc_start_main, qui continue d'appeler main. Compilez ce fichier shim_main.ccomme indiqué dans le commentaire au début et exécutez-le comme indiqué. J'ai laissé un printfdans le code afin de vérifier qu'il est bien appelé. Par exemple, exécutez

LD_PRELOAD=/tmp/shim_main.so /bin/sleep 100

puis faites un ps et vous verrez une commande vide et des arguments affichés.

Il y a encore un peu de temps pendant lequel la commande args peut être visible. Pour éviter cela, vous pouvez, par exemple, changer le shim pour lire votre secret à partir d'un fichier et l'ajouter aux arguments passés au programme.


12
Mais il y aura toujours une courte fenêtre pendant laquelle /proc/pid/cmdlineaffichera le secret (comme quand on curlessaie de cacher le mot de passe il est donné sur la ligne de commande). Pendant que vous utilisez LD_PRELOAD, vous pouvez envelopper main pour que le secret soit copié de l'environnement vers l'argv que principal reçoit. Comme appeler LD_PRELOAD=x SECRET=y cmdoù vous appelez main()avec l' argv[]être[argv[0], getenv("SECRET")]
Stéphane Chazelas

Vous ne pouvez pas utiliser l'environnement pour cacher un secret tel qu'il est visible via /proc/pid/environ. Cela peut être écrasable de la même manière que les arguments, mais il laisse la même fenêtre.
meuh

11
/proc/pid/cmdlineest public, /proc/pid/environn'est pas. Il y avait des systèmes où ps(un exécutable setuid là-bas) exposait l'environnement de tout processus, mais je ne pense pas que vous en rencontriez de nos jours. L'environnement est généralement considéré comme suffisamment sûr . Il n'est pas sûr de détacher des processus avec le même euid, mais ceux-ci peuvent souvent lire la mémoire des processus par le même euid de toute façon, donc il n'y a pas grand-chose que vous puissiez faire à ce sujet.
Stéphane Chazelas

4
@ StéphaneChazelas: Si l'on utilise l'environnement pour transmettre des secrets, idéalement le wrapper qui le transmet à la mainméthode du programme encapsulé supprime également la variable d'environnement pour éviter les fuites accidentelles aux processus enfants. Le wrapper peut également lire tous les arguments de ligne de commande à partir d'un fichier.
David Foerster

@DavidFoerster, bon point. J'ai mis à jour ma réponse pour en tenir compte.
Stéphane Chazelas

16
  1. Lisez la documentation de l'interface de ligne de commande de l'application en question. Il peut bien y avoir une option pour fournir le secret à partir d'un fichier au lieu d'un argument directement.

  2. Si cela échoue, déposez un rapport de bogue sur l'application au motif qu'il n'existe aucun moyen sécurisé de lui fournir un secret.

  3. Vous pouvez toujours soigneusement (!) Adapter la solution dans la réponse de meuh à vos besoins spécifiques. Portez une attention particulière au commentaire de Stéphane et à ses suites.


12

Si vous devez transmettre des arguments au programme pour le faire fonctionner, vous n'aurez pas de chance quoi que vous fassiez si vous ne pouvez pas utiliser hidepid sur procfs.

Puisque vous avez mentionné qu'il s'agit d'un script bash, vous devriez déjà avoir le code source disponible, car bash n'est pas un langage compilé.

À défaut, vous pourrez peut- être réécrire la ligne de commande du processus en utilisant gdbou similaire et en jouant avec argc/ argvune fois qu'il a déjà commencé, mais:

  1. Ce n'est pas sécurisé, car vous exposez toujours vos arguments de programme initialement avant de les modifier
  2. C'est assez hacky, même si vous pouviez le faire fonctionner, je ne recommanderais pas de vous y fier

Je recommanderais vraiment d'obtenir le code source ou de parler au fournisseur pour faire modifier le code. La fourniture de secrets sur la ligne de commande dans un système d'exploitation POSIX est incompatible avec un fonctionnement sécurisé.


11

Lorsqu'un processus exécute une commande (via l' execve()appel système), sa mémoire est effacée. Pour transmettre des informations à travers l'exécution, les execve()appels système prennent deux arguments pour cela: le argv[]etenvp[] tableaux .

Ce sont deux tableaux de chaînes:

  • argv[] contient les arguments
  • envp[]contient les définitions de variables d'environnement sous forme de chaînes au var=valueformat (par convention).

Quand vous faites:

export SECRET=value; cmd "$SECRET"

(ici ajouté les guillemets manquants autour de l'expansion des paramètres).

Vous exécutez cmdavec le secret ( value) passé à la fois dans argv[]et envp[]. argv[]sera ["cmd", "value"]et envp[]quelque chose comme [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Comme cmdne fait rien getenv("SECRET")ou équivalent pour récupérer la valeur du secret de cetteSECRET variable d'environnement, le mettre dans l'environnement n'est pas utile.

argv[]est de notoriété publique. Il apparaît dans la sortie de ps. envp[]n'est pas de nos jours. Sous Linux, cela apparaît /proc/pid/environ. Il apparaît dans la sortie de ps ewwwsur BSD (et avec procps-ng pssur Linux), mais uniquement pour les processus fonctionnant avec le même uid effectif (et avec plus de restrictions pour les exécutables setuid / setgid). Il peut apparaître dans certains journaux d'audit, mais ces journaux d'audit ne doivent être accessibles qu'aux administrateurs.

En bref, l'environnement qui est transmis à un exécutable est censé être privé ou au moins aussi privé que la mémoire interne d'un processus (auquel, dans certaines circonstances, un autre processus doté des privilèges appropriés peut également accéder avec un débogueur par exemple et peut également être vidée sur le disque).

Étant donné que cela argv[]est de notoriété publique, une commande qui attend des données censées être secrètes sur sa ligne de commande est brisée par conception.

Habituellement, les commandes qui doivent recevoir un secret vous fournissent une autre interface pour le faire, comme via une variable d'environnement. Par exemple:

IPMI_PASSWORD=secret ipmitool -I lan -U admin...

Ou via un descripteur de fichier dédié comme stdin:

echo secret | openssl rsa -passin stdin ...

( echoétant intégré, il n'apparaît pas dans la sortie deps )

Ou un fichier, comme le .netrcfor ftpet quelques autres commandes ou

mysql --defaults-extra-file=/some/file/with/password ....

Certaines applications comme curl(et c'est aussi l'approche adoptée par @meuh ici ) essaient de cacher le mot de passe qu'ils ont reçu argv[]des regards indiscrets (sur certains systèmes en écrasant la partie de la mémoire où les argv[]chaînes ont été stockées). Mais cela n'aide pas vraiment et donne une fausse promesse de sécurité. Cela laisse une fenêtre entre le execve()et l'écrasement oùps montrera toujours le secret.

Par exemple, si un attaquant sait que vous exécutez un script faisant un curl -u user:somesecret https://...(par exemple dans un travail cron), tout ce qu'il a à faire est d'expulser du cache les (nombreuses) bibliothèques qui curlutilisent (par exemple en exécutant a sh -c 'a=a;while :; do a=$a$a;done') afin pour ralentir son démarrage, et même faire un très inefficaceuntil grep 'curl.*[-]u' /proc/*/cmdline; do :; done est suffisant pour attraper ce mot de passe dans mes tests.

Si les arguments sont le seul moyen de transmettre le secret aux commandes, vous pouvez toujours essayer certaines choses.

Sur certains systèmes, y compris les anciennes versions de Linux, seuls les premiers octets (4096 sur Linux 4.1 et avant) des chaînes argv[]peuvent être interrogés.

Là, vous pourriez faire:

(exec -a "$(printf %-4096s cmd)" cmd "$secret")

Et le secret serait caché car il a dépassé les 4096 premiers octets. Maintenant, les gens qui ont utilisé cette méthode doivent le regretter maintenant puisque Linux depuis 4.2 ne tronque plus la liste des arguments dans /proc/pid/cmdline. Notez également que ce n'est pas parce psque ne montrera pas plus de tant d'octets d'une ligne de commande (comme sur FreeBSD où il semble être limité à 2048) que l'on ne peut pas utiliser les mêmes utilisations d'API pspour en obtenir plus. Cette approche est cependant valable sur les systèmes où psest le seul moyen pour un utilisateur régulier de récupérer ces informations (comme lorsque l'API est privilégiée et psest setgid ou setuid pour l'utiliser), mais n'est toujours pas potentiellement pérenne là-bas.

Une autre approche consisterait à ne pas transmettre le secret argv[]mais à injecter du code dans le programme (en utilisant gdbou un $LD_PRELOADhack) avant son main()démarrage qui insère le secret dans le argv[]reçu deexecve() .

Avec LD_PRELOAD, pour les exécutables liés non-setuid / setgid dynamiquement sur un système GNU:

/* 
 * replace ***** with secret read from fd 9
 * gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl 
 * LD_PRELOAD=/.../inject_secret.so cmd -p '*****' 9<<< secret
 */
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>

#define PLACEHOLDER "*****"
static char secret[1024];

int __libc_start_main(int (*main) (int, char**, char**),
                      int argc,
                      char **argv,
                      void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void),
                      void (*stack_end)){
    static int (*real_libc_start_main)() = NULL;
    int n;

    if (!real_libc_start_main) {
        real_libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if (!real_libc_start_main) abort();
    }

    n = read(9, secret, sizeof(secret));
    if (n > 0) {
      int i;

      if (secret[n - 1] == '\n') secret[--n] = '\0'; 
      for (i = 1; i < argc; i++)
        if (strcmp(argv[i], PLACEHOLDER) == 0)
          argv[i] = secret;
    }

    return real_libc_start_main(main, argc, argv, init, fini,
                                rtld_fini, stack_end);
}

Alors:

$ gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl
$ LD_PRELOAD=$PWD/inject_secret.so  ps '*****' 9<<< "-opid,args"
  PID COMMAND
 7659 /bin/zsh
 8828 ps *****

À aucun moment n'aurait psmontré le ps -opid,argslà ( -opid,argsétant le secret dans cet exemple). Notez que nous remplaçons les éléments du argv[]tableau de pointeurs , sans remplacer les chaînes pointées par ces pointeurs, c'est pourquoi nos modifications n'apparaissent pas dans la sortie deps .

Avec gdb, toujours pour les exécutables liés dynamiquement non-setuid / setgid et sur les systèmes GNU:

tmp=$(mktemp) && cat << EOF > "$tmp" &&
break __libc_start_main
commands 1
set argv[1]="-opid,args"
continue
end
run
EOF

gdb -n --batch-silent --return-child-result -x "$tmp" --args ps '*****'
rm -f -- "$tmp"

Toujours avec gdb, une approche non spécifique à GNU qui ne repose pas sur des exécutables liés dynamiquement ou ayant des symboles de débogage et devrait fonctionner pour tout exécutable ELF sous Linux pourrait être au moins:

#! /bin/sh -
# gdb+sh polyglot script to replace "*****" arguments with the content
# of the SECRET environment variable *after* execve and before calling
# the executable's main() function.
#
# Usage: SECRET=somesecret cmd --password '*****'

if ':' - ':'
then
  # running in sh
  # retrieve the start address for the executable
  start=$(
    LC_ALL=C objdump -f -- "$(command -v -- "${1?}")" |
    sed -n 's/^start address //p'
  )
  [ -n "$start" ] || exit
  # re-exec ourself with gdb.
  exec gdb -n --batch-silent --return-child-result -iex "set \$start = $start" -x "$0" --args "$@"
  exit 1
fi
end
# running in gdb
break *$start
commands 1
  # The stack on startup contains:
  # argc argv[0]... argv[argc-1] 0 envp[0] envp[1]... 0 argv[] and envp[] strings
  set $argc = *((int*)$sp)
  set $argv = &((char**)$sp)[1]
  set $envp = &($argv[$argc+1])
  set $i = 0
  while $envp[$i]
    # look for an envp[] string starting with "SECRET=". We can't use strcmp()
    # here as there's no guarantee that the debugged executable has such
    # a function
    set $e = $envp[$i]
    if $e[0] == 'S' && \
       $e[1] == 'E' && \
       $e[2] == 'C' && \
       $e[3] == 'R' && \
       $e[4] == 'E' && \
       $e[5] == 'T' && \
       $e[6] == '='
      set $secret = &($e[7])
      # replace SECRET=xxx<NUL> with SECRE=<NUL>
      set $e[5] = '='
      set $e[6] = '\0'
      # not calling loop_break as that causes a SEGV with my version of gdb
    end
    set $i = $i + 1
  end
  if $secret
    # now looking for argv[] strings being "*****" and replace them with
    # the secret identified earlier
    set $i = 0
    while $i < $argc
      set $a = $argv[$i]
      if $a[0] == '*' && \
       $a[1] == '*' && \
       $a[2] == '*' && \
       $a[3] == '*' && \
       $a[4] == '*' && \
       $a[5] == '\0'
        set $argv[$i] = $secret
      end
      set $i = $i + 1
    end
  end
  # using "continue" as "detach" causes a SEGV with my version of gdb.
  continue
end
run

Test avec un exécutable lié statiquement:

$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****

Lorsque l'exécutable peut être statique, nous n'avons pas de moyen fiable d'allouer de la mémoire pour stocker le secret, nous devons donc obtenir le secret ailleurs qui est déjà dans la mémoire de processus. C'est pourquoi l'environnement est le choix évident ici. Nous masquons également cette SECRETvariable env au processus (en le changeant en SECRE=) pour éviter qu'il ne coule si le processus décide de vider son environnement pour une raison quelconque ou d'exécuter des applications non fiables.

Cela fonctionne également sous Solaris 11 (fourni gdb et GNU binutils sont installés (vous devrez peut-être renommer objdumpà gobjdump).

Sur FreeBSD (au moins x86_64, je ne sais pas ce que sont les 24 premiers octets (qui deviennent 16 lorsque gdb (8.0.1) est interactif suggérant qu'il peut y avoir un bogue dans gdb là-bas) sur la pile), remplacez les définitions argcet argvavec:

set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]

(vous devrez peut-être également installer le gdbpackage / port car la version fournie avec le système est ancienne).


Re (ici ajouté les guillemets manquants autour de l'expansion des paramètres): Quel est le problème de ne pas utiliser les guillemets? Y a-t-il vraiment une différence?
yukashima huksay

@yukashimahuksay, voir par exemple les implications pour la sécurité d'oublier de citer une variable dans des shells bash / POSIX et les questions qui y sont liées.
Stéphane Chazelas

3

Ce que vous pourriez faire, c'est

 export SECRET=somesecretstuff

puis, en supposant que vous écrivez votre ./programen C (ou que quelqu'un d'autre le fasse et que vous pouvez le changer ou l'améliorer pour vous), utilisez getenv (3) dans ce programme, peut-être comme

char* secret= getenv("SECRET");

et après export vous venez de courir ./programdans le même shell. Ou le nom de la variable d'environnement peut lui être transmis (en exécutant./program --secret-var=SECRET etc ...)

psne dira pas votre secret, mais proc (5) peut toujours donner beaucoup d'informations (au moins à d'autres processus du même utilisateur).

Voir aussi ceci pour aider à concevoir une meilleure façon de passer des arguments de programme.

Voir cette réponse pour une meilleure explication sur la globalisation et le rôle d'un shell.

Peut-être avez-vous d' programautres moyens d'obtenir des données (ou d'utiliser la communication interprocessus plus judicieusement) que des arguments de programme simples (cela devrait certainement le cas, s'il est destiné à traiter des informations sensibles). Lisez sa documentation. Ou peut-être que vous abusez de ce programme (qui n'est pas destiné à traiter des données secrètes).

Cacher des données secrètes est vraiment difficile. Ne pas le transmettre via les arguments du programme ne suffit pas.


5
Il est assez clair de la question qu'il n'a même pas le code source pour ./program, donc la première moitié de cette réponse ne semble pas être pertinent.
pipe
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.