Saya tidak berpikir ada cara yang dapat diandalkan untuk melakukan ini. Format kode mesin sangat rumit, lebih rumit daripada file rakitan. Sangat tidak mungkin untuk mengambil biner yang dikompilasi (katakanlah, dalam format ELF) dan menghasilkan program rakitan sumber yang akan dikompilasi ke biner yang sama (atau cukup mirip). Untuk memahami perbedaannya, bandingkan keluaran kompilasi GCC langsung ke assembler (gcc -S
) versus output dari objdump pada file yang dapat dieksekusi (objdump -D
).
Ada dua komplikasi utama yang dapat saya pikirkan. Pertama, kode mesin itu sendiri bukanlah korespondensi 1-ke-1 dengan kode rakitan, karena hal-hal seperti offset penunjuk.
Misalnya, pertimbangkan kode C untuk Halo dunia:
int main()
{
printf("Hello, world!\n");
return 0;
}
Ini dikompilasi ke kode rakitan x86:
.LC0:
.string "hello"
.text
<snip>
movl $.LC0, %eax
movl %eax, (%esp)
call printf
Di mana .LCO adalah konstanta bernama, dan printf adalah simbol dalam tabel simbol perpustakaan bersama. Bandingkan dengan output dari objdump:
80483cd: b8 b0 84 04 08 mov $0x80484b0,%eax
80483d2: 89 04 24 mov %eax,(%esp)
80483d5: e8 1a ff ff ff call 80482f4 <[email protected]>
Pertama, konstanta .LC0 sekarang hanyalah offset acak dalam memori di suatu tempat -- akan sulit untuk membuat file sumber perakitan yang berisi konstanta ini di tempat yang tepat, karena assembler dan linker bebas memilih lokasi untuk konstanta ini.
Kedua, saya tidak sepenuhnya yakin tentang ini (dan itu tergantung pada hal-hal seperti kode independen posisi), tetapi saya yakin referensi ke printf sebenarnya tidak dikodekan pada alamat penunjuk dalam kode itu di sana sama sekali, tetapi header ELF berisi a tabel pencarian yang secara dinamis menggantikan alamatnya saat runtime. Oleh karena itu, kode yang dibongkar tidak sesuai dengan kode rakitan sumber.
Singkatnya, rakitan sumber memiliki simbol sementara kode mesin yang dikompilasi memiliki alamat yang sulit untuk dibalik.
Komplikasi utama kedua adalah bahwa file sumber perakitan tidak dapat berisi semua informasi yang ada di header file ELF asli, seperti perpustakaan mana yang akan ditautkan secara dinamis, dan metadata lain yang ditempatkan di sana oleh kompiler asli. Akan sulit untuk merekonstruksi ini.
Seperti yang saya katakan, ada kemungkinan alat khusus dapat memanipulasi semua informasi ini, tetapi tidak mungkin seseorang dapat dengan mudah menghasilkan kode rakitan yang dapat dipasang kembali ke executable.
Jika Anda tertarik untuk memodifikasi hanya sebagian kecil dari file yang dapat dieksekusi, saya merekomendasikan pendekatan yang jauh lebih halus daripada mengkompilasi ulang seluruh aplikasi. Gunakan objdump untuk mendapatkan kode rakitan untuk fungsi yang Anda minati. Ubah menjadi "sintaks rakitan sumber" dengan tangan (dan di sini, saya berharap ada alat yang benar-benar menghasilkan pembongkaran dalam sintaks yang sama dengan input) , dan modifikasi sesuai keinginan. Setelah selesai, kompilasi ulang hanya fungsi-fungsi itu dan gunakan objdump untuk mencari tahu kode mesin untuk program yang Anda modifikasi. Kemudian, gunakan editor hex untuk menempelkan kode mesin baru secara manual di atas bagian yang sesuai dari program asli, berhati-hatilah agar kode baru Anda memiliki jumlah byte yang persis sama dengan kode lama (atau semua offset akan salah ). Jika kode baru lebih pendek, Anda dapat menambahkannya menggunakan instruksi NOP. Jika lebih panjang, Anda mungkin dalam masalah, dan mungkin harus membuat fungsi baru dan memanggilnya sebagai gantinya.
Saya melakukan ini dengan hexdump
dan editor teks. Anda harus benar-benar nyaman dengan kode mesin dan format file yang menyimpannya, dan fleksibel dengan apa yang dianggap sebagai "membongkar, memodifikasi, lalu memasang kembali".
Jika Anda dapat melakukan "perubahan tempat" saja (menulis ulang byte, tetapi tidak menambah atau menghapus byte), itu akan mudah (secara relatif).
Kamu benar-benar tidak ingin mengganti instruksi yang ada, karena Anda harus secara manual menyesuaikan setiap offset relatif yang terpengaruh dalam kode mesin, untuk lompatan/cabang/beban/penyimpanan relatif ke penghitung program, keduanya dalam hardcoded langsung nilai dan yang dihitung melalui register .
Anda harus selalu bisa lolos dengan tidak menghapus byte. Menambahkan byte mungkin diperlukan untuk modifikasi yang lebih kompleks, dan menjadi jauh lebih sulit.
Langkah 0 (persiapan)
Setelah Anda sebenarnya membongkar file dengan benar dengan objdump -D
atau apa pun yang biasanya Anda gunakan terlebih dahulu untuk benar-benar memahaminya dan menemukan tempat yang perlu Anda ubah, Anda perlu mencatat hal-hal berikut untuk membantu Anda menemukan byte yang benar untuk diubah:
- "Alamat" (offset dari awal file) byte yang perlu Anda ubah.
- Nilai mentah dari byte tersebut seperti saat ini (
--show-raw-insn
opsi untukobjdump
sangat membantu di sini).
Anda juga harus memeriksa apakah hexdump -R
bekerja pada sistem Anda. Jika tidak, maka untuk langkah selanjutnya, gunakan xxd
perintah atau sejenisnya, bukan hexdump
dalam semua langkah di bawah ini (lihat dokumentasi untuk alat apa pun yang Anda gunakan, saya hanya menjelaskan hexdump
untuk saat ini dalam jawaban ini karena itulah yang saya kenal).
Langkah 1
Buang representasi heksadesimal mentah dari file biner dengan hexdump -Cv
.
Langkah 2
Buka hexdump
ed dan temukan byte di alamat yang ingin Anda ubah.
Kursus kilat cepat di hexdump -Cv
keluaran:
- Kolom paling kiri adalah alamat byte (relatif terhadap awal file biner itu sendiri, seperti
objdump
menyediakan). - Kolom paling kanan (dikelilingi oleh
|
karakter) hanyalah representasi byte yang "dapat dibaca manusia" - karakter ASCII yang cocok dengan setiap byte ditulis di sana, dengan.
mendukung semua byte yang tidak dipetakan ke karakter ASCII yang dapat dicetak. - Hal-hal penting ada di antaranya - setiap byte sebagai dua digit heksadesimal yang dipisahkan oleh spasi, 16 byte per baris.
Hati-hati:Tidak seperti objdump -D
, yang memberi Anda alamat setiap instruksi dan menampilkan hex mentah dari instruksi tersebut berdasarkan bagaimana itu didokumentasikan sebagai dikodekan, hexdump -Cv
membuang setiap byte persis sesuai urutan yang muncul di file. Ini bisa sedikit membingungkan karena pertama pada mesin di mana byte instruksi berada dalam urutan berlawanan karena perbedaan endianness, yang juga dapat membingungkan ketika Anda mengharapkan byte tertentu sebagai alamat tertentu.
Langkah 3
Ubah byte yang perlu diubah - Anda jelas perlu mengetahui pengkodean instruksi mesin mentah (bukan mnemonik rakitan) dan menulis secara manual dalam byte yang benar.
Catatan:Anda tidak perlu mengubah representasi yang dapat dibaca manusia di kolom paling kanan. hexdump
akan mengabaikannya saat Anda "membatalkannya".
Langkah 4
"Un-dump" file hexdump yang dimodifikasi menggunakan hexdump -R
.
Langkah 5 (pemeriksaan kewarasan)
objdump
unhexdump
Anda yang baru ed dan verifikasi bahwa pembongkaran yang Anda ubah terlihat benar. diff
terhadap objdump
dari aslinya.
Serius, jangan lewatkan langkah ini. Saya lebih sering membuat kesalahan saat mengedit kode mesin secara manual dan inilah cara saya mengetahui sebagian besar kesalahan tersebut.
Contoh
Ini adalah contoh kerja nyata dari saat saya memodifikasi biner ARMv8 (little endian) baru-baru ini. (Saya tahu, pertanyaannya diberi tag x86
, tetapi saya tidak memiliki contoh x86, dan prinsip dasarnya sama, hanya instruksinya yang berbeda.)
Dalam situasi saya, saya perlu menonaktifkan pemeriksaan pegangan tangan khusus "Anda seharusnya tidak melakukan ini":dalam contoh biner saya, di objdump --show-raw-insn -d
keluaran baris yang saya pedulikan terlihat seperti ini (satu instruksi sebelum dan sesudah diberikan untuk konteks):
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <[email protected]>
f48: f94013f7 ldr x23, [sp, #32]
Seperti yang Anda lihat, program kami "membantu" keluar dengan melompat ke error
fungsi (yang menghentikan program). Tidak dapat diterima. Jadi kita akan mengubah instruksi itu menjadi tanpa operasi. Jadi kita sedang mencari byte 0x97fffeeb
di alamat/file-offset 0xf44
.
Ini adalah hexdump -Cv
baris yang berisi offset itu.
00000f40 e3 03 15 aa eb fe ff 97 f7 13 40 f9 e8 02 40 39 |[email protected]@9|
Perhatikan bagaimana byte yang relevan benar-benar dibalik (pengodean little endian dalam arsitektur berlaku untuk instruksi mesin seperti yang lainnya) dan bagaimana hal ini secara tidak sengaja berhubungan dengan byte apa pada offset byte apa:
00000f40 -- -- -- -- eb fe ff 97 -- -- -- -- -- -- -- -- |[email protected]@9|
^
This is offset f44, holding the least significant byte
So the *instruction as a whole* is at the expected offset,
just the bytes are flipped around. Of course, whether the
order matches or not will vary with the architecture.
Bagaimanapun, saya tahu dari melihat pembongkaran lain bahwa 0xd503201f
dibongkar menjadi nop
jadi sepertinya kandidat yang bagus untuk instruksi tanpa operasi saya. Saya memodifikasi baris di hexdump
ed sesuai:
00000f40 e3 03 15 aa 1f 20 03 d5 f7 13 40 f9 e8 02 40 39 |[email protected]@9|
Dikonversi kembali menjadi biner dengan hexdump -R
, membongkar biner baru dengan objdump --show-raw-insn -d
dan memverifikasi bahwa perubahannya benar:
f40: aa1503e3 mov x3, x21
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Kemudian saya menjalankan biner dan mendapatkan perilaku yang saya inginkan - pemeriksaan yang relevan tidak lagi menyebabkan program dibatalkan.
Modifikasi kode mesin berhasil.
!!! Peringatan !!!
Atau apakah saya berhasil? Apakah Anda melihat apa yang saya lewatkan dalam contoh ini?
Saya yakin Anda melakukannya - karena Anda bertanya tentang cara memodifikasi kode mesin suatu program secara manual, Anda mungkin tahu apa yang Anda lakukan. Tetapi untuk kepentingan setiap pembaca yang mungkin membaca untuk belajar, saya akan menjelaskan:
Saya hanya mengubah terakhir instruksi di cabang error-case! Lompatan ke fungsi yang keluar dari program. Tapi seperti yang Anda lihat, daftarkan x3
sedang dimodifikasi oleh mov
tepat di atas! Faktanya, total empat (4) register dimodifikasi sebagai bagian dari pembukaan untuk memanggil error
, dan satu register adalah. Inilah kode mesin lengkap untuk cabang tersebut, mulai dari lompatan bersyarat pada if
blokir dan akhiri tempat lompatan jika if
bersyarat tidak diambil:
f2c: 350000e8 cbnz w8, f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <[email protected]>
f48: f94013f7 ldr x23, [sp, #32]
Semua kode setelah cabang dihasilkan oleh kompiler dengan asumsi bahwa status program seperti sebelum lompatan bersyarat ! Namun dengan melakukan lompatan terakhir ke error
kode fungsi tanpa operasi, saya membuat jalur kode tempat kami mencapai kode itu dengan status program yang tidak konsisten/salah !
Dalam kasus saya, ini sebenarnya sepertinya tidak menimbulkan masalah. Jadi saya beruntung. Sangat beruntung:hanya setelah saya menjalankan biner saya yang dimodifikasi (yang, kebetulan, adalah biner kritis keamanan :itu memiliki kemampuan untuk setuid
, setgid
, dan ubah konteks SELinux !) apakah saya menyadari bahwa saya lupa untuk benar-benar melacak jalur kode apakah perubahan register tersebut memengaruhi jalur kode yang muncul kemudian!
Itu bisa menjadi bencana - salah satu dari register itu mungkin telah digunakan dalam kode selanjutnya dengan asumsi bahwa itu berisi nilai sebelumnya yang sekarang ditimpa! Dan saya adalah tipe orang yang dikenal orang karena pemikiran cermat yang cermat tentang kode dan sebagai orang yang rajin dan gigih karena selalu memperhatikan keamanan komputer.
Bagaimana jika saya memanggil fungsi di mana argumen tumpah dari register ke stack (seperti yang sangat umum terjadi, misalnya, x86)? Bagaimana jika sebenarnya ada beberapa instruksi bersyarat dalam set instruksi yang mendahului lompatan bersyarat (seperti yang umum terjadi, misalnya, versi ARM yang lebih lama)? Saya akan berada dalam keadaan yang bahkan lebih tidak konsisten secara sembrono setelah melakukan perubahan yang tampak paling sederhana itu!
Jadi ini pengingat peringatan saya: Mengutak-atik binari secara manual benar-benar melucuti setiap keamanan antara Anda dan apa yang diizinkan oleh mesin dan sistem operasi. Secara harfiah semua kemajuan yang telah kami buat dalam alat kami untuk secara otomatis menangkap kesalahan program kami, hilang .
Jadi bagaimana kita memperbaikinya dengan lebih baik? Baca terus.
Menghapus Kode
Untuk efektif /secara logis "hapus" lebih dari satu instruksi, Anda dapat mengganti instruksi pertama yang ingin Anda "hapus" dengan lompatan tanpa syarat ke instruksi pertama di akhir instruksi "dihapus". Untuk biner ARMv8 ini, terlihat seperti ini:
f2c: 14000007 b f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <[email protected]>
f48: f94013f7 ldr x23, [sp, #32]
Pada dasarnya, Anda "membunuh" kode (mengubahnya menjadi "kode mati"). Sidenote:Anda dapat melakukan sesuatu yang mirip dengan string literal yang disematkan dalam biner:selama Anda ingin menggantinya dengan string yang lebih kecil, Anda hampir selalu dapat lolos dengan menimpa string (termasuk penghentian null byte jika itu adalah "C- string") dan jika perlu, timpa ukuran hard-code dari string dalam kode mesin yang menggunakannya.
Anda juga dapat mengganti semua instruksi yang tidak diinginkan dengan no-ops. Dengan kata lain, kita dapat mengubah kode yang tidak diinginkan menjadi apa yang disebut "no-op sled":
f2c: d503201f nop
f30: d503201f nop
f34: d503201f nop
f38: d503201f nop
f3c: d503201f nop
f40: d503201f nop
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Saya berharap itu hanya membuang-buang siklus CPU relatif untuk melompati mereka, tapi itu sederhana sehingga lebih aman dari kesalahan , karena Anda tidak perlu memikirkan cara menyandikan instruksi lompatan secara manual termasuk mencari tahu offset/alamat yang akan digunakan di dalamnya - Anda tidak perlu berpikir terlalu banyak untuk kereta luncur tanpa operasi.
Agar jelas, kesalahan itu mudah:Saya mengacaukan dua (2) kali ketika secara manual menyandikan instruksi cabang tanpa syarat itu. Dan itu tidak selalu salah kami:pertama kali karena dokumentasi yang saya miliki sudah usang/salah dan mengatakan satu bit diabaikan dalam pengodean, padahal sebenarnya tidak, jadi saya menyetelnya ke nol pada percobaan pertama saya.
Menambahkan Kode
Anda bisa secara teoritis gunakan teknik ini untuk menambahkan instruksi mesin juga, tetapi ini lebih kompleks, dan saya belum pernah melakukannya, jadi saya tidak memiliki contoh yang berfungsi saat ini.
Dari perspektif kode mesin, ini agak mudah:pilih satu instruksi di tempat yang ingin Anda tambahkan kode, dan ubah menjadi instruksi lompat ke kode baru yang perlu Anda tambahkan (jangan lupa untuk menambahkan instruksi yang Anda inginkan). diganti menjadi kode baru, kecuali jika Anda tidak memerlukannya untuk logika tambahan Anda, dan untuk melompat kembali ke instruksi yang ingin Anda kembalikan di akhir penambahan). Pada dasarnya, Anda "menyambungkan" kode baru.
Namun Anda harus menemukan tempat untuk benar-benar memasukkan kode baru tersebut, dan ini adalah bagian yang sulit.
Jika Anda benar-benar beruntung, Anda bisa menambahkan kode mesin baru di akhir file, dan itu akan "berfungsi":kode baru akan dimuat bersama dengan yang lainnya ke dalam instruksi mesin yang diharapkan sama, ke ruang alamat Anda ruang yang jatuh ke halaman memori yang ditandai dengan benar dapat dieksekusi.
Dalam pengalaman saya hexdump -R
mengabaikan tidak hanya kolom paling kanan tetapi juga kolom paling kiri - jadi Anda benar-benar dapat memasukkan nol alamat untuk semua baris yang ditambahkan secara manual dan itu akan berhasil.
Jika Anda kurang beruntung, setelah menambahkan kode, Anda harus benar-benar menyesuaikan beberapa nilai header dalam file yang sama:jika loader untuk sistem operasi Anda mengharapkan biner berisi metadata yang menjelaskan ukuran bagian yang dapat dieksekusi (untuk alasan historis sering disebut bagian "teks") Anda harus menemukan dan menyesuaikannya. Di masa lalu, biner hanyalah kode mesin mentah - saat ini kode mesin dibungkus dengan banyak metadata (misalnya ELF di Linux dan beberapa lainnya).
Jika Anda masih sedikit beruntung, Anda mungkin memiliki beberapa titik "mati" di file yang dimuat dengan benar sebagai bagian dari biner pada offset relatif yang sama dengan kode lainnya yang sudah ada di file (dan itu titik mati dapat sesuai dengan kode Anda dan diselaraskan dengan benar jika CPU Anda memerlukan penyelarasan kata untuk instruksi CPU). Kemudian Anda dapat menimpanya.
Jika Anda benar-benar tidak beruntung, Anda tidak dapat menambahkan kode begitu saja dan tidak ada ruang kosong yang dapat Anda isi dengan kode mesin Anda. Pada saat itu, Anda pada dasarnya harus sangat akrab dengan format yang dapat dieksekusi dan berharap bahwa Anda dapat menemukan sesuatu dalam batasan tersebut yang secara manusiawi dapat dilakukan secara manual dalam waktu yang wajar dan dengan peluang yang wajar untuk tidak mengacaukannya. .
@mgiuca telah menjawab jawaban ini dengan benar dari sudut pandang teknis. Nyatanya, membongkar program yang dapat dijalankan menjadi sumber rakitan yang mudah dikompilasi ulang bukanlah tugas yang mudah.
Untuk menambah sedikit diskusi, ada beberapa teknik/alat yang mungkin menarik untuk dijelajahi, meskipun secara teknis rumit.
- Instrumentasi Statis/Dinamis . Teknik ini memerlukan analisis format yang dapat dieksekusi, menyisipkan/menghapus/mengganti instruksi perakitan khusus untuk tujuan tertentu, memperbaiki semua referensi ke variabel/fungsi dalam yang dapat dieksekusi, dan mengeluarkan yang dapat dieksekusi baru yang dimodifikasi. Beberapa alat yang saya ketahui adalah:PIN, Hijacker, PEBIL, DynamoRIO. Pertimbangkan bahwa mengonfigurasi alat tersebut untuk tujuan yang berbeda dari yang dirancang untuknya dapat menjadi rumit, dan membutuhkan pemahaman tentang format yang dapat dijalankan dan set instruksi.
- Dekompilasi penuh yang dapat dieksekusi . Teknik ini mencoba merekonstruksi sumber perakitan lengkap dari yang dapat dieksekusi. Anda mungkin ingin melihat Pembongkar Online, yang mencoba melakukan pekerjaan itu. Bagaimanapun Anda kehilangan informasi tentang berbagai modul sumber dan kemungkinan nama fungsi/variabel.
- Dekompilasi yang dapat ditargetkan ulang . Teknik ini mencoba mengekstrak lebih banyak informasi dari yang dapat dieksekusi, dengan melihat sidik jari penyusun (yaitu, pola kode yang dihasilkan oleh kompiler yang dikenal) dan hal-hal deterministik lainnya. Tujuan utamanya adalah merekonstruksi kode sumber tingkat tinggi, seperti sumber C, dari yang dapat dieksekusi. Ini terkadang dapat memperoleh kembali informasi tentang fungsi/nama variabel. Pertimbangkan bahwa kompilasi sumber dengan
-g
sering menawarkan hasil yang lebih baik. Anda mungkin ingin mencoba Retargetable Decompiler.
Sebagian besar ini berasal dari penilaian kerentanan dan bidang penelitian analisis eksekusi. Itu adalah teknik yang rumit dan seringkali alatnya tidak dapat langsung digunakan begitu dikeluarkan dari kotaknya. Namun demikian, mereka memberikan bantuan yang sangat berharga saat mencoba merekayasa balik beberapa perangkat lunak.