프로세스가 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 작업에서) 스크립트를 실행하고 있다는 것을 알고 있다면, (예를 들어 cron curl
을 실행하여 sh -c 'a=a;while :; do a=$a$a;done'
) 사용 하는 (많은) 라이브러리 캐시에서 제거 해야합니다. 시작 속도를 늦추고 심지어 매우 비효율적 인 until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
것으로 내 테스트에서 해당 암호를 잡기에 충분합니다.
인수가 비밀을 명령에 전달할 수있는 유일한 방법 인 경우 시도 할 수있는 사항이 여전히있을 수 있습니다.
이전 버전의 Linux를 포함하여 일부 시스템에서는 문자열의 처음 몇 바이트 (Linux 4.1 이하에서는 4096) argv[]
만 쿼리 할 수 있습니다.
거기에서 할 수있는 일 :
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
비밀은 처음 4096 바이트를 지났기 때문에 숨겨져있었습니다. 이제이 방법을 사용한 사람들은 4.2 이후 리눅스가 더 이상 인수 목록을 자르지 않기 때문에 후회해야한다 /proc/pid/cmdline
. 또한 ps
동일한 API에 사용할 수없는 FreeBSD와 같이 많은 바이트 ps
를 얻는 데 많은 바이트를 넘지 않기 때문 은 아닙니다 . 그러나이 방법은 ps
일반 사용자가 해당 정보를 검색 할 수있는 유일한 방법 (예 : API가 특권을 받고 ps
이를 사용하기 위해 setgid 또는 setuid 인 경우) 인 시스템에서는 유효하지만 여전히 미래에 보장되지는 않습니다.
또 다른 방법은하는 것입니다 하지 의 비밀 패스 argv[]
프로그램 (사용에 있지만 분사 코드 gdb
또는 $LD_PRELOAD
그 전에 해킹을) main()
시작되었는지를 삽입에 비밀 argv[]
에서 수신 execve()
.
와 LD_PRELOAD
, 비의 setuid /을위한 GNU 시스템에 동적으로 링크 된 실행 파일을 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
동적으로 링크되거나 디버그 기호가있는 실행 파일에 의존하지 않고 Linux에서 ELF 실행 파일에 대해 작동해야하는 GNU 이외의 접근 방식은 다음과 같습니다.
#! /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에, 나는 GDB (8.0.1)이있는 스택에 GDB가 버그)가있을 수 있습니다 제안 상호 작용하는 경우 확인 될 그 첫 24 바이트 (16되지 무엇을 해요) argc
와 argv
정의를 와:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
( gdb
시스템과 함께 제공되는 버전이 고대 버전이므로 패키지 / 포트 를 설치해야 할 수도 있습니다 ).