首页
社区
课程
招聘
[原创]深入浅出 Unicorn 框架学习
发表于: 23小时前 999

[原创]深入浅出 Unicorn 框架学习

23小时前
999

Unicorn 是一个基于 QEMU 的轻量级、多平台、多架构 CPU 模拟器框架。

相比于重量级的符号执行框架(如 Angr),Unicorn 虽然没有内置 Z3 约束求解器,但凭借其极致的性能、友好的 API 设计以及强大的 Hook 机制(指令级回调),成为了二进制分析、反混淆和 CTF 竞赛中的利器。

简单来说,Unicorn 是一款 ​CPU 模拟器​。
但与 VMWare 或 Android 模拟器不同,Unicorn 不负责模拟整个操作系统或完整的硬件环境,它​不支持系统调用 (Syscall)

你需要像操作“裸机”一样:

为了避免污染系统的 Python 环境,推荐使用 Conda 进行环境隔离。

使用 pip 安装官方的 Python 绑定:

验证安装
在终端输入 python 进入交互模式,尝试导入:

图片描述

如果无报错且输出了版本号,说明环境搭建成功。

打开你的 IDE(如 PyCharm 或 VSCode),创建一个新的 Python 项目。在解释器设置(Interpreter Settings)中,选择 Existing Environment(现有环境),并指向刚刚创建的 Conda 环境路径。

图片描述

学习 Unicorn 的过程,其实就是学习如何​手动扮演操作系统的角色,为 CPU 准备好它运行所需的一切资源。

我们将按照以下顺序攻克 Unicorn 的核心要素:

在开始编写 Unicorn 代码之前,我们必须建立一个清晰的思维模型。使用 Unicorn 就像是在扮演一个“手动挡”的操作系统,你必须亲手为 CPU 准备好它运行所需的一切资源。所有的 Unicorn 脚本(无论多么复杂)都逃不出以下 5 个标准步骤:

首先,我们需要引入 Unicorn 库,并实例化一个 Uc 对象。这一步决定了我们要模拟的硬件环境。

Unicorn 中的 CPU 只能访问已被映射的内存区域。我们需要像操作系统一样,规划并申请虚拟内存空间。

有了内存空间后,我们需要将机器码(Shellcode)或二进制文件内容写入其中。这相当于加载器(Loader)将程序加载到内存的过程。

在运行前,必须初始化 CPU 的关键寄存器。最重要的是设置 指令指针 (RIP/EIP) 和 ​栈指针 (RSP/ESP)

一切准备就绪,指定入口点和结束点,按下“启动键”。

API​:mu.emu_start(begin, end)

在 Unicorn 中,一切的起点都是 Uc 类的实例化。这个步骤决定了你要模拟的“硬件规格”——即 CPU 的架构(Architecture)和运行模式(Mode)。

要创建一个模拟器实例,我们需要传入两个核心参数:

Unicorn 支持多种主流架构,以下是逆向分析中最常用的几种:

模式常量用于进一步细分 CPU 的工作状态。需要注意的是,​不同的架构支持的模式不同​,且模式可以通过 + 号进行组合。

在实际使用中,我们经常需要组合使用这些常量。

场景 A:模拟 PC 上的 32位 Windows 程序

场景 B:模拟 Android 上的 ARM64 代码

场景 C:模拟 ARM Thumb 指令
ARM 处理器可以在 ARM 状态(4字节指令)和 Thumb 状态(2字节指令)之间切换。

场景 D:模拟 IoT 设备的 MIPS 大端序程序
某些路由器固件使用大端序 MIPS。

Unicorn 模拟器内部没有操作系统的 malloc​ 或 Heap 管理器。作为“上帝视角”的控制者,你必须手动管理每一页内存的分配、读写和释放。

Unicorn 的内存操作 API 非常精简,核心只有三个:​映射(申请) ​、写入 和 ​读取

在 CPU 访问任何内存地址之前,该地址必须先被“映射”。访问未映射的内存会导致 UC_ERR_READ_UNMAPPED​ 或 UC_ERR_WRITE_UNMAPPED 异常。

API​: uc.mem_map(address, size, perms=UC_PROT_ALL)

4KB 对齐:
现代操作系统和 CPU 通常以“页(Page)”为单位管理内存,一页通常是 4096 字节 (0x1000)。
如果你尝试申请 0x100​ 字节,Unicorn 会直接报错。必须向上取整到 0x1000

有了内存空间后,我们需要将机器码(Shellcode)或数据填充进去。

API​: uc.mem_write(address, data)

在模拟执行结束后,或者在 Hook 回调中,我们通常需要读取内存中的数据来验证计算结果。

API​: uc.mem_read(address, size)

mem_map​ 或 mem_protect​ 中使用,用于控制内存页的读写执行权限(类似于 Linux 的 mprotect)。

示例:修改内存权限

寄存器是 CPU 的“内部工作台”,几乎所有的运算指令都依赖于它。在 Unicorn 中,我们通过统一的接口来读写不同架构下的数百个寄存器。

核心 API 非常直观:读 (Read) 和 ​写 (Write)

在开始模拟执行之前,通常需要初始化一些关键寄存器:

模拟执行结束后,或者在 Hook 回调函数中,我们需要读取寄存器的值来检查程序的运行状态或计算结果。

API​: uc.reg_read(reg_id)

Unicorn 为每种架构都定义了海量的寄存器常量。为了方便使用,我们需要导入对应架构的 const 模块。

x86 / x64 架构

ARM 架构

ARM64 架构

当内存和寄存器都准备就绪后,最后一步就是按下“启动键”,让虚拟 CPU 开始运转。Unicorn 提供了灵活的执行控制接口,允许我们指定运行范围、时间限制甚至指令数量。

这是 Unicorn 中唯一用于启动 CPU 的 API。调用它是阻塞的,意味着直到模拟结束(或报错),Python 脚本才会继续往下执行。

