Cara menulis kode assembler dengan instruksi yang tumpang tindih (teknik lain untuk mengaburkan bytecode)

Kami hadir untuk perhatian Anda teknik membuat program assembler dengan instruksi yang tumpang tindih - untuk melindungi kode bytile yang dikompilasi dari pembongkaran. Teknik ini mampu menahan analisis bytecode statis dan dinamis. Idenya adalah untuk memilih aliran byte yang, ketika dibongkar dari dua offset yang berbeda, menghasilkan dua rantai instruksi yang berbeda, yaitu, dua cara berbeda dalam menjalankan program. Untuk melakukan ini, kami mengambil instruksi assembler multibyte, dan menyembunyikan kode yang dilindungi di bagian variabel bytecode dari instruksi ini. Untuk menipu disassembler dengan meletakkannya di jalur yang salah (sesuai dengan rantai instruksi), dan untuk melindungi dari matanya rantai instruksi yang tersembunyi.



Tiga prasyarat untuk menciptakan "tumpang tindih" yang efektif


Untuk menipu disassembler, kode yang tumpang tindih harus memenuhi tiga kondisi berikut: 1) Instruksi dari rantai penutup dan rantai tersembunyi harus selalu bersinggungan satu sama lain, yaitu. seharusnya tidak disejajarkan relatif satu sama lain (byte pertama dan terakhir tidak harus bersamaan). Jika tidak, bagian dari kode tersembunyi akan terlihat di rantai penutup. 2) Kedua rantai harus terdiri dari instruksi perakitan yang masuk akal. Jika tidak, masking akan terdeteksi pada tahap analisis statis (setelah menemukan kode yang tidak sesuai untuk dieksekusi, disassembler akan memperbaiki penunjuk perintah dan mengekspos masking). 3) Semua instruksi dari kedua rantai seharusnya tidak hanya masuk akal, tetapi juga dieksekusi dengan benar (untuk mencegah hal ini terjadi, program macet ketika Anda mencoba menjalankannya). Kalau tidak, selama analisis dinamis, kegagalan akan menarik perhatian dari kebalikannya, dan topeng akan terungkap.


Deskripsi teknik instruksi assembler "tumpang tindih"


Untuk membuat proses pembuatan kode yang tumpang tindih menjadi sefleksibel mungkin, perlu untuk memilih hanya instruksi multibyte, yang sebanyak mungkin byte dapat mengambil nilai apa pun. Instruksi multibyte ini akan membentuk rantai instruksi masking.


Dalam mengejar tujuan menciptakan kode yang tumpang tindih yang akan memenuhi ketiga kondisi di atas, kami menganggap setiap instruksi masking sebagai urutan byte dari formulir: XX YY ZZ.


Di sini XX adalah awalan instruksi (kode instruksi dan byte statis lainnya - yang tidak dapat diubah).


YY adalah byte yang dapat diubah secara sewenang-wenang (sebagai aturan, byte ini menyimpan nilai numerik langsung yang diteruskan ke instruksi; atau alamat operan yang disimpan dalam memori). Harus ada byte YY sebanyak mungkin sehingga lebih banyak instruksi tersembunyi yang bisa masuk ke dalamnya.


ZZ - ini juga byte yang dapat diubah secara sewenang-wenang, dengan satu-satunya perbedaan adalah bahwa kombinasi ZZ byte dengan byte berikutnya XX (ZZ XX) harus membentuk instruksi yang valid yang memenuhi tiga kondisi yang dirumuskan pada awal artikel. Idealnya, ZZ hanya menempati satu byte, sehingga pada YY (ini pada dasarnya adalah bagian yang paling penting - kode tersembunyi kami ditempatkan di sini) harus ada byte sebanyak mungkin. Instruksi tersembunyi terakhir harus diakhiri dengan ZZ, - menciptakan titik konvergensi untuk dua rantai eksekusi.


Melekatkan instruksi


Kombinasi ZZ XX - kita akan memanggil instruksi perekatan. Instruksi perekatan diperlukan, pertama, untuk bergabung dengan instruksi tersembunyi yang terletak di instruksi masking yang berdekatan dan, kedua, untuk memenuhi kondisi pertama yang diperlukan yang dinyatakan pada awal artikel: instruksi dari kedua rantai harus selalu bersilangan satu sama lain (oleh karena itu, instruksi perekatan selalu terletak di persimpangan dua instruksi masking).


