当进程执行命令(通过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 的输出中显示(ps
在Linux上是procps-ng的输出),但仅显示给使用相同有效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
使用(例如,通过运行sh -c 'a=a;while :; do a=$a$a;done'
)的(许多)库中退出缓存。以减慢启动速度,即使效率很低until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
也足以在我的测试中捕获该密码。
如果参数是将秘密传递给命令的唯一方法,则可能仍然可以尝试某些操作。
在某些系统上,包括较旧的Linux版本,只能argv[]
查询其中的字符串的前几个字节(在Linux 4.1及更高版本上为4096)。
在那里,您可以执行以下操作:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
秘密将被隐藏,因为它超过了前4096个字节。现在使用该方法的人现在必须后悔,因为从4.2版本开始,Linux不再截断/proc/pid/cmdline
。中的args列表。还要注意,并不是因为ps
命令行显示的字节数不多(例如在FreeBSD上似乎限制为2048),所以不能使用相同的API ps
来获取更多信息。但是,ps
该方法在常规用户检索该信息的唯一方法(例如,当对API进行特权设置并且ps
为了使用它而使用setgid或setuid进行访问)的唯一方式下是有效的,但在那儿仍然可能无法满足未来需求。
另一种方法是不通过的秘密argv[]
,但将代码注入程序(使用gdb
或$LD_PRELOAD
黑客)它之前main()
开始进行的插入秘密进入argv[]
从接收execve()
。
使用LD_PRELOAD
,对于GNU系统上非setuid / setgid动态链接的可执行文件:
/*
* 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的非特定方法,该方法不依赖于动态链接的可执行文件或具有调试符号,并且至少应适用于Linux上的任何ELF可执行文件:
#! /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个字节是什么(当gdb(8.0.1)处于交互状态,这表明gdb中可能存在bug时变为16)),替换argc
和argv
定义与:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(您可能还需要安装gdb
软件包/端口,因为系统附带的版本是古老的)。