Unicorn引擎教程(一)
在该篇教程中,你将通过实际操作来学习Unicorn引擎。接下来将会有4个练习,我会解决第一个。对于其他的我将提供提示和解决方案。(译注:原网页中的提示和解决方案内容通过按钮可以显示或者隐藏,翻译后的文章不具备此功能,所有内容将直接呈现出来)
FAST FAQ:
Unocorn引擎是什么?
简单的来讲,一款模拟器。尽管不太常见,你不能用来模拟整个程序或者系统,同时它也不支持syscall。你只能通过手动的方式来映射内存以及数据写入,然后就可以从某个指定的地址开始执行模拟了。
模拟器在什么时候是有用的?
你可以执行一些恶意软件中你感兴趣的函数而不必创建整个进程
CTF比赛中也很常用
Fuzzing
GDB插件扩充,例如支持长跳转
模拟混淆后的代码
在这篇教程开始之前有什么需要准备的?
安装带有Python支持的Unicorn引擎
一个反汇编器
目录
任务1
一些笔记
任务2
任务3
任务4
备忘录
参考
任务1
该任务是hxp CTF 2017上的一个叫做Fibonacci(译注:斐波那契)的例子。二进制文件可以从这里下载
当我们运行这个程序的时候,可以注意到这个程序计算和输出Flag非常的慢。Flag的下一个字节计算的越来越慢。
The Flag is:hxp {F
这就意味着有必要优化程序来获取Flag(在合理的时间内)。
在IDA Pro的帮助下,我们得到了像C语言一样的伪代码。虽然反编译代码不一定正确,但是仍然可以知道大致发生了一些事。
__int64 __fastcall main(__int64 a1, char **a2, char **a3){void *v3; // rbp@1int v4; // ebx@1signed __int64 v5; // r8@2char v6; // r9@3__int64 v7; // r8@3char v8; // cl@3__int64 v9; // r9@5int a2a; // [sp+Ch] [bp-1Ch]@3v3 = &encrypted_flag;v4 = 0;setbuf(stdout, 0LL);printf("The flag is: ", 0LL);while ( 1 ){LODWORD(v5) = 0;do{a2a = 0;fibonacci(v4 + v5, &a2a);v8 = v7;v5 = v7 + 1;}while ( v5 != 8 );v4 += 8;if ( (unsigned __int8)(a2a << v8) == v6 )break;v3 = (char *)v3 + 1;_IO_putc((char)(v6 ^ ((_BYTE)a2a << v8)), stdout);v9 = *((char *)v3 - 1);}_IO_putc(10, stdout);return 0LL;}
unsigned int __fastcall fibonacci(int i, _DWORD *a2){_DWORD *v2; // rbp@1unsigned int v3; // er12@3unsigned int result; // eax@3unsigned int v5; // edx@3unsigned int v6; // esi@3unsigned int v7; // edx@4v2 = a2;if ( i ){if ( i == 1 ){result = fibonacci(0, a2);v5 = result - ((result >> 1) & 0x55555555);v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;}else{v3 = fibonacci(i - 2, a2);result = v3 + fibonacci(i - 1, a2);v5 = result - ((result >> 1) & 0x55555555);v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;}v7 = v6 + (v5 & 0x33333333) + ((v6 + (v5 & 0x33333333)) >> 4);*v2 ^= ((BYTE1(v7) & 0xF) + (v7 & 0xF) + (unsigned __int8)((((v7 >> 8) & 0xF0F0F) + (v7 & 0xF0F0F0F)) >> 16)) & 1;}else{*a2 ^= 1u;result = 1;}return result;}
接下来是main
函数的反汇编代码
.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
fibonacci
的反汇编代码:
.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
有许多种方法来解决该任务。例如,可以通过某种编程语言重新构建代码,然后在该语言中应用优化。重建代码的过程并不容易,可能会引入一些BUG和错误。盯着代码找错误并不好笑。。通过Unicorn引擎来解决这个任务可以跳过重构代码的过程,避免上面提到的问题。当然也可以通过其他的几种方法来避免重写代码-例如gdb脚本或者使用Frida.
在开始优化之前,我们先来用Unicorn引擎模拟一个正常的程序,不进行优化,成功之后,再开始优化。
Part1:模拟一个程序
首先,创建一个名为fibonacci.py的脚本,然后将二进制文件放到同一目录下。
先向脚本中添加如下代码:
from unicorn import *from unicorn.x86_const import *
第一行代码会加载主要的二进制模块和一些Unicorn中的一些基本常量。第二行加载了一些特定的x86和x64的常量。
接下来,加入以下代码:
import structdef 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将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函数需要一下参数:
第一个参数:架构类型。这些常量以UC_ATCH_为前缀
第二个参数:架构细节说明。这些常量以UC_MODE_为前缀
你可以在备忘录中找到这些常量。
正如我之前所写,使用Unicorn引擎之前需要手动初始化内存。针对这个二进制文件来说,我们需要将代码写入到某个地方,并且分配一些栈空间。
二进制文件的基址是0x400000。栈的话不妨从地址0x0开始,大小为1024*1024字节(译注:即1M)。或许并不需要这么大的栈空间,但是也没有多大的影响。
可以使用mem_map函数来映射内存。
使用如下代码:
BASE = 0x400000STACK_ADDR = 0x0STACK_SIZE = 1024*1024mu.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)
可以开始模拟和执行代码了,但是需要先知道从哪开始从哪结束。
我们可以从地址0x00000000004004E0 开始执行代码,这是main函数开始的地方。结尾的话可以是0x0000000000400575。这是puts("\n")函数所在,在Flag输出之后被调用。汇编代码如下:
.text:0x400570 mov edi, 0Ah ; c.text:0x400575 call __IO_putc
可以开始执行模拟了:
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
此时,可以执行这个脚本:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.pyTraceback (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_startraise UcError(status)unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
oooooops,发生了一些我们不知道的错误呀。在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。我们自定的函数hook_code在执行每一条代码模拟的时候都将被调用。参数如下:
Uc实例句柄
指令的地址
执行的长度
用户自定义数据(我们可以在hook_add的可选参数中传递这个值)
此时,我们的脚本应该是这样子的 solve1.py
运行的时候就可以有以下的输出了:
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 = 0x7Traceback (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_startraise 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 = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F]
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)
同样的针对一个字节一个字节输出Flag的代码也需要做一些处理:
.text:0x400558 movsx edi, dil ; c
.text:0x40055C add rbp, 1
.text:0x400560 call __IO_putc
__IO_putc将一个字节输出到第一个参数所在(RDI寄存器)
这里就可以直接读取RDI寄存器,然后输出,跳过模拟执行这部分代码。这里的hook_code函数如下:
instructions_skip_list = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F]
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)
elif address == 0x400560: #that instruction writes a byte of the flag
c = mu.reg_read(UC_X86_REG_RDI)
print(chr(c))
mu.reg_write(UC_X86_REG_RIP, address+size)
此时,整个代码看起来应该是这样的solve2.py.
可以运行以下看一下,尽管它依然很慢。
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py
h
x
Part2:提升速度!
考虑以下速度提升,为什么这个程序跑这么慢?
观察一下反编译代码,我们可以看到main函数调用了fibonacci函数若干次,并且fibonacci函数是个递归函数。
看看这个函数,我们可以看到它有2个参数,返回2个值。第一个返回值存放在RAX寄存器中,第二个是第二个参数的指针。深入研究一下main和fibonacci函数可以发现,第二个参数只能取值为0或者1。如果发现不了的话,可以运行gdb然后在fibonacci函数的起始地址下个断点观察。
为了优化这个函数,我们可以使用动态编程来记住所给参数的返回值。因为第二个参数只有两种取值,只需记住只记住2 * MAX_OF_FIRST_ARGUMENT对就足够了。
可以在RIP指向fibonacci函数开始的时候获取函数的参数,值的返回在函数退出的时候。因为不能同时获取这两个值,所以我们需要一个栈来帮助我们在函数退出的时候获取这些值-在fibonacci入口我们需要将参数入栈,最后的时候出栈。可以使用dict来记住这些值。
如何保存这些成对的值?
在函数开始的时候,我们可以检查参数对应的值是否已经被dict记录
如果是,直接返回这个key-value就行,只需将返回值写入到RAX中,同时设置RIP为RET指令的值,退出这个函数。不能在fabonacci函数内直接跳转到RET,因为这条指令已经被HOOK了,所以我们跳转到main中的ret。
如果dict中没有出现参数和对应的值,将参数添加到dict中。
当退出函数的时候,保存返回值。可以从我们的栈结构中读取参数和返回值。
代码如下:
FIBONACCI_ENTRY = 0x0000000000400670FIBONACCI_END = [0x00000000004006F1, 0x0000000000400709]stack = [] # Stack for storing the argumentsd = {} # Dictionary that holds return values for given function argumentsdef 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)elif address == 0x400560: # That instruction writes a byte of the flagc = mu.reg_read(UC_X86_REG_RDI)print(chr(c))mu.reg_write(UC_X86_REG_RIP, address+size)elif address == FIBONACCI_ENTRY: # Are we at the beginning of fibonacci function?arg0 = mu.reg_read(UC_X86_REG_RDI) # Read the first argument. Tt is passed via RDIr_rsi = mu.reg_read(UC_X86_REG_RSI) # Read the second argument which is a referencearg1 = u32(mu.mem_read(r_rsi, 4)) # Read the second argument from referenceif (arg0,arg1) in d: # Check whether return values for this function are already saved.(ret_rax, ret_ref) = d[(arg0,arg1)]mu.reg_write(UC_X86_REG_RAX, ret_rax) # Set return value in RAX registermu.mem_write(r_rsi, p32(ret_ref)) # Set retun value through referencemu.reg_write(UC_X86_REG_RIP, 0x400582) # Set RIP to point at RET instruction. We want to return from fibonacci functionelse:stack.append((arg0,arg1,r_rsi)) # If return values are not saved for these arguments, add them to stack.elif address in FIBONACCI_END:(arg0, arg1, r_rsi) = stack.pop() # We know arguments when exiting the functionret_rax = mu.reg_read(UC_X86_REG_RAX) # Read the return value that is stored in RAXret_ref = u32(mu.mem_read(r_rsi,4)) # Read the return value that is passed referenced[(arg0, arg1)]=(ret_rax, ret_ref) # Remember the return values for this argument pair
以防万一,完整的代码可以在这里找到。solve3.py
欢呼吧!我们已经成功地使用Unicorn引擎来优化程序。非常棒!
一些笔记
现在,我强烈建议你做一些小作业。在下边你可以找到我之前说的三个任务,每一个都有提示和可行的解决方案。你可以在解决这些小任务的时候参考一下备忘录
我觉得其中之一的难题是知道一些感兴趣的常量的名字。最好的方法是使用IPython tab completion。当你安装IPyhton后,你可以输入from unicorn import UC_ARCH_然后使用TAB键,然后所有的以UC_ARCH_为前缀的常量都将被列举出来。
本文由看雪翻译小组 zplusplus 编译,来源eternal
转载请注明来自看雪社区
热门阅读
点击阅读原文/read,
更多干货等着你~