Khi một quá trình thực thi một lệnh (thông qua execve()
cuộc gọi hệ thống), bộ nhớ của nó sẽ bị xóa. Để truyền một số thông tin qua thực thi, các execve()
cuộc gọi hệ thống cần hai đối số cho điều đó: argv[]
và envp[]
mảng.
Đó là hai mảng của chuỗi:
argv[]
chứa các đối số
envp[]
chứa các định nghĩa biến môi trường dưới dạng các chuỗi trong var=value
định dạng (theo quy ước).
Khi bạn làm:
export SECRET=value; cmd "$SECRET"
(ở đây đã thêm các trích dẫn còn thiếu xung quanh việc mở rộng tham số).
Bạn đang thực hiện cmd
với bí mật ( value
) được truyền cả trong argv[]
và envp[]
. argv[]
sẽ được ["cmd", "value"]
và envp[]
một cái gì đó như [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. Vì cmd
không thực hiện bất kỳ getenv("SECRET")
hoặc tương đương để lấy giá trị của bí mật từ SECRET
biến môi trường đó, nên đặt nó vào môi trường là không hữu ích.
argv[]
là kiến thức công cộng. Nó cho thấy trong đầu ra của ps
. envp[]
ngày nay thì không. Trên Linux, nó hiển thị trong /proc/pid/environ
. Nó cho thấy trong đầu ra của ps ewww
BSD (và với Procps-ng ps
trên Linux), nhưng chỉ với các quy trình chạy với cùng một uid hiệu quả (và có nhiều hạn chế hơn đối với các tệp thực thi setuid / setgid). Nó có thể hiển thị trong một số nhật ký kiểm toán, nhưng những nhật ký kiểm toán đó chỉ có thể được truy cập bởi các quản trị viên.
Nói tóm lại, môi trường được truyền cho một thực thi có nghĩa là riêng tư hoặc ít nhất là riêng tư như bộ nhớ trong của một tiến trình (trong một số trường hợp, một quy trình khác có đặc quyền phù hợp cũng có thể truy cập bằng trình gỡ lỗi và có thể cũng được đổ vào đĩa).
Vì argv[]
là kiến thức công cộng, một lệnh dự kiến dữ liệu có nghĩa là bí mật trên dòng lệnh của nó bị phá vỡ theo thiết kế.
Thông thường, các lệnh cần được cung cấp một bí mật, cung cấp cho bạn một giao diện khác để thực hiện điều đó, như thông qua một biến môi trường. Ví dụ:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Hoặc thông qua một mô tả tập tin chuyên dụng như stdin:
echo secret | openssl rsa -passin stdin ...
( echo
đang được dựng sẵn, nó không hiển thị trong đầu ra của ps
)
Hoặc một tệp, như .netrc
for ftp
và một vài lệnh khác hoặc
mysql --defaults-extra-file=/some/file/with/password ....
Một số ứng dụng như curl
(và đó cũng là cách tiếp cận được thực hiện bởi @meuh tại đây ) cố gắng ẩn mật khẩu mà họ nhận được argv[]
khỏi con mắt tò mò (trên một số hệ thống bằng cách ghi đè lên phần bộ nhớ nơi argv[]
lưu trữ chuỗi). Nhưng điều đó không thực sự giúp ích và đưa ra một lời hứa sai về bảo mật. Điều đó để lại một cửa sổ ở giữa execve()
và ghi đè nơi ps
vẫn sẽ hiển thị bí mật.
Chẳng hạn, nếu kẻ tấn công biết rằng bạn đang chạy tập lệnh đang thực hiện curl -u user:somesecret https://...
(ví dụ như trong công việc định kỳ), tất cả những gì anh ta phải làm là xóa khỏi bộ đệm (các) thư viện curl
sử dụng (ví dụ bằng cách chạy a sh -c 'a=a;while :; do a=$a$a;done'
) như làm chậm quá trình khởi động của nó và thậm chí làm rất kém hiệu quả until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
là đủ để bắt được mật khẩu đó trong các thử nghiệm của tôi.
Nếu các đối số là cách duy nhất bạn có thể truyền bí mật cho các lệnh, thì vẫn có thể có một số điều bạn có thể thử.
Trên một số hệ thống, bao gồm các phiên bản cũ hơn của Linux, chỉ argv[]
có thể truy vấn một vài byte đầu tiên (4096 trên Linux 4.1 trở lên) của chuỗi .
Ở đó, bạn có thể làm:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
Và bí mật sẽ bị ẩn bởi vì nó đã vượt qua 4096 byte đầu tiên. Bây giờ những người đã sử dụng phương pháp đó phải hối hận vì Linux kể từ phiên bản 4.2 không còn cắt xén danh sách các đối số /proc/pid/cmdline
. Cũng lưu ý rằng không phải vì ps
sẽ không hiển thị nhiều hơn một byte dòng lệnh (như trên FreeBSD, nơi dường như bị giới hạn ở năm 2048) mà người ta không thể sử dụng cùng một API ps
để sử dụng nhiều hơn. Tuy nhiên, cách tiếp cận đó là hợp lệ trên các hệ thống ps
là cách duy nhất để người dùng thông thường truy xuất thông tin đó (như khi API được đặc quyền và ps
được setgid hoặc setuid để sử dụng), nhưng vẫn không có khả năng trong tương lai.
Một cách tiếp cận khác là không truyền bí mật vào argv[]
mà tiêm mã vào chương trình (sử dụng gdb
hoặc $LD_PRELOAD
hack) trước khi main()
bắt đầu chèn bí mật vào argv[]
nhận được từ đó execve()
.
Với LD_PRELOAD
, đối với các tệp thực thi được liên kết động không phải setuid / setgid trên hệ thống 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);
}
Sau đó:
$ 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 *****
Không có điểm nào ps
cho thấy điều ps -opid,args
đó ( -opid,args
là bí mật trong ví dụ này). Lưu ý rằng chúng tôi thay thế các phần tử của argv[]
mảng con trỏ , không ghi đè các chuỗi được trỏ bởi các con trỏ đó, đó là lý do tại sao các sửa đổi của chúng tôi không hiển thị trong đầu ra của ps
.
Với gdb
, vẫn dành cho các tệp thực thi được liên kết động không setuid / setgid và trên các hệ thống 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"
Tuy nhiên gdb
, một cách tiếp cận cụ thể không phải là GNU không dựa vào các tệp thực thi được liên kết động hoặc có các ký hiệu gỡ lỗi và nên hoạt động cho mọi ELF thực thi trên Linux ít nhất có thể là:
#! /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
Kiểm tra với một thực thi được liên kết tĩnh:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Khi thực thi có thể là tĩnh, chúng ta không có cách đáng tin cậy để phân bổ bộ nhớ để lưu trữ bí mật, vì vậy chúng ta phải lấy bí mật từ một nơi khác đã có trong bộ nhớ quy trình. Đó là lý do tại sao môi trường là sự lựa chọn rõ ràng ở đây. Chúng tôi cũng ẩn SECRET
env var đó vào quy trình (bằng cách thay đổi nó SECRE=
) để tránh nó bị rò rỉ nếu quá trình quyết định kết xuất môi trường của nó vì một số lý do hoặc thực thi các ứng dụng không tin cậy.
Điều đó cũng hoạt động trên Solaris 11 (được cung cấp gdb và GNU binutils (bạn có thể phải đổi tên objdump
thành gobjdump
).
Trên FreeBSD (ít nhất là x86_64, tôi không chắc 24 byte đầu tiên (trở thành 16 khi gdb (8.0.1) tương tác cho thấy có thể có lỗi trong gdb ở đó) trên stack), thay thế argc
và argv
định nghĩa với:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(bạn cũng có thể cần cài đặt gdb
gói / cổng vì phiên bản đi kèm với hệ thống là cổ xưa).