Quando un processo esegue un comando (tramite la execve()
chiamata di sistema), la sua memoria viene cancellata. Per passare alcune informazioni attraverso l'esecuzione, le execve()
chiamate di sistema prendono due argomenti per questo: the argv[]
e envp[]
array.
Quelle sono due matrici di stringhe:
argv[]
contiene gli argomenti
envp[]
contiene le definizioni delle variabili di ambiente come stringhe nel var=value
formato (per convenzione).
Quando lo fai:
export SECRET=value; cmd "$SECRET"
(qui sono state aggiunte le virgolette mancanti attorno all'espansione del parametro).
Stai eseguendo cmd
con il segreto ( value
) passato sia in argv[]
che envp[]
. argv[]
sarà ["cmd", "value"]
e envp[]
qualcosa del genere [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Come cmd
non sta facendo alcuno getenv("SECRET")
o equivalente per recuperare il valore del segreto da quella SECRET
variabile d'ambiente, metterlo nell'ambiente non è utile.
argv[]
è conoscenza pubblica. Mostra nell'output di ps
. envp[]
al giorno d'oggi non lo è. Su Linux, mostra in /proc/pid/environ
. Mostra nell'output di ps ewww
su BSD (e con procps-ng'sps
su Linux), ma solo ai processi in esecuzione con lo stesso uid efficace (e con più restrizioni per gli eseguibili setuid / setgid). Può essere visualizzato in alcuni registri di controllo, ma tali registri di controllo dovrebbero essere accessibili solo agli amministratori.
In breve, l'ambiente che viene passato a un eseguibile deve essere privato o almeno privato quanto la memoria interna di un processo (a cui in alcune circostanze un altro processo con i giusti privilegi può anche accedere con un debugger e può anche essere scaricato su disco).
Dato che argv[]
è di dominio pubblico, un comando che prevede che i dati intesi come segreti siano segreti sulla sua riga di comando viene interrotto in base alla progettazione.
Di solito, i comandi che devono avere un segreto, forniscono un'altra interfaccia per farlo, come tramite una variabile d'ambiente. Per esempio:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
O tramite un descrittore di file dedicato come stdin:
echo secret | openssl rsa -passin stdin ...
( echo
essendo incorporato, non viene mostrato nell'output di ps
)
O un file, come il .netrc
for ftp
e alcuni altri comandi o
mysql --defaults-extra-file=/some/file/with/password ....
Alcune applicazioni come curl
(e questo è anche l'approccio adottato da @meuh qui ) cercano di nascondere la password che hanno ricevuto argv[]
da occhi indiscreti (su alcuni sistemi sovrascrivendo la porzione di memoria in cui argv[]
erano archiviate le stringhe). Ma questo non aiuta davvero e dà una falsa promessa di sicurezza. Ciò lascia una finestra tra la execve()
e la sovrascrittura dove ps
mostrerà ancora il segreto.
Ad esempio, se un utente malintenzionato sa che stai eseguendo uno script eseguendo un curl -u user:somesecret https://...
(ad esempio in un processo cron), tutto ciò che deve fare è eliminare dalla cache le (molte) librerie che curl
utilizza (ad esempio eseguendo a sh -c 'a=a;while :; do a=$a$a;done'
) quindi come rallentare il suo avvio, e anche fare un metodo molto inefficiente until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
è sufficiente per catturare quella password nei miei test.
Se gli argomenti sono l'unico modo per passare il segreto ai comandi, potrebbero esserci ancora alcune cose che potresti provare.
Su alcuni sistemi, comprese le versioni precedenti di Linux, solo i primi byte (4096 su Linux 4.1 e precedenti) delle stringhe in argv[]
possibile interrogare .
Lì, potresti fare:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
E il segreto sarebbe nascosto perché è passato i primi 4096 byte. Ora le persone che hanno usato quel metodo ora devono rimpiangerlo dal momento che Linux dal 4.2 non tronca più l'elenco degli arg in /proc/pid/cmdline
. Nota anche che non è perché ps
non mostrerà più di così tanti byte di una riga di comando (come su FreeBSD dove sembra essere limitato al 2048) che non si può usare con la stessa API ps
utilizzata per ottenere di più. Tale approccio è valido tuttavia su sistemi in cui ps
è l'unico modo per un utente normale di recuperare tali informazioni (come quando l'API è privilegiata ed ps
è setgid o setuid per usarla), ma potenzialmente non è ancora a prova di futuro.
Un altro approccio sarebbe quello di non passare il segreto argv[]
ma di iniettare codice nel programma (usando gdb
o un $LD_PRELOAD
hack) prima che main()
venga avviato che inserisca il segreto nel argv[]
ricevuto da execve()
.
Con LD_PRELOAD
, per eseguibili non setuid / setgid collegati dinamicamente su un sistema 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);
}
Poi:
$ 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 *****
In nessun momento avrebbe ps
mostrato il ps -opid,args
lì ( -opid,args
essendo il segreto in questo esempio). Si noti che stiamo sostituendo elementi argv[]
dell'array di puntatori , non sovrascrivendo le stringhe indicate da quei puntatori, motivo per cui le nostre modifiche non vengono visualizzate nell'output di ps
.
Con gdb
, ancora per eseguibili non setuid / setgid collegati dinamicamente e su sistemi 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"
Tuttavia gdb
, un approccio non GNU specifico che non si basa sul fatto che gli eseguibili siano collegati dinamicamente o che abbiano simboli di debug e che dovrebbe funzionare almeno per qualsiasi eseguibile ELF su Linux potrebbe essere:
#! /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 con un eseguibile collegato staticamente:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Quando l'eseguibile può essere statico, non abbiamo un modo affidabile di allocare memoria per archiviare il segreto, quindi dobbiamo ottenere il segreto da qualche altra parte che è già nella memoria di processo. Ecco perché l'ambiente è la scelta ovvia qui. Inoltre, nascondiamo SECRET
quell'ambiente var al processo (modificandolo inSECRE=
) per evitare perdite se il processo decide di scaricare il suo ambiente per qualche motivo o eseguire applicazioni non attendibili.
Che funziona anche su Solaris 11 (fornito gdb e GNU binutils sono installate (potrebbe essere necessario cambiare titolo objdump
algobjdump
).
Su FreeBSD (almeno x86_64, non sono sicuro di quali siano quei primi 24 byte (che diventano 16 quando gdb (8.0.1) è interattivo e suggerisce che potrebbero esserci degli errori in gdb) nello stack), sostituisci le definizioni argc
e argv
con:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(potrebbe anche essere necessario installare il gdb
pacchetto / porta poiché la versione che altrimenti viene fornita con il sistema è antica).