Коли процес виконує команду (через 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's ps
в Linux), але тільки для процесів, що працюють з тим же ефективним uid (і з більшими обмеженнями для setuid / setgid виконуваних файлів). Це може відображатися в деяких журналах аудиту, але до цих журналів аудиту повинні бути доступні лише адміністратори.
Коротше кажучи, середовище, яке передається виконуваному файлу, повинно бути приватним або принаймні таким же приватним, як внутрішня пам'ять процесу (який, за певних обставин, інший процес з правами привілеїв може також отримати доступ до відладчика, наприклад, і може також скидаються на диск).
Оскільки argv[]
це загальнодоступне знання, команда, яка очікує, що дані, які повинні бути секретними у своєму командному рядку, розбита конструкцією.
Зазвичай команди, яким потрібно надати секрет, надають вам інший інтерфейс для цього, наприклад, через змінну середовища. Наприклад:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Або через спеціальний дескриптор файлу, наприклад stdin:
echo secret | openssl rsa -passin stdin ...
( echo
будується вбудовано, воно не відображається на виходіps
)
Або файл, як, наприклад, .netrc
для 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, лише перші кілька байтів (4096 в Linux 4.1 і раніше) рядків у argv[]
можна запитувати .
Там ви можете зробити:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
І секрет був би прихований, оскільки він минув перші 4096 байт. Тепер люди, які використовували цей метод, повинні пошкодувати про це вже зараз, оскільки Linux з 4.2 більше не скорочує список аргументів /proc/pid/cmdline
. Також зауважте, що це не тому ps
, що не відображатиметься більше, ніж стільки байтів командного рядка (як, наприклад, у FreeBSD, де, здається, обмежено 2048), що не можна використовувати для тих самих API, ps
щоб отримати більше. Цей підхід є дійсним, але в системах, де ps
звичайний користувач - єдиний спосіб отримати цю інформацію (наприклад, коли привілейований API іps
встановлений жорсткий або налаштований для його використання), але він все ще не може бути майбутньому.
Іншим підходом було б не передавати секрет, argv[]
а вводити код у програму (за допомогою gdb
або $LD_PRELOAD
хак) перед її main()
запуском, який вставляє секрет у argv[]
отримане відexecve()
.
З LD_PRELOAD
, для не встановлених / 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
, як і раніше для не встановлених / встановлених жорстких динамічно пов'язаних виконуваних файлів та для 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 (можливо, доведеться перейменувати 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
пакет / порт, оскільки версія, яка в іншому випадку поставляється із системою, є давньою).