
Penulis artikel https://github.com/Nalen98
Selamat siang
Topik penelitian saya sebagai bagian dari Summer of Hack 2019 magang musim panas di Digital Security adalah "Mengurai eBPF di Ghidra." Perlu dikembangkan dalam bahasa Sleigh sistem terjemahan bytecode eBPF di PCode Ghidra agar dapat membongkar dan mendekompilasi program-program eBPF. Hasil dari penelitian ini adalah ekstensi yang dikembangkan untuk Ghidra yang menambahkan dukungan untuk prosesor eBPF. Studi ini, seperti halnya peserta pelatihan lainnya, dapat dianggap sebagai "pertama kali", karena sebelumnya tidak mungkin untuk mendekompilasi eBPF dalam alat rekayasa balik lainnya.
Latar belakang
Topik ini bagi saya dalam ironi besar nasib, karena saya tidak terbiasa dengan eBPF sebelumnya, dan Ghidr tidak digunakan sebelumnya, karena ada dogma tertentu bahwa "IDA Pro lebih baik." Ternyata, ini tidak sepenuhnya benar.
Berkenalan dengan Ghidra ternyata sangat cepat, karena pengembangnya membuat dokumentasi yang sangat kompeten dan mudah diakses. Juga, saya harus menguasai bahasa spesifikasi prosesor Sleigh, di mana pengembangan dilakukan. Para pengembang melakukan yang terbaik dan membuat dokumentasi yang sangat rinci untuk alat itu sendiri dan untuk Giring , yang banyak terima kasih kepada mereka.
Di sisi lain barikade itu ada Berkeley Packet Filter yang diperluas. eBPF adalah mesin virtual di kernel Linux yang memungkinkan Anda memuat kode pengguna yang sewenang-wenang yang dapat digunakan untuk melacak proses dan memfilter paket dalam ruang kernel. Arsitekturnya adalah mesin register RISC dengan 11 register 64-bit, penghitung perangkat lunak, dan tumpukan 512-byte. Ada beberapa batasan untuk eBPF:
- siklus dilarang;
- akses ke memori hanya dimungkinkan melalui tumpukan (akan ada cerita terpisah tentang hal itu);
- Fungsi kernel hanya tersedia melalui fungsi wrapper khusus (eBPF-helpers).

Struktur teknologi eBPF. Sumber gambar: http://www.brendangregg.com/ebpf.html .
Pada dasarnya, teknologi ini digunakan untuk tugas-tugas jaringan - debugging, packet filtering, dan sebagainya di tingkat kernel. Dukungan EBPF telah ditambahkan sejak versi 3.15 dari kernel, beberapa laporan dikhususkan untuk teknologi ini pada konferensi tukang ledeng Linux 2019. Tetapi di eBPF, tidak seperti Ghidra, dokumentasinya tidak lengkap dan tidak banyak mengandung. Karena itu, klarifikasi dan informasi yang hilang harus dicari di Internet. Butuh waktu cukup lama untuk menemukan jawabannya, dan yang tersisa hanyalah berharap bahwa teknologi tersebut akan selesai dan dokumentasi normal akan dibuat.
Dokumentasi buruk
Untuk mengembangkan spesifikasi untuk Sleigh, Anda harus terlebih dahulu memahami bagaimana arsitektur prosesor target bekerja. Dan di sini kita beralih ke dokumentasi resmi.
Ini berisi sejumlah kekurangan:
Struktur instruksi eBPF tidak sepenuhnya dijelaskan.
Sebagian besar spesifikasi, seperti Intel x86, biasanya menunjukkan masing-masing bit instruksi, yang menjadi miliknya. Sayangnya, dalam spesifikasi eBPF, perincian ini tersebar di seluruh dokumen atau tidak ada sama sekali, sehingga kami harus mengambil butir yang hilang dari detail implementasi di kernel Linux.
Sebagai contoh, dalam struktur instruksi op:8, dst_reg:4, src_reg:4, off:16, imm:32
tidak ada kata yang mengatakan bahwa offset (off) dan langsung (imm) signed
, dan ini sangat penting, karena ia mempengaruhi untuk bekerja dari instruksi aritmatika ke melompat. Kode sumber untuk kernel Linux membantu.
Tidak ada gambar lengkap dari semua mnemonics arsitektur yang mungkin.
Dalam beberapa dokumentasi, tidak hanya semua instruksi, operan mereka ditunjukkan, tetapi juga semantik mereka dalam C, kasus aplikasi, fitur operan, dan sebagainya. Dokumentasi eBPF berisi kelas instruksi, tetapi ini tidak cukup untuk pengembang. Mari kita pertimbangkan secara lebih detail.
Semua instruksi eBPF adalah 64-bit, kecuali untuk LDDW
(Load double word), ia memiliki ukuran 128 bit, itu terdiri dari dua imm dengan masing-masing 32 bit. Instruksi eBPF memiliki struktur berikut.

