http://www.unicorn-engine.org/docs/tutorial.html
先来了解下什么是unicorn引擎。。
Unicorn 是一个轻量级, 多平台, 多架构的 CPU 模拟器框架. 我们可以更好地关注 CPU 操作, 忽略机器设备的差异. 想象一下, 我们可以将其应用于这些情景: 比如我们单纯只是需要模拟代码的执行而非需要一个真的 CPU 去完成那些操作, 又或者想要更安全地分析恶意代码, 检测病毒特征, 或者想要在逆向过程中验证某些代码的含义. 使用 CPU 模拟器可以很好地帮助我们提供便捷.
它的亮点 (这也归功于 Unicorn 是基于 qemu 而开发的) 包括:
支持多种架构: Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64).
对 Windows 和 nix 系统 (已确认包含 Mac OSX, Linux, BSD & Solaris) 的原生支持
具有平台独立且简洁易于使用的 API
使用 JIT 编译技术, 性能表现优异
那么它应用的场景有哪些呢?
• 你可以调用恶意软件中一些有趣的函数, 而不用创建一个有害的进程.
• 用于 CTF 竞赛
• 用于模糊测试
• 用于 gdb 插件, 基于代码模拟执行的插件
• 模拟执行一些混淆代码
这款神器是在2015年BlackHat大会上发布的,作者的ppt可以在这儿下载到
http://www.unicorn-engine.org/BHUSA2015-unicorn.pdf
在具体操作演示之前,我们简要看看作者所做的一些介绍
上图是作者的议题流程,我们只介绍红圈部分,具体使用unicorn提供的API进行编程是本系列实验的中心任务,在这一部分暂时先不提及。
Unicorn的设计目的就是称为一款轻量级的CPU模拟器(或称仿真器)
首先要明确CPU模拟器的应用,包括:
不需要真实的CPU就能模拟代码;可以安全地分析恶意代码,侦测病毒签名;在逆向工程中明确代码的含义
CPU模拟器的定义是仅用软件来模拟物理CPU,只关注CPU操作,而忽略机器设备。
在CPU模拟器内部,给定二进制形式的输入代码,CPU模拟器需要:
1.将二进制代码解码为单独的指令
2.精确地模拟每个指令所做的工作,涉及:参考所需的指令集手册,处理所需的内存访问及IO等
3.在每一步执行后更新CPU上下文,包括寄存器、内存等。
以模拟x86 32位的指令为例,所以的工作如下所示
可以看到要做一个CPU模拟器,工作量很大,而且需要对CPU架构、指令集有精通理解,还需要知道指令集的一些副作用(文档里不会提及,但是经验丰富的人都清楚),而且重点是需要支持所有已知的各种类型的代码,这是非常困难的
一个好的CPU模拟器需要具备以下特点:多种架构支持、多平台支持、保持更新,尽量保持独立减少对其他工具的依赖,性能高
unicorn可以满足上述的要求,相比于已有的工具,比较如下
unicorn是基于QEMU的,QEMU的架构如下所示
为什么是QEMU呢?因为它支持所有架构而且保持更新,在CPU模拟中已经支持JIT
那么unicorn在QEMU基础上做了哪些工作呢?
- 删去不必要的功能,只保留CPU模拟功能
- 支持子系统,包括Qobject,Qom
- 重写一些组件但保持CP模拟代码完整(将来很容易与Qemu同步)
- 隔离常量和结构体,通过设计确保线程安全
- 重构以允许同时使用Unicorn的多个实例
- 修改了构建系统以按需支持多种架构
- 从头构建动态的细粒度指令层
- 支持各层级的指令,包括:特定指令的单步(TCG层),访问内存的指令(TLB层),在模拟过程中动态读写寄存器或内存,通过用户提供的回调函数处理异常、终端、系统调用(架构层)
- 发现并修复所有的内存泄露问题
- 重构各种子系统以跟踪和清理悬空指针。
基于以上的工作,unicorn相对于QEMU的优势在于:
unicorn的核心提供了C实现的API
如果用python编程,其库unicorn已经处理好细节了,我们使用pip install unicorn后就可以直接使用了
使用pip install unicorn即可安装使用
安装完成后先来写一个demo,来模拟x86架构下的32位机器
我们是在python2的环境下编写,但是要超前使用python3的print函数,所以第一句代码是
接下来
在使用Unicorn之前导入unicorn模块,由于我们这个dmeo使用了一些x86寄存器常量,所以还需要unicorn.x86_const
这是我们要模拟的代码,这里使用16进制表示,表示两个x86指令“INC ecx”和”DEC dex“
指定虚拟地址,我们将在这儿模拟上述代码
使用Uc类初始化Unicorn, 该类接受2个参数:硬件架构和硬件模式。 在示例中,我们希望模拟X86体系结构的32位代码。
使用mem_map方法在前面声明的地址处映射2MB内存。此过程中的所有CPU操作都只能访问此内存。 此内存使用默认权限READ,WRITE和EXECUTE进行映射。
将要模拟的代码写入我们刚刚映射到的内存中。 mem_write方法有两个参数:要写入的地址和要写入内存的代码。
使用reg_write方法设置ECX,EDX寄存器的值
使用emu_start方法开始模拟。该API采用4个参数:需要模拟的代码的地址、模拟停止的地址(正好在X86_CODE32的最后一个字节之后)、要模拟的时间和要模拟的指令数量。如果我们忽略最后两个参数,比如这个例子,unicorn将在无限时间和无限数量的指令中模拟代码。
通过reg_read函数读取,打印出寄存器ECX,EDX的值
完整代码在test.py,运行后如图所示
模仿其他架构的也很简单,我们看看模拟MIPS的
MIPS架构(英语:MIPS architecture,为Microprocessor without interlocked piped stages architecture的缩写,亦为Millions of Instructions Per Second的双关语),是一种采取精简指令集(RISC)的处理器架构,1981年出现,由MIPS科技公司开发并授权,广泛被使用在许多电子产品、网络设备、个人娱乐装置与商业装置上。目前很多路由器都是采用MIPS架构。来分析代码。
超前使用python3的print函数
导入unicorn模块,以及将要模拟的是unicorn.mips_const
要模拟的代码
EB表示big endian即大端,EL表示little endain即小端
所以其实这两句代码表示的同一条指令。
另外我们需要知道,MIPS有32个通用寄存器($0-$31),
1
为
1为
1为at,为汇编保留,由于I型指令的立即数字段只有16位,在加载大常数时,编译器或汇编程序需要把大常数拆开,然后重新组合到寄存器里。比如加载一个32位立即数需要 lui(装入高位立即数)和addi两条指令。像MIPS程序拆散和重装大常数由汇编程序来完成,汇编程序必需一个临时寄存器来重组大常数,这也是为汇编保留$at的原因之一。就是我们demo里的指令使用到的寄存器
ori指令用于进行“或“操作,原型ori
r
s
,
rs,
rs,rd,im;功能为rd=rd|im
在我们demo中就是
a
t
=
at=
at=at|0x3456
指定内存地址,我们从这儿开始模拟
分别用于测试大端和小端,都是差不多的,我们来分析大端的
使用Uc类创建一个对象,创建时指定架构为MIPS,为32位和大端格式
为这次模拟映射2MB内存
将要被模拟的代码写入内存
初始化寄存器
没有指定要模拟的时间和指令数量,所以会在无限时间内模拟
读出寄存器的值并打印出来
完整代码在test1.py
运行后结果如图
其他demo可以参考学习,链接为:https://github.com/unicorn-engine/unicorn/tree/master/bindings/python