Pada bagian sebelumnya, saya secara kasar menjelaskan bagaimana Anda dapat memuat fungsi eBPF dari file ELF. Sekarang saatnya untuk beralih dari fantasi ke kartun Soviet, dan mengikuti saran bijak, setelah menghabiskan sejumlah upaya sekali, membuat alat instrumentasi universal (atau, singkatnya, UII !!!) . Dengan melakukan itu, saya akan mengambil keuntungan dari desain antipattern Golden Hammer dan membangun alat dari QEMU yang relatif akrab. Sebagai bonus untuk ini, kami mendapatkan instrumentasi lintas-arsitektur, serta instrumentasi di level seluruh komputer virtual. Instrumentasi akan berupa "file asli kecil + file kecil .o dengan eBPF". Dalam hal ini, fungsi eBPF akan diganti sebelum instruksi yang sesuai dari representasi internal QEMU sebelum optimasi dan pembuatan kode.
Akibatnya, instrumentasi itu sendiri, yang ditambahkan selama pembuatan kode (yaitu, tidak menghitung beberapa kilobyte dari runtime sistem normal), terlihat seperti ini, dan ini bukan kode pseudo:
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
Nah, saatnya untuk memuat peri kita ke dalam Matrix. Nah, bagaimana cara mengunduh menampar semprotan.
Seperti yang telah disebutkan dalam artikel tentang QEMU.js , salah satu mode operasi QEMU adalah pembuatan kode mesin host JIT dari tamu (berpotensi, untuk arsitektur yang sama sekali berbeda). Jika terakhir kali saya mengimplementasikan backend pembuatan kode saya, maka kali ini saya akan memproses representasi internal dengan memotong tepat di depan pengoptimal. Apakah ini keputusan yang sewenang-wenang? Tidak. Ada harapan bahwa pengoptimal akan memotong sudut berlebih, membuang variabel yang tidak perlu, dll. Sejauh yang saya mengerti, dia, pada kenyataannya, melakukan hal-hal sederhana dan cepat dapat dilakukan: mendorong konstanta, membuang ekspresi seperti "x: = x + 0" dan menghapus kode yang tidak terjangkau. Dan kita bisa mendapatkan jumlah yang layak.
Konfigurasi skrip perakitan
Pertama, mari kita tambahkan file sumber kami: tcg/bpf-loader.c
dan tcg/instrument.c
ke Makefiles. Secara umum, ada keinginan untuk suatu hari nanti mendorong ini ke hulu, jadi Anda harus melakukannya pada akhirnya dengan bijak, tetapi untuk sekarang saya hanya akan tanpa syarat menambahkan file-file ini ke majelis. Dan saya akan mengambil parameter dalam tradisi terbaik AFL - melalui variabel lingkungan. Ngomong-ngomong, saya akan menguji ini lagi pada instrumentasi untuk AFL.
Lihat saja penyebutan "tetangga" - file grep -R
dengan grep -R
dan kami tidak akan menemukan apa pun. Karena itu perlu mencari optimize.o
:
Pertama, mari kita tambahkan bpf-loader.c
dari seri terakhir dengan kode yang menarik keluar titik masuk yang sesuai dengan operasi QEMU. Dan file tcg-opc.h
misterius akan membantu kita dengan ini. Ini terlihat seperti ini:
DEF(discard, 1, 0, 0, TCG_OPF_NOT_PRESENT) DEF(set_label, 0, 0, 1, TCG_OPF_BB_END | TCG_OPF_NOT_PRESENT) DEF(call, 0, 0, 3, TCG_OPF_CALL_CLOBBER | TCG_OPF_NOT_PRESENT) DEF(br, 0, 0, 1, TCG_OPF_BB_END)
Omong kosong apa Dan masalahnya adalah bahwa itu tidak terhubung di header sumber - Anda perlu mendefinisikan makro DEF
, termasuk file ini, dan segera menghapus makro. Lihat, dia bahkan tidak punya penjaga.
static const char *inst_function_names[] = { #define DEF(name, a, b, c, d) stringify(inst_qemu_##name), #include "tcg-opc.h" #undef DEF NULL };
Sebagai hasilnya, kita mendapatkan array rapi dari nama fungsi target, diindeks oleh opcodes dan diakhiri dengan NULL, yang dapat kita jalankan untuk setiap karakter dalam file. Saya mengerti bahwa ini tidak efektif. Tapi itu sederhana, yang penting, mengingat sifat operasi ini satu kali. Selanjutnya, kita lewati saja semua karakternya
ELF64_ST_BIND(sym->st_info) == STB_LOCAL || ELF64_ST_TYPE(sym->st_info) != STT_FUNC
Sisanya diperiksa berdasarkan daftar.
Kami melekat pada aliran eksekusi
Sekarang Anda harus bangun di suatu tempat pada aliran mekanisme pembuatan kode, dan tunggu sampai instruksi yang diinginkan lewat. Tetapi pertama-tama Anda perlu mendefinisikan fungsi instrumentation_init
, tcg_instrument
dan instrumentation_shutdown
dalam file tcg/tcg.h
dan menuliskan panggilan mereka: inisialisasi - setelah backend diinisialisasi, instrumentasi - tepat sebelum panggilan tcg_optimize
. Tampaknya instrumentation_shutdown
dapat digantung di instrumentation_init
di atexit
dan tidak melonjak. Saya pikir juga begitu, dan kemungkinan besar itu akan bekerja dalam mode emulasi sistem penuh, tetapi dalam mode emulasi usermode QEMU menerjemahkan panggilan sistem exit_group
dan kadang-kadang exit
ke panggilan fungsi _exit
, yang mengabaikan semua penangan-atexit ini, oleh karena itu, kita akan mencarinya di linux-user/syscall.c
dan linux-user/syscall.c
panggilan ke kode kita di depannya.
Menafsirkan Bytecode
Jadi sudah waktunya untuk membaca apa yang dihasilkan kompiler untuk kita. Ini mudah dilakukan menggunakan llvm-objdump
dengan opsi -x
, atau lebih baik, segera -d -t -r
.
Contoh keluaran $ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 inst_brcond_i64: 0: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000000: R_BPF_64_64 prev 2: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 3: 77 03 00 00 01 00 00 00 r3 >>= 1 4: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 5: af 13 00 00 00 00 00 00 r3 ^= r1 6: 57 03 00 00 ff ff 00 00 r3 &= 65535 7: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 0000000000000038: R_BPF_64_64 __afl_area_ptr 9: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 10: 0f 34 00 00 00 00 00 00 r4 += r3 11: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 12: 07 03 00 00 01 00 00 00 r3 += 1 13: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 14: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 15: 95 00 00 00 00 00 00 00 exit 0000000000000080 inst_brcond_i32: 16: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000080: R_BPF_64_64 prev 18: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 19: 77 03 00 00 01 00 00 00 r3 >>= 1 20: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 21: af 13 00 00 00 00 00 00 r3 ^= r1 22: 57 03 00 00 ff ff 00 00 r3 &= 65535 23: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 00000000000000b8: R_BPF_64_64 __afl_area_ptr 25: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 26: 0f 34 00 00 00 00 00 00 r4 += r3 27: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 28: 07 03 00 00 01 00 00 00 r3 += 1 29: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 30: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 31: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 *UND* 00000000 __afl_area_ptr 0000000000000080 g F .text 00000080 inst_brcond_i32 0000000000000000 g F .text 00000080 inst_brcond_i64 0000000000000008 g O *COM* 00000008 prev
Jika Anda mencoba mencari deskripsi opcode eBPF, ternyata di tempat-tempat yang jelas (sumber dan halaman manual dari kernel Linux) ada deskripsi tentang bagaimana menggunakannya, cara mengkompilasi, dll. Kemudian Anda menemukan halaman tim alat iovisor dengan referensi eBPF tidak resmi yang nyaman.
Instruksi menempati satu kata 64-bit (sekitar dua) dan memiliki bentuk
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Mereka yang menempati dua kata hanya terdiri dari instruksi pertama dengan semua logika dan "trailer" dengan 32 bit lebih dari nilai langsung dan sangat jelas terlihat pada pembongkaran objek.
Opcode sendiri juga memiliki struktur reguler: tiga bit yang lebih rendah adalah kelas operasi: ALU 32-bit, 64-bit ALU, memuat / menyimpan, percabangan bersyarat. Oleh karena itu, sangat mudah untuk menerapkannya pada makro dalam tradisi terbaik QEMU. Saya tidak akan melakukan instruksi terperinci pada basis kode kita tidak sedang meninjau kode Saya lebih baik memberi tahu Anda tentang jebakan.
Masalah pertama saya adalah saya membuat pengalokasi register eBPF yang malas dalam bentuk QEMU- local_temp
, dan mulai tanpa berpikir mentransfer panggilan fungsi ini ke makro. Ternyata seperti dalam meme terkenal: "Kami memasukkan abstraksi ke abstraksi sehingga Anda dapat menghasilkan instruksi saat Anda menghasilkan instruksi." Pasca factum, saya sudah tidak mengerti dengan baik apa yang rusak saat itu, tetapi sesuatu yang aneh tampaknya terjadi dengan urutan instruksi yang dihasilkan. Setelah itu, saya membuat analog dari fungsi tcg_gen_...
untuk mendorong instruksi baru ke tengah daftar, mengambil operan sebagai argumen ke fungsi, dan urutan secara otomatis menjadi sebagaimana mestinya (karena argumen sepenuhnya dihitung tepat sekali sebelum panggilan).
Masalah kedua adalah mencoba untuk mendorong konstanta TCG sebagai operan dari instruksi sewenang-wenang ketika melihat operan langsung di eBPF. Meminta tcg-opc.h yang telah disebutkan, komposisi daftar argumen operasi benar-benar diperbaiki: n
argumen input, m
output dan k
konstan. Ngomong-ngomong, ketika debugging kode seperti itu, akan membantu untuk memberikan QEMU argumen baris perintah -d op,op_opt
atau bahkan -d op,op_opt,out_asm
.
Kemungkinan Argumen $ ./x86_64-linux-user/qemu-x86_64 -d help Log items (comma separated): out_asm show generated host assembly code for each compiled TB in_asm show target assembly code for each compiled TB op show micro ops for each compiled TB op_opt show micro ops after optimization op_ind show micro ops before indirect lowering int show interrupts/exceptions in short format exec show trace before each executed TB (lots of logs) cpu show CPU registers before entering a TB (lots of logs) fpu include FPU registers in the 'cpu' logging mmu log MMU-related activities pcall x86 only: show protected mode far calls/returns/exceptions cpu_reset show CPU state before CPU resets unimp log unimplemented functionality guest_errors log when the guest OS does something invalid (eg accessing a non-existent register) page dump pages at beginning of user mode emulation nochain do not chain compiled TBs so that "exec" and "cpu" show complete traces trace:PATTERN enable trace events Use "-d trace:help" to get a list of trace events.
Nah, jangan ulangi kesalahan saya: instruksi internal disassembler cukup maju, dan jika Anda melihat sesuatu seperti add_i64 loc15,loc15,$554412123213
, maka hal ini setelah tanda dolar bukan pointer. Lebih tepatnya, ini, tentu saja, adalah sebuah pointer, tetapi mungkin digantung dengan bendera dan dalam peran nilai literal operan, dan bukan pointer. Semua ini berlaku, tentu saja, jika Anda tahu bahwa harus ada angka tertentu, seperti $0
atau $ff
, Anda tidak perlu takut sama sekali pada petunjuk. :) Cara movi
- Anda hanya perlu membuat fungsi yang mengembalikan temp
baru, ke mana melalui movi
menempatkan konstan yang diinginkan.
Omong-omong, jika Anda berkomentar #define USE_TCG_OPTIMIZATIONS
di tcg/tcg.c
#define USE_TCG_OPTIMIZATIONS
, maka, tiba-tiba, pengoptimalan akan mati dan akan lebih mudah untuk menganalisis transformasi kode.
Untuk sim, saya akan mengirim pembaca yang tertarik memilih QEMU ke dalam dokumentasi , bahkan yang resmi! Selebihnya, saya akan mendemonstrasikan instrumentasi yang dijanjikan untuk AFL.
Sama dan kelinci
Untuk teks lengkap runtime, saya, sekali lagi, akan mengirim pembaca ke repositori, karena itu (teks) tidak memiliki nilai artistik dan secara jujur โโdikeraskan dari qemu_mode
dari pengiriman AFL, dan secara umum, adalah bagian reguler dari kode C. Tapi di sini adalah bagaimana instrumentasi itu sendiri terlihat :
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
Penting bahwa fungsi kait memiliki argumen sebanyak iargs
untuk operasi QEMU yang sesuai. Dua extern
di header akan ditautkan ke runtime selama proses relokasi. Pada prinsipnya, prev
dapat didefinisikan di sini, tetapi kemudian harus didefinisikan sebagai static
, jika tidak akan jatuh ke bagian UMUM yang tidak saya dukung. Sebenarnya, kami, sebenarnya, hanya menulis ulang kode semu dari dokumentasi, tetapi ini bisa dibaca oleh mesin!
Untuk memeriksa, buat file bug.c
:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char *argv[]) { char buf[16]; int res = read(0, buf, 4); if (buf[0] == 'T' && buf[1] == 'E' && buf[2] == 'S' && buf[3] == 'T') abort(); return res * 0; }
Dan juga - file forksrv
, yang nyaman untuk memberi makan AFL:
Dan jalankan fuzzing:
AFL_SKIP_BIN_CHECK=1 afl-fuzz -i ../input -o ../output -m none -- ./forksrv
American Fuzzy Lop 1234 T234 TE34 TES4 TEST <- crashes, 2200
Sejauh ini, kecepatan tidak begitu panas, tetapi sebagai alasan saya akan mengatakan bahwa di sini (untuk saat ini) fitur penting dari qemu_mode
asli tidak digunakan: mengirim alamat kode yang dapat dieksekusi ke server fork. Tapi tidak ada AFL dalam basis kode QEMU sekarang, dan ada harapan bahwa instrumentasi umum ini suatu hari nanti akan dijejalkan ke hulu.
Proyek GitHub