penyandian instruksi eBPF
Struktur bidang OPAQUE
tergantung pada kelas instruksi (ALU / JMP, Load / Store).
Sebagai contoh, kelas instruksi ALU
:

Pengodean instruksi ALU
dan kelas JMP
memiliki struktur bidang mereka sendiri:

Pengkodean instruksi cabang
Untuk instruksi Muat / Simpan, strukturnya berbeda:

Memuat / Menyimpan instruksi penyandian
Dokumentasi eBPF tidak resmi membantu menyelesaikan masalah ini .
Tidak ada informasi tentang pembantu panggilan, di mana sebagian besar logika program eBPF untuk kernel Linux dibangun.
Dan ini sangat aneh, karena pembantu adalah hal terpenting dalam program eBPF, mereka hanya melakukan tugas-tugas yang menjadi fokus teknologinya.

Interoperabilitas EBPF dengan Fungsi Nuklir
Program menarik fungsi-fungsi ini dari kernel, dan mereka hanya bekerja dengan proses, memanipulasi paket jaringan, bekerja dengan peta eBPF, soket akses, berinteraksi dengan ruang pengguna. Terlepas dari kenyataan bahwa fungsinya masih bersifat nuklir, dalam dokumentasi resmi ada baiknya menulis secara lebih rinci tentang mereka. Rincian lengkap ditemukan di sumber Linux.
- Tidak sepatah kata pun tentang panggilan ekor.

Panggilan ekor EBPF. Sumber gambar: https://cilium.readthedocs.io/en/latest/bpf/#tail-calls .
Panggilan ekor adalah mekanisme yang memungkinkan satu program eBPF untuk memanggil yang lain tanpa kembali ke yang sebelumnya, yaitu, melompat di antara berbagai program eBPF. Mereka tidak diimplementasikan dalam ekstensi yang dikembangkan, informasi rinci dapat ditemukan dalam dokumentasi Cilium .
Dokumentasi yang buruk dan sejumlah fitur arsitektur eBPF adalah "serpihan" utama dalam pengembangan, karena mereka menciptakan masalah lain. Untungnya, kebanyakan dari mereka berhasil diselesaikan.
Tentang lingkungan pengembangan

Tidak semua pengembang tahu bahwa untuk membuat dan mengedit kode Sleigh dan umumnya semua file ekstensi / plugin untuk Ghidra ada alat yang lebih nyaman - Eclipse IDE dengan dukungan untuk GhidraDev dan plugin GhidraSleighEditor . Saat membuat ekstensi, itu akan segera dibingkai sebagai konsep kerja, ada sorot yang lebih nyaman untuk kode Sleigh, serta pemeriksa kesalahan utama dalam sintaks bahasa.
Di Eclipse, Anda dapat menjalankan Ghidra (sudah dengan ekstensi dihidupkan), debug, yang sangat nyaman. Tapi mungkin kesempatan paling keren adalah untuk mendukung mode "Ghidra Headless", Anda tidak perlu me-restart Ghidr dari GUI 100500 kali untuk menemukan kesalahan dalam kode, semua proses dilakukan di latar belakang.
Notepad bisa ditutup! Dan Anda dapat mengunduh Eclipse dari situs resmi . Untuk menginstal plugin, di Ecplise, pilih Help → Install New Software ... , klik Add dan pilih arsip zip plugin.
Pengembangan ekstensi
Untuk ekstensi, file spesifikasi prosesor dikembangkan, sebuah loader yang mewarisi dari loader ELF utama dan memperluas kemampuannya dalam hal mengenali program-program eBPF, sebuah prosesor relokasi untuk mengimplementasikan Peta-peta eBPF dalam disassembler dan decompiler Ghidra , serta sebuah alat analisis untuk menentukan tanda tangan penolong eBPF.


