Mencari LD_PRELOAD

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() { //     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); //  unset     for(int j = 0; environ[i][j] != '\0'; j++) environ[i][j] = '\0'; break; } } } 


 $ 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, &regs); // 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, &regs); } } 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 = 5

Terima kasih banyak

Charles Hubain
Chokepoint
Valdiks
Philippe teuwen
derhass

artikel-artikel, kode-kode sumber, dan komentar-komentarnya lebih dari saya untuk membuat artikel ini muncul di sini.

Source: https://habr.com/ru/post/id479858/


All Articles