Instruksi perekatan dieksekusi dalam rantai perintah tersembunyi, dan oleh karena itu harus dipilih sedemikian rupa untuk memaksakan pembatasan sesedikit mungkin pada kode tersembunyi. Misalkan ketika dieksekusi, register tujuan umum dan register EFLAGS diubah, maka kode tersembunyi tidak akan dapat secara efektif menggunakan register dan perintah kondisional yang sesuai (misalnya, jika instruksi pengeleman didahului oleh operator perbandingan, dan instruksi yang menempel itu sendiri mengubah nilai register EFLAGS, kemudian transisi kondisional, yang berdiri setelah instruksi menempel tidak akan berfungsi dengan benar).


Deskripsi teknik tumpang tindih di atas diilustrasikan pada gambar berikut. Jika eksekusi dimulai dengan byte awal (XX), maka rantai instruksi masking diaktifkan. Dan jika dari byte YY, rantai instruksi tersembunyi diaktifkan.



Instruksi assembler cocok untuk peran "instruksi masking"


Instruksi terpanjang, yang pada pandangan pertama paling cocok untuk kita, adalah versi 10-byte MOV, di mana offset yang ditentukan oleh register dan alamat 32-bit ditransfer sebagai operan pertama, dan nomor 32-bit sebagai operan kedua. Instruksi ini mengandung sebagian besar byte yang dapat diubah secara sewenang-wenang (sebanyak 8 buah).



Namun, walaupun instruksi ini kelihatannya masuk akal (secara teoritis, ini dapat dieksekusi dengan benar), itu masih tidak sesuai dengan kita, karena operan pertamanya, sebagai suatu peraturan, akan menunjukkan alamat yang tidak dapat diakses, dan oleh karena itu, ketika mencoba menjalankan MOV seperti itu, program akan runtuh. T.O. MOV 10-byte ini tidak memenuhi kondisi yang diperlukan ketiga: semua instruksi dari kedua rantai harus dieksekusi dengan benar.


Oleh karena itu, kami akan memilih untuk peran instruksi masking hanya pelamar yang tidak menimbulkan risiko runtuhnya program. Kondisi ini secara signifikan mempersempit kisaran instruksi yang cocok untuk membuat kode yang tumpang tindih, tetapi masih ada yang sesuai. Di bawah ini adalah empat dari mereka. Masing-masing dari empat instruksi ini berisi lima byte, yang dapat diubah secara sewenang-wenang, tanpa risiko crash program.


  • LEA. Instruksi ini menghitung alamat memori yang ditentukan oleh ekspresi dalam operan kedua dan menyimpan hasilnya dalam operan pertama. Karena kita dapat merujuk ke memori tanpa akses yang sebenarnya (dan, dengan demikian, tanpa risiko crash program), lima byte terakhir dari instruksi ini dapat mengambil nilai arbitrer.


  • CMOVcc. Instruksi ini melakukan operasi MOV jika kondisi "cc" terpenuhi. Agar instruksi ini memenuhi persyaratan ketiga, syaratnya harus dipilih sehingga dalam keadaan apa pun ia memiliki nilai SALAH. Jika tidak, instruksi ini dapat mencoba mengakses alamat memori yang tidak dapat diakses, dan sebagainya. menurunkan program.


  • SETcc Ini beroperasi pada prinsip yang sama dengan CMOVcc: mengatur byte ke satu jika kondisi "cc" terpenuhi. Instruksi ini memiliki masalah yang sama dengan CMOVcc: mengakses alamat yang tidak valid akan menyebabkan program macet. Karena itu, pilihan kondisi "cc" harus didekati dengan sangat hati-hati.


  • NOP. NOP dapat memiliki panjang yang berbeda (dari 2 hingga 15 byte), tergantung pada operan yang ditunjukkan di dalamnya. Dalam hal ini, tidak akan ada risiko crash program (karena akses ke alamat memori yang tidak valid). Karena satu-satunya hal yang NOP lakukan adalah menambah penghitung instruksi, (mereka tidak melakukan operasi pada operan). Oleh karena itu, byte NOP di mana operan ditentukan dapat mengambil nilai arbitrer. Untuk keperluan kami, NOP 9-byte paling cocok.