Ekstensi file sebagai proyek di Eclipse IDE
Sekarang tentang file utama:
.cspec
- ini menunjukkan tipe data mana yang digunakan, berapa banyak memori yang dialokasikan padanya di eBPF, ukuran stack diatur, label "stackpointer" diatur untuk mendaftarkan R10
, dan perjanjian panggilan ditandatangani. Perjanjian (seperti yang lain) diimplementasikan sesuai dengan dokumentasi:
Oleh karena itu, konvensi pemanggilan eBPF didefinisikan sebagai:
- R0 - mengembalikan nilai dari fungsi in-kernel, dan nilai keluar untuk program eBPF
- R1 - R5 - argumen dari program eBPF ke fungsi in-kernel
- R6 - R9 - tinggalkan register yang tersimpan yang akan dipertahankan fungsi in-kernel
- R10 - frame pointer read-only untuk mengakses stack
eBPF.cspec <?xml version="1.0" encoding="UTF-8"?> <compiler_spec> <data_organization> <absolute_max_alignment value="0" /> <machine_alignment value="2" /> <default_alignment value="1" /> <default_pointer_alignment value="4" /> <pointer_size value="4" /> <wchar_size value="4" /> <short_size value="2" /> <integer_size value="4" /> <long_size value="4" /> <long_long_size value="8" /> <float_size value="4" /> <double_size value="8" /> <long_double_size value="8" /> <size_alignment_map> <entry size="1" alignment="1" /> <entry size="2" alignment="2" /> <entry size="4" alignment="4" /> <entry size="8" alignment="8" /> </size_alignment_map> </data_organization> <global> <range space="ram"/> <range space="syscall"/> </global> <stackpointer register="R10" space="ram"/> <default_proto> <prototype name="__fastcall" extrapop="0" stackshift="0"> <input> <pentry minsize="1" maxsize="8"> <register name="R1"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R2"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R3"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R4"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R5"/> </pentry> </input> <output killedbycall="true"> <pentry minsize="1" maxsize="8"> <register name="R0"/> </pentry> </output> <unaffected> <varnode space="ram" offset="8" size="8"/> <register name="R6"/> <register name="R7"/> <register name="R8"/> <register name="R9"/> <register name="R10"/> </unaffected> </prototype> </default_proto> </compiler_spec>
Sebelum melanjutkan mendeskripsikan file pengembangan, saya akan membahas satu baris kecil file .cspec
.
<stackpointer register="R10" space="ram"/>
Ini adalah sumber utama kejahatan ketika mendekompilasi eBPF di Ghidra, dan memulai perjalanan yang mengasyikkan ke tumpukan eBPF, yang memiliki sejumlah momen tidak menyenangkan, dan yang paling menyakitkan bagi pengembangan.
Yang kita butuhkan adalah ... Stack
Mari kita lihat dokumentasi kernel resmi :
T: Dapatkah program BPF mengakses penunjuk instruksi atau mengembalikan alamat?
A: TIDAK.
T: Dapatkah program BPF mengakses stack pointer?
A: TIDAK. Hanya penunjuk bingkai (register R10) yang dapat diakses. Dari sudut pandang kompiler, perlu memiliki penunjuk tumpukan. Sebagai contoh, LLVM mendefinisikan register R11 sebagai stack pointer di backend BPF, tetapi ia memastikan bahwa kode yang dihasilkan tidak pernah menggunakannya.
Prosesor tidak memiliki penunjuk instruksi (IP) atau penunjuk tumpukan (SP), dan yang terakhir sangat penting untuk Ghidra, dan kualitas dekompilasi tergantung padanya. Dalam file cspec
, Anda perlu menentukan register mana yang merupakan stackpointer (seperti yang ditunjukkan di atas). R10
adalah satu-satunya register eBPF yang memungkinkan mengakses tumpukan program, ini adalah framepointer, statis dan selalu nol. Menggantung label "stackpointer" pada R10
di file cspec
dasarnya salah, tetapi tidak ada opsi lain, karena Ghidra tidak akan bekerja dengan stack program. Karenanya, SP asli tidak ada, dan tidak ada yang menggantikannya dalam arsitektur eBPF.
Beberapa masalah muncul dari ini:
Bidang "Stack Depth" di Ghidra akan dijamin nol, karena kita hanya harus menunjuk R10
penumpuk dalam kondisi arsitektur ini, dan pada dasarnya selalu nol, yang diperdebatkan sebelumnya. "Stack Depth" akan mencerminkan register dengan label "stackpointer".
Dan Anda harus tahan dengan itu, ini adalah fitur arsitektur.
Petunjuk yang beroperasi pada R10
(yaitu, yang menangani tumpukan) sering tidak diurai. Ghidra umumnya tidak mendekompilasi apa yang dianggapnya kode mati (yaitu, snippet yang tidak pernah dieksekusi). Dan karena R10
dapat diubah, banyak instruksi store / load dikenali oleh Ghidr sebagai deadcode dan menghilang dari dekompiler.
Untungnya, masalah ini diselesaikan dengan menulis penganalisis khusus, serta mendeklarasikan ruang alamat tambahan dengan pembantu eBPF dalam file pspec
, yang diminta oleh salah satu pengembang Ghidra dalam proyek Issue .
Pengembangan ekstensi (lanjutan)
.ldefs
menjelaskan fitur prosesor, menentukan file spesifikasi.
eBPF.ldefs <?xml version="1.0" encoding="UTF-8"?> <language_definitions> <language processor="eBPF" endian="little" size="64" variant="default" version="1.0" slafile="eBPF.sla" processorspec="eBPF.pspec" id="eBPF:LE:64:default"> <description>eBPF processor 64-bit little-endian</description> <compiler name="default" spec="eBPF.cspec" id="default"/> <external_name tool="DWARF.register.mapping.file" name="eBPF.dwarf"/> </language> </language_definitions>
File .opinion
loader ke prosesor.
Opini eBPF <opinions> <constraint loader="Executable and Linking Format (ELF)" compilerSpecID="default"> <constraint primary="247" processor="eBPF" endian="little" size="64" /> </constraint> </opinions>
Penghitung program dideklarasikan dalam .pspec, tetapi dengan eBPF itu adalah implisit dan tidak digunakan dalam spesifikasi dengan cara apa pun, oleh karena itu hanya untuk tujuan pro forma. Omong-omong, PC
eBPF adalah aritmatika, bukan alamat (ini menunjukkan instruksi, bukan byte spesifik dari program), ingatlah ini ketika melompat.
File ini juga berisi ruang alamat tambahan untuk pembantu eBPF, di sini mereka dinyatakan sebagai karakter.
eBPF.pspec <?xml version="1.0" encoding="UTF-8"?> <processor_spec> <programcounter register="PC"/> <default_symbols> <symbol name="bpf_unspec" address="syscall:0x0"/> <symbol name="bpf_map_lookup_elem" address="syscall:0x1"/> <symbol name="bpf_map_update_elem" address="syscall:0x2"/> <symbol name="bpf_map_delete_elem" address="syscall:0x3"/> <symbol name="bpf_probe_read" address="syscall:0x4"/> <symbol name="bpf_ktime_get_ns" address="syscall:0x5"/> <symbol name="bpf_trace_printk" address="syscall:0x6"/> <symbol name="bpf_get_prandom_u32" address="syscall:0x7"/> <symbol name="bpf_get_smp_processor_id" address="syscall:0x8"/> <symbol name="bpf_skb_store_bytes" address="syscall:0x9"/> <symbol name="bpf_l3_csum_replace" address="syscall:0xa"/> <symbol name="bpf_l4_csum_replace" address="syscall:0xb"/> <symbol name="bpf_tail_call" address="syscall:0xc"/> <symbol name="bpf_clone_redirect" address="syscall:0xd"/> <symbol name="bpf_get_current_pid_tgid" address="syscall:0xe"/> <symbol name="bpf_get_current_uid_gid" address="syscall:0xf"/> <symbol name="bpf_get_current_comm" address="syscall:0x10"/> <symbol name="bpf_get_cgroup_classid" address="syscall:0x11"/> <symbol name="bpf_skb_vlan_push" address="syscall:0x12"/> <symbol name="bpf_skb_vlan_pop" address="syscall:0x13"/> <symbol name="bpf_skb_get_tunnel_key" address="syscall:0x14"/> <symbol name="bpf_skb_set_tunnel_key" address="syscall:0x15"/> <symbol name="bpf_perf_event_read" address="syscall:0x16"/> <symbol name="bpf_redirect" address="syscall:0x17"/> <symbol name="bpf_get_route_realm" address="syscall:0x18"/> <symbol name="bpf_perf_event_output" address="syscall:0x19"/> <symbol name="bpf_skb_load_bytes" address="syscall:0x1a"/> <symbol name="bpf_get_stackid" address="syscall:0x1b"/> <symbol name="bpf_csum_diff" address="syscall:0x1c"/> <symbol name="bpf_skb_get_tunnel_opt" address="syscall:0x1d"/> <symbol name="bpf_skb_set_tunnel_opt" address="syscall:0x1e"/> <symbol name="bpf_skb_change_proto" address="syscall:0x1f"/> <symbol name="bpf_skb_change_type" address="syscall:0x20"/> <symbol name="bpf_skb_under_cgroup" address="syscall:0x21"/> <symbol name="bpf_get_hash_recalc" address="syscall:0x22"/> <symbol name="bpf_get_current_task" address="syscall:0x23"/> <symbol name="bpf_probe_write_user" address="syscall:0x24"/> </default_symbols> <default_memory_blocks> <memory_block name="eBPFHelper_functions" start_address="syscall:0" length="0x200" initialized="true"/> </default_memory_blocks> </processor_spec>
.sinc
adalah file ekstensi paling bervolume, semua register, struktur instruksi eBPF, token, mnemonik dan semantik instruksi di Sleigh didefinisikan di sini.
Potongan kecil EBPF define space ram type=ram_space size=8 default; define space register type=register_space size=4; define space syscall type=ram_space size=2; define register offset=0 size=8 [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 PC ]; define token instr(64) imm=(32, 63) signed off=(16, 31) signed src=(12, 15) dst=(8, 11) op_alu_jmp_opcode=(4, 7) op_alu_jmp_source=(3, 3) op_ld_st_mode=(5, 7) op_ld_st_size=(3, 4) op_insn_class=(0, 2) ; #We'll need this token to operate with LDDW instruction, which has 64 bit imm value define token immtoken(64) imm2=(32, 63) ; #To operate with registers attach variables [ src dst ] [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 _ _ _ _ _ ]; … :ADD dst, src is src & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=1 & op_insn_class=0x7 { dst=dst + src; } :ADD dst, imm is imm & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=0 & op_insn_class=0x7 { dst=dst + imm; } …
Loader eBPF memperluas kemampuan dasar loader ELF sehingga dapat mengenali bahwa program yang Anda unduh ke Ghidra memiliki prosesor eBPF. Baginya, konstanta BPF dialokasikan dalam ElfConstants
Ghidra, dan loader menentukan prosesor eBPF darinya.
eBPF_ElfExtension.java package ghidra.app.util.bin.format.elf.extend; import ghidra.app.util.bin.format.elf.*; import ghidra.program.model.lang.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; public class eBPF_ElfExtension extends ElfExtension { @Override public boolean canHandle(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF && elf.is64Bit(); } @Override public boolean canHandle(ElfLoadHelper elfLoadHelper) { Language language = elfLoadHelper.getProgram().getLanguage(); return canHandle(elfLoadHelper.getElfHeader()) && "eBPF".equals(language.getProcessor().toString()) && language.getLanguageDescription().getSize() == 64; } @Override public String getDataTypeSuffix() { return "eBPF"; } @Override public void processGotPlt(ElfLoadHelper elfLoadHelper, TaskMonitor monitor) throws CancelledException { if (!canHandle(elfLoadHelper)) { return; } super.processGotPlt(elfLoadHelper, monitor); } }
Handler handler diperlukan untuk mengimplementasikan peta eBPF di disassembler dan decompiler. Interaksi dengan mereka dilakukan melalui sejumlah pembantu, fungsinya menggunakan deskriptor file untuk menunjukkan peta. Berdasarkan tabel relokasi, dapat dilihat bahwa loader menambal instruksi LDDW, yang menghasilkan Rn
untuk pembantu ini (misalnya, bpf_map_lookup_elem(…)
).
Oleh karena itu, pawang mem-parsing tabel relokasi program, menemukan alamat relokasi (instruksi), dan juga mengumpulkan informasi string tentang nama peta. Selanjutnya, merujuk pada tabel simbol, itu menghitung alamat sebenarnya dari peta-peta ini dan menambal instruksi.
eBPF_ElfRelokasiHandler.java public class eBPF_ElfRelocationHandler extends ElfRelocationHandler { @Override public boolean canRelocate(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF; } @Override public void relocate(ElfRelocationContext elfRelocationContext, ElfRelocation relocation, Address relocationAddress) throws MemoryAccessException, NotFoundException { ElfHeader elf = elfRelocationContext.getElfHeader(); if (elf.e_machine() != ElfConstants.EM_BPF) { return; } Program program = elfRelocationContext.getProgram(); Memory memory = program.getMemory(); int type = relocation.getType(); int symbolIndex = relocation.getSymbolIndex(); long value; boolean appliedSymbol = true;

Hasil pembongkaran dan dekompilasi eBPF
Dan pada akhirnya, kita mendapatkan disassembler dan decompiler eBPF! Gunakan untuk kesehatan!
Ekstensi pada GitHub: eBPF untuk Ghidra .
Rilis di sini: di sini .
PS
Banyak terima kasih kepada Digital Security untuk magang yang menarik, terutama kepada mentor dari departemen penelitian (Alexander dan Nikolai). Aku tunduk padamu!