背景
?? Unicorn 是一款基于 QEMU 的快速 CPU 模擬器框架,可以模擬多種體系結(jié)構(gòu)的指令集,包括 ARM、MIPS、PowerPC、SPARC 和 x86 等。Unicorn使我們可以更好地關(guān)注 CPU 操作, 忽略機(jī)器設(shè)備的差異。它能夠在虛擬內(nèi)存中加載和運(yùn)行二進(jìn)制代碼,并提供對模擬器狀態(tài)的完全控制,包括內(nèi)存、寄存器和標(biāo)志位等。該項(xiàng)目最初是作為一個(gè) QEMU 插件而啟動(dòng)的,但隨著時(shí)間的推移,它已經(jīng)成長為一款獨(dú)立的模擬器框架。現(xiàn)在 Unicorn 在許多領(lǐng)域都有應(yīng)用,如二進(jìn)制代碼分析、系統(tǒng)仿真、漏洞測試等。
unicorn基礎(chǔ)
??? unicorn安裝:
?
pip3 install?unicorn
?
????手動(dòng)編譯方式如下:
?
wget https://github.com/unicorn-engine/unicorn/archive/2.0.1.zip unzip 2.0.1.zip cd?unicorn-2.0.1/bingings/python sudo make?install
?
????在unicorn/bindings/python目錄下,下面有官方提供的example腳本可以供我們學(xué)習(xí)。
?
from?unicorn import?* # 在使用Unicorn前導(dǎo)入unicorn模塊. 樣例中使用了一些x86寄存器常量, 所以也需要導(dǎo)入unicorn.x86_const模塊 from?unicorn.x86_const import?* # 需要模擬的二進(jìn)制機(jī)器碼, 需要使用十六進(jìn)制表示, 代表的匯編指令是: "INC ecx" 和 "DEC edx",即ecx+=1,edx-=1 X86_CODE32 = b"x41x4a"?# INC ecx; DEC edx # 我們將模擬執(zhí)行上述指令的所在虛擬地址 ADDRESS = 0x80000 print("Emulate i386 code") try: ??# 使用Uc類初始化Unicorn, 該類接受2個(gè)參數(shù): 硬件架構(gòu)和32/64位(模式),在這里我們需要模擬執(zhí)行x86架構(gòu)的32位代碼, 并使用變量mu來接受返回值。 ??mu = Uc(UC_ARCH_X86, UC_MODE_32) ??# 使用mem_map函數(shù)根據(jù)ADDRESS映射2MB用于模擬執(zhí)行的內(nèi)存空間。所有進(jìn)程中的CPU操作都應(yīng)該只訪問該內(nèi)存區(qū)域,映射的內(nèi)存具有默認(rèn)的讀,寫和執(zhí)行權(quán)限。 ??mu.mem_map(ADDRESS, 2?* 1024?* 1024) ??# 將需要模擬執(zhí)行的代碼寫入我們剛剛映射的內(nèi)存中。mem_write函數(shù)2個(gè)參數(shù): 要寫入的內(nèi)存地址和需要寫入內(nèi)存的代碼。 ??mu.mem_write(ADDRESS, X86_CODE32) ??# 使用reg_write函數(shù)設(shè)置ECX和EDX寄存器的值 ??mu.reg_write(UC_X86_REG_ECX, 0x1234) ??mu.reg_write(UC_X86_REG_EDX, 0x7890) ??# 使用emu_start方法開始模擬執(zhí)行, 該函數(shù)接受4個(gè)參數(shù): 要模擬執(zhí)行的代碼地址, 模擬執(zhí)行停止的內(nèi)存地址(這里是X86_CODE32的最后1字節(jié)處), 模擬執(zhí)行的時(shí)間和需要執(zhí)行的指令數(shù)目。如果我們忽略后兩個(gè)參數(shù), Unicorn將會(huì)默認(rèn)以無窮時(shí)間和無窮指令數(shù)目的條件來模擬執(zhí)行代碼。 ??mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32)) ??# 我們使用reg_read函數(shù)來讀取寄存器中的值,打印輸出ECX和EDX寄存器的值。 ??print("Emulation done. Below is the CPU context") ??r_ecx = mu.reg_read(UC_X86_REG_ECX) ??r_edx = mu.reg_read(UC_X86_REG_EDX) ??print(">>> ECX = 0x%x"?%r_ecx) ??print(">>> EDX = 0x%x"?%r_edx) except?UcError as?e: ??print("ERROR: %s"?% e)
?
????上面的代碼大致過程設(shè)置虛擬地址并初始化unicorn引擎,并設(shè)置內(nèi)存映射空間,隨后將要模擬執(zhí)行代碼寫入內(nèi)存虛擬空間中。程序執(zhí)行前給ecx寄存器賦值為0x1234,edx寄存器賦值為0x7890,當(dāng)執(zhí)行emu_start函數(shù)時(shí),程序從要模擬代碼的開始進(jìn)行執(zhí)行。此時(shí)運(yùn)行仿真代碼為"INC ecx; DEC edx"。即,對ecx進(jìn)行加一操作,對edx進(jìn)行減一操作。運(yùn)行后打印結(jié)果如下:
????我們可以從項(xiàng)目中所有的example案例中可以提取出unicorn模板腳本:
?
from?unicorn import?* from?unicorn.x86_const import?* # 相應(yīng)架構(gòu)的常量信息: # arch:UC_ARCH_ARM、UC_ARCH_ARM64、UC_ARCH_M68K、UC_ARCH_MAX、UC_ARCH_MIPS、UC_ARCH_PPC、UC_ARCH_SPARC、UC_ARCH_X86 # mode:UC_MODE_16、UC_MODE_32、UC_MODE_64、UC_MODE_ARM、UC_MODE_BIG_ENDIAN、UC_MODE_LITTLE_ENDIAN、UC_MODE_MCLASS、UC_MODE_MICRO、UC_MODE_MIPS3、UC_MODE_MIPS32、UC_MODE_MIPS32R6、UC_MODE_MIPS64、UC_MODE_PPC32、UC_MODE_PPC64、UC_MODE_QPX、UC_MODE_SPARC32、UC_MODE_SPARC64、UC_MODE_THUMB、UC_MODE_V8、UC_MODE_V9 # 該模板中的UC_ARCH_X86可替換成為其他架構(gòu)的常量,且相應(yīng)寄存器常量名稱也要相應(yīng)改變。 # 定義要執(zhí)行的指令 CODE = b"xXX" # 指定內(nèi)存地址 BASE_ADDRESS = 0x100000 # 定義hook函數(shù) def?hook_code(uc, address, size, user_data): ??# 輸出寄存器值和內(nèi)存內(nèi)容 ??print("[+] RIP=0x%x RAX=0x%x RBX=0x%x RCX=0x%x RDX=0x%x"?% (uc.reg_read(UC_X86_REG_RIP), uc.reg_read(UC_X86_REG_RAX), uc.reg_read(UC_X86_REG_RBX), uc.reg_read(UC_X86_REG_RCX), uc.reg_read(UC_X86_REG_RDX))) ??print("[+] Memory:") ??for?i in?range(0x1000): ??????if?uc.mem_read(BASE_ADDRESS+i, 1) != b'x00': ??????????print("0x%x: %s"?% (BASE_ADDRESS+i, uc.mem_read(BASE_ADDRESS+i, 16).hex())) # 初始化 Unicorn 引擎和內(nèi)存空間 mu = Uc(UC_ARCH_X86, UC_MODE_64) mu.mem_map(BASE_ADDRESS, 0x10000) mu.mem_write(BASE_ADDRESS, CODE) # 設(shè)置 RIP 和 RSP mu.reg_write(UC_X86_REG_RIP, BASE_ADDRESS) mu.reg_write(UC_X86_REG_RSP, BASE_ADDRESS + 0x10000) # 注冊hook函數(shù) mu.hook_add(UC_HOOK_CODE, hook_code) # 開始模擬執(zhí)行 mu.emu_start(BASE_ADDRESS, BASE_ADDRESS + len(CODE))
?
????注:函數(shù)(或鉤子函數(shù))是一種用戶自定義函數(shù),用于在模擬執(zhí)行指令時(shí)對特定事件進(jìn)行處理。當(dāng)程序執(zhí)行到某個(gè)地址時(shí),引擎會(huì)調(diào)用已注冊的 Hook 函數(shù),并將當(dāng)前的 CPU 狀態(tài)、指令地址和指令大小等信息傳遞給函數(shù)。這樣,用戶就可以利用 Hook 函數(shù)來監(jiān)測程序的執(zhí)行狀態(tài)、修改寄存器/內(nèi)存值,或者實(shí)現(xiàn)其他自定義功能。
unicorn實(shí)例
????以ctf題目為例,下載題目附件后,拖入IDA進(jìn)行分析。進(jìn)行main函數(shù)分析發(fā)現(xiàn)整個(gè)程序執(zhí)行完畢后就會(huì)輸出flag的值,不考慮指令集架構(gòu)及運(yùn)行程序的情況下,正常逆向思路便是逆向程序邏輯以及函數(shù)代碼并編寫相應(yīng)解密程序進(jìn)行運(yùn)行獲取flag。
??? sub_400670函數(shù)為加密函數(shù),如果我們基礎(chǔ)不夠或者并不會(huì)逆向,這里便可以使用unicorn仿真執(zhí)行程序。那么這里unicorn仿真的整體流程就是仿真執(zhí)行整個(gè)main函數(shù),main函數(shù)地址為0x4004E0~0x400475。
????根據(jù)以上我們得出的信息,對模板腳本進(jìn)行修改:
?
from?unicorn import?* from?unicorn.x86_const import?* # 定義要執(zhí)行的指令 def?read(name): ??with?open(name,"rb") as?f: ??????return?f.read() # 指定內(nèi)存地址 BASE_ADDRESS = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 # 定義hook函數(shù) def?hook_code(uc, address, size, user_data): ??print('>>> Tracing instruction at 0x%x, instruction size = 0x%x'?%(address, size)) # 初始化 Unicorn 引擎和內(nèi)存空間 mu = Uc (UC_ARCH_X86, UC_MODE_64) mu.mem_map(BASE_ADDRESS, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE) # 設(shè)置 RIP 和 RSP mu.mem_write(BASE_ADDRESS, read("./test")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1) # 注冊hook函數(shù) mu.hook_add(UC_HOOK_CODE, hook_code) # 開始模擬執(zhí)行 mu.emu_start(0x00000000004004E0, 0x0000000000400575)
?
????運(yùn)行發(fā)現(xiàn)在地址0x4004ef處的指令在運(yùn)行時(shí)報(bào)錯(cuò)了。unicorn顯示Invalid memory read,猜測為地址讀取問題。
????逆向代碼發(fā)現(xiàn),報(bào)錯(cuò)處的指令為將mov rdi, cs:stdout,原因是由于沒有設(shè)置cs寄存器以及stdout stream地址導(dǎo)致無法訪問。但是這條指令對我們仿真結(jié)果沒有影響,我們可以手動(dòng)對程序報(bào)錯(cuò)地址0x4004ef處的指令進(jìn)行patch,使其跳過。patch好以后,再次運(yùn)行發(fā)現(xiàn)又報(bào)錯(cuò)了。
????逆向發(fā)現(xiàn),此處和上面形成原因類似,訪問了沒有設(shè)置bss段地址,并且也對仿真結(jié)果沒有影響。循環(huán)往復(fù)patch并運(yùn)行后發(fā)現(xiàn)在0x4004EF,0x4004F6,0x400502,0x40054F 地址處都會(huì)報(bào)錯(cuò)。
????針對于以上遇到的問題的出現(xiàn)并沒有對結(jié)果產(chǎn)生影響,我們可以在代碼中手動(dòng)過濾這些地址,使其跳過。隨后在程序執(zhí)行最后put函數(shù),我們可以取出打印結(jié)果。腳本中添加代碼如下:
?
nop_address = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F] def?hook_code(mu, address, size, user_data):?? ??if?address in?nop_address: ??????mu.reg_write(UC_X86_REG_RIP, address+size) ??elif?address == 0x400560: ??????c = mu.reg_read(UC_X86_REG_RDI) ??????print(chr(c)) ??????mu.reg_write(UC_X86_REG_RIP, address+size)
?
????運(yùn)行腳本后,我們已經(jīng)可以讓程序在運(yùn)行解密過程了。但是速度極慢,5分鐘打印3個(gè)字符。
????在調(diào)試過程中我們發(fā)現(xiàn)在運(yùn)行sub_400670函數(shù)時(shí),程序內(nèi)部條件分支會(huì)不斷調(diào)用函數(shù)自身,從而進(jìn)入遞歸狀態(tài),導(dǎo)致解密時(shí)間非常長。我們想到在參數(shù)一致的情況下,重復(fù)執(zhí)行sub_400670函數(shù)非常占用資源和時(shí)間,這里對其進(jìn)行優(yōu)化。思路如下,我們可以使用棧空間來保存一個(gè)不同輸入?yún)?shù)以及對應(yīng)計(jì)算結(jié)果的字典來避免重復(fù)計(jì)算。具體可分為參數(shù)保存和返回值取出倆個(gè)步驟:
????參數(shù)保存步驟為:當(dāng)程序運(yùn)行到 sub_400670 函數(shù)時(shí),會(huì)讀取函數(shù)的兩個(gè)輸入?yún)?shù)(x86_64架構(gòu)中倆個(gè)參數(shù)分別保存在rdi和rsi寄存器中),將(arg0, arg1)保存一下。然后,我們檢查字典中是否包含這個(gè)元組作為鍵的條目。如果存在,說明之前已經(jīng)計(jì)算過這個(gè)函數(shù),可以直接從字典中取出對應(yīng)的計(jì)算結(jié)果并執(zhí)行ret進(jìn)行返回。如果不存在,則說明對應(yīng)參數(shù)的函數(shù)還沒有被計(jì)算過,程序需要進(jìn)行函數(shù)運(yùn)算。返回值取出的步驟為:當(dāng)我們將輸入?yún)?shù)壓入一個(gè)棧中,程序執(zhí)行完成后,會(huì)執(zhí)行到到函數(shù)的結(jié)尾處(ret),我們可以在此處取出函數(shù)的返回值,并將其存儲(chǔ)在 (ret_rax, ret_ref) 中(ret_rax 是函數(shù)返回值,ret_ref 是保存返回值的地址)。然后,我們將這個(gè)元組作為值,將 (arg0, arg1) 作為鍵,將其存儲(chǔ)到字典 d 中。這樣,下一次計(jì)算相同參數(shù)的sub_400670函數(shù)時(shí),就可以直接從字典中取出對應(yīng)的計(jì)算結(jié)果,而無需再次進(jìn)行計(jì)算。
????在原來的腳本之上,我們添加的代碼如下:
?
from?pwn import?* stack = [] direct = {} ENTRY = [0x0000000000400670] END = [0x00000000004006F1, 0x0000000000400709] def?hook_code(mu, address, size, user_data): ??if?address in?ENTRY: ??????arg0 = mu.reg_read(UC_X86_REG_RDI) ??????r_rsi = mu.reg_read(UC_X86_REG_RSI) ??????arg1 = u32(mu.mem_read(r_rsi, 4)) ??????if?(arg0,arg1) in?direct: ??????????(ret_rax, ret_ref) = direct[(arg0,arg1)] ??????????mu.reg_write(UC_X86_REG_RAX, ret_rax) ??????????mu.mem_write(r_rsi, p32(ret_ref)) ??????????mu.reg_write(UC_X86_REG_RIP, 0x400582) ??????else: ??????????stack.append((arg0,arg1,r_rsi)) ??????? ??elif?address in?END: ??????(arg0, arg1, r_rsi) = stack.pop() ??????ret_rax = mu.reg_read(UC_X86_REG_RAX) ??????ret_ref = u32(mu.mem_read(r_rsi,4)) ??????direct[(arg0, arg1)]=(ret_rax, ret_ref)
?
????此時(shí)再次運(yùn)行腳本,爆破速度提升,且運(yùn)行結(jié)果已經(jīng)完全顯示。
????unicorn也固件解密中也發(fā)揮了重要作用,在文章(https://www.shielder.com/blog/2022/03/reversing-embedded-device-bootloader-u-boot-p.2)中,作者通過對某華設(shè)備固件進(jìn)行逆向分析以及unicorn仿真執(zhí)行解密出了kernel文件。大致思路如下,作者通過binwalk提取固件,發(fā)現(xiàn)固件已經(jīng)被加密,并對提取出來的部分進(jìn)行分析后發(fā)現(xiàn)uboot.bin具有可利用信息。
????對uboot.bin進(jìn)行逆向分析后,通過開源uboot代碼恢復(fù)符號表,定位出了uboot解密kernel時(shí)的對應(yīng)加密函數(shù)。
????在解密算法時(shí)發(fā)現(xiàn)uboot載入kernel.img并對其進(jìn)行AES解密。解密共有倆種方法,一種方法為逆向解密,需要一定的逆向技術(shù)才可完成,較復(fù)雜。另一種方式便是使用unicorn仿真執(zhí)行解密函數(shù),該種方法較為簡單便捷。這里選取了第二種方式來解密,核心代碼如下,代碼使用 unicorn 待解密文件加載到虛擬內(nèi)存并執(zhí)行模擬執(zhí)行解密代碼 ,并使用disas_single函數(shù)打印出此時(shí)正在執(zhí)行的匯編指令來便于我們調(diào)試。
????執(zhí)行后便解密出了vmlinux前512字節(jié),完善腳本后便可解密整個(gè)vmlinux文件。隨后,我們可以使用vmlinux-to-elf工具對vmlinux恢復(fù)函數(shù)符號表。一般情況下,linux下對固件升級和固件加密都是放在內(nèi)核完成的,接下來我們對kernel文件進(jìn)行逆向分析就可能得出rootfs的解密流程。
??? unicorn除了在ctf和固件解密方向有實(shí)質(zhì)性作用,在漏洞挖掘的fuzz方向也具有一定研究價(jià)值,但是基于unicorn的fuzzer較為復(fù)雜且難度較高。與此同時(shí),qiling框架的出現(xiàn)使得仿真fuzz變得較為簡單。qiling是一個(gè)基于unicorn引擎開發(fā)的高級框架,它可以利用unicorn來模擬CPU指令,但是它同樣可以理解操作系統(tǒng)上下文,它集成了可執(zhí)行文件格式加載器、動(dòng)態(tài)鏈接、系統(tǒng)調(diào)用和I/O處理器。更重要的是,qiling可以在不需要原生操作系統(tǒng)的環(huán)境下運(yùn)行可執(zhí)行文件源碼。現(xiàn)階段來看qiling框架更加適合安全研究人員,這也是我們后面需要學(xué)習(xí)的內(nèi)容。
總結(jié)
????這一小節(jié),我們學(xué)習(xí)了unicorn框架的使用基礎(chǔ),并通過一道ctf題目仿真并解出了flag。同時(shí)學(xué)習(xí)了unicorn在固件解密方向的思路,使我們更加了解unicorn框架。
審核編輯:劉清
評論
查看更多