Tutorial for Unicorn:Unicorn Engine 的開發和使用

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_engineuc_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 參考文檔。


  1. http://www.unicorn-engine.org/ ↩︎

  2. https://github.com/kabeor/Micro-Unicorn-Engine-API-Documentation/blob/master/Micro%20Unicorn-Engine%20API%20Documentation.md ↩︎

  3. http://www.unicorn-engine.org/docs/tutorial.html ↩︎

  4. https://blog.csdn.net/qq_38410428/article/details/102720550 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章