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=value
format (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 cmd
avec 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 cmd
ne 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 ewww
sur BSD (et avec procps-ng ps
sur 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 .netrc
for ftp
et 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 curl
utilisent (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 ps
que 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 ps
pour en obtenir plus. Cette approche est cependant valable sur les systèmes où ps
est le seul moyen pour un utilisateur régulier de récupérer ces informations (comme lorsque l'API est privilégiée et ps
est 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 gdb
ou un $LD_PRELOAD
hack) 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 ps
montré le ps -opid,args
là ( -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 SECRET
variable 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 argc
et argv
avec:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(vous devrez peut-être également installer le gdb
package / port car la version fournie avec le système est ancienne).