在该篇教程中,你将通过实际操作来学习Unicorn引擎。接下来将会有4
个练习,我会解决第一个。对于其他的我将提供提示和解决方案。(译注:原网页中的提示和解决方案内容通过按钮可以显示或者隐藏,翻译后的文章不具备此功能,所有内容将直接呈现出来)
FAST FAQ:
Unocorn引擎是什么?
简单的来讲,一款模拟器。尽管不太常见,你不能用来模拟整个程序或者系统,同时它也不支持syscall。你只能通过手动的方式来映射内存以及数据写入,然后就可以从某个指定的地址开始执行模拟了。
模拟器在什么时候是有用的?
在这篇教程开始之前有什么需要准备的?
该任务是hxp CTF 2017上的一个叫做Fibonacci
(译注:斐波那契)的例子。二进制文件可以从这里下载
当我们运行这个程序的时候,可以注意到这个程序计算和输出Flag非常的慢。Flag的下一个字节计算的越来越慢。
The Flag is:hxp {F
这就意味着有必要优化程序来获取Flag(在合理的时间内)。
在IDA Pro的帮助下,我们得到了像C语言一样的伪代码。虽然反编译代码不一定正确,但是仍然可以知道大致发生了一些事。
接下来是main
函数的反汇编代码
fibonacci
的反汇编代码:
有许多种方法来解决该任务。例如,可以通过某种编程语言重新构建代码,然后在该语言中应用优化。重建代码的过程并不容易,可能会引入一些BUG和错误。盯着代码找错误并不好笑。。通过Unicorn引擎来解决这个任务可以跳过重构代码的过程,避免上面提到的问题。当然也可以通过其他的几种方法来避免重写代码-例如gdb脚本或者使用Frida.
在开始优化之前,我们先来用Unicorn引擎模拟一个正常的程序,不进行优化,成功之后,再开始优化。
首先,创建一个名为fibonacci.py
的脚本,然后将二进制文件放到同一目录下。
先向脚本中添加如下代码:
第一行代码会加载主要的二进制模块和一些Unicorn中的一些基本常量。第二行加载了一些特定的x86和x64的常量。
接下来,加入以下代码:
这里,我只添加了一些常用的在之后会用到的一些函数
read
仅仅返回文件中的内容
u32
将4字节的string(译注:python2中的string等同于bytes array)转换为integer,以小端序表示这个数据。
p32
与u32
相反,将一个数字转换为以小端序保存的4字节string.
如果你安装了pwntools,可以不创建这两个函数,直接from pwn import*
就可以了。
接下来为x86-64架构初始化一下Unicorn引擎。
mu = Uc (UC_ARCH_X86, UC_MODE_64)
Uc
函数需要一下参数:
你可以在备忘录中找到这些常量。
正如我之前所写,使用Unicorn引擎之前需要手动初始化内存。针对这个二进制文件来说,我们需要将代码写入到某个地方,并且分配一些栈空间。
二进制文件的基址是0x400000
。栈的话不妨从地址0x0
开始,大小为1024*1024
字节(译注:即1M)。或许并不需要这么大的栈空间,但是也没有多大的影响。
可以使用mem_map
函数来映射内存。
使用如下代码:
此时,我们可以和加载器一样,加载二进制文件到准备好的基址上来了。然后需要设置RSP
指向我们申请的栈空间底部。
可以开始模拟和执行代码了,但是需要先知道从哪开始从哪结束。
我们可以从地址0x00000000004004E0
开始执行代码,这是main
函数开始的地方。结尾的话可以是0x0000000000400575
。这是puts("\n")
函数所在,在Flag输出之后被调用。汇编代码如下:
可以开始执行模拟了:
此时,可以执行这个脚本:
oooooops,发生了一些我们不知道的错误呀。在mu.emu_start
之前我们可以加入:
这几行代码加入了一个hook。我们自定的函数hook_code
在执行每一条代码模拟的时候都将被调用。参数如下:
此时,我们的脚本应该是这样子的 solve1.py
运行的时候就可以有以下的输出了:
这些输出表明,脚本执行以下的指令的时候发生了错误:
这条指令从地址0x601038
处读取内存(你可以在IDA Pro中看)。这是.bss
区段所在,然而我们并没有来分配这个区段。我的解决方案是跳过所有出现问题的指令。
接下来有一条指令:
我们没有办法来调用glibc里的函数,因为没有在被模拟的对象的内存中加载glibc。其实我们并不需要调用这个函数,所以直接跳过去就好了。
以下是直接跳过的指令:
可以通过修改RIP
寄存器的方式来跳过这些指令。
hook_code
函数现在看起来是这样的:
同样的针对一个字节一个字节输出Flag的代码也需要做一些处理:
__IO_putc将一个字节输出到第一个参数
所在(RDI
寄存器)
这里就可以直接读取RDI
寄存器,然后输出,跳过模拟执行这部分代码。这里的hook_code
函数如下:
此时,整个代码看起来应该是这样的solve2.py.
可以运行以下看一下,尽管它依然很慢。
考虑以下速度提升,为什么这个程序跑这么慢?
观察一下反编译代码,我们可以看到main
函数调用了fibonacci
函数若干次,并且fibonacci
函数是个递归函数。
看看这个函数,我们可以看到它有2个参数,返回2个值。第一个返回值存放在RAX
寄存器中,第二个是第二个参数的指针。深入研究一下main
和fibonacci
函数可以发现,第二个参数只能取值为0
或者1
。如果发现不了的话,可以运行gdb
然后在fibonacci
函数的起始地址下个断点观察。
为了优化这个函数,我们可以使用动态编程来记住所给参数的返回值。因为第二个参数只有两种取值,只需记住只记住2 * MAX_OF_FIRST_ARGUMENT
对就足够了。
可以在RIP
指向fibonacci
函数开始的时候获取函数的参数,值的返回在函数退出的时候。因为不能同时获取这两个值,所以我们需要一个栈来帮助我们在函数退出的时候获取这些值-在fibonacci
入口我们需要将参数入栈,最后的时候出栈。可以使用dict
来记住这些值。
如何保存这些成对的值?
代码如下:
以防万一,完整的代码可以在这里找到。solve3.py
欢呼吧!我们已经成功地使用Unicorn引擎来优化程序。非常棒!
现在,我强烈建议你做一些小作业。在下边你可以找到我之前说的三个任务,每一个都有提示和可行的解决方案。你可以在解决这些小任务的时候参考一下备忘录
我觉得其中之一的难题是知道一些感兴趣的常量的名字。最好的方法是使用IPython tab completion。当你安装IPyhton后,你可以输入from unicorn import UC_ARCH_
然后使用TAB
键,然后所有的以UC_ARCH_
为前缀的常量都将被列举出来。
分析一下代码:
如你所见,代码被混淆了(命令disasm
是pwntools
的一部分):
注意:代码基于x86-32
架构。syscall的调用号所对应的功能可以在这里找到
提示:
你可以HOOKint 80h
指令,二进制代码是cd 80
,然后读取寄存器和内存。请记住,shellcode是可以在任何地址被加载执行的代码,绝大多数的shellcode使用栈来执行。
解决方案:
在本贴回复中
可以从这里下载这个二进制文件。该代码使用了以下的编译选项:
gcc function.c -m32 -o function
源码如下:
任务是通过某种方法调用super_function
使它返回1
.
反汇编代码如下:
提示:
根据STDCALL
调用约定,当模拟开始的时候栈应该开起来像下边的这张图图片一样。在这张图片中,RET
仅仅是返回某个地址(可以是任何值)。
![](/pic/stack.png)
解决方案:
在本贴回复中
这个任务和第一个很像。区别是目标架构不是x86
而是小端ARM32
可以在这里下载到二进制文件
ARM调用约定可能会帮助到你
正确答案:
提示:
解决方案:
在本贴回复中
from unicorn import *
- 加载Unicorn库。包含一些函数和基本的常量。
from unicorn.x86_const import*
- 加载 X86 和X64架构相关的常量
所有unicorn
模块中的常量
一些unicorn.x86_const
中的常量
mu = Uc(arch,mode)
- 获取Uc
实例。在这里指定目标架构,例如:
mu.mem_map(ADDRESS,4096)
- 映射一片内存区域
mu.mem_write(ADDRESS,DATA)
- 向内存中写入数据
tmp = mu.mem_read(ADDRESS,SIZE)
- 从内存中读取数据
mu.reg_write(UC_X86_REG_ECX,0X0)
- 设置ECX值。
r_esp = mu.reg_read(UC_X86_REG_ESP)
- 读取ESP的值。
mu.emu_start(ADDRESS_START,ADDRESS_END)
- 开始执行模拟。
命令追踪:
这段代码添加了一个HOOK(向Unicorn引擎中),我们定义的函数会在执行每一条命令之前被执行。参数含义如下:
参考:
原文链接:
http://eternal.red/2018/unicorn-engine-tutorial/
本文由看雪翻译小组zplusplus翻译
[注意]看雪招聘,专注安全领域的专业人才平台!