0x10 Unicorn
Unicorn 是一個輕量級的多平臺多架構的 CPU 仿真框架。作爲一款著名的開源CPU 模擬框架,很多二進制逆向分析軟件都用到了 Unicorn ,或者使用到了它的思想。比如 Radare2、Pwndbg、gdb-gef。Unicorn 在 QEMU 的基礎上,增加了許多新特性,以及對 CPU 仿真更好的支持 1
- 多架構支持:Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)
- 輕量級的 API
- 純 C 語言實現,並支持 Pharo,Crystal,Clojure,Visual Basic,Perl,Rust,Haskell,Ruby,Python,Java,Go,.NET,Delphi / Pascal 和 MSVC 的編譯
- 原生支持 Windows 和類 Unix 系統,如 Mac OSX, Linux, *BSD & Solaris
- 使用 JIT(Just-In-Time,即時編譯技術)提高性能
- 支持各種級別的細粒度分析
- 線程安全
- 根據免費軟件許可 GPLv2 分發
原作者在 2015 年的 BlackHat 上,發表了相關議題, BlackHat USA 2015 slides 提供更多的信息,有興趣的讀者可以觀看一波。
0x20 基於 C/C++ 使用 Unicorn 進行開發
0x21 編譯生成庫文件
項目地址:https://github.com/unicorn-engine/unicorn,以下是該項目的詳細結構 2
. <- 主要引擎core engine + README + 編譯文檔COMPILE.TXT 等
├── arch <- 各語言反編譯支持的代碼實現
│ ├── AArch64 <- ARM64 (aka ARMv8) 引擎
│ ├── ARM <- ARM 引擎
│ ├── EVM <- Ethereum 引擎
│ ├── M680X <- M680X 引擎
│ ├── M68K <- M68K 引擎
│ ├── Mips <- Mips 引擎
│ ├── PowerPC <- PowerPC 引擎
│ ├── Sparc <- Sparc 引擎
│ ├── SystemZ <- SystemZ 引擎
│ ├── TMS320C64x <- TMS320C64x 引擎
│ ├── X86 <- X86 引擎
│ └── XCore <- XCore 引擎
├── bindings <- 中間件
│ ├── java <- Java 中間件 + 測試代碼
│ ├── ocaml <- Ocaml 中間件 + 測試代碼
│ └── python <- Python 中間件 + 測試代碼
├── contrib <- 社區代碼
├── cstool <- Cstool 檢測工具源碼
├── docs <- 文檔,主要是capstone的實現思路
├── include <- C頭文件
├── msvc <- Microsoft Visual Studio 支持(Windows)
├── packages <- Linux/OSX/BSD包
├── windows <- Windows 支持(Windows內核驅動編譯)
├── suite <- Capstone開發測試工具
├── tests <- C語言測試用例
└── xcode <- Xcode 支持 (MacOSX 編譯)
由於項目原生支持 Windows,本次使用 Win10+Microsoft Visual Studio 2015 進行編譯。使用 VS 2015 直接打開 msvc 目錄下的 unicorn.sln
,即可自動載入項目。右擊 解決方案
-> 屬性
,可以彈出如下屬性頁,從而選擇自己想編譯的項目。
當然,你也可以直接在項目右下角的 屬性
視圖裏面進行快捷設置
在項目的編譯屬性中,設置如下,其餘設置默認即可
- 不使用預編譯頭
- 附加選項 /wd4018 /wd4244 /wd4267
編譯完成之後,會在項目文件夾的編譯器目錄下(如果你使用的是 Win32 編譯器,那麼就是 Win32)的 Debug
目錄中生成相應的鏈接庫(unicorn.lib 靜態鏈接庫 + unicorn.dll 動態鏈接庫)
這兩個庫以及項目的頭文件,就是我們開發需要的東西了。
0x22 新建項目調用引擎
新建一個VS工程(Win32 項目 或者 控制檯項目)。將…\unicorn-master\include\unicorn中的頭文件以及編譯好的 lib 和 dll 文件全部拷貝到新建項目的主目錄下
#include<iostream>
#include "include\unicorn\unicorn.h"
#define X86_CODE "\x41\x4a" // 要模擬的指令
#define ADDRESS 0x2000000 // 起始地址
using namespace std;
int main()
{
uc_engine * uc;
uc_err err;
int r_ecx = 0x1234;
int r_edx = 0x5678;
cout << "Emulate i386 code" << endl;
/*x86模式初始化*/
err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc);
if (err != UC_ERR_OK)
{
cout << "Something error in uc_open() %u" << err << endl;
return -1;
}
/*申請模擬器內存大小*/
uc_mem_map(uc, ADDRESS, 4 * 1024 * 1024, UC_PROT_ALL); // 4MB
/*要模擬的指令寫入寄存器*/
if (uc_mem_write(uc, ADDRESS, X86_CODE, sizeof(X86_CODE) - 1))
{
cout << "Failed to write emulation code to memory, abort" << endl;
return -1;
}
/*初始化寄存器*/
uc_reg_write(uc, UC_X86_REG_ECX, &r_ecx);
uc_reg_write(uc, UC_X86_REG_EDX, &r_edx);
printf(">>> ecx = 0x%x\n", r_ecx);
printf(">>> edx = 0x%x\n", r_edx);
/*模擬代碼*/
err = uc_emu_start(uc, ADDRESS, ADDRESS + sizeof(X86_CODE) - 1, 0, 0);
if (err)
{
printf("Something wrong in uc_emu_start(), error code: %u %s\n", err, uc_strerror(err));
return -1;
}
/*打印寄存器內容*/
printf("Emulation done. Blew is the CPU context\n");
uc_reg_read(uc, UC_X86_REG_ECX, &r_ecx);
uc_reg_read(uc, UC_X86_REG_EDX, &r_edx);
printf(">>> ecx = 0x%x\n", r_ecx);
printf(">>> edx = 0x%x\n", r_edx);
uc_close(uc);
return 0;
}
在 解決方案資源管理器
頭文件添加現有項 unicorn.h
,資源文件中添加 unicorn.lib
,重新生成解決方案。編譯並運行。如下圖所示,成功的執行指令 ecx + 1
edx - 1
。如果報錯,請參考 0x3 Q&A
0x23 關鍵代碼解析
- 第4行:我們要模擬的原始二進制代碼。此示例中的代碼處於十六進制模式,代表兩個X86指令“ INC ecx ”和“ DEC edx ” 3
- 第11行:聲明一個指向uc_engine類型的句柄的指針。該句柄將在Unicorn的每個API中使用
- 第12行:聲明數據類型爲uc_err的變量,以防 Unicor API返回錯誤。
- 第53行:調用uc_close函數完成仿真
其餘代碼請看下面的函數解析
0x24 API 解析: unicorn.h 關鍵函數
uc_engine
是 uc_struct
的別名,這裏第一行代碼,是定義 uc
爲指向 unicorn engine
的指針。
uc_err
是錯誤類型,是 uc_errno()
的返回值
typedef enum uc_err {
UC_ERR_OK = 0, // 無錯誤
UC_ERR_NOMEM, // 內存不足: uc_open(), uc_emulate()
UC_ERR_ARCH, // 不支持的架構: uc_open()
UC_ERR_HANDLE, // 不可用句柄
UC_ERR_MODE, // 不可用/不支持架構: uc_open()
UC_ERR_VERSION, // 不支持版本 (中間件)
UC_ERR_READ_UNMAPPED, // 由於在未映射的內存上讀取而退出模擬: uc_emu_start()
UC_ERR_WRITE_UNMAPPED, // 由於在未映射的內存上寫入而退出模擬: uc_emu_start()
UC_ERR_FETCH_UNMAPPED, // 由於在未映射的內存中獲取數據而退出模擬: uc_emu_start()
UC_ERR_HOOK, // 無效的hook類型: uc_hook_add()
UC_ERR_INSN_INVALID, // 由於指令無效而退出模擬: uc_emu_start()
UC_ERR_MAP, // 無效的內存映射: uc_mem_map()
UC_ERR_WRITE_PROT, // 由於UC_MEM_WRITE_PROT衝突而停止模擬: uc_emu_start()
UC_ERR_READ_PROT, // 由於UC_MEM_READ_PROT衝突而停止模擬: uc_emu_start()
UC_ERR_FETCH_PROT, // 由於UC_MEM_FETCH_PROT衝突而停止模擬: uc_emu_start()
UC_ERR_ARG, // 提供給uc_xxx函數的無效參數
UC_ERR_READ_UNALIGNED, // 未對齊讀取
UC_ERR_WRITE_UNALIGNED, // 未對齊寫入
UC_ERR_FETCH_UNALIGNED, // 未對齊的提取
UC_ERR_HOOK_EXIST, // 此事件的鉤子已經存在
UC_ERR_RESOURCE, // 資源不足: uc_emu_start()
UC_ERR_EXCEPTION, // 未處理的CPU異常
UC_ERR_TIMEOUT // 模擬超時
} uc_err;
uc_open()
用於創建 unicorn 實例
uc_err uc_open(uc_arch arch, uc_mode mode, uc_engine **uc);
@arch: 架構類型 (UC_ARCH_*)
@mode: 硬件模式. 由 UC_MODE_* 組合
@uc: 指向 uc_engine 的指針, 返回時更新
@return 成功則返回UC_ERR_OK , 否則返回 uc_err 枚舉的其他錯誤類型
uc_mem_map()
爲模擬器映射一塊內存
uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);
@uc: uc_open() 返回的句柄
@address: 要映射到的新內存區域的起始地址。這個地址必須與4KB對齊,否則將返回UC_ERR_ARG錯誤。
@size: 要映射到的新內存區域的大小。這個大小必須是4KB的倍數,否則將返回UC_ERR_ARG錯誤。
@perms: 新映射區域的權限。參數必須是UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC或這些的組合,否則返回UC_ERR_ARG錯誤。
@return 成功則返回UC_ERR_OK , 否則返回 uc_err 枚舉的其他錯誤類型
uc_mem_write()
向內存寫入一段字節碼
uc_err uc_mem_write(uc_engine *uc, uint64_t address, const void *bytes, size_t size);
@uc: uc_open() 返回的句柄
@address: 寫入字節的起始地址
@bytes: 指向一個包含要寫入內存的數據的指針
@size: 要寫入的內存大小。
注意: @bytes 必須足夠大以包含 @size 字節。
@return 成功則返回UC_ERR_OK , 否則返回 uc_err 枚舉的其他錯誤類型
uc_reg_write()
向寄存器寫入值
uc_err uc_reg_write(uc_engine *uc, int regid, const void *value);
@uc: uc_open()返回的句柄
@regid: 將被修改的寄存器ID
@value: 指向寄存器將被修改成的值的指針
@return 成功則返回UC_ERR_OK , 否則返回 uc_err 枚舉的其他錯誤類型
uc_reg_read
讀取寄存器的值
uc_err uc_reg_read(uc_engine *uc, int regid, void *value);
@uc: uc_open()返回的句柄
@regid: 將被讀取的寄存器ID
@value: 指向保存寄存器值的指針
@return 成功則返回UC_ERR_OK , 否則返回 uc_err 枚舉的其他錯誤類型
0x30 Q&A
0x31 使用了不安全函數
在預處理器中,添加相應的定義即可
或者在程序中添加以下任意一行代碼
#define _CRT_SECURE_NO_DEPRECATE;
#define _CRT_SECURE_NO_WARNINGS;
#pragma warning(disable:4996);
重新編譯,並運行。
0x32 error LNK2019: 無法解析的外部符號 _main
打開項目屬性頁,修改 調試器
>系統
> 子系統
,切換成控制檯(如果原來是控制檯,則切換爲窗口)
0x32 無法查找或打開 PDB 文件
【工具】->【選項】->【調試】->【常規]】勾選“啓用源服務器支持” 4
【工具】->【選項】->【調試】->【符號】,勾選“Microsoft符號服務器”
0x40 基於 Python 調用 Unicorn Engine
使用 Python 調用 Unicorn 相對來說,要簡單許多。當然首先得安裝相應的包
pip install unicorn -i https://pypi.mirrors.ustc.edu.cn/simple/
from __future__ import print_function
from unicorn import *
from unicorn.x86_const import *
# code to be emulated
X86_CODE32 = b"\x41\x4a" # INC ecx; DEC edx
# memory address where emulation starts
ADDRESS = 0x1000000
print("Emulate i386 code")
try:
# Initialize emulator in X86-32bit mode
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# map 2MB memory for this emulation
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# write machine code to be emulated to memory
mu.mem_write(ADDRESS, X86_CODE32)
# initialize machine registers
mu.reg_write(UC_X86_REG_ECX, 0x1234)
mu.reg_write(UC_X86_REG_EDX, 0x5678)
# emulate code in infinite time & unlimited instructions
mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
# now print out some registers
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)
輸出結果
- 第2〜3行:在使用Unicorn之前,導入unicorn模塊。此示例還使用了一些X86寄存器常量,因此也需要unicorn.x86_const
- 第6行:要模擬的原始二進制代碼。此示例中的代碼處於十六進制模式,代表兩個X86指令“ INC ecx ”和“ DEC edx ”
- 第9行:將在其中模擬上面的代碼的虛擬地址
- 第14行:使用類Uc初始化Unicorn 。此類接受2個參數:硬件體系結構和硬件模式。在此示例中,我們要模擬X86體系結構的32位代碼。mu接受返回值
- 第17行:mem_map在第9行聲明的地址處映射2MB的內存用於此仿真。在此過程中,所有CPU操作都只能訪問該內存。該內存使用默認權限READ,WRITE和EXECUTE映射
- 第20行:將要模擬的代碼寫入到我們上面剛剛映射的內存中。mem_write方法採用2個參數:要寫入的地址和要寫入內存的代碼
- 23〜24行:使用reg_write方法設置ECX和EDX寄存器的值
- 第27行:使用方法emu_start啓動仿真。該API包含4個參數:仿真代碼的地址,仿真停止的地址(緊隨X86_CODE32的最後一個字節之後),要仿真的時間以及要仿真的指令數。如果像本例一樣忽略最後兩個參數,Unicorn將在無限的時間和無限數量的指令中模擬代碼
- 第32〜35行:打印出寄存器ECX和EDX的值。我們使用reg_read函數讀取寄存器的值
0x50 總結
Unicorn Engine 用於仿真各種架構的 CPU 指令集,在 QEMU 的基礎上,擴展了很多新特性和新功能,本文初步介紹了其簡單的用法,利用 C 和 Python 調用了其 API,模擬了 x86 指令集,如果用其實際開發一個項目,你將會體會到 Unicorn 的獨特魅力。在這裏,是希望通過簡短的介紹,讓大家能夠從源碼以及用法的角度上,對 Unicorn 有一個更加全面的瞭解,爲後續 Afl-Unicorn 進行 Fuzz 測試,或者編寫自己的模擬器,提供一種思想上的理念。附錄中的參考網址,都是值得學習的平臺,尤其感謝 kabeor製作的非官方 API 參考文檔。