プロセスが(execve()
システムコールを介して)コマンドを実行すると、そのメモリは消去されます。実行中に情報を渡すために、execve()
システム呼び出しはそのための2つの引数を取ります:argv[]
およびenvp[]
配列。
これらは文字列の2つの配列です。
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のアプローチもここにあります)は、受け取ったパスワードをpr索好きな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を含む一部のシステムでは、文字列の最初の数バイト(Linux 4.1以前では4096)のみをargv[]
照会できます。
そこで、次のことができます。
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
そして、最初の4096バイトを超えているため、秘密が隠されます。Linuxは4.2以降ではargsのリストを切り捨てないため、このメソッドを使用した人は今では後悔する必要があり/proc/pid/cmdline
ます。またps
、同じAPIをps
使用してより多くを取得することができないのは、(2048に制限されているように見えるFreeBSDのように)コマンドラインを多バイト以上表示しないからではないことに注意してください。ただし、このアプローチは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
実行可能ファイルが動的にリンクされていることやデバッグシンボルを持つことに依存せず、少なくとも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、スタック上のそれらの最初の24バイト(gdb(8.0.1)がgdbにバグがあるかもしれないことを示唆する対話型の場合16バイトになる)が何であるかわからない)、argc
およびargv
定義を置き換えるで:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(gdb
パッケージ/ポートをインストールする必要があるかもしれません。そうでなければ、システムに付属しているバージョンは古いためです)