Wenn ein Prozess einen Befehl ausführt (über den execve()
Systemaufruf), wird sein Speicher gelöscht. Um einige Informationen über die Ausführung weiterzuleiten, benötigt der execve()
Systemaufruf zwei Argumente: die Arrays argv[]
und envp[]
.
Das sind zwei Arrays von Strings:
argv[]
enthält die Argumente
envp[]
Enthält die Umgebungsvariablendefinitionen als Zeichenfolgen im var=value
Format (gemäß Konvention).
Wenn Sie das tun:
export SECRET=value; cmd "$SECRET"
(hier die fehlenden Anführungszeichen um die Parametererweiterung hinzugefügt).
Sie führen cmd
mit secret ( value
) aus, das sowohl in argv[]
als auch übergeben wurdeenvp[]
. argv[]
wird ["cmd", "value"]
und envp[]
so ähnlich sein [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Da cmd
keine getenv("SECRET")
oder eine gleichwertige Aktion zum Abrufen des Werts des Geheimnisses von dieser SECRET
Umgebungsvariablen ausgeführt wird, ist es nicht sinnvoll, es in die Umgebung zu stellen.
argv[]
ist öffentliches Wissen. Es zeigt in der Ausgabe von ps
. envp[]
heutzutage nicht. Unter Linux wird dies in angezeigt /proc/pid/environ
. Es wird in der Ausgabe von ps ewww
BSDs (und mit procps-ngs ps
unter Linux) angezeigt , jedoch nur für Prozesse, die mit derselben effektiven UID ausgeführt werden (und mit weiteren Einschränkungen für ausführbare setuid / setgid-Dateien). Es wird möglicherweise in einigen Überwachungsprotokollen angezeigt, aber auf diese Überwachungsprotokolle sollten nur Administratoren zugreifen können.
Kurz gesagt, die Umgebung, die an eine ausführbare Datei übergeben wird, soll privat sein oder zumindest ungefähr so privat wie der interne Speicher eines Prozesses (auf den ein anderer Prozess mit den richtigen Berechtigungen unter Umständen auch mit einem Debugger zugreifen kann und kann) auch auf die Festplatte gespeichert werden).
Da dies argv[]
allgemein bekannt ist, ist ein Befehl, der Daten erwartet, die in seiner Befehlszeile geheim sein sollen, vom Entwurf her fehlerhaft.
In der Regel stellen Befehle, die geheim gehalten werden müssen, eine andere Schnittstelle zur Verfügung, z. B. über eine Umgebungsvariable. Zum Beispiel:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Oder über einen dedizierten Dateideskriptor wie stdin:
echo secret | openssl rsa -passin stdin ...
(echo
Wird eingebaut, wird in der Ausgabe von nicht angezeigt. ps
)
Oder eine Datei, wie die .netrc
fürftp
und ein paar andere Befehle oder
mysql --defaults-extra-file=/some/file/with/password ....
Einige Anwendungen wie curl
(und das ist auch der Ansatz von @meuh hier ) versuchen, das Passwort, das sie argv[]
von neugierigen Blicken erhalten haben , zu verbergen (auf einigen Systemen, indem sie den Teil des Speichers überschreiben, in dem sich das befindet)argv[]
Zeichenfolgen gespeichert wurden). Aber das hilft nicht wirklich und gibt ein falsches Sicherheitsversprechen. Dadurch bleibt ein Fenster zwischen dem execve()
und dem Überschreiben, in dem ps
das Geheimnis noch angezeigt wird.
Wenn ein Angreifer zum Beispiel weiß, dass Sie ein Skript ausführen, das einen curl -u user:somesecret https://...
(zum Beispiel in einem Cron-Job) ausführt, muss er nur die (vielen) Bibliotheken aus dem Cache entfernen, curl
die (zum Beispiel durch Ausführen eines) verwendet werdensh -c 'a=a;while :; do a=$a$a;done'
) verwendet werden Um den Startvorgang zu verlangsamen, until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
reicht es aus, dieses Kennwort in meinen Tests abzufangen.
Wenn die Argumente die einzige Möglichkeit sind, das Geheimnis an die Befehle weiterzugeben, gibt es möglicherweise noch einige Dinge, die Sie versuchen könnten.
Auf einigen Systemen, einschließlich älterer Linux-Versionen, können nur die ersten Bytes (4096 unter Linux 4.1 und früher) der eingegebenen Zeichenfolgen argv[]
abgefragt werden.
Dort könnten Sie tun:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
Und das Geheimnis wäre verborgen, weil es hinter den ersten 4096 Bytes ist. Jetzt müssen Leute, die diese Methode verwendet haben, es jetzt bereuen, da Linux seit 4.2 die Liste der Argumente in nicht mehr abschneidet /proc/pid/cmdline
. Beachten Sie auch, dass es nicht so ist, weil ps
nicht mehr als so viele Bytes einer Befehlszeile angezeigt werden (wie bei FreeBSD, wo es auf 2048 beschränkt zu sein scheint), dass man nicht dieselbe API ps
verwenden kann, um mehr zu erhalten. Dieser Ansatz ist jedoch auf Systemen gültig, auf denen ps
nur ein normaler Benutzer diese Informationen abrufen kann (z. B. wenn die API privilegiert ist undps
setgid oder setuid ist, um sie zu verwenden), die dort jedoch möglicherweise nicht zukunftssicher sind.
Ein anderer Ansatz wäre, das Geheimnis nicht weiterzugeben, argv[]
sondern Code in das Programm einzufügen (mithilfe von gdb
oder einem $LD_PRELOAD
Hack), bevor main()
es gestartet wird, der das Geheimnis in das argv[]
empfangene von einfügt execve()
.
Mit LD_PRELOAD
für nicht-setuid / setgid dynamisch verknüpfte ausführbare Dateien auf einem GNU-System:
/*
* 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);
}
Dann:
$ 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 *****
Zu keinem Zeitpunkt hätte ps
sich das ps -opid,args
dort gezeigt ( -opid,args
in diesem Beispiel das Geheimnis). Beachten Sie, dass wir Elemente der sind zu ersetzen argv[]
Array von Zeigern , die Saiten nicht zwingend durch diese Zeiger zeigten auf, weshalb unsere Änderungen werden nicht in dem Ausgang ps
.
Mit gdb
, immer noch für nicht-setuid / setgid dynamisch verknüpfte ausführbare Dateien und auf GNU-Systemen:
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"
Noch mit gdb
, ein nicht-GNU spezifischen Ansatz, der nicht auf ausführbare Dateien angewiesen ist dynamisch gelinkt oder Debug - Symbole werden und sollte für jede ELF ausführbare Datei auf Linux zumindest sein könnte funktionieren:
#! /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
Testen mit einer statisch verknüpften ausführbaren Datei:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Wenn die ausführbare Datei möglicherweise statisch ist, haben wir keine zuverlässige Möglichkeit, Speicher zum Speichern des Geheimnisses zuzuweisen. Daher müssen wir das Geheimnis von einer anderen Stelle abrufen, die sich bereits im Prozessspeicher befindet. Deshalb ist die Umwelt hier die naheliegende Wahl. Wir verbergen auch diese Umgebungsvariable für SECRET
den Prozess (indem wir sie in ändern SECRE=
), um zu verhindern, dass sie verloren geht, wenn der Prozess aus irgendeinem Grund beschließt, seine Umgebung zu sichern oder nicht vertrauenswürdige Anwendungen auszuführen.
Das funktioniert auch auf Solaris 11 (vorausgesetzt , GDB und GNU binutils installiert sind (Sie müssen umbenennen können objdump
zu gobjdump
).
Unter FreeBSD (mindestens x86_64, ich bin mir nicht sicher, welche der ersten 24 Bytes (die 16 werden, wenn gdb (8.0.1) interaktiv ist und darauf hindeutet, dass es dort einen Fehler in gdb gibt) auf dem Stack sind), ersetzen Sie die argc
und argv
-Definitionen mit:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(Möglicherweise müssen Sie auch das gdb
Paket / den Port installieren, da die Version, die sonst mit dem System geliefert wird, veraltet ist.)