เมื่อกระบวนการดำเนินการคำสั่ง (ผ่านการ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
บน BSDs (และกับ procps-ng ps
บน Linux) แต่เฉพาะกับกระบวนการทำงานด้วย uid ที่มีประสิทธิภาพเดียวกัน (และมีข้อ จำกัด มากขึ้นสำหรับ executables 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 เวอร์ชันเก่ามีเพียงไม่กี่ไบต์แรก (4096 บน Linux 4.1 และก่อน) ของสตริงที่argv[]
สามารถสอบถามได้
ที่นั่นคุณสามารถทำ:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
และความลับจะถูกซ่อนไว้เพราะผ่าน 4096 ไบต์แรก ตอนนี้คนที่ได้ใช้วิธีการที่จะต้องเสียใจมันตั้งแต่ตอนนี้ลินุกซ์ตั้งแต่ 4.2 ไม่ตัดทอนรายการ args /proc/pid/cmdline
ใน โปรดทราบด้วยว่านั่นไม่ใช่เพราะps
จะไม่แสดงมากกว่าหนึ่งไบต์ของบรรทัดคำสั่ง (เช่น FreeBSD ซึ่งดูเหมือนว่าจะถูก จำกัด อยู่ที่ 2048) ซึ่งไม่สามารถใช้กับ API เดียวกันps
ได้เพื่อให้ได้มากขึ้น วิธีการนั้นใช้ได้ในระบบที่ps
เป็นวิธีเดียวที่ผู้ใช้ทั่วไปสามารถดึงข้อมูลนั้นได้ (เช่นเมื่อ API ได้รับการยกเว้นและps
setgid หรือ setuid เพื่อใช้งาน) แต่ก็อาจไม่สามารถพิสูจน์ได้ในอนาคต
อีกวิธีหนึ่งที่จะไม่ผ่านความลับในargv[]
แต่รหัสฉีดลงในโปรแกรม (ใช้gdb
หรือ$LD_PRELOAD
สับ) ก่อนที่จะmain()
มีการเริ่มต้นที่แทรกลับเข้าไปในที่ได้รับจากargv[]
execve()
ด้วยLD_PRELOAD
, สำหรับ non-setuid / setgid ที่เชื่อมโยงแบบไดนามิก executables บนระบบ 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
ยังสำหรับ non-setuid / setgid ที่เชื่อมโยงแบบไดนามิก executables และระบบ 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 binutils มีการติดตั้ง (คุณอาจต้องเปลี่ยนชื่อ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
แพ็กเกจ / พอร์ตเนื่องจากเวอร์ชันที่มาพร้อมกับระบบนั้นเก่าแก่)