参数详解

begin(int) ​: ​起始地址。CPU 将从这里提取第一条指令。

end(int) ​: ​结束地址

timeout(int, 可选) ​: ​超时时间​(单位:​微秒 / microseconds)。

count(int, 可选) ​: ​指令计数

在某些情况下(例如在 Hook 回调函数中检测到了某个特定条件,或者想要实现断点功能),我们需要中途强行停止模拟。

注意​:emu_stop​ 通常配合 Hook 使用。

如果说 emu_start​ 是让程序跑起来,那么 Hook 就是让程序“透明化”。
Hook 机制是 Unicorn 最强大的特性之一。它允许我们在模拟执行的过程中插入自定义的回调函数(Callback),相当于在 CPU 内部安装了无数个“监控探头”。

通过 Hook,我们可以实现:

API​: uc.hook_add(hook_type, callback, user_data=None, begin=1, end=0)

API​: uc.hook_del(hook_handle)

Unicorn 的 Hook 系统非常灵活,以下是三种最基础且最常用的 Hook 类型:

回调函数签名

注册示例

作用:当 CPU 尝试读取、写入或获取内存指令时触发。

常量组合

回调函数签名

注册示例

回调函数签名

注册示例

将上述三种 Hook 结合起来,我们就能得到一个功能完备的“调试监视器”。它可以帮我们实时追踪指令流,并在发生非法内存访问时自动报警。

纸上得来终觉浅。现在,我们将前面的所有知识点串联起来,编写一个完整的 Unicorn 脚本。
我们将从零开始构建一个 x86-32 虚拟环境,并让它执行一条最简单的汇编指令:INC EAX (将 EAX 寄存器的值加 1)。

当你运行这段代码时,终端输出如下内容:

图片描述

在代码中我们使用了 try...except UcError​ 结构。这是编写 Unicorn 脚本的最佳实践。
如果我们在 mem_map​ 时传入了非 4KB 对齐的大小,或者试图执行未写入指令的内存区域,Unicorn 会抛出 UcError

常见错误代码:

在掌握了 Unicorn 的基础操作后,我们将目光转向更复杂的真实场景。真实的程序不仅仅是简单的加减乘除,它们涉及文件 IO、内存管理、函数调用以及复杂的控制流。

本章节将围绕 “如何模拟一个真实的 ELF/PE 可执行文件” 展开,逐步解决以下核心挑战:

在二进制分析的战场上,单打独斗往往力不从心。为了构建一个功能完备的分析环境,我们通常会将 Unicorn 与另外两款神器——CapstoneKeystone 结合使用。

这三者同宗同源(均由同一团队开发),被誉为逆向工程界的“三剑客”:

在 Unicorn 的 UC_HOOK_CODE 回调中,我们只能拿到当前指令的地址和长度。如果不进行反汇编,我们无法知道具体执行了什么逻辑。

基本用法示例

有时候我们需要在模拟过程中动态修改程序行为,比如将一条复杂的 CALL​ 指令替换为 NOP,或者直接注入一段 Shellcode。Keystone 让我们可以直接写汇编,而不用手动拼凑十六进制机器码。

基本用法示例

这里只做简单了解,具体的学习文章后续更新。

挑战​:Unicorn 只是一个裸机 CPU,它没有操作系统内核,也没有加载标准库(libc.so / kernel32.dll)。
当被模拟的程序执行到 call printf​ 或 call malloc 时,CPU 会跳转到这些函数的地址。但在 Unicorn 的内存中,这些地址通常是未映射的空洞,或者只有符号没有代码。直接执行会导致 Crash。

为了让程序继续跑下去,我们需要手动接管这些“外部调用”。

对于那些不影响核心逻辑的函数(如打印日志、Sleep、复杂的系统初始化),我们可以直接跳过。

对于影响程序逻辑的关键函数(如 malloc​、strcpy​、AES_encrypt​),我们不能简单跳过,而是要用 Python 代码来“重写”它们的功能。这被称为 ​High Level Emulation (HLE)

概念
在现代操作系统中,用户态程序无法直接访问硬件(如读写磁盘、网络通信)。它们必须通过特殊的指令——SYSCALL​ (x64) 或 INT 0x80 (x86)——陷入内核态,请求操作系统服务。
由于 Unicorn 只是用户态模拟器,遇到这些指令时会停止或报错。我们需要捕获这些信号,并用 Python 代码扮演“操作系统内核”的角色。

Unicorn 提供了专门的 Hook 类型来拦截这些事件:

注意: 通常我们统一使用 UC_HOOK_INTR 来捕获所有类型的中断和系统调用请求,然后根据中断号(intno)进行判断。

我们将实现一个迷你 Linux 内核,支持程序打印字符串 (sys_write​) 和正常退出 (sys_exit)。

Unicorn 不仅仅是一个执行器,它更是一个“全知全能”的观察者。通过模拟执行,我们可以拿到静态分析(IDA)无法获取的运行时数据。利用这些数据,我们可以对抗 OLLVM 等现代混淆技术。

痛点​:OLLVM 将简单的 if-else​ 或 while​ 循环打碎,塞进一个巨大的 switch-case 分发器中(平坦化),导致流程图像一团乱麻。

还原思路

纸上得来终觉浅,我们通过一个真实的 CTF 题目来实战演练 Unicorn 的使用技巧。本题源自 ​**hxpCTF 2017**,名为 Fibonacci

