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=valueformacie (zgodnie z konwencją).
Kiedy to zrobisz:
export SECRET=value; cmd "$SECRET"
(tutaj dodano brakujące cudzysłowy wokół rozszerzenia parametru).
Wykonujesz cmdz 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ż cmdnie robi się nic, getenv("SECRET")ani nie można pobrać wartości tajnej z tej SECRETzmiennej ś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 ewwwwynikach BSD (i procps-ng w psLinuksie), 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 ...
( echojest wbudowany, nie wyświetla się w wynikach ps)
Lub plik, taki jak .netrcfor ftpi 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 psnadal 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 curluż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 :; donewystarczy, 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, psaby uzyskać więcej. Podejście to jest jednak ważne w systemach, w których pszwykły użytkownik jest jedynym sposobem na uzyskanie tych informacji (na przykład, gdy interfejs API jest uprzywilejowany i psjest 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ą gdblub $LD_PRELOADwł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 pspokazałbym tego ps -opid,args(co -opid,argsjest 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 gdbto 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ż SECRETzmienną 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ę objdumpna 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ń argci argvdefinicje z:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(może być również konieczne zainstalowanie gdbpakietu / portu, ponieważ wersja dostarczana z systemem jest starodawna).