Cuando un proceso ejecuta un comando (a través de la execve()
llamada al sistema), su memoria se borra. Para pasar información a través de la ejecución, las execve()
llamadas al sistema toman dos argumentos para eso: las matrices argv[]
y envp[]
.
Esas son dos matrices de cadenas:
argv[]
contiene los argumentos
envp[]
contiene las definiciones de variables de entorno como cadenas en el var=value
formato (por convención).
Cuando tu lo hagas:
export SECRET=value; cmd "$SECRET"
(Aquí se agregan las comillas faltantes alrededor de la expansión del parámetro).
Estás ejecutando cmd
con el secreto ( value
) pasado tanto en argv[]
y envp[]
. argv[]
será ["cmd", "value"]
y envp[]
algo así [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Como cmd
no está haciendo nada getenv("SECRET")
o equivalente para recuperar el valor del secreto de esa SECRET
variable de entorno, ponerlo en el entorno no es útil.
argv[]
Es de conocimiento público. Se muestra en la salida de ps
. envp[]
hoy en día no lo es. En Linux, se muestra en /proc/pid/environ
. Se muestra en la salida de los ps ewww
BSD (y con procps-ng ps
en Linux), pero solo en los procesos que se ejecutan con el mismo uid efectivo (y con más restricciones para los ejecutables setuid / setgid). Puede aparecer en algunos registros de auditoría, pero solo los administradores deben tener acceso a esos registros de auditoría.
En resumen, el entorno que se pasa a un ejecutable debe ser privado o al menos tan privado como la memoria interna de un proceso (que, en algunas circunstancias, otro proceso con los privilegios correctos también puede acceder con un depurador, por ejemplo, y puede también ser volcado al disco).
Dado que argv[]
es de conocimiento público, el diseño rompe un comando que espera que los datos destinados a ser secretos en su línea de comando se rompan.
Por lo general, los comandos que necesitan un secreto, le proporcionan otra interfaz para hacerlo, como a través de una variable de entorno. Por ejemplo:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
O a través de un descriptor de archivo dedicado como stdin:
echo secret | openssl rsa -passin stdin ...
( echo
estando integrado, no se muestra en la salida de ps
)
O un archivo, como .netrc
for ftp
y algunos otros comandos o
mysql --defaults-extra-file=/some/file/with/password ....
Algunas aplicaciones como curl
(y ese también es el enfoque adoptado por @meuh aquí ) intentan ocultar la contraseña que recibieron argv[]
de miradas indiscretas (en algunos sistemas sobrescribiendo la porción de memoria donde argv[]
se almacenaron las cadenas). Pero eso no ayuda mucho y da una falsa promesa de seguridad. Eso deja una ventana entre execve()
y la sobrescritura donde ps
aún se mostrará el secreto.
Por ejemplo, si un atacante sabe que está ejecutando un script haciendo curl -u user:somesecret https://...
(por ejemplo, en un trabajo cron), todo lo que tiene que hacer es expulsar del caché las (muchas) bibliotecas que curl
usa (por ejemplo ejecutando a sh -c 'a=a;while :; do a=$a$a;done'
) para ralentizar su inicio, e incluso hacer un método muy ineficiente until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
es suficiente para atrapar esa contraseña en mis pruebas.
Si los argumentos son la única forma en que puede pasar el secreto a los comandos, aún puede intentar hacer algunas cosas.
En algunos sistemas, incluidas las versiones anteriores de Linux, solo se pueden consultar los primeros bytes (4096 en Linux 4.1 y anteriores) de las cadenas argv[]
.
Ahí puedes hacer:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
Y el secreto estaría oculto porque ya pasaron los primeros 4096 bytes. Ahora las personas que han usado ese método deben arrepentirse ahora ya que Linux desde 4.2 ya no trunca la lista de argumentos /proc/pid/cmdline
. También tenga en cuenta que no es porque ps
no muestre más que tantos bytes de una línea de comando (como en FreeBSD donde parece estar limitado a 2048) que no se puede usar los mismos ps
usos de API para obtener más. Sin embargo, ese enfoque es válido en sistemas en los que ps
es la única forma en que un usuario normal puede recuperar esa información (como cuando la API tiene privilegios y ps
está configurada o configurada para usarla), pero aún no está allí a prueba de futuro.
Otro enfoque sería no pasar el secreto argv[]
sino inyectar código en el programa (usando gdb
o $LD_PRELOAD
piratear) antes de que main()
se inicie, que inserta el secreto en el argv[]
recibido execve()
.
Con LD_PRELOAD
, para ejecutables no setuid / setgid vinculados dinámicamente en 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);
}
Luego:
$ 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 *****
En ningún momento habría ps
mostrado el ps -opid,args
allí ( -opid,args
siendo el secreto en este ejemplo). Tenga en cuenta que estamos reemplazando elementos de la argv[]
matriz de punteros , no anulando las cadenas señaladas por esos punteros, por lo que nuestras modificaciones no se muestran en la salida de ps
.
Con gdb
, aún para ejecutables vinculados dinámicamente no setuid / setgid y en sistemas 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"
Aún así gdb
, un enfoque específico que no sea GNU que no se base en que los ejecutables estén vinculados dinámicamente o que tengan símbolos de depuración y que debería funcionar para cualquier ejecutable ELF en Linux al menos podría ser:
#! /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
Prueba con un ejecutable vinculado estáticamente:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Cuando el ejecutable puede ser estático, no tenemos una forma confiable de asignar memoria para almacenar el secreto, por lo que debemos obtener el secreto de otro lugar que ya esté en la memoria del proceso. Es por eso que el medio ambiente es la opción obvia aquí. También ocultamos ese SECRET
entorno al proceso (cambiándolo a SECRE=
) para evitar que se filtre si el proceso decide volcar su entorno por algún motivo o ejecutar aplicaciones no confiables.
Que también funciona en Solaris 11 (siempre que el BGF y se instalan GNU binutils (puede que tenga que cambiar el nombre objdump
a gobjdump
).
En FreeBSD (al menos x86_64, no estoy seguro de lo que los primeros 24 bytes (que se convierten en 16 cuando GDB (8.0.1) es interactivo sugiere que puede haber un error en el BGF allí) en la pila son), reemplazan el argc
y argv
definiciones con:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(es posible que también necesite instalar el gdb
paquete / puerto ya que la versión que viene con el sistema es antigua).