Saat mencari "Mesin Unicorn" di Habr, saya terkejut menemukan bahwa alat ini tidak pernah ditampilkan dalam artikel. Saya akan mencoba mengisi kekosongan ini. Mari kita mulai dengan dasar-dasarnya, dan lihat contoh menggunakan emulator di kehidupan nyata. Agar tidak menemukan kembali roda, saya memutuskan untuk hanya menerjemahkan manual ini . Sebelum memulai, saya akan mengatakan bahwa semua komentar atau komentar saya akan terlihat seperti ini .
Apa itu Mesin Unicorn?
Para pengembang sendiri menulis tentang Mesin unicorn Unicorn Engine menyukai ini:
Unicorn adalah emulator prosesor ringan, multi-platform dan multi-arsitektur.
Ini bukan emulator standar. Itu tidak meniru operasi seluruh program atau seluruh OS. Itu tidak mendukung perintah sistem (seperti membuka file, mengeluarkan karakter ke konsol, dll.). Anda harus melakukan markup memori dan memuat data ke dalamnya sendiri, dan kemudian Anda cukup memulai eksekusi dari beberapa alamat tertentu.
Jadi bagaimana manfaatnya?
- Saat menganalisis virus, Anda dapat memanggil fungsi tunggal tanpa membuat proses berbahaya.
- Untuk mengatasi CTF.
- Untuk fuzzing .
- Plugin untuk gdb untuk memprediksi keadaan masa depan, misalnya, hop masa depan atau nilai register.
- Emulasi kode kaya fitur.
Apa yang kamu butuhkan
- Diinstal Mesin Unicorn dengan Python mengikat.
- Disassembler
Contoh
Sebagai contoh, ambil tugas dengan hxp CTF 2017 dengan nama Fibonacci . Biner dapat diunduh di sini .
Ketika Anda memulai program, itu mulai menampilkan bendera kami di konsol, tetapi sangat lambat. Setiap byte bendera berikutnya dianggap lebih lambat dan lebih lambat.
The flag is: hxp{F
Ini berarti bahwa untuk mendapatkan bendera dalam jumlah waktu yang wajar, kita perlu mengoptimalkan pengoperasian aplikasi ini.
Menggunakan IDA Pro ( saya pribadi menggunakan radare2 + Cutter ) kami mendekompilasi kode menjadi pseudocode seperti-C. Terlepas dari kenyataan bahwa kode tidak diurai dengan benar, kita masih bisa mendapatkan informasi darinya tentang apa yang terjadi di dalam.
Kode yang didekompilasi __int64 __fastcall main(__int64 a1, char **a2, char **a3) { void *v3;
unsigned int __fastcall fibonacci(int i, _DWORD *a2) { _DWORD *v2;
Berikut adalah kode assembler dari fungsi utama dan Fibonacci :
utama .text:0x4004E0 main proc near ; DATA XREF: start+1Do .text:0x4004E0 .text:0x4004E0 var_1C = dword ptr -1Ch .text:0x4004E0 .text:0x4004E0 push rbp .text:0x4004E1 push rbx .text:0x4004E2 xor esi, esi ; buf .text:0x4004E4 mov ebp, offset unk_4007E1 .text:0x4004E9 xor ebx, ebx .text:0x4004EB sub rsp, 18h .text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x4004FB mov edi, offset format ; "The flag is: " .text:0x400500 xor eax, eax .text:0x400502 call _printf .text:0x400507 mov r9d, 49h .text:0x40050D nop dword ptr [rax] .text:0x400510 .text:0x400510 loc_400510: ; CODE XREF: main+8Aj .text:0x400510 xor r8d, r8d .text:0x400513 jmp short loc_40051B .text:0x400513 ; --------------------------------------------------------------------------- .text:0x400515 align 8 .text:0x400518 .text:0x400518 loc_400518: ; CODE XREF: main+67j .text:0x400518 mov r9d, edi .text:0x40051B .text:0x40051B loc_40051B: ; CODE XREF: main+33j .text:0x40051B lea edi, [rbx+r8] .text:0x40051F lea rsi, [rsp+28h+var_1C] .text:0x400524 mov [rsp+28h+var_1C], 0 .text:0x40052C call fibonacci .text:0x400531 mov edi, [rsp+28h+var_1C] .text:0x400535 mov ecx, r8d .text:0x400538 add r8, 1 .text:0x40053C shl edi, cl .text:0x40053E mov eax, edi .text:0x400540 xor edi, r9d .text:0x400543 cmp r8, 8 .text:0x400547 jnz short loc_400518 .text:0x400549 add ebx, 8 .text:0x40054C cmp al, r9b .text:0x40054F mov rsi, cs:stdout ; fp .text:0x400556 jz short loc_400570 .text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc .text:0x400565 movzx r9d, byte ptr [rbp-1] .text:0x40056A jmp short loc_400510 .text:0x40056A ; --------------------------------------------------------------------------- .text:0x40056C align 10h .text:0x400570 .text:0x400570 loc_400570: ; CODE XREF: main+76j .text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc .text:0x40057A add rsp, 18h .text:0x40057E xor eax, eax .text:0x400580 pop rbx .text:0x400581 pop rbp .text:0x400582 retn .text:0x400582 main endp
fibonacci .text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp .text:0x400670 ; fibonacci+19p ... .text:0x400670 test edi, edi .text:0x400672 push r12 .text:0x400674 push rbp .text:0x400675 mov rbp, rsi .text:0x400678 push rbx .text:0x400679 jz short loc_4006F8 .text:0x40067B cmp edi, 1 .text:0x40067E mov ebx, edi .text:0x400680 jz loc_400710 .text:0x400686 lea edi, [rdi-2] .text:0x400689 call fibonacci .text:0x40068E lea edi, [rbx-1] .text:0x400691 mov r12d, eax .text:0x400694 mov rsi, rbp .text:0x400697 call fibonacci .text:0x40069C add eax, r12d .text:0x40069F mov edx, eax .text:0x4006A1 mov ebx, eax .text:0x4006A3 shr edx, 1 .text:0x4006A5 and edx, 55555555h .text:0x4006AB sub ebx, edx .text:0x4006AD mov ecx, ebx .text:0x4006AF mov edx, ebx .text:0x4006B1 shr ecx, 2 .text:0x4006B4 and ecx, 33333333h .text:0x4006BA mov esi, ecx .text:0x4006BC .text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j .text:0x4006BC and edx, 33333333h .text:0x4006C2 lea ecx, [rsi+rdx] .text:0x4006C5 mov edx, ecx .text:0x4006C7 shr edx, 4 .text:0x4006CA add edx, ecx .text:0x4006CC mov esi, edx .text:0x4006CE and edx, 0F0F0F0Fh .text:0x4006D4 shr esi, 8 .text:0x4006D7 and esi, 0F0F0Fh .text:0x4006DD lea ecx, [rsi+rdx] .text:0x4006E0 mov edx, ecx .text:0x4006E2 shr edx, 10h .text:0x4006E5 add edx, ecx .text:0x4006E7 and edx, 1 .text:0x4006EA xor [rbp+0], edx .text:0x4006ED pop rbx .text:0x4006EE pop rbp .text:0x4006EF pop r12 .text:0x4006F1 retn .text:0x4006F1 ; --------------------------------------------------------------------------- .text:0x4006F2 align 8 .text:0x4006F8 .text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j .text:0x4006F8 mov edx, 1 .text:0x4006FD xor [rbp+0], edx .text:0x400700 mov eax, 1 .text:0x400705 pop rbx .text:0x400706 pop rbp .text:0x400707 pop r12 .text:0x400709 retn .text:0x400709 ; --------------------------------------------------------------------------- .text:0x40070A align 10h .text:0x400710 .text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j .text:0x400710 xor edi, edi .text:0x400712 call fibonacci .text:0x400717 mov edx, eax .text:0x400719 mov edi, eax .text:0x40071B shr edx, 1 .text:0x40071D and edx, 55555555h .text:0x400723 sub edi, edx .text:0x400725 mov esi, edi .text:0x400727 mov edx, edi .text:0x400729 shr esi, 2 .text:0x40072C and esi, 33333333h .text:0x400732 jmp short loc_4006BC .text:0x400732 fibonacci endp
Pada tahap ini, kami memiliki banyak peluang untuk menyelesaikan masalah ini. Sebagai contoh, kita dapat mengembalikan kode menggunakan salah satu bahasa pemrograman dan menerapkan optimisasi di sana, tetapi proses mengembalikan kode adalah tugas yang sangat sulit, di mana kita dapat membuat kesalahan. Nah, kemudian membandingkan kode untuk menemukan kesalahan umumnya tidak berharga. Tetapi, jika kita menggunakan Mesin Unicorn, maka kita dapat melewati tahap rekonstruksi kode dan menghindari masalah yang dijelaskan di atas. Tentu saja, kita dapat menghindari masalah ini menggunakan frida atau menulis skrip untuk gdb, tetapi ini bukan tentang itu.
Sebelum memulai optimasi, kami akan menjalankan persaingan di Unicorn Engine tanpa mengubah program. Dan hanya setelah peluncuran yang sukses, mari beralih ke pengoptimalan.
Langkah 1: Biarkan Virtualisasi Datang
Mari kita buat file fibonacci.py dan simpan di sebelah biner.
Mari kita mulai dengan mengimpor perpustakaan yang diperlukan:
from unicorn import * from unicorn.x86_const import * import struct
Baris pertama memuat konstanta Unicorn biner dan dasar. Baris kedua memuat konstanta untuk dua arsitektur x86 dan x86_64.
Selanjutnya, tambahkan beberapa fungsi yang diperlukan:
def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num)
Di sini kami mengumumkan fungsi yang akan kami butuhkan nanti:
- baca cukup mengembalikan isi file,
- u32 mengambil string 4-byte dalam LE encoding dan mengkonversi ke int,
- p32 melakukan yang sebaliknya - ia mengambil angka dan mengubahnya menjadi string 4-byte dalam LE encoding.
Catatan: Jika Anda telah menginstal pwntools , maka Anda tidak perlu membuat fungsi-fungsi ini, Anda hanya perlu mengimpornya:
from pwn import *
Dan akhirnya, mari mulai menginisialisasi kelas Mesin Unicorn untuk arsitektur x86_64:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
Di sini kita memanggil fungsi Uc dengan parameter berikut:
- Parameter pertama adalah arsitektur utama. Konstanta mulai dengan UC_ARCH_ ;
- parameter kedua adalah spesifikasi arsitektur. Konstanta mulai dengan UC_MODE_ .
Anda dapat menemukan semua konstanta di lembar contekan .
Seperti yang saya tulis di atas, untuk menggunakan Mesin Unicorn, kita perlu menginisialisasi memori virtual secara manual. Untuk contoh ini, kita perlu menempatkan kode dan menumpuk di suatu tempat di memori.
Alamat dasar (Basis addr) dari biner dimulai pada 0x400000. Mari kita meletakkan tumpukan kita pada 0x0 dan mengalokasikan 1024 * 1024 memori untuk itu. Kemungkinan besar, kita tidak membutuhkan begitu banyak ruang, tetapi tetap tidak sakit.
Kita dapat menandai memori dengan memanggil metode mem_map .
Tambahkan baris ini:
BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE)
Sekarang kita perlu memuat biner ke alamat utamanya dengan cara yang sama seperti bootloader. Setelah itu kita perlu mengatur RSP ke ujung tumpukan.
mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
Sekarang kita dapat memulai emulasi dan menjalankan kodenya, tetapi kita perlu mencari tahu di mana alamat untuk mulai bekerja dan kapan emulator harus berhenti.
Ambil alamat dari perintah pertama dari main () , kita dapat memulai emulasi dari 0x004004e0. Akhir akan dianggap sebagai panggilan ke putc ("\ n") , yang terletak di 0x00400575, setelah menampilkan seluruh bendera.
.text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc
Kita dapat mulai meniru:
mu.emu_start(0x004004e0,0x00400575)
Sekarang jalankan skrip:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py Traceback (most recent call last): File "solve.py", line 32, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Ups, ada yang salah, tapi kami bahkan tidak tahu. Tepat sebelum memanggil mu.emu_start, kita dapat menambahkan:
def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) mu.hook_add(UC_HOOK_CODE, hook_code)
Kode ini menambahkan kait. Kita mendeklarasikan fungsi hook_code kita sendiri, yang dipanggil oleh emulator sebelum setiap perintah. Dibutuhkan parameter berikut:
- salinan Uc kami ,
- alamat instruksi
- instruksi ukuran
- data pengguna (kami dapat meneruskan nilai ini dengan argumen opsional ke hook_add () ).
Sekarang, jika kita menjalankan skrip, kita akan melihat output berikut:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Di alamat tempat kesalahan terjadi, kita dapat memahami bahwa skrip kami tidak dapat memproses perintah ini:
.text:0x4004EF mov rdi, cs:stdout ; stream
Instruksi ini membaca data dari alamat 0x601038 (Anda dapat melihatnya di IDA Pro). Ini adalah bagian .bss yang tidak kami tandai. Solusi saya adalah dengan hanya melewatkan semua instruksi yang bermasalah jika ini tidak mempengaruhi logika program.
Di bawah ini adalah instruksi bermasalah lainnya:
.text:0x4004F6 call _setbuf
Kami tidak dapat memanggil fungsi apa pun dengan glibc, karena kami tidak memiliki glibc yang dimuat dalam memori. Bagaimanapun, kita tidak memerlukan perintah ini, jadi kita juga bisa melewatinya.
Berikut daftar lengkap perintah yang harus dilewati:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
Untuk melewati perintah, kita perlu menulis ulang RIP dengan instruksi berikut:
mu.reg_write(UC_X86_REG_RIP, address+size)
Sekarang hook_code akan terlihat seperti ini:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
Kita juga perlu melakukan sesuatu dengan instruksi yang menampilkan bendera di konsol byte-by-byte.
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
__IO_putc mengambil byte untuk keluaran sebagai argumen pertama (ini adalah register RDI ).
Kita dapat membaca data secara langsung dari register, menampilkan data ke konsol dan melewati set instruksi ini. Kode kait yang diperbarui disajikan di bawah ini:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data):
Kita bisa berlari dan semuanya akan bekerja, tetapi masih lambat.
Langkah 2: Tingkatkan Kecepatan!
Mari kita pikirkan tentang meningkatkan kecepatan kerja. Mengapa program ini sangat lambat?
Jika kita melihat kode yang didekompilasi, kita akan melihat bahwa main () memanggil fibonacci () beberapa kali dan fibonacci () adalah fungsi rekursif. Mari kita melihat lebih dekat fungsi ini, ia mengambil dan mengembalikan dua argumen. Nilai pengembalian pertama dilewatkan melalui register RAX , yang kedua dikembalikan melalui tautan yang dilewatkan melalui argumen kedua ke fungsi. Jika kita melihat lebih dalam pada hubungan antara main () dan fibonacci () , maka kita akan melihat bahwa argumen kedua hanya mengambil dua nilai yang mungkin: 0 atau 1. Jika Anda masih tidak melihat ini, jalankan gdb dan letakkan breakpoint di awal fungsi Fibonacci () .
Untuk mengoptimalkan operasi algoritma, kita dapat menggunakan pemrograman dinamis untuk mengingat nilai kembali untuk parameter yang masuk. Pikirkan sendiri, argumen kedua hanya dapat mengambil dua nilai yang mungkin, jadi yang harus kita lakukan adalah mengingat $ inline $ 2 * MAX \ _OF \ _FIRST \ _ARGUMENT $ inline $ kukus
Bagi yang tidak mengertifibonacci adalah fungsi rekursif yang menghitung nilai selanjutnya sebagai jumlah dari dua sebelumnya. Di setiap langkah dia melangkah lebih dalam. Setiap kali dia memulai, dia berjalan dengan cara yang sama seperti sebelumnya, ditambah satu makna baru.
Contoh:
Asumsikan kedalaman = 6, maka: 1 1 2 3 5 8 .
Dan sekarang depth = 8, lalu: 1 1 2 3 5 8 13 21.
Kita hanya dapat mengingat bahwa 6 anggota pertama adalah 1 1 2 3 5 8 , dan ketika mereka meminta kita untuk menghitung lebih dari yang kita ingat, kita mengambil apa yang kita ingat dan hanya mempertimbangkan apa yang hilang.
Setelah RIP berada di awal fibonacci () , kita bisa mendapatkan argumen fungsi. Kita tahu bahwa suatu fungsi mengembalikan hasil ketika ia keluar dari suatu fungsi. Karena kita tidak dapat beroperasi dengan dua parameter sekaligus, kita perlu tumpukan untuk mengembalikan parameter. Ketika kita memasukkan fibonacci (), kita perlu meletakkan argumen di stack, dan mengambilnya ketika kita keluar. Untuk menyimpan pasangan yang dihitung, kita dapat menggunakan kamus.
Bagaimana cara memproses sepasang nilai?
- Di awal fungsi, kita dapat memeriksa apakah pasangan ini dalam hasil yang sudah kita ketahui:
- jika ada, maka kita dapat mengembalikan pasangan ini. Kami hanya perlu menulis nilai kembali di RAX dan di alamat tautan, yang ada di argumen kedua. Kami juga menetapkan alamat RIP untuk keluar dari fungsi. Kami tidak dapat menggunakan RET di fibonacci () , karena panggilan ini terhubung, jadi kami akan mengambil beberapa RET dari main () ;
- jika nilai-nilai ini tidak, maka kita cukup menambahkannya ke stack.
- Sebelum keluar dari fungsi, kita dapat menyimpan pasangan yang dikembalikan. Kami tahu argumen input, karena kami bisa membacanya dari tumpukan kami.
Kode ini disajikan di sini. FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f]
Inilah keseluruhan skripnya
Hore, kami akhirnya dapat mengoptimalkan aplikasi menggunakan Mesin Unicorn. Kerja bagus!
Sebuah catatan
Sekarang saya memutuskan untuk memberi Anda sedikit pekerjaan rumah.
Di sini Anda dapat menemukan tiga tugas lagi, masing-masing memiliki petunjuk dan solusi lengkap. Anda dapat mengintip lembar contekan sambil menyelesaikan masalah.
Salah satu masalah yang paling menyebalkan adalah mengingat nama konstanta yang diinginkan. Ini mudah ditangani jika Anda menggunakan add-on Tab di IPython . Ketika Anda menginstal IPython, Anda dapat menulis dari unicorn import UC_ARCH_ tekan Tab dan Anda akan ditampilkan semua konstanta yang memulai dengan cara yang sama.