Quando um processo executa um comando (por meio da execve()
chamada do sistema), sua memória é limpa. Para passar algumas informações pela execução, as execve()
chamadas do sistema usam dois argumentos para isso: the argv[]
e envp[]
matrizes.
Essas são duas matrizes de strings:
argv[]
contém os argumentos
envp[]
contém as definições de variáveis de ambiente como cadeias de caracteres no var=value
formato (por convenção).
Quando você faz:
export SECRET=value; cmd "$SECRET"
(aqui foram adicionadas as aspas ausentes ao redor da expansão do parâmetro).
Você está executando cmd
com o secret ( value
) passado em argv[]
e envp[]
. argv[]
será ["cmd", "value"]
e envp[]
algo assim [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Como cmd
não está fazendo nada getenv("SECRET")
ou equivalente para recuperar o valor do segredo dessa SECRET
variável de ambiente, colocá-lo no ambiente não é útil.
argv[]
é conhecimento público. Mostra na saída de ps
. envp[]
hoje em dia não é. No Linux, mostra em /proc/pid/environ
. Ele é mostrado na saída de ps ewww
BSDs (e com procps-ng's ps
no Linux), mas apenas para processos em execução com o mesmo uid efetivo (e com mais restrições para executáveis setuid / setgid). Pode aparecer em alguns logs de auditoria, mas esses logs de auditoria devem ser acessíveis apenas pelos administradores.
Em resumo, o ambiente que é passado para um executável deve ser privado ou pelo menos tão privado quanto a memória interna de um processo (que, em algumas circunstâncias, outro processo com os privilégios corretos também pode acessar com um depurador, por exemplo, e pode também ser despejado no disco).
Como argv[]
é de conhecimento público, um comando que espera que os dados sigam em sua linha de comando é quebrado por design.
Geralmente, os comandos que precisam receber um segredo fornecem uma outra interface para isso, como por meio de uma variável de ambiente. Por exemplo:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Ou através de um descritor de arquivo dedicado como stdin:
echo secret | openssl rsa -passin stdin ...
( echo
sendo incorporado, ele não aparece na saída de ps
)
Ou um arquivo, como o .netrc
for ftp
e alguns outros comandos ou
mysql --defaults-extra-file=/some/file/with/password ....
Alguns aplicativos como curl
(e essa também é a abordagem adotada por @meuh aqui ) tentam ocultar a senha que eles receberam argv[]
de olhares indiscretos (em alguns sistemas, substituindo a parte da memória em que as argv[]
strings estavam armazenadas). Mas isso realmente não está ajudando e oferece uma falsa promessa de segurança. Isso deixa uma janela entre a execve()
e a substituição, onde ps
ainda mostrará o segredo.
Por exemplo, se um invasor sabe que você está executando um script curl -u user:somesecret https://...
(por exemplo em um trabalho cron), tudo o que ele precisa fazer é remover do cache as (muitas) bibliotecas que curl
usam (por exemplo, executando a sh -c 'a=a;while :; do a=$a$a;done'
), para para desacelerar sua inicialização e até mesmo fazer uma ineficiência until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
é suficiente para capturar essa senha nos meus testes.
Se os argumentos forem a única maneira de transmitir o segredo aos comandos, ainda pode haver algumas coisas que você pode tentar.
Em alguns sistemas, incluindo versões mais antigas do Linux, apenas os primeiros bytes (4096 no Linux 4.1 e anterior) das cadeias argv[]
podem ser consultados.
Lá, você pode fazer:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
E o segredo seria oculto porque passou dos primeiros 4096 bytes. Agora, as pessoas que usaram esse método devem se arrepender agora, já que o Linux, desde o 4.2, não trunca mais a lista de argumentos /proc/pid/cmdline
. Observe também que não é porque ps
não mostrará mais do que muitos bytes de uma linha de comando (como no FreeBSD, onde parece estar limitado a 2048) que não se pode usar o mesmo uso da API ps
para obter mais. Entretanto, essa abordagem é válida em sistemas onde ps
é a única maneira de um usuário comum recuperar essas informações (como quando a API é privilegiada e ps
é setgid ou setuid para usá-la), mas ainda não é potencialmente disponível para o futuro.
Outra abordagem seria não passar o segredo, argv[]
mas injetar código no programa (usando gdb
ou $LD_PRELOAD
hackear) antes de main()
iniciar, que insere o segredo no argv[]
recebido execve()
.
Com LD_PRELOAD
, para executáveis dinamicamente não-setuid / setgid vinculados em um 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);
}
Então:
$ 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 *****
Em nenhum momento teria ps
mostrado o ps -opid,args
lá ( -opid,args
sendo o segredo neste exemplo). Observe que estamos substituindo elementos da argv[]
matriz de ponteiros , não substituindo as seqüências apontadas por esses ponteiros, razão pela qual nossas modificações não aparecem na saída de ps
.
With gdb
, ainda para executáveis dinamicamente não-setuid / setgid vinculados e em 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"
Ainda assim gdb
, uma abordagem não-GNU específica, que não depende de executáveis vinculados dinamicamente ou que possuam símbolos de depuração, deve funcionar para qualquer ELF executável no Linux, pelo menos, poderia 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
Testando com um executável vinculado estaticamente:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Quando o executável pode ser estático, não temos uma maneira confiável de alocar memória para armazenar o segredo; portanto, precisamos obtê-lo de outro lugar que já esteja na memória do processo. É por isso que o ambiente é a escolha óbvia aqui. Também ocultamos esse SECRET
env var ao processo (alterando-o para SECRE=
) para evitar vazamentos se o processo decidir despejar seu ambiente por algum motivo ou executar aplicativos não confiáveis.
Isso também funciona no Solaris 11 (desde que os binutils gdb e GNU estejam instalados (pode ser necessário renomear objdump
para gobjdump
).
No FreeBSD (pelo menos x86_64, eu não sei o que aqueles primeiros 24 bytes (que se tornam 16 quando gdb (8.0.1) é interativo sugerindo que pode haver um erro no gdb lá) na pilha são), substitua o argc
e argv
definições com:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(você também pode precisar instalar o gdb
pacote / porta, pois a versão que vem com o sistema é antiga).