Unidbg的底層支持-Unicorn
Unicorn簡介
Unicorn的官網簡介很簡單明了:Unicorn是一個基於Qemu的輕量級的多平台、多架構的 CPU 模擬器框架。一句話讓我們明白了它是做什么的。在Unidbg對一個Elf文件進行模擬執行的時候,我們一般是在跨平台運行的,所以就底層就需要一個模擬器Backend。其中一種就是Unicorn,下面我們就來看看Unicorn的使用
Unicorn使用步驟
- 語言: Java
- 架構: Arm
我們使用一個例子,就可以講述Unicorn的使用。
引入Unicorn
Unicorn是開源的,可自行編譯。凱神已經編譯好了,可以直接使用
<dependency>
<groupId>com.github.zhkl0228</groupId>
<artifactId>unicorn</artifactId>
<version>1.0.12</version>
</dependency>
使用Unicorn
我們先來寫一段簡單的Thumb匯編代碼
movs r0, #3
movs r1, #2
add r0, r1
這三行匯編如果被CPU執行,結束后R0寄存器的值應該為5對吧。那么我們就看一下用Unicorn如何來模擬執行這三條指令
Unicorn是不認識匯編代碼的,它跟CPU一樣,只認識機器碼,所以我們可以去下面的網站在線轉換成機器碼:
轉換成機器碼后
0x0320
0x0221
0x0844
public class UnicornTest {
long BASE = 0x1000;
@Test
public void test(){
// 先來定義我們的機器碼
byte[] code = new byte[]{
0x03,0x20,
0x02,0x21,
0x08,0x44};
// 創建 Unicorn 對象, 它就像一個CPU, 我們可以使用它的接口來操作這個CPU
// 參數一:架構
// 參數二:運行模式(在ARM、Thumb中無關緊要,最終還是靠運行時判斷)
Unicorn unicorn = new Unicorn(UnicornConst.UC_ARCH_ARM,UnicornConst.UC_MODE_THUMB);
// mem_map 進行內存映射,在使用Unicorn提供的內存時,必須先進行映射
// 參數一:起始地址
// 參數二:映射內存區域的大小
// 參數三:內存操作權限
unicorn.mem_map(BASE,0x1000,UnicornConst.UC_PROT_WRITE | UnicornConst.UC_PROT_READ | UnicornConst.UC_PROT_EXEC);
// mem_write 可以進行對映射出的內存進行寫入操作
// 參數一:寫入的地址
// 參數二:寫入的內容
unicorn.mem_write(BASE,code);
// emu_start 開始執行CPU
// 參數一:開始執行的地址(Thumb指令地址+1)
// 參數二:結束地址(當結束地址命中時,結束執行)
// 參數三:超時時間(ms),當該值為0時,Unicorn將在無限時間內模擬代碼,直到模擬完成
unicorn.emu_start(BASE+1,BASE+code.length,0,0);
// 此時三條Thumb指令已經模擬完成
// reg_read 可以讀取寄存器,相應的reg_write可以寫入寄存器的值
// 參數:寄存器常量
Long o = (Long) unicorn.reg_read(ArmConst.UC_ARM_REG_R0);
System.out.println("the emulate finished result is ==> "+o.intValue());
}
}
我們來看下執行結果, 符合我們的預期
the emulate finished result is ==> 5
對於上面代碼的補充:在mem_map的時候,參數一起始地址必須是1K(32位下)/4K(64位下)對齊的,否則會拋出Invalid argument (UC_ERR_ARG)異常,參數三的操作權限可參考下表:
public static final int UC_PROT_NONE = 0;
public static final int UC_PROT_READ = 1;
public static final int UC_PROT_WRITE = 2;
public static final int UC_PROT_EXEC = 4;
public static final int UC_PROT_ALL = 7;
Unicorn原生Hook
CodeHook
// hook_add 添加一個Hook(后面的Hook參數通用,只有第一個不同)
// 參數一:Hook回調
// 參數二:Hook起始地址
// 參數三:Hook結束地址
// 參數四:自定義參數,可以在Hook的回調中拿到,也就是 Object user
unicorn.hook_add(new CodeHook() {
/* CodeHook將注冊UC_HOOK_CODE類型的Hook,它將在Unicorn對起始地址跟結束地址中間每一條指令的執行時進行調用,當結束地址 < 起始地址時,則將對每一條指令執行時進行調用,相當於Trace*/
/* 回調函數有當前Unicorn對象,所以我們可以基於此對象做關於CPU的任何事情*/
@Override
public void hook(Unicorn u, long address, int size, Object user) {
System.out.print(String.format(">>> Tracing instruction at 0x%x, instruction size = 0x%x\n", address, size));
}
},0,-1,null);
>>> Tracing instruction at 0x1000, instruction size = 0x2
>>> Tracing instruction at 0x1002, instruction size = 0x2
>>> Tracing instruction at 0x1004, instruction size = 0x2
the emulate finished result is ==> 5
BlockHook
/* BlockHook將注冊UC_HOOK_BLOCK類型的Hook,當輸入的基本塊且基本塊的地址(BB)在起始地址<=BB<=結束地址范圍內時,將調用已注冊的回調函數。當結束地址 < 起始地址時,輸入任何基本塊時,將調用回調*/
/* 基本塊:在Unicorn中,基本塊就相當於未發生跳轉的所有指令為一基本塊,發生跳轉就是另一塊*/
unicorn.hook_add(new BlockHook() {
@Override
public void hook(Unicorn u, long address, int size, Object user) {
System.out.print(String.format(">>> Tracing basic block at 0x%x, block size = 0x%x\n", address, size));
}
}, 0, -1, null);
>>> Tracing basic block at 0x1000, block size = 0x6
the emulate finished result is ==> 5
如果我們執行一段有跳轉的匯編(后面我們測試也使用這段匯編代碼)
0x1000: movs r0, #3
0x1002: movs r1, #2
0x1004: add r0, r1
0x1006: bl #0x1100
0x100a: add r0, r1
...
0x1100: add r0, r1
0x1102: movs.w r2, #0x1200
0x1106: str.w lr, [r2]
0x110a: ldr.w pc, [r2]
...
那么我們再來看BlockHook的結果
>>> Tracing basic block at 0x1000, block size = 0xa
>>> Tracing basic block at 0x1100, block size = 0xe
>>> Tracing basic block at 0x100a, block size = 0x2
the emulate finished result is ==> 9
ReadHook
/* ReadHook注冊類型為UC_HOOK_MEM_READ的Hook,只要在地址范圍起始地址<=read_addr<=結束地址內執行內存讀取,就會調用已注冊的回調函數。當結束地址 < 起始地址時,將為所有內存讀取調用回調*/
/* 注意:這個內存讀取必須由指令進行,如果使用Unicorn的api mem_read進行讀取內存,是不會進行回調的*/
unicorn.hook_add(new ReadHook() {
@Override
public void hook(Unicorn u, long address, int size, Object user) {
byte[] bytes = u.mem_read(address, size);
System.out.print(String.format(">>> Memory read at 0x%x, block size = 0x%x, value is = 0x%s\n", address, size, Integer.toHexString(bytes[0] & 0xff)));
}
}, 0, -1, null);
我們執行上面的匯編指令,輸出結果。就說明在0x1200地址上,有對內存的讀取操作
>>> Memory read at 0x1200, block size = 0x4
the emulate finished result is ==> 9
WriteHook
/* WriteHook注冊類型為UC_HOOK_MEM_WRITE的Hook,每當在地址范圍起始地址<=write_addr<=結束地址內執行內存寫入時,將調用已注冊的回調函數。當結束地址 < 起始地址時,將為所有內存寫入調用回調。*/
/* 注意:這個內存寫入操作必須由指令進行,同ReadHook*/
unicorn.hook_add(new WriteHook() {
@Override
public void hook(Unicorn u, long address, int size, long value, Object user) {
System.out.print(String.format(">>> Memory write at 0x%x, block size = 0x%x, value is = 0x%x\n", address, size, value));
}
}, 0, -1, null);
>>> Memory write at 0x1200, block size = 0x4, value is = 0x100b
the emulate finished result is ==> 9
MemHook
MemHook就是ReadHook跟WriteHook的結合體,就不單獨介紹了
InterruptHook
/* InterruptHook注冊類型為UC_HOOK_INTR的Hook,每當執行中斷指令時,將調用已注冊的回調函數。*/
/* 在Linux的Arm架構中,所有的系統調用都是通過中斷進入,且只有這一條入口,所以我們可以通過InterruptHook來處理系統調用*/
unicorn.hook_add(new InterruptHook() {
@Override
public void hook(Unicorn u, int intno, Object user) {
System.out.print(String.format(">>> Interrup occur, intno = %d\n",intno));
}
}, null);
加一條 svc #0 指令,產生異常
>>> Interrup occur, intno = 2
the emulate finished result is ==> 9
EventMemHook
/* 注冊類型為UC_HOOK_MEM_XXX_UNMAPPED 或者 UC_HOOK_MEM_XXX_PROT的Hook,每當嘗試從無效或受保護的內存地址進行讀取或寫入時,將調用注冊的回調函數。*/
unicorn.hook_add(new EventMemHook() {
@Override
public boolean hook(Unicorn u, long address, int size, long value, Object user) {
System.out.println(String.format("UC_HOOK_MEM_READ_UNMAPPED at 0x%x, value = 0x%x",address,value));
return true;
}
}, UC_HOOK_MEM_READ_UNMAPPED, null);
我們加段指令,來讀取0x2000(此處未map)的內容
movs r2, 0x2000
ldr r3, [r2]
UC_HOOK_MEM_READ_UNMAPPED at 0x2000, value = 0x0
unicorn.UnicornException: Invalid memory read (UC_ERR_READ_UNMAPPED)
Keystone
接下來我們介紹下Keystone跟Capstone這對兄弟,先來看Keystone。
上面我們使用在線網站進行匯編代碼與機器碼的相互轉換,使用起來非常的麻煩,我們就可以借助Keystone這個項目來幫助我們對匯編代碼,直接轉換成機器碼
引入
<dependency>
<groupId>com.github.zhkl0228</groupId>
<artifactId>keystone</artifactId>
<version>0.9.5</version>
</dependency>
使用
Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
String assembly =
"movs r0, #3\n" +
"movs r1, #2\n" +
"add r0,r1";
byte[] code = keystone.assemble(assembly).getMachineCode();
// code = [0x03, 0x20, 0x02, 0x21, 0x08, 0x44]
Capstone
Capstone跟Keystone是相反的
引入
<dependency>
<groupId>com.github.zhkl0228</groupId>
<artifactId>capstone</artifactId>
<version>3.0.11</version>
</dependency>
使用
我們跟CodeHook結合來看下Capstone如何使用,也順便看下它們的結合使用
unicorn.hook_add(new CodeHook() {
@Override
public void hook(Unicorn u, long address, int size, Object user) {
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM, Capstone.CS_MODE_THUMB);
Capstone.CsInsn[] disasm = capstone.disasm(u.mem_read(address, size), address);
for (Capstone.CsInsn ins : disasm) {
System.out.println("0x"+Long.toHexString(ins.address) + ": " + ins.mnemonic + " " + ins.opStr );
}
}
}, 0, -1, null);
0x1000: movs r0, #3
0x1002: movs r1, #2
0x1004: add r0, r1
0x1006: bl #0x1100
0x1100: add r0, r1
0x1102: movs.w r2, #0x1200
0x1106: str.w lr, [r2]
0x110a: ldr.w pc, [r2]
0x100a: svc #0
0x100c: add r0, r1
the emulate finished result is ==> 9
總結
上面我們介紹了Unicorn的詳細使用,有了Unicorn的加持,我們就可以模擬Arm架構的指令集,從而執行So中的各種指令,當然多條指令組合成的函數都是可以的。我們可以看到Unicorn本身提供了豐富的API供我們使用,它本身提供的Hook,覆蓋了我們需要實現的所有場景。最后介紹了Keystone跟Capstone這對兄弟,那么本次的分享就結束啦,對本塊內容有興趣的朋友可以加個VX一起學習呀: roy5ue