Untuk referensi, berikut adalah beberapa opsi NOP lainnya.



Instruksi assembler cocok untuk peran "instruksi menempel"


Daftar instruksi yang sesuai untuk peran instruksi perekatan adalah unik untuk setiap instruksi masking tertentu. Di bawah ini adalah daftar (dihasilkan oleh algoritma yang ditunjukkan pada gambar berikut) menggunakan 9-byte NOP sebagai contoh.



Membentuk daftar ini, kami hanya memperhitungkan opsi-opsi di mana ZZ mengambil 1 byte (jika tidak, akan ada sedikit ruang tersisa untuk kode tersembunyi). Berikut adalah daftar instruksi tempel yang sesuai untuk NOP 9-byte.



Di antara daftar instruksi ini tidak ada satu pun yang akan bebas dari efek samping. Masing-masing dari mereka mengubah EFLAGS, atau register tujuan umum, atau keduanya sekaligus. Daftar ini dibagi menjadi 4 kategori, sesuai dengan efek samping yang dimiliki instruksi.


Kategori pertama mencakup instruksi yang mengubah register EFLAGS, tetapi tidak mengubah register tujuan umum. Instruksi dari kategori ini dapat digunakan ketika tidak ada lompatan bersyarat atau instruksi dalam rantai instruksi tersembunyi berdasarkan evaluasi informasi dari register EFLAGS. Dalam hal ini dalam hal ini (untuk NOP 9-byte) hanya ada dua instruksi: TEST dan CMP.



Berikut ini adalah contoh sederhana kode tersembunyi yang menggunakan TEST sebagai instruksi pengeleman. Contoh ini membuat panggilan sistem keluar, yang mengembalikan nilai 1 untuk versi Linux apa pun. Untuk dapat membentuk instruksi TEST dengan tepat untuk kebutuhan kita, kita perlu mengatur byte terakhir dari NOP pertama ke 0xA9. Byte ini, ketika digabungkan dengan empat byte pertama dari NOP berikutnya (66 0F 1F 84), akan berubah menjadi instruksi TEST EAX, 0x841F0F66. Dua gambar berikut menunjukkan kode assembler yang sesuai (untuk rantai penutup dan rantai tersembunyi). Rantai tersembunyi diaktifkan ketika kontrol ditransfer ke byte ke-4 dari NOP pertama.




Kategori kedua termasuk instruksi yang mengubah nilai register umum atau memori yang tersedia (stack, misalnya), tetapi jangan mengubah register EFLAGS. Saat menjalankan instruksi PUSH atau varian MOV apa pun, di mana nilai langsung ditentukan sebagai operan kedua, register EFLAGS tetap tidak berubah. T.O. menempelkan instruksi dari kategori kedua bahkan dapat ditempatkan antara instruksi perbandingan (TEST, misalnya) dan instruksi mengevaluasi register EFLAGS. Namun, instruksi dalam kategori ini membatasi penggunaan register yang muncul dalam instruksi perekatan yang sesuai. Sebagai contoh, jika MOV EBP, 0x841F0F66 digunakan sebagai instruksi pengeleman, maka kemungkinan menggunakan register EBP (dari sisa kode tersembunyi) sangat terbatas.


Kategori ketiga termasuk instruksi yang mengubah register EFLAGS, dan register tujuan umum (atau memori) berubah. Instruksi ini tidak memiliki keunggulan yang jelas dibandingkan instruksi dari dua kategori pertama. Namun, mereka juga dapat digunakan, karena mereka tidak bertentangan dengan tiga kondisi yang dirumuskan pada awal artikel. Kategori keempat termasuk instruksi, implementasi yang tidak ada jaminan bahwa program tidak akan crash - ada risiko akses ilegal ke memori. Sangat tidak diinginkan untuk menggunakannya, karena mereka tidak memenuhi syarat ketiga.


Instruksi assembler yang dapat digunakan dalam rantai tersembunyi


