在Habr上搜索“ Unicorn Engine”时,我很惊讶地发现文章中从未使用过此工具。 我将尽力填补这一空白。 让我们从基础开始,然后看一个在现实生活中使用模拟器的示例。 为了不重蹈覆辙,我决定只翻译本手册。 在开始之前,我会说我的所有评论或评论将看起来像这样 。
什么是独角兽引擎?
开发人员自己写 独角兽引擎 独角兽引擎是这样的:
Unicorn是一个轻量级,多平台和多体系结构的处理器仿真器。
这不是标准的模拟器。 它不会模拟整个程序或整个OS的操作。 它不支持系统命令(例如打开文件,将字符输出到控制台等)。 您将必须对内存进行标记并将数据自己加载到其中,然后只需从某个特定地址开始执行即可。
那么它有什么用呢?
- 分析病毒时,您可以调用单个功能,而无需创建恶意进程。
- 解决周大福。
- 对于起毛 。
- gdb的插件,用于预测将来的状态,例如将来的跃点或寄存器值。
- 模拟功能丰富的代码。
你需要什么
- 已安装具有Python绑定的Unicorn Engine。
- 拆装机
例子
例如,以名称为Fibonacci的 hxp CTF 2017进行任务。 二进制文件可以在这里下载。
当您启动程序时,它将开始在控制台中显示我们的标志,但是速度非常慢。 每个后续标志字节被认为越来越慢。
The flag is: hxp{F
这意味着为了在合理的时间内获取标志,我们需要优化此应用程序的操作。
使用IDA Pro( 我个人使用radare2 + Cutter ),我们将代码反编译为类似C的伪代码。 尽管代码未正确反编译,但我们仍然可以从中获取有关内部发生的信息。
反编译的代码 __int64 __fastcall main(__int64 a1, char **a2, char **a3) { void *v3;
unsigned int __fastcall fibonacci(int i, _DWORD *a2) { _DWORD *v2;
这是main和fibonacci函数的汇编代码:
主要的 .text:0x4004E0 main proc near ; DATA XREF: start+1Do .text:0x4004E0 .text:0x4004E0 var_1C = dword ptr -1Ch .text:0x4004E0 .text:0x4004E0 push rbp .text:0x4004E1 push rbx .text:0x4004E2 xor esi, esi ; buf .text:0x4004E4 mov ebp, offset unk_4007E1 .text:0x4004E9 xor ebx, ebx .text:0x4004EB sub rsp, 18h .text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x4004FB mov edi, offset format ; "The flag is: " .text:0x400500 xor eax, eax .text:0x400502 call _printf .text:0x400507 mov r9d, 49h .text:0x40050D nop dword ptr [rax] .text:0x400510 .text:0x400510 loc_400510: ; CODE XREF: main+8Aj .text:0x400510 xor r8d, r8d .text:0x400513 jmp short loc_40051B .text:0x400513 ; --------------------------------------------------------------------------- .text:0x400515 align 8 .text:0x400518 .text:0x400518 loc_400518: ; CODE XREF: main+67j .text:0x400518 mov r9d, edi .text:0x40051B .text:0x40051B loc_40051B: ; CODE XREF: main+33j .text:0x40051B lea edi, [rbx+r8] .text:0x40051F lea rsi, [rsp+28h+var_1C] .text:0x400524 mov [rsp+28h+var_1C], 0 .text:0x40052C call fibonacci .text:0x400531 mov edi, [rsp+28h+var_1C] .text:0x400535 mov ecx, r8d .text:0x400538 add r8, 1 .text:0x40053C shl edi, cl .text:0x40053E mov eax, edi .text:0x400540 xor edi, r9d .text:0x400543 cmp r8, 8 .text:0x400547 jnz short loc_400518 .text:0x400549 add ebx, 8 .text:0x40054C cmp al, r9b .text:0x40054F mov rsi, cs:stdout ; fp .text:0x400556 jz short loc_400570 .text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc .text:0x400565 movzx r9d, byte ptr [rbp-1] .text:0x40056A jmp short loc_400510 .text:0x40056A ; --------------------------------------------------------------------------- .text:0x40056C align 10h .text:0x400570 .text:0x400570 loc_400570: ; CODE XREF: main+76j .text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc .text:0x40057A add rsp, 18h .text:0x40057E xor eax, eax .text:0x400580 pop rbx .text:0x400581 pop rbp .text:0x400582 retn .text:0x400582 main endp
斐波那契 .text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp .text:0x400670 ; fibonacci+19p ... .text:0x400670 test edi, edi .text:0x400672 push r12 .text:0x400674 push rbp .text:0x400675 mov rbp, rsi .text:0x400678 push rbx .text:0x400679 jz short loc_4006F8 .text:0x40067B cmp edi, 1 .text:0x40067E mov ebx, edi .text:0x400680 jz loc_400710 .text:0x400686 lea edi, [rdi-2] .text:0x400689 call fibonacci .text:0x40068E lea edi, [rbx-1] .text:0x400691 mov r12d, eax .text:0x400694 mov rsi, rbp .text:0x400697 call fibonacci .text:0x40069C add eax, r12d .text:0x40069F mov edx, eax .text:0x4006A1 mov ebx, eax .text:0x4006A3 shr edx, 1 .text:0x4006A5 and edx, 55555555h .text:0x4006AB sub ebx, edx .text:0x4006AD mov ecx, ebx .text:0x4006AF mov edx, ebx .text:0x4006B1 shr ecx, 2 .text:0x4006B4 and ecx, 33333333h .text:0x4006BA mov esi, ecx .text:0x4006BC .text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j .text:0x4006BC and edx, 33333333h .text:0x4006C2 lea ecx, [rsi+rdx] .text:0x4006C5 mov edx, ecx .text:0x4006C7 shr edx, 4 .text:0x4006CA add edx, ecx .text:0x4006CC mov esi, edx .text:0x4006CE and edx, 0F0F0F0Fh .text:0x4006D4 shr esi, 8 .text:0x4006D7 and esi, 0F0F0Fh .text:0x4006DD lea ecx, [rsi+rdx] .text:0x4006E0 mov edx, ecx .text:0x4006E2 shr edx, 10h .text:0x4006E5 add edx, ecx .text:0x4006E7 and edx, 1 .text:0x4006EA xor [rbp+0], edx .text:0x4006ED pop rbx .text:0x4006EE pop rbp .text:0x4006EF pop r12 .text:0x4006F1 retn .text:0x4006F1 ; --------------------------------------------------------------------------- .text:0x4006F2 align 8 .text:0x4006F8 .text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j .text:0x4006F8 mov edx, 1 .text:0x4006FD xor [rbp+0], edx .text:0x400700 mov eax, 1 .text:0x400705 pop rbx .text:0x400706 pop rbp .text:0x400707 pop r12 .text:0x400709 retn .text:0x400709 ; --------------------------------------------------------------------------- .text:0x40070A align 10h .text:0x400710 .text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j .text:0x400710 xor edi, edi .text:0x400712 call fibonacci .text:0x400717 mov edx, eax .text:0x400719 mov edi, eax .text:0x40071B shr edx, 1 .text:0x40071D and edx, 55555555h .text:0x400723 sub edi, edx .text:0x400725 mov esi, edi .text:0x400727 mov edx, edi .text:0x400729 shr esi, 2 .text:0x40072C and esi, 33333333h .text:0x400732 jmp short loc_4006BC .text:0x400732 fibonacci endp
在这个阶段,我们有很多机会来解决这个问题。 例如,我们可以使用一种编程语言来还原代码并在那里进行优化,但是恢复代码的过程是非常困难的任务,在此期间我们可能会犯错误。 好吧,然后比较代码以发现错误通常是毫无价值的。 但是,如果使用Unicorn Engine,则可以跳过代码重建的阶段,从而避免了上述问题。 当然,我们可以使用frida或为gdb编写脚本来避免这些麻烦,但这并非如此。
在开始优化之前,我们将在Unicorn Engine中运行仿真而不更改程序。 只有成功启动后,我们才能继续进行优化。
步骤1:让虚拟化来
让我们创建fibonacci.py文件并将其保存在二进制文件旁边。
让我们从导入所需的库开始:
from unicorn import * from unicorn.x86_const import * import struct
第一行加载主要的二进制和基本Unicorn常数。 第二行加载两个x86和x86_64体系结构的常量。
接下来,添加一些必要的功能:
def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num)
在这里,我们宣布了以后将需要的功能:
- read只是返回文件的内容,
- u32采用LE编码的4字节字符串并将其转换为int,
- p32做相反的事情-它接受一个数字并将其转换为LE编码的4字节字符串。
注意:如果已安装pwntools ,则无需创建这些功能,只需导入它们:
from pwn import *
因此,最后,让我们开始为x86_64体系结构初始化Unicorn Engine类:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
在这里,我们使用以下参数调用Uc函数:
- 第一个参数是主要架构。 常量以UC_ARCH_开头 ;
- 第二个参数是体系结构的规范。 常量以UC_MODE_开头 。
您可以在备忘单中找到所有常量。
如我上面所写,要使用Unicorn Engine,我们需要手动初始化虚拟内存。 对于此示例,我们需要将代码和堆栈放在内存中的某个位置。
二进制文件的基址(Base addr)从0x400000开始。 让我们将堆栈放在0x0并为其分配1024 * 1024内存。 最有可能的是,我们不需要那么多的空间,但是仍然没有伤害。
我们可以通过调用mem_map方法来标记内存。
添加这些行:
BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE)
现在,我们需要以与引导加载程序相同的方式将二进制文件加载到其主地址中。 之后,我们需要将RSP设置到堆栈的末尾。
mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
现在我们可以开始仿真并运行代码,但是我们需要弄清楚开始使用哪个地址以及仿真器何时停止。
从main()获取第一个命令的地址,我们可以从0x004004e0开始仿真。 显示整个标志后,将认为结尾是对位于0x00400575的putc(“ \ n”)的调用。
.text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc
我们可以开始模拟:
mu.emu_start(0x004004e0,0x00400575)
现在运行脚本:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py Traceback (most recent call last): File "solve.py", line 32, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
糟糕,出了点问题,但我们什至都不知道。 在调用mu.emu_start之前,我们可以添加:
def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) mu.hook_add(UC_HOOK_CODE, hook_code)
此代码添加了一个钩子。 我们声明了自己的hook_code函数,模拟器在每个命令之前调用该函数。 它采用以下参数:
- 我们的Uc副本,
- 指令地址
- 尺寸说明
- 用户数据(我们可以将此值与可选参数一起传递给hook_add() )。
现在,如果我们运行脚本,我们应该看到以下输出:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
在发生错误的地址,我们可以理解我们的脚本无法处理此命令:
.text:0x4004EF mov rdi, cs:stdout ; stream
该指令从地址0x601038中读取数据(您可以在IDA Pro中看到它)。 这是我们未标记的.bss部分。 如果不影响程序逻辑,我的解决方案就是简单地跳过所有有问题的指令。
以下是另一个有问题的说明:
.text:0x4004F6 call _setbuf
我们无法使用glibc调用任何函数,因为我们没有在内存中加载glibc。 无论如何,我们不需要此命令,因此我们也可以跳过它。
这是要跳过的命令的完整列表:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
要跳过命令,我们需要使用以下指令重写RIP :
mu.reg_write(UC_X86_REG_RIP, address+size)
现在hook_code应该看起来像这样:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
我们还需要对在逐字节控制台中显示标志的指令执行某些操作。
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
__IO_putc将字节输出作为第一个参数 (这是RDI寄存器)。
我们可以直接从寄存器读取数据,将数据输出到控制台,并跳过这组指令。 更新后的hook_code如下所示:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data):
我们可以运行,它将全部运行,但是仍然很慢。
步骤2:提高速度!
让我们考虑一下提高工作速度。 为什么这个程序这么慢?
如果查看反编译的代码,将会看到main()多次调用fibonacci() ,而fibonacci()是一个递归函数。 让我们仔细看一下这个函数;它接受并返回两个参数。 第一个返回值通过RAX寄存器传递,第二个返回值通过第二个参数传递给函数的链接返回。 如果我们更深入地研究main()和fibonacci()之间的关系,那么我们将看到第二个参数仅采用两个可能的值:0或1。如果仍然看不到,请运行gdb并将断点放在函数的开头fibonacci() 。
为了优化算法的操作,我们可以使用动态编程来记住输入参数的返回值。 自己想想,第二个参数只能采用两个可能的值,所以我们要做的就是记住 $内联$ 2 *最大\ _OF \ _FIRST \ _ARGUMENT $内联$ 蒸
对于那些不了解的人fibonacci是一个递归函数,它计算下一个值作为前两个值的和。 在每一步中,她都会更加深入。 每次她重新开始时,都按照以前的方式进行操作,再加上一种新的含义。
一个例子:
假设深度= 6,则: 1 1 2 3 5 8 。
现在深度= 8,则: 1 1 2 3 5 8 13 21。
我们可能只记得前6个成员是1 1 2 3 5 8 ,当他们要求我们计数比我们记住的更多时,我们会记住我们所记的内容,只计算缺失的内容。
一旦RIP位于fibonacci()的开头,我们就可以获取函数参数。 我们知道一个函数在退出函数时会返回结果。 由于我们不能一次使用两个参数,因此我们需要一个堆栈来返回参数。 当输入fibonacci()时,我们需要将参数放在堆栈上,并在退出时将其提取。 要存储计数对,我们可以使用字典。
如何处理一对值?
- 在函数的最开始,我们可以检查该对是否在我们已经知道的结果中:
- 如果有的话,我们可以退还这对。 我们只需要在RAX和链接的地址(第二个参数中)中编写返回值。 我们还分配了一个RIP地址以退出该功能。 我们无法在fibonacci()中使用RET ,因为这些调用已被钩住,因此我们将从main()中获取一些RET ;
- 如果这些值不是,那么我们只需将它们添加到堆栈中。
- 在退出函数之前,我们可以保存返回的对。 我们知道输入参数,因为我们可以从堆栈中读取它们。
此代码在此处显示。 FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f]
Hooray,我们终于能够使用Unicorn Engine优化应用程序。 干得好!
笔记
现在我决定给你一点功课。
在这里您可以找到另外三个任务,每个任务都有提示和完整的解决方案。 解决问题时,您可以查看备忘单 。
最烦人的问题之一是记住所需常量的名称。 如果在IPython中使用Tab插件,这很容易处理。 安装IPython后,您可以从unicorn import UC_ARCH_编写,按Tab键,将显示以相同方式启动的所有常量。