题目附件: cbdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2N6r3g2J5L8X3q4D9i4K6u0W2M7X3g2V1i4K6u0r3j5i4y4K6k6i4c8K6i4K6u0r3k6X3W2D9k6i4y4Q4x3V1j5J5x3o6p5%4i4K6u0r3g2f1g2Q4x3V1k6X3K9h3u0G2L8X3q4U0j5$3V1`.
目标: 程序会计算并输出 Flag。我们的任务是让 Unicorn 模拟运行它,并优化执行速度以获取完整 Flag。

当我们运行这个程序的时候,可以注意到这个程序计算和输出Flag非常的慢。Flag的下一个字节计算的越来越慢。

图片描述

在拿到二进制文件后,第一步是让它跑起来。我们需要解决内存映射、寄存器初始化以及外部函数调用等问题。

根据前面学习的知识,我们先搭好架子:

当我们首次运行脚本时,Unicorn 会抛出 UC_ERR_READ_UNMAPPED​ 错误,提示读取 0x601038 失败。

原因分析
在 IDA 中查看该地址,发现它是 stdout(标准输出流指针)。

图片描述

程序在 main​ 函数中调用 setbuf(stdout, NULL)​ 或 putc(..., stdout)​ 时,会尝试读取这个指针。由于 Unicorn 没有操作系统环境,.bss 段的这个变量未被 libc 初始化,因此指向了无效区域。

解决方案
对于这种只影响输出缓冲、不影响核心算法逻辑的代码,直接 Patch (跳过) 即可。我们需要跳过以下地址的指令:

图片描述

图片描述

程序使用 printf​ 输出提示信息,用 putc​ 逐个输出 Flag 字符。我们需要 Hook 这些地址,用 Python 的 print 替代。

运行结果

程序成功运行并输出了前三个字符 hxp,但随后似乎陷入了卡顿。这是因为程序内部的算法效率极低。

在能够成功运行程序后,我们发现输出 Flag 的速度极慢。通过分析代码可知,该程序使用的是递归方式计算斐波那契数列,时间复杂度为指数级 O(2^n) 。如果不进行优化,模拟可能需要数小时甚至数天。

图片描述

为了解决这个问题,我们采用 Unicorn Hook 实现 记忆化搜索 (Memoization) 算法,以空间换时间。

我们需要拦截目标函数 fibonacci​ 的入口和​出口

建立缓存 (Cache) ​:
使用字典 fibonacci_dp​ 存储计算结果,映射关系为:{(参数1, 参数2): (返回值RAX, 返回值RSI)}

处理函数入口 (On Enter)

处理函数出口 (On Leave)

加入记忆化搜索算法后,原本需要数小时的计算过程被瞬间完成:

图片描述

通过 Unicorn,我们在没有修改任何二进制文件的情况下,仅通过 Python 脚本就实现了对目标程序算法的动态热补丁 (Hot Patching) 优化。

5cbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3e0$3&6D9P5g2)9J5k6s2S2A6j5h3!0^5K9h3q4G2i4K6u0r3M7q4)9J5c8U0p5%4x3K6p5$3x3K6b7K6i4K6u0W2K9s2c8E0L8l9`.`.

https://bbs.kanxue.com/thread-224330-1.htm

f5dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2N6r3g2J5L8X3q4D9i4K6u0W2M7X3g2V1i4K6u0r3x3U0l9I4z5q4)9J5c8Y4g2F1K9h3y4G2M7X3&6Q4x3X3c8W2L8X3N6A6L8X3g2Q4x3X3c8@1N6i4c8G2M7X3W2S2L8q4)9J5c8R3`.`.

50bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1j5I4K9K6m8U0N6q4)9J5k6h3N6A6N6r3S2#2j5W2)9J5k6h3W2G2i4K6u0r3x3U0l9J5y4g2)9J5c8U0l9@1i4K6u0r3x3U0y4Q4x3V1k6g2L8X3W2U0L8%4u0F1i4K6t1#2c8e0g2Q4x3U0g2n7b7#2)9J5y4e0V1#2i4K6t1#2c8e0k6Q4x3U0f1&6x3#2)9J5y4e0S2q4i4K6t1#2c8e0g2Q4x3U0g2m8c8q4)9J5y4f1p5$3i4K6t1#2c8e0c8Q4x3U0g2n7z5g2)9J5y4f1p5H3i4K6u0r3

758K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2A6L8%4c8K6k6h3y4Q4x3X3c8*7L8$3&6W2i4K6u0W2j5$3!0E0i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8U0x3^5x3l9`.`.

# 创建名为 "Unicorn" 的环境,指定 python 版本为 3.9
conda create -n Unicorn python=3.9
# 创建名为 "Unicorn" 的环境,指定 python 版本为 3.9
conda create -n Unicorn python=3.9
# 进入虚拟环境
conda activate Unicorn
# 进入虚拟环境
conda activate Unicorn
# 安装 unicorn 核心库
pip install unicorn
# 安装 unicorn 核心库
pip install unicorn
import unicorn
print(unicorn.__version__)
import unicorn
print(unicorn.__version__)
from unicorn import *
# 加载 x86 和 x86-64 两种架构的常量
from unicorn.x86_const import *
 
# 初始化一个 X86-64 架构的模拟器实例
# UC_ARCH_X86: 架构
# UC_MODE_64:  64位模式
mu = Uc(UC_ARCH_X86, UC_MODE_64)
from unicorn import *
# 加载 x86 和 x86-64 两种架构的常量
from unicorn.x86_const import *
 
# 初始化一个 X86-64 架构的模拟器实例
# UC_ARCH_X86: 架构
# UC_MODE_64:  64位模式
mu = Uc(UC_ARCH_X86, UC_MODE_64)
# 定义内存布局常量
ADDRESS = 0x400000          # 代码段基址
MEM_SIZE = 2 * 1024 * 1024  # 申请 2MB 内存
 
STACK_ADDR = 0x0            # 栈内存基址(示例)
STACK_SIZE = 1024 * 1024    # 1MB 栈空间
 
# 1. 映射代码段内存
mu.mem_map(ADDRESS, MEM_SIZE)
 
# 2. 映射栈内存
mu.mem_map(STACK_ADDR, STACK_SIZE)
# 定义内存布局常量
ADDRESS = 0x400000          # 代码段基址
MEM_SIZE = 2 * 1024 * 1024  # 申请 2MB 内存
 
STACK_ADDR = 0x0            # 栈内存基址(示例)
STACK_SIZE = 1024 * 1024    # 1MB 栈空间
 
# 1. 映射代码段内存
mu.mem_map(ADDRESS, MEM_SIZE)
 
# 2. 映射栈内存
mu.mem_map(STACK_ADDR, STACK_SIZE)
# 读取本地二进制文件(Shellcode)
with open("./test", "rb") as f:
    CODE = f.read()
 
# 将代码写入到基址 0x400000 处
mu.mem_write(ADDRESS, CODE)
# 读取本地二进制文件(Shellcode)
with open("./test", "rb") as f:
    CODE = f.read()
 
# 将代码写入到基址 0x400000 处
mu.mem_write(ADDRESS, CODE)
# 初始化栈指针 (RSP)
# 注意:栈通常是向下增长的,所以指针应指向栈内存的高地址末端
# 并且通常需要保持对齐(如 8字节或16字节对齐)
mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 8)
 
# (可选) 初始化其他通用寄存器,作为函数参数
# mu.reg_write(UC_X86_REG_RAX, 0x1)
# 初始化栈指针 (RSP)
# 注意:栈通常是向下增长的,所以指针应指向栈内存的高地址末端
# 并且通常需要保持对齐(如 8字节或16字节对齐)
mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 8)
 
# (可选) 初始化其他通用寄存器,作为函数参数
# mu.reg_write(UC_X86_REG_RAX, 0x1)
# 开始模拟执行
# 从代码基址开始,执行到代码末尾
try:
    print(">>> Start emulation...")
    mu.emu_start(ADDRESS, ADDRESS + len(CODE))
    print(">>> Emulation done.")
except UcError as e:
    print(f"ERROR: {e}")
 
# 执行结束后,可以读取寄存器查看结果
r_rax = mu.reg_read(UC_X86_REG_RAX)
print(f">>> RAX = 0x{r_rax:x}")
# 开始模拟执行
# 从代码基址开始,执行到代码末尾
try:
    print(">>> Start emulation...")
    mu.emu_start(ADDRESS, ADDRESS + len(CODE))
    print(">>> Emulation done.")
except UcError as e:
    print(f"ERROR: {e}")
 
# 执行结束后,可以读取寄存器查看结果
r_rax = mu.reg_read(UC_X86_REG_RAX)
print(f">>> RAX = 0x{r_rax:x}")
# 原型:Uc(arch, mode)
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# 原型:Uc(arch, mode)
mu = Uc(UC_ARCH_X86, UC_MODE_32)
常量名 对应架构 说明
UC_ARCH_X86 x86 / x64 包含 Intel/AMD 的 16位、32位和 64位架构。
UC_ARCH_ARM ARM 经典的 32位 ARM 架构(常见于旧版 Android)。
UC_ARCH_ARM64 AArch64 现代移动设备(Android/iOS)的主流 64位架构。
UC_ARCH_MIPS MIPS 常见于路由器、IoT 设备。
常量名 说明 适用架构
UC_MODE_32 32位模式 x86, ARM, MIPS 等
UC_MODE_64 64位模式 x86, ARM64, MIPS64 等
UC_MODE_THUMB Thumb 模式 仅限 ARM。用于模拟 16位 Thumb 指令集。
UC_MODE_LITTLE_ENDIAN 小端序 (默认) 所有架构。数据低位存储在低地址。
UC_MODE_BIG_ENDIAN 大端序 MIPS, PowerPC 等。数据高位存储在低地址。
# 标准的 x86 32位环境
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# 标准的 x86 32位环境
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# 标准的 ARM64 小端序环境
mu = Uc(UC_ARCH_ARM64, UC_MODE_64)
# 标准的 ARM64 小端序环境
mu = Uc(UC_ARCH_ARM64, UC_MODE_64)
# 开启 ARM 的 Thumb 模式
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
# 开启 ARM 的 Thumb 模式
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
# 组合模式:32位 + 大端序
mu = Uc(UC_ARCH_MIPS, UC_MODE_32 + UC_MODE_BIG_ENDIAN)
# 组合模式:32位 + 大端序
mu = Uc(UC_ARCH_MIPS, UC_MODE_32 + UC_MODE_BIG_ENDIAN)
# 定义基址和大小
ADDRESS = 0x400000
# 错误写法:SIZE = 1024 (不是 4KB 倍数,会报错)
SIZE = 2 * 1024 * 1024  # 正确:2MB
 
# 申请 2MB 内存,默认权限 rwx (可读可写可执行)
mu.mem_map(ADDRESS, SIZE)
 
# 进阶:申请一块“只读”数据区 (UC_PROT_READ)
# 常用于模拟 .rodata 段
DATA_ADDR = 0x800000
mu.mem_map(DATA_ADDR, 0x1000, UC_PROT_READ)
# 定义基址和大小
ADDRESS = 0x400000
# 错误写法:SIZE = 1024 (不是 4KB 倍数,会报错)
SIZE = 2 * 1024 * 1024  # 正确:2MB
 
# 申请 2MB 内存,默认权限 rwx (可读可写可执行)
mu.mem_map(ADDRESS, SIZE)
 
# 进阶:申请一块“只读”数据区 (UC_PROT_READ)
# 常用于模拟 .rodata 段
DATA_ADDR = 0x800000
mu.mem_map(DATA_ADDR, 0x1000, UC_PROT_READ)
# 机器码:INC EAX (0x40)
machine_code = b"\x40"
 
# 将机器码写入到刚才申请的代码段基址
mu.mem_write(ADDRESS, machine_code)
 
# 写入一个字符串到数据段
mu.mem_write(DATA_ADDR, b"Hello Unicorn")
# 机器码:INC EAX (0x40)
machine_code = b"\x40"
 
# 将机器码写入到刚才申请的代码段基址
mu.mem_write(ADDRESS, machine_code)
 
# 写入一个字符串到数据段
mu.mem_write(DATA_ADDR, b"Hello Unicorn")
# 读取刚才写入的字符串
# 读取 5 个字节 -> b'Hello'
data = mu.mem_read(DATA_ADDR, 5)
print(f"Read from memory: {bytes(data)}")
# 读取刚才写入的字符串
# 读取 5 个字节 -> b'Hello'
data = mu.mem_read(DATA_ADDR, 5)
print(f"Read from memory: {bytes(data)}")
常量名 权限 说明
UC_PROT_READ 可读 (r) 允许读取数据。
UC_PROT_WRITE 可写 (w) 允许写入数据。
UC_PROT_EXEC 可执行 (x) 允许 CPU 在此区域执行指令。
UC_PROT_ALL rwx 全权限(默认值)。
UC_PROT_NONE 无权限 禁止任何访问。
# 将代码段设为“只读且可执行” (r-x),模拟真实程序的 .text 段属性
# 防止代码在运行中被意外篡改
mu.mem_protect(ADDRESS, SIZE, UC_PROT_READ | UC_PROT_EXEC)
# 将代码段设为“只读且可执行” (r-x),模拟真实程序的 .text 段属性
# 防止代码在运行中被意外篡改
mu.mem_protect(ADDRESS, SIZE, UC_PROT_READ | UC_PROT_EXEC)
# --- 场景:模拟 x86 环境 ---
 
# 1. 初始化通用寄存器 EAX 为 100
mu.reg_write(UC_X86_REG_EAX, 100)
 
# 2. 初始化栈指针 ESP
# 栈通常是“向下增长”的,所以我们将 ESP 指向栈底(最高地址)
# 减去 4 或 8 是为了留出一点安全边距或对齐
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 4)
# --- 场景:模拟 x86 环境 ---
 
# 1. 初始化通用寄存器 EAX 为 100
mu.reg_write(UC_X86_REG_EAX, 100)
 
# 2. 初始化栈指针 ESP
# 栈通常是“向下增长”的,所以我们将 ESP 指向栈底(最高地址)
# 减去 4 或 8 是为了留出一点安全边距或对齐
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 4)
# 读取 EAX 的值,查看计算结果
eax_val = mu.reg_read(UC_X86_REG_EAX)
print(f">>> EAX = 0x{eax_val:x}")
 
# 在 Hook 中读取当前的指令指针 RIP (查看执行到了哪一行)
rip_val = mu.reg_read(UC_X86_REG_RIP)
# 读取 EAX 的值,查看计算结果
eax_val = mu.reg_read(UC_X86_REG_EAX)
print(f">>> EAX = 0x{eax_val:x}")
 
# 在 Hook 中读取当前的指令指针 RIP (查看执行到了哪一行)
rip_val = mu.reg_read(UC_X86_REG_RIP)
from unicorn.x86_const import *
 
# 通用寄存器
UC_X86_REG_EAX, UC_X86_REG_EBX, UC_X86_REG_ECX ...
UC_X86_REG_RAX, UC_X86_REG_RBX ... (64位)
 
# 关键指针
UC_X86_REG_EIP / UC_X86_REG_RIP  # 指令指针 (PC)
UC_X86_REG_ESP / UC_X86_REG_RSP  # 栈指针 (Stack)
UC_X86_REG_EFLAGS                # 标志位寄存器
from unicorn.x86_const import *
 
# 通用寄存器
UC_X86_REG_EAX, UC_X86_REG_EBX, UC_X86_REG_ECX ...
UC_X86_REG_RAX, UC_X86_REG_RBX ... (64位)
 
# 关键指针
UC_X86_REG_EIP / UC_X86_REG_RIP  # 指令指针 (PC)
UC_X86_REG_ESP / UC_X86_REG_RSP  # 栈指针 (Stack)
UC_X86_REG_EFLAGS                # 标志位寄存器
from unicorn.arm_const import *
 
# R0 - R15
UC_ARM_REG_R0, UC_ARM_REG_R1 ...
 
# 关键别名
UC_ARM_REG_PC  # Program Counter (R15)
UC_ARM_REG_SP  # Stack Pointer (R13)
UC_ARM_REG_LR  # Link Register (R14, 保存返回地址)
UC_ARM_REG_CPSR # 状态寄存器
from unicorn.arm_const import *
 
# R0 - R15
UC_ARM_REG_R0, UC_ARM_REG_R1 ...
 
# 关键别名
UC_ARM_REG_PC  # Program Counter (R15)
UC_ARM_REG_SP  # Stack Pointer (R13)
UC_ARM_REG_LR  # Link Register (R14, 保存返回地址)
UC_ARM_REG_CPSR # 状态寄存器
from unicorn.arm64_const import *
 
# X0 - X30
UC_ARM64_REG_X0 ...
 
# 关键指针
UC_ARM64_REG_PC
UC_ARM64_REG_SP
UC_ARM64_REG_LR  # Link Register (X30)
from unicorn.arm64_const import *
 
# X0 - X30
UC_ARM64_REG_X0 ...
 
# 关键指针
UC_ARM64_REG_PC
UC_ARM64_REG_SP
UC_ARM64_REG_LR  # Link Register (X30)
ADDRESS = 0x400000  # 代码基址
CODE_LEN = 2 * 1024 * 1024      # 代码长度
 
# 场景 A: 正常执行所有代码
# 从头跑到尾,没有时间限制
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN)
 
# 场景 B: 防止死循环 (设置 2秒 超时)
# 如果代码里有 while(1),2秒后会自动抛出 UC_ERR_TIMEOUT 异常
try:
    mu.emu_start(ADDRESS, ADDRESS + CODE_LEN, timeout=2 * 1000 * 1000)
except UcError as e:
    print(f"Execution stopped: {e}")
 
# 场景 C: 单步执行 (只跑第一条指令)
# 无论 end 设多大,跑完 1 条指令后立即暂停
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN, count=1)
ADDRESS = 0x400000  # 代码基址
CODE_LEN = 2 * 1024 * 1024      # 代码长度
 
# 场景 A: 正常执行所有代码
# 从头跑到尾,没有时间限制
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN)
 
# 场景 B: 防止死循环 (设置 2秒 超时)
# 如果代码里有 while(1),2秒后会自动抛出 UC_ERR_TIMEOUT 异常
try:
    mu.emu_start(ADDRESS, ADDRESS + CODE_LEN, timeout=2 * 1000 * 1000)
except UcError as e:
    print(f"Execution stopped: {e}")
 
# 场景 C: 单步执行 (只跑第一条指令)
# 无论 end 设多大,跑完 1 条指令后立即暂停
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN, count=1)
# 这是一个 Hook 回调函数 (后面会详细讲)
def hook_code(uc, address, size, user_data):
    print(f">>> Tracing instruction at 0x{address:x}")
     
    # 如果执行到了 0x1000020,强制停止
    if address == 0x1000020:
        print(">>> Breakpoint hit! Stopping emulation.")
        uc.emu_stop()
 
# 注册 Hook,然后启动
# ...
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN)
print(">>> Emulation finished or stopped by hook.")
# 这是一个 Hook 回调函数 (后面会详细讲)
def hook_code(uc, address, size, user_data):
    print(f">>> Tracing instruction at 0x{address:x}")
     
    # 如果执行到了 0x1000020,强制停止
    if address == 0x1000020:
        print(">>> Breakpoint hit! Stopping emulation.")
        uc.emu_stop()
 
# 注册 Hook,然后启动
# ...
mu.emu_start(ADDRESS, ADDRESS + CODE_LEN)
print(">>> Emulation finished or stopped by hook.")
def hook_code(uc, address, size, user_data):
    """
    uc:        Unicorn 引擎实例
    address:   当前即将执行的指令地址
    size:      当前指令的长度 (字节)
    user_data: hook_add 时传入的额外数据 (可以是任意对象)
    """
    print(f">>> Tracing instruction at 0x{address:x}, instruction size = {size}")
def hook_code(uc, address, size, user_data):
    """
    uc:        Unicorn 引擎实例
    address:   当前即将执行的指令地址
    size:      当前指令的长度 (字节)
    user_data: hook_add 时传入的额外数据 (可以是任意对象)
    """
    print(f">>> Tracing instruction at 0x{address:x}, instruction size = {size}")
# 1. 监控所有代码的执行 (全局)
mu.hook_add(UC_HOOK_CODE, hook_code)
 
# 2. 仅监控特定范围 (例如仅监控某个函数内部)
# begin: 起始地址, end: 结束地址
mu.hook_add(UC_HOOK_CODE, hook_code, begin=0x400000, end=0x400008)
# 1. 监控所有代码的执行 (全局)
mu.hook_add(UC_HOOK_CODE, hook_code)
 
# 2. 仅监控特定范围 (例如仅监控某个函数内部)
# begin: 起始地址, end: 结束地址
mu.hook_add(UC_HOOK_CODE, hook_code, begin=0x400000, end=0x400008)
def hook_mem_access(uc, access, address, size, value, user_data):
    """
    access:  访问类型 (如 UC_MEM_READ, UC_MEM_WRITE, UC_MEM_READ_UNMAPPED)
    address: 正在访问的内存地址
    size:    读写的数据长度
    value:   正在写入的数据值 (仅在 WRITE 类操作时有效,READ 时通常为 0)
    """
    if access == UC_MEM_WRITE:
        print(f">>> Memory WRITE at 0x{address:x}, size={size}, value=0x{value:x}")
    else:
        print(f">>> Memory READ at 0x{address:x}, size={size}")
         
    # 对于 UNMAPPED 类型的 Hook:
    # 返回 True  -> 忽略错误,跳过该指令继续执行 (非常危险,通常用于 Fuzz)
    # 返回 False -> 停止模拟,抛出 UcError 异常 (默认行为)
    return False
def hook_mem_access(uc, access, address, size, value, user_data):
    """
    access:  访问类型 (如 UC_MEM_READ, UC_MEM_WRITE, UC_MEM_READ_UNMAPPED)
    address: 正在访问的内存地址
    size:    读写的数据长度
    value:   正在写入的数据值 (仅在 WRITE 类操作时有效,READ 时通常为 0)
    """
    if access == UC_MEM_WRITE:
        print(f">>> Memory WRITE at 0x{address:x}, size={size}, value=0x{value:x}")
    else:
        print(f">>> Memory READ at 0x{address:x}, size={size}")
         
    # 对于 UNMAPPED 类型的 Hook:
    # 返回 True  -> 忽略错误,跳过该指令继续执行 (非常危险,通常用于 Fuzz)
    # 返回 False -> 停止模拟,抛出 UcError 异常 (默认行为)
    return False
# 同时监控内存读取和写入
mu.hook_add(UC_HOOK_MEM_READ | UC_HOOK_MEM_WRITE, hook_mem_access)
# 同时监控内存读取和写入
mu.hook_add(UC_HOOK_MEM_READ | UC_HOOK_MEM_WRITE, hook_mem_access)
def hook_intr(uc, intno, user_data):
    """
    intno: 中断号 (Interrupt Number)
    """
    # 可以在这里判断 intno,模拟系统调用逻辑
    print(f"[INT] Triggered interrupt number: {intno}")
     
    # 通常不需要返回值,或者通过修改 EIP/RIP 跳过中断指令
def hook_intr(uc, intno, user_data):
    """
    intno: 中断号 (Interrupt Number)
    """
    # 可以在这里判断 intno,模拟系统调用逻辑
    print(f"[INT] Triggered interrupt number: {intno}")
     
    # 通常不需要返回值,或者通过修改 EIP/RIP 跳过中断指令
mu.hook_add(UC_HOOK_INTR, hook_intr)
mu.hook_add(UC_HOOK_INTR, hook_intr)
# 1. 定义指令追踪回调
def trace_inst(uc, address, size, user_data):
    # 读取当前的 RIP 寄存器,验证 CPU 状态
    # 注意:需要确保已导入 unicorn.x86_const
    rip = uc.reg_read(UC_X86_REG_RIP)
    print(f"--- IP: 0x{rip:x} | Inst Size: {size} ---")
 
# 2. 定义异常内存访问回调 (Crash 捕获器)
def hook_mem_invalid(uc, access, address, size, value, user_data):
    # 建立映射表,将常量转换为可读字符串
    access_type = {
        UC_MEM_READ_UNMAPPED: "READ_UNMAPPED",
        UC_MEM_WRITE_UNMAPPED: "WRITE_UNMAPPED",
        UC_MEM_FETCH_UNMAPPED: "FETCH_UNMAPPED", # 指令预取错误(通常是EIP跑飞了)
    }.get(access, "UNKNOWN")
 
    print(f"[CRASH] Invalid Memory {access_type} at 0x{address:x}, size={size}")
 
    # 返回 False:通知 Unicorn 停止模拟,并抛出 Python 异常
    # 返回 True:强行忽略错误(不推荐,除非你手动修复了映射)
    return False
 
# 3. 定义中断回调
def hook_intr(uc, intno, user_data):
    print(f"[INT] Interrupt {intno} hit!")
 
# 4. 注册所有 Hook
print("[*] Installing hooks...")
 
# 追踪执行流
mu.hook_add(UC_HOOK_CODE, trace_inst)
 
# 监控非法内存访问 (Crash)
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_invalid)
 
# 监控中断
mu.hook_add(UC_HOOK_INTR, hook_intr)
 
# 5. 开始执行...
# mu.emu_start(...)
# 1. 定义指令追踪回调
def trace_inst(uc, address, size, user_data):
    # 读取当前的 RIP 寄存器,验证 CPU 状态
    # 注意:需要确保已导入 unicorn.x86_const
    rip = uc.reg_read(UC_X86_REG_RIP)
    print(f"--- IP: 0x{rip:x} | Inst Size: {size} ---")
 
# 2. 定义异常内存访问回调 (Crash 捕获器)
def hook_mem_invalid(uc, access, address, size, value, user_data):
    # 建立映射表,将常量转换为可读字符串
    access_type = {
        UC_MEM_READ_UNMAPPED: "READ_UNMAPPED",
        UC_MEM_WRITE_UNMAPPED: "WRITE_UNMAPPED",
        UC_MEM_FETCH_UNMAPPED: "FETCH_UNMAPPED", # 指令预取错误(通常是EIP跑飞了)
    }.get(access, "UNKNOWN")
 
    print(f"[CRASH] Invalid Memory {access_type} at 0x{address:x}, size={size}")
 
    # 返回 False:通知 Unicorn 停止模拟,并抛出 Python 异常
    # 返回 True:强行忽略错误(不推荐,除非你手动修复了映射)
    return False
 
# 3. 定义中断回调
def hook_intr(uc, intno, user_data):
    print(f"[INT] Interrupt {intno} hit!")
 
# 4. 注册所有 Hook
print("[*] Installing hooks...")
 
# 追踪执行流
mu.hook_add(UC_HOOK_CODE, trace_inst)
 
# 监控非法内存访问 (Crash)
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_invalid)
 
# 监控中断
mu.hook_add(UC_HOOK_INTR, hook_intr)
 
# 5. 开始执行...
# mu.emu_start(...)
from unicorn import *
from unicorn.x86_const import *
 
# ==========================================
# 1. 定义配置常量
# ==========================================
# x86机器码: INC EAX
# 对应的十六进制是 0x40 (在 64位模式下通常表示为 REX 前缀或直接操作)
# 这里为了演示简单,我们使用兼容的机器码
X86_CODE32 = b"\x40"  # INC EAX
 
# 内存布局
ADDRESS = 0x1000000  # 代码段基址
MEM_SIZE = 2 * 1024 * 1024  # 申请 2MB 内存
 
 
def test_x86():
    print("=== Unicorn x86-64 Demo Start ===")
 
    try:
        # ==========================================
        # 2. 初始化 (Initialize)
        # ==========================================
        # 初始化一个 x86 架构、32位 模式的模拟器
        mu = Uc(UC_ARCH_X86, UC_MODE_32)
 
        # ==========================================
        # 3. 映射内存 (Map Memory)
        # ==========================================
        # 申请 2MB 内存空间
        print(f"[*] Mapping memory at 0x{ADDRESS:x}, size={MEM_SIZE} bytes")
        mu.mem_map(ADDRESS, MEM_SIZE)
 
        # ==========================================
        # 4. 写入数据 (Write Data)
        # ==========================================
        # 将机器码写入内存基址
        print(f"[*] Writing machine code to 0x{ADDRESS:x}")
        mu.mem_write(ADDRESS, X86_CODE32)
 
        # ==========================================
        # 5. 设置环境 (Setup Context)
        # ==========================================
        # 将 EAX 寄存器初始化为 100
        print("[*] Setting EAX = 100")
        mu.reg_write(UC_X86_REG_EAX, 100)
 
        # ==========================================
        # 6. 开始执行 (Start Emulation)
        # ==========================================
        # 从 ADDRESS 开始,执行完代码长度后停止
        print("[*] Starting emulation...")
        mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
        print("[*] Emulation done.")
 
        # ==========================================
        # 7. 验证结果 (Read Register)
        # ==========================================
        # 读取 EAX 的值
        r_eax = mu.reg_read(UC_X86_REG_EAX)
        print(f">>> Result: EAX = {r_eax}")
 
        # 逻辑校验
        if r_eax == 101:
            print(">>> SUCCESS! (100 + 1 = 101)")
        else:
            print(">>> FAILED!")
 
    except UcError as e:
        # 异常处理:捕获 Unicorn 内部错误(如内存未映射、非法指令等)
        print(f"ERROR: {e}")
 
 
if __name__ == '__main__':
    test_x86()
from unicorn import *
from unicorn.x86_const import *
 
# ==========================================
# 1. 定义配置常量
# ==========================================
# x86机器码: INC EAX
# 对应的十六进制是 0x40 (在 64位模式下通常表示为 REX 前缀或直接操作)
# 这里为了演示简单,我们使用兼容的机器码
X86_CODE32 = b"\x40"  # INC EAX
 
# 内存布局
ADDRESS = 0x1000000  # 代码段基址
MEM_SIZE = 2 * 1024 * 1024  # 申请 2MB 内存
 
 
def test_x86():
    print("=== Unicorn x86-64 Demo Start ===")
 
    try:
        # ==========================================
        # 2. 初始化 (Initialize)
        # ==========================================
        # 初始化一个 x86 架构、32位 模式的模拟器
        mu = Uc(UC_ARCH_X86, UC_MODE_32)
 
        # ==========================================
        # 3. 映射内存 (Map Memory)
        # ==========================================
        # 申请 2MB 内存空间
        print(f"[*] Mapping memory at 0x{ADDRESS:x}, size={MEM_SIZE} bytes")
        mu.mem_map(ADDRESS, MEM_SIZE)
 
        # ==========================================
        # 4. 写入数据 (Write Data)
        # ==========================================
        # 将机器码写入内存基址
        print(f"[*] Writing machine code to 0x{ADDRESS:x}")
        mu.mem_write(ADDRESS, X86_CODE32)
 
        # ==========================================
        # 5. 设置环境 (Setup Context)
        # ==========================================
        # 将 EAX 寄存器初始化为 100
        print("[*] Setting EAX = 100")
        mu.reg_write(UC_X86_REG_EAX, 100)
 
        # ==========================================
        # 6. 开始执行 (Start Emulation)
        # ==========================================
        # 从 ADDRESS 开始,执行完代码长度后停止
        print("[*] Starting emulation...")
        mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
        print("[*] Emulation done.")
 
        # ==========================================
        # 7. 验证结果 (Read Register)
        # ==========================================
        # 读取 EAX 的值
        r_eax = mu.reg_read(UC_X86_REG_EAX)
        print(f">>> Result: EAX = {r_eax}")
 
        # 逻辑校验
        if r_eax == 101:
            print(">>> SUCCESS! (100 + 1 = 101)")
        else:
            print(">>> FAILED!")
 
    except UcError as e:
        # 异常处理:捕获 Unicorn 内部错误(如内存未映射、非法指令等)
        print(f"ERROR: {e}")
 
 
if __name__ == '__main__':
    test_x86()
工具 角色 功能描述 核心能力
Unicorn 大脑 (CPU) 模拟执行引擎 负责跑代码,改变寄存器与内存状态。
Capstone 眼睛 (Eye) 反汇编框架 将二进制机器码翻译成汇编指令,让我们“看懂”正在执行什么。
Keystone 双手 (Hand) 汇编框架 将汇编指令编译成机器码,用于动态 Patch 代码或生成 Shellcode。
from capstone import *
 
# 1. 初始化反汇编引擎 (x86-32 架构)
cs = Cs(CS_ARCH_X86, CS_MODE_32)
 
# 2. 在 Hook 回调中使用
def hook_code(uc, address, size, user_data):
    # 读取当前指令的机器码
    code = uc.mem_read(address, size)
     
    # 反汇编 (返回一个生成器,通常只取第一条)
    for insn in cs.disasm(code, address):
        print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# 输出效果:
# 0x400000:  mov  eax, 1
# 0x400005:  inc  ebx
from capstone import *
 
# 1. 初始化反汇编引擎 (x86-32 架构)
cs = Cs(CS_ARCH_X86, CS_MODE_32)
 
# 2. 在 Hook 回调中使用
def hook_code(uc, address, size, user_data):
    # 读取当前指令的机器码
    code = uc.mem_read(address, size)
     
    # 反汇编 (返回一个生成器,通常只取第一条)
    for insn in cs.disasm(code, address):
        print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# 输出效果:
# 0x400000:  mov  eax, 1
# 0x400005:  inc  ebx
from keystone import *
 
# 1. 初始化汇编引擎 (x86-32 架构)
ks = Ks(KS_ARCH_X86, KS_MODE_32)
 
# 2. 将汇编代码编译为机器码
# encoding: 机器码列表, count: 语句数量
encoding, count = ks.asm(b"mov rax, 0x1234; inc rax")
 
# 3. 写入 Unicorn 内存 (Patch)
# 将 Python 的 list 转换为 bytes
machine_code = bytes(encoding)
uc.mem_write(0x400000, machine_code)
from keystone import *
 
# 1. 初始化汇编引擎 (x86-32 架构)
ks = Ks(KS_ARCH_X86, KS_MODE_32)
 
# 2. 将汇编代码编译为机器码
# encoding: 机器码列表, count: 语句数量
encoding, count = ks.asm(b"mov rax, 0x1234; inc rax")

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 22
支持
分享
最新回复 (5)
雪    币: 144
活跃值: (1878)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
666
20小时前
0
雪    币: 798
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
听说这个模拟框架牛皮,特来学习一下
20小时前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
收藏学习看看
12小时前
0
雪    币: 5606
活跃值: (9382)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5

ojbk nb

最后于 8小时前 被huangjw编辑 ,原因:
8小时前
0
雪    币: 51
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
tql
5小时前
0
游客
登录 | 注册 方可回帖
返回