Dalam kasus kami (ketika NOP 9-byte digunakan sebagai instruksi masking), panjang setiap instruksi dari rantai tersembunyi tidak boleh melebihi empat byte (pembatasan ini tidak berlaku untuk instruksi lengket yang menempati 5 byte). Namun, ini bukan batasan yang sangat kritis, karena sebagian besar instruksi yang lebih panjang dari empat byte dapat diuraikan menjadi beberapa instruksi yang lebih pendek. Berikut ini adalah contoh MOV 5-byte yang terlalu besar untuk ditampung dalam rantai tersembunyi.



Namun, MOV lima byte ini dapat didekomposisi menjadi tiga instruksi, yang panjangnya tidak melebihi empat byte.



Meningkatkan masking dengan menyebarkan NOP masking di seluruh program


Sejumlah besar NOP berturut-turut terlihat, dari sudut pandang sebaliknya, sangat mencurigakan. Memfokuskan minatnya pada NOP yang mencurigakan ini, pembalik yang berpengalaman bisa sampai ke bagian bawah kode yang tersembunyi di dalamnya. Untuk menghindari paparan ini, NOP bertopeng dapat tersebar di seluruh program.


Rantai eksekusi kode tersembunyi yang benar dalam kasus ini dapat didukung oleh instruksi bita lompatan tanpa syarat. Dalam hal ini, dua byte terakhir dari masing-masing NOP akan menempati 2-byte JMP.


Trik ini memungkinkan Anda untuk membagi satu urutan panjang NOP menjadi beberapa yang pendek (atau bahkan masing-masing menggunakan satu NOP). Dalam NOP terakhir dari urutan pendek seperti itu, hanya 3 byte dari payload dapat dialokasikan (byte ke-4 akan diambil oleh instruksi lompatan tanpa syarat). T.O. di sini ada batasan tambahan pada ukuran instruksi yang valid. Namun, seperti yang disebutkan di atas, instruksi panjang dapat diletakkan pada rantai instruksi yang lebih pendek. Di bawah ini adalah contoh dari MOV 5-byte yang sama, yang telah kami susun agar sesuai dengan batas 4-byte. Namun, sekarang kami menguraikan MOV ini sedemikian rupa agar sesuai dengan batas 3 byte.



Setelah mendekomposisi semua instruksi panjang menjadi lebih pendek sesuai dengan prinsip yang sama, kita dapat, untuk menutupi lebih banyak, umumnya hanya menggunakan NOP tunggal yang tersebar di seluruh program. Instruksi JMP dua byte dapat melompat maju dan mundur dengan 127 byte, yang berarti bahwa dua NOP berturut-turut (berturut-turut, dalam hal rangkaian instruksi tersembunyi) harus berada dalam 127 byte.


Trik ini memiliki keuntungan signifikan lainnya (selain masking yang disempurnakan): dengan bantuannya, Anda dapat menempatkan kode tersembunyi di NOP yang ada pada file biner yang dikompilasi (mis., Memasukkan muatan ke dalam biner setelah mengkompilasinya). Dalam hal ini, tidak perlu bahwa NOP yatim piatu ini menjadi 9-byte. Misalnya, jika ada beberapa byte NOP tunggal dalam satu baris dalam biner, maka mereka dapat dikonversi ke multi-byte NOP, tanpa mengganggu fungsi program. Di bawah ini adalah contoh teknik untuk mendispersikan NOP (kode ini secara fungsional setara dengan contoh yang dibahas di atas).



Kode tersembunyi seperti itu, tersembunyi di NOP yang tersebar di seluruh program, sudah jauh lebih sulit untuk dideteksi.


Pembaca yang penuh perhatian harus memperhatikan bahwa NOP pertama tidak memiliki byte terakhir. Namun, tidak ada yang perlu dikhawatirkan. Karena byte yang tidak diklaim ini didahului oleh lompatan tanpa syarat. T.O. kontrol tidak akan pernah ditransfer kepadanya. Jadi semuanya beres.


Berikut adalah teknik untuk membuat kode yang tumpang tindih. Gunakan untuk kesehatan. Sembunyikan kode berharga Anda dari mata yang mengintip. Tetapi hanya mengadopsi beberapa instruksi lain, bukan NOP 9-byte. Karena pembalik mungkin akan membaca artikel ini juga.

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


All Articles