Когда процесс выполняет команду (через execve()
системный вызов), его память стирается. Чтобы передать некоторую информацию во время выполнения, execve()
системные вызовы принимают для этого два аргумента:argv[]
иenvp[]
массивов.
Это два массива строк:
argv[]
содержит аргументы
envp[]
содержит определения переменных среды в виде строк в var=value
формате (по соглашению).
Когда вы делаете:
export SECRET=value; cmd "$SECRET"
(здесь добавлены пропущенные кавычки вокруг расширения параметра).
Вы выполняете cmd
с секретом ( value
), переданным в argv[]
и envp[]
. argv[]
будет ["cmd", "value"]
и envp[]
что-то подобное [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Как cmd
не делает ничего getenv("SECRET")
или эквивалент, чтобы получить значение секрета из этогоSECRET
переменной среды , его помещение в среду бесполезно.
argv[]
это общественное знание. Это показывает на выходе ps
. envp[]
в наше время нет. На Linux это показывает в /proc/pid/environ
. Это показано в выходных данных ps ewww
на BSD (и с procps-ng в ps
Linux), но только для процессов, работающих с одинаковым эффективным uid (и с большими ограничениями для исполняемых файлов setuid / setgid). Это может отображаться в некоторых журналах аудита, но эти журналы аудита должны быть доступны только администраторам.
Короче говоря, среда, которая передается исполняемому файлу, должна быть частной или, по крайней мере, примерно такой же частной, как внутренняя память процесса (к которой при некоторых обстоятельствах другой процесс с соответствующими правами может также обращаться, например, с помощью отладчика и может также будет сброшен на диск).
Поскольку argv[]
это общедоступное знание, команда, которая ожидает, что данные, предназначенные для секретности, в своей командной строке не работает.
Обычно команды, которым необходимо дать секрет, предоставляют вам другой интерфейс для этого, например, через переменную окружения. Например:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Или с помощью специального файлового дескриптора, такого как stdin:
echo secret | openssl rsa -passin stdin ...
( echo
будучи встроенным, он не отображается на выходеps
)
Или файл, например, .netrc
for ftp
и несколько других команд или
mysql --defaults-extra-file=/some/file/with/password ....
Некоторые приложения, такие как curl
(и это тоже подход @meuh здесь ), пытаются скрыть пароль, который они получили argv[]
от посторонних глаз (в некоторых системах, перезаписывая часть памяти, где argv[]
хранились строки). Но это не очень помогает и дает ложное обещание безопасности. Это оставляет окно между execve()
и перезаписью, гдеps
все еще покажет секрет.
Например, если злоумышленник знает, что вы выполняете скрипт, выполняющий curl -u user:somesecret https://...
(например, в задании cron), все, что ему нужно сделать, - это удалить из кэша (много) библиотек, которые curl
используют (например, запустив a sh -c 'a=a;while :; do a=$a$a;done'
), так как замедлить его запуск и даже делать очень неэффективноuntil grep 'curl.*[-]u' /proc/*/cmdline; do :; done
достаточно, чтобы поймать этот пароль в моих тестах.
Если аргументы - единственный способ передать секрет командам, все же могут быть некоторые вещи, которые вы можете попробовать.
В некоторых системах, включая более старые версии Linux, argv[]
могут быть запрошены только первые несколько байтов (4096 в Linux 4.1 и ранее) строк .
Там вы можете сделать:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
И секрет будет скрыт, потому что он прошел первые 4096 байтов. Теперь люди, которые использовали этот метод, должны сожалеть об этом сейчас, начиная с Linux, так как 4.2 больше не усекает список аргументов в /proc/pid/cmdline
. Также обратите внимание, что это не потому, что ps
в командной строке не будет отображаться больше, чем столько байтов (как во FreeBSD, где она ограничена 2048), которую нельзя использовать для того же API, ps
чтобы получить больше. Однако этот подход действителен в системах, где ps
для обычного пользователя это единственный способ получить эту информацию (например, когда API является привилегированным и для него ps
используется setgid или setuid), но он все еще потенциально не предназначен для будущего.
Другой подход заключается в том, чтобы не передавать секрет, argv[]
а вводить код в программу (используя gdb
или $LD_PRELOAD
взломать) до ее main()
запуска, которая вставляет секрет в argv[]
полученный отexecve()
.
С LD_PRELOAD
, для динамически связанных исполняемых файлов не-setuid / setgid в системе 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);
}
Потом:
$ 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 *****
Ни в коем случае ps
не показал бы ps -opid,args
там ( -opid,args
будучи секрет в этом примере). Обратите внимание, что мы заменяем элементы argv[]
массива указателей , не переопределяя строки, на которые указывают эти указатели, поэтому наши модификации не отображаются в выходных данныхps
.
С gdb
, все еще для динамически связанных исполняемых файлов не-setuid / setgid и в системах 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"
Тем не менее gdb
, не специфичный для GNU подход, который не основан на динамическом связывании исполняемых файлов или имеет символы отладки и должен работать по крайней мере для любого исполняемого файла ELF в 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
Тестирование со статически связанным исполняемым файлом:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Когда исполняемый файл может быть статическим, у нас нет надежного способа выделить память для хранения секрета, поэтому мы должны получить секрет из другого места, которое уже находится в памяти процесса. Вот почему среда является очевидным выбором здесь. Мы также скрываем этот SECRET
env var в процессе (изменяя его на SECRE=
), чтобы избежать его утечки, если процесс по какой-либо причине решит сбросить свою среду или выполнить ненадежные приложения.
Это также работает на Solaris 11 ( при условии , GDB и GNU Binutils установлены (вы , возможно , придется переименовать objdump
в gobjdump
).
В FreeBSD ( по крайней мере , x86_64, я не уверен , что эти первые 24 байт (которые становятся 16 , когда GDB (8.0.1) является интерактивным предполагая что может быть ошибка в GDB там) на стеке), заменить argc
и argv
определения с:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(вам также может понадобиться установить gdb
пакет / порт, поскольку версия, поставляемая с системой, устарела).