Gdy proces wykonuje polecenie (poprzez execve()
wywołanie systemowe), jego pamięć jest czyszczona. Aby przekazać pewne informacje w trakcie wykonywania, execve()
wywołania systemowe wymagają dwóch argumentów: argv[]
i envp[]
tablic.
Są to dwie tablice ciągów:
argv[]
zawiera argumenty
envp[]
zawiera definicje zmiennych środowiskowych jako ciągi znaków w var=value
formacie (zgodnie z konwencją).
Kiedy to zrobisz:
export SECRET=value; cmd "$SECRET"
(tutaj dodano brakujące cudzysłowy wokół rozszerzenia parametru).
Wykonujesz cmd
z sekretem ( value
) przekazanym zarówno do, jak argv[]
i do envp[]
. argv[]
będzie ["cmd", "value"]
i envp[]
coś w tym rodzaju [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Ponieważ cmd
nie robi się nic, getenv("SECRET")
ani nie można pobrać wartości tajnej z tej SECRET
zmiennej środowiskowej, umieszczenie jej w środowisku nie jest przydatne.
argv[]
to wiedza publiczna. Pokazuje na wyjściu ps
. envp[]
obecnie nie jest. W systemie Linux pokazuje się w /proc/pid/environ
. Pokazuje się w ps ewww
wynikach BSD (i procps-ng w ps
Linuksie), ale tylko dla procesów działających z tym samym efektywnym UID (i z większymi ograniczeniami dla plików wykonywalnych setuid / setgid). Może być wyświetlany w niektórych dziennikach kontroli, ale te dzienniki kontroli powinny być dostępne tylko dla administratorów.
Krótko mówiąc, środowisko przekazywane do pliku wykonywalnego ma być prywatne lub przynajmniej tak prywatne jak wewnętrzna pamięć procesu (do którego w niektórych okolicznościach inny proces z odpowiednimi uprawnieniami może również uzyskać dostęp za pomocą debuggera i może również zrzucony na dysk).
Ponieważ argv[]
jest to wiedza publiczna, polecenie, które oczekuje, że dane, które mają być tajne w wierszu poleceń, jest zepsute przez projekt.
Zazwyczaj polecenia, które wymagają tajnego klucza, zapewniają inny interfejs, np. Poprzez zmienną środowiskową. Na przykład:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Lub za pomocą dedykowanego deskryptora pliku, takiego jak stdin:
echo secret | openssl rsa -passin stdin ...
( echo
jest wbudowany, nie wyświetla się w wynikach ps
)
Lub plik, taki jak .netrc
for ftp
i kilka innych poleceń lub
mysql --defaults-extra-file=/some/file/with/password ....
Niektóre aplikacje, takie jak curl
(i takie podejście przyjęła tutaj @meuh ), próbują ukryć hasło, które otrzymali argv[]
przed wścibskimi oczami (w niektórych systemach, nadpisując część pamięci, w której argv[]
przechowywano ciągi znaków). Ale to naprawdę nie pomaga i daje fałszywą obietnicę bezpieczeństwa. To pozostawia okno pomiędzy execve()
i nadpisywaniem, gdzie ps
nadal będzie pokazywany sekret.
Na przykład, jeśli osoba atakująca wie, że uruchamiasz skrypt wykonujący curl -u user:somesecret https://...
(na przykład zadanie crona), wszystko, co musi zrobić, to eksmitować z pamięci podręcznej (wiele) bibliotek, które curl
używają (na przykład uruchamiając a sh -c 'a=a;while :; do a=$a$a;done'
), więc aby spowolnić jego uruchomienie, a nawet zrobienie bardzo nieefektywnego until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
wystarczy, aby złapać to hasło w moich testach.
Jeśli argumenty to jedyny sposób na przekazanie sekretu komendom, nadal możesz spróbować kilku rzeczy.
W niektórych systemach, w tym starszych wersjach Linuksa, argv[]
można przeszukiwać tylko kilka pierwszych bajtów (4096 w Linuksie 4.1 i wcześniejszych) ciągów .
Tam możesz zrobić:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
I sekret zostałby ukryty, ponieważ przekroczyłby pierwsze 4096 bajtów. Teraz ludzie, którzy używali tej metody, muszą teraz tego żałować, ponieważ Linux od wersji 4.2 nie obcina już listy argumentów /proc/pid/cmdline
. Zauważ też, że nie dlatego, ps
że nie będzie wyświetlał więcej niż tak wielu bajtów linii poleceń (jak na FreeBSD, gdzie wydaje się być ograniczony do 2048), których nie można użyć do tego samego interfejsu API, ps
aby uzyskać więcej. Podejście to jest jednak ważne w systemach, w których ps
zwykły użytkownik jest jedynym sposobem na uzyskanie tych informacji (na przykład, gdy interfejs API jest uprzywilejowany i ps
jest ustawiony jako setgid lub setuid, aby z niego skorzystać), ale wciąż nie jest tam potencjalnie zabezpieczony na przyszłość.
Innym podejściem byłoby nie przekazywanie sekretu, argv[]
ale wstrzykiwanie kodu do programu (za pomocą gdb
lub $LD_PRELOAD
włamania) przed jego main()
uruchomieniem, które wstawia sekret do argv[]
otrzymanego z execve()
.
Z LD_PRELOAD
, dla dynamicznie połączonych plików wykonywalnych innych niż setuid / setgid w systemie 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);
}
Następnie:
$ 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 *****
W żadnym momencie nie ps
pokazałbym tego ps -opid,args
(co -opid,args
jest tajemnicą w tym przykładzie). Zauważ, że zastępujemy elementy argv[]
tablicy wskaźników , nie zastępując ciągów wskazanych przez te wskaźniki, dlatego nasze modyfikacje nie pokazują się na wyjściu ps
.
Z gdb
, wciąż dla dynamicznie powiązanych plików wykonywalnych innych niż setuid / setgid oraz w systemach 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"
Mimo gdb
to podejście nie specyficzne dla GNU, które nie polega na dynamicznym łączeniu plików wykonywalnych lub symboli debugowania, i powinno działać przynajmniej dla każdego pliku wykonywalnego ELF w systemie Linux:
#! /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
Testowanie ze statycznie połączonym plikiem wykonywalnym:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Gdy plik wykonywalny może być statyczny, nie mamy niezawodnego sposobu na przydzielenie pamięci do przechowywania sekretu, więc musimy uzyskać sekret z innego miejsca, które jest już w pamięci procesu. Właśnie dlatego środowisko jest tutaj oczywistym wyborem. Ukrywamy również SECRET
zmienną env var procesu (zmieniając ją na SECRE=
), aby uniknąć wycieku, jeśli proces z jakiegoś powodu zdecyduje się zrzucić swoje środowisko lub uruchomić niezaufane aplikacje.
Który działa również w systemie Solaris 11 (pod warunkiem, gdb i binutils GNU są zainstalowane (być może trzeba będzie zmienić nazwę objdump
na gobjdump
).
W FreeBSD (przynajmniej x86_64 nie jestem pewien, jakie są te pierwsze 24 bajty (które stają się 16, gdy gdb (8.0.1) jest interaktywny, co sugeruje, że może tam być błąd w gdb) na stosie), zamień argc
i argv
definicje z:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(może być również konieczne zainstalowanie gdb
pakietu / portu, ponieważ wersja dostarczana z systemem jest starodawna).