Ketika suatu proses mengeksekusi perintah (melalui execve()
panggilan sistem), memorinya dihapus. Untuk meneruskan beberapa informasi di eksekusi, execve()
panggilan sistem membutuhkan dua argumen untuk itu: argv[]
danenvp[]
array.
Itu adalah dua array string:
argv[]
berisi argumen
envp[]
berisi definisi variabel lingkungan sebagai string dalam var=value
format (berdasarkan konvensi).
Saat kamu melakukan:
export SECRET=value; cmd "$SECRET"
(di sini menambahkan tanda kutip yang hilang di sekitar ekspansi parameter).
Anda mengeksekusi cmd
dengan rahasia ( value
) yang dilewati di argv[]
dan envp[]
. argv[]
akan ada ["cmd", "value"]
dan envp[]
sesuatu seperti [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]
. As cmd
tidak melakukan apa pun getenv("SECRET")
atau yang setara untuk mengambil nilai rahasia dari ituSECRET
variabel lingkungan itu, meletakkannya di lingkungan tidak berguna.
argv[]
adalah pengetahuan umum. Itu ditunjukkan dalam output dari ps
. envp[]
saat ini tidak. Di Linux, ini terlihat di /proc/pid/environ
. Ini ditunjukkan dalam output ps ewww
pada BSD (dan dengan procps-ng ps
di Linux), tetapi hanya untuk proses yang berjalan dengan uid efektif yang sama (dan dengan lebih banyak pembatasan untuk executable setuid / setgid). Mungkin terlihat di beberapa log audit, tetapi log audit tersebut hanya dapat diakses oleh administrator.
Singkatnya, lingkungan yang diteruskan ke yang dapat dieksekusi dimaksudkan untuk bersifat pribadi atau paling tidak sama privatnya dengan memori internal suatu proses (yang dalam beberapa keadaan proses lain dengan hak istimewa juga dapat mengakses dengan debugger misalnya dan dapat juga akan dibuang ke disk).
Karena argv[]
pengetahuan publik, perintah yang mengharapkan data yang dirahasiakan pada baris perintahnya rusak oleh desain.
Biasanya, perintah yang perlu diberi rahasia, memberi Anda antarmuka lain untuk melakukannya, seperti melalui variabel lingkungan. Contohnya:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Atau melalui deskriptor file khusus seperti stdin:
echo secret | openssl rsa -passin stdin ...
( echo
sedang dibangun, itu tidak ditampilkan dalam output of ps
)
Atau file, seperti .netrc
untuk ftp
dan beberapa perintah lain atau
mysql --defaults-extra-file=/some/file/with/password ....
Beberapa aplikasi seperti curl
(dan itu juga pendekatan yang diambil oleh @meuh di sini ) mencoba untuk menyembunyikan kata sandi yang mereka terima argv[]
dari mencongkel mata (pada beberapa sistem dengan menimpa bagian memori di mana argv[]
string disimpan). Tapi itu tidak benar-benar membantu dan memberikan janji keamanan palsu. Yang meninggalkan jendela di antara execve()
dan menimpa di mana ps
masih akan menunjukkan rahasianya.
Misalnya, jika penyerang tahu bahwa Anda menjalankan skrip yang melakukan curl -u user:somesecret https://...
(misalnya dalam tugas cron), yang harus ia lakukan adalah mengusir dari cache (banyak) perpustakaan yang curl
menggunakan (misalnya dengan menjalankan a sh -c 'a=a;while :; do a=$a$a;done'
) jadi untuk memperlambat startup-nya, dan bahkan melakukan yang sangat tidak efisien until grep 'curl.*[-]u' /proc/*/cmdline; do :; done
sudah cukup untuk menangkap kata sandi itu dalam pengujian saya.
Jika argumen adalah satu-satunya cara Anda bisa meneruskan rahasia ke perintah, mungkin masih ada beberapa hal yang bisa Anda coba.
Pada beberapa sistem, termasuk versi Linux yang lebih lama, hanya beberapa byte pertama (4096 pada Linux 4.1 dan sebelumnya) dari string yang argv[]
dapat di -query.
Di sana, Anda bisa melakukan:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
Dan rahasianya akan disembunyikan karena melewati 4096 byte pertama. Sekarang orang yang telah menggunakan metode itu harus menyesal sekarang karena Linux sejak 4.2 tidak lagi memotong daftar argumen di /proc/pid/cmdline
. Perhatikan juga bahwa ini bukan karena tidak ps
akan menampilkan lebih dari byte byte perintah (seperti pada FreeBSD yang sepertinya terbatas pada 2048) yang tidak bisa digunakan oleh pengguna API yang sama ps
untuk mendapatkan lebih banyak. Namun pendekatan itu valid pada sistem di mana ps
satu-satunya cara bagi pengguna biasa untuk mengambil informasi itu (seperti ketika API diistimewakan danps
ditetapkan atau digunakan untuk menggunakannya), tetapi masih berpotensi tidak menjadi bukti di masa depan di sana.
Pendekatan lain adalah dengan tidak meneruskan rahasia argv[]
tetapi menyuntikkan kode ke dalam program (menggunakan gdb
atau $LD_PRELOAD
hack) sebelum main()
dimulai yang memasukkan rahasia ke dalam yang argv[]
diterima dariexecve()
.
Dengan LD_PRELOAD
, untuk executable yang non-setuid / setgid yang terhubung secara dinamis pada sistem 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);
}
Kemudian:
$ 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 *****
Pada titik tidak akan ps
menunjukkan di ps -opid,args
sana ( -opid,args
menjadi rahasia dalam contoh ini). Perhatikan bahwa kami mengganti elemen argv[]
array pointer , bukan mengesampingkan string yang ditunjukkan oleh pointer tersebut, itulah sebabnya modifikasi kami tidak ditampilkan dalam outputps
.
Dengan gdb
, masih untuk executable yang non-setuid / setgid yang terhubung secara dinamis dan pada sistem 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"
Masih dengan gdb
, pendekatan spesifik non-GNU yang tidak bergantung pada executable yang dihubungkan secara dinamis atau memiliki simbol debug dan harus bekerja untuk ELF yang dapat dieksekusi di Linux setidaknya bisa menjadi:
#! /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
Pengujian dengan executable yang terhubung secara statis:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Ketika executable mungkin statis, kami tidak memiliki cara yang dapat diandalkan untuk mengalokasikan memori untuk menyimpan rahasia, jadi kami harus mendapatkan rahasia dari tempat lain yang sudah ada dalam memori proses. Itu sebabnya lingkungan adalah pilihan yang jelas di sini. Kami juga menyembunyikan SECRET
env var itu ke proses (dengan mengubahnya ke SECRE=
) untuk menghindari kebocoran jika proses memutuskan untuk membuang lingkungannya untuk beberapa alasan atau menjalankan aplikasi yang tidak dipercaya.
Itu juga berfungsi pada Solaris 11 (asalkan gdb dan GNU binutils diinstal (Anda mungkin harus mengganti nama objdump
menjadi gobjdump
).
Pada FreeBSD (setidaknya x86_64, saya tidak yakin apa itu 24 byte pertama (yang menjadi 16 ketika gdb (8.0.1) interaktif menunjukkan ada bug di gdb di sana) pada stack), ganti argc
dan argv
definisi dengan:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
(Anda mungkin juga perlu menginstal gdb
paket / port karena versi yang sebelumnya datang dengan sistem kuno).