首页
社区
课程
招聘
[翻译]Unicorn引擎教程
发表于: 2018-1-27 18:09 44572

[翻译]Unicorn引擎教程

2018-1-27 18:09
44572

在该篇教程中,你将通过实际操作来学习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,以小端序表示这个数据。

p32u32相反,将一个数字转换为以小端序保存的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寄存器中,第二个是第二个参数的指针。深入研究一下mainfibonacci函数可以发现,第二个参数只能取值为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_为前缀的常量都将被列举出来。

分析一下代码:

如你所见,代码被混淆了(命令disasmpwntools的一部分):

注意:代码基于x86-32架构。syscall的调用号所对应的功能可以在这里找到

提示

你可以HOOKint 80h指令,二进制代码是cd 80,然后读取寄存器和内存。请记住,shellcode是可以在任何地址被加载执行的代码,绝大多数的shellcode使用栈来执行。

解决方案

在本贴回复中

可以从这里下载这个二进制文件。该代码使用了以下的编译选项:

gcc function.c -m32 -o function

源码如下:

任务是通过某种方法调用super_function使它返回1.

反汇编代码如下:

提示

根据STDCALL调用约定,当模拟开始的时候栈应该开起来像下边的这张图图片一样。在这张图片中,RET仅仅是返回某个地址(可以是任何值)。

解决方案

在本贴回复中

这个任务和第一个很像。区别是目标架构不是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翻译

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 14
支持
分享
最新回复 (18)
雪    币: 689
活跃值: (422)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
2

以下的代码并不是一次性写出来的,通过UC的错误消息,获得可能的线索然后来辅助实施最终的解决方案

TASK 2

from unicorn import *
from unicorn.x86_const import *

shellcode = "\xe8\xff\xff\xff\xff\xc0\x5d\x6a\x05\x5b\x29\xdd\x83\xc5\x4e\x89\xe9\x6a\x02\x03\x0c\x24\x5b\x31\xd2\x66\xba\x12\x00\x8b\x39\xc1\xe7\x10\xc1\xef\x10\x81\xe9\xfe\xff\xff\xff\x8b\x45\x00\xc1\xe0\x10\xc1\xe8\x10\x89\xc3\x09\xfb\x21\xf8\xf7\xd0\x21\xd8\x66\x89\x45\x00\x83\xc5\x02\x4a\x85\xd2\x0f\x85\xcf\xff\xff\xff\xec\x37\x75\x5d\x7a\x05\x28\xed\x24\xed\x24\xed\x0b\x88\x7f\xeb\x50\x98\x38\xf9\x5c\x96\x2b\x96\x70\xfe\xc6\xff\xc6\xff\x9f\x32\x1f\x58\x1e\x00\xd3\x80" 


BASE = 0x400000
STACK_ADDR = 0x0
STACK_SIZE = 1024*1024

mu = Uc (UC_ARCH_X86, UC_MODE_32)

mu.mem_map(BASE, 1024*1024)
mu.mem_map(STACK_ADDR, STACK_SIZE)


mu.mem_write(BASE, shellcode)
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE/2)

def syscall_num_to_name(num):
    syscalls = {1: "sys_exit", 15: "sys_chmod"}
    return syscalls[num]

def hook_code(mu, address, size, user_data):
    #print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))  

    machine_code = mu.mem_read(address, size)
    if machine_code == "\xcd\x80":

        r_eax = mu.reg_read(UC_X86_REG_EAX)
        r_ebx = mu.reg_read(UC_X86_REG_EBX)
        r_ecx = mu.reg_read(UC_X86_REG_ECX)
        r_edx = mu.reg_read(UC_X86_REG_EDX)
        syscall_name = syscall_num_to_name(r_eax)

        print "--------------"
        print "We intercepted system call: "+syscall_name

        if syscall_name == "sys_chmod":
            s = mu.mem_read(r_ebx, 20).split("\x00")[0]
            print "arg0 = 0x%x -> %s" % (r_ebx, s)
            print "arg1 = " + oct(r_ecx)
        elif syscall_name == "sys_exit":
            print "arg0 = " + hex(r_ebx)
            exit()

        mu.reg_write(UC_X86_REG_EIP, address + size)

mu.hook_add(UC_HOOK_CODE, hook_code)

mu.emu_start(BASE, BASE-1)
2018-1-27 18:10
0
雪    币: 689
活跃值: (422)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
3

TASK 3

from unicorn import *
from unicorn.x86_const import *
import struct


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)

mu = Uc (UC_ARCH_X86, UC_MODE_32)

BASE = 0x08048000
STACK_ADDR = 0x0
STACK_SIZE = 1024*1024

mu.mem_map(BASE, 1024*1024)
mu.mem_map(STACK_ADDR, STACK_SIZE)


mu.mem_write(BASE, read("./function"))
r_esp = STACK_ADDR + (STACK_SIZE/2)     #ESP points to this address at function call

STRING_ADDR = 0x0
mu.mem_write(STRING_ADDR, "batman\x00") #write "batman" somewhere. We have choosen an address 0x0 which belongs to the stack.

mu.reg_write(UC_X86_REG_ESP, r_esp)     #set ESP
mu.mem_write(r_esp+4, p32(5))           #set the first argument. It is integer 5
mu.mem_write(r_esp+8, p32(STRING_ADDR)) #set the second argument. This is a pointer to the string "batman"


mu.emu_start(0x8048464, 0x804849A)      #start emulation from the beginning of super_function, end at RET instruction
return_value = mu.reg_read(UC_X86_REG_EAX)
print "The returned value is: %d" % return_value
2018-1-27 18:11
0
雪    币: 689
活跃值: (422)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
4

TASK 4

from unicorn import *
from unicorn.arm_const import *


import struct

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)


mu = Uc (UC_ARCH_ARM, UC_MODE_LITTLE_ENDIAN)


BASE = 0x10000
STACK_ADDR = 0x300000
STACK_SIZE = 1024*1024

mu.mem_map(BASE, 1024*1024)
mu.mem_map(STACK_ADDR, STACK_SIZE)


mu.mem_write(BASE, read("./task4"))
mu.reg_write(UC_ARM_REG_SP, STACK_ADDR + STACK_SIZE/2)

instructions_skip_list = []

CCC_ENTRY = 0x000104D0
CCC_END = 0x00010580

stack = []                                          # Stack for storing the arguments
d = {}                                              # Dictionary that holds return values for given function arguments 

def hook_code(mu, address, size, user_data):  
    #print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))

    if address == CCC_ENTRY:                        # Are we at the beginning of ccc function?
        arg0 = mu.reg_read(UC_ARM_REG_R0)           # Read the first argument. it is passed by R0

        if arg0 in d:                               # Check whether return value for this function is already saved.
            ret = d[arg0]
            mu.reg_write(UC_ARM_REG_R0, ret)        # Set return value in R0
            mu.reg_write(UC_ARM_REG_PC, 0x105BC)    # Set PC to point at "BX LR" instruction. We want to return from fibonacci function

        else:
            stack.append(arg0)                      # If return value is not saved for this argument, add it to stack.

    elif address == CCC_END:
        arg0 = stack.pop()                          # We know arguments when exiting the function

        ret = mu.reg_read(UC_ARM_REG_R0)            # Read the return value (R0)
        d[arg0] = ret                               # Remember the return value for this argument

mu.hook_add(UC_HOOK_CODE, hook_code)

mu.emu_start(0x00010584, 0x000105A8)

return_value = mu.reg_read(UC_ARM_REG_R1)           # We end the emulation at printf("%d\n", ccc(x)).
print "The return value is %d" % return_value
2018-1-27 18:13
0
雪    币: 36
活跃值: (1061)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
mark
2018-1-27 18:49
0
雪    币: 1187
活跃值: (410)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
M一下
2018-1-29 22:36
0
雪    币: 229
活跃值: (130)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
学习了
2018-1-31 11:52
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
2018-1-31 22:42
0
雪    币: 5
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
学习了
2018-2-1 16:21
0
雪    币: 210
活跃值: (631)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
10
2018-2-1 18:05
0
雪    币: 324
活跃值: (384)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
11
mark
2018-2-3 02:13
0
雪    币: 1491
活跃值: (985)
能力值: (RANK:860 )
在线值:
发帖
回帖
粉丝
12
代码着色看起来很是舒服
2018-2-5 13:38
0
雪    币: 1
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
学到了~  话说。。。dp不是翻译成“动态编程”吧,一般都翻译为“动态规划”
2018-2-13 21:33
0
雪    币: 689
活跃值: (422)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
14
kcarc 学到了~ 话说。。。dp不是翻译成“动态编程”吧,一般都翻译为“动态规划”
谢谢,学习了
2018-2-16 21:20
0
雪    币: 1462
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
慢慢研究研究
2019-10-8 14:34
0
雪    币: 1759
活跃值: (2334)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
16
没有jni_load的so怎么调用java_开头的函数?直接注释调用jni_load那行代码的话,后面执行直接就报错了,提示如下




2019-12-3 18:41
0
雪    币: 248
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17

TASK 4


未进行stack长度判断(小bug)

elif address == CCC_END:
    if len(stack):
      arg0 = stack.pop()                     # We know arguments when exiting the function
      ret = mu.reg_read(UC_ARM_REG_R0)      # Read the return value (R0)
      d[arg0] = ret

疑问:


mu.reg_write(UC_ARM_REG_PC, 0x105bc)     # Set PC to point at "BX LR" instruction.
#mu.reg_write(UC_ARM_REG_PC, 0x10580)    # Set PC to point at "BX LR" instruction.


设置PC为 BX LR 指令,其中0x105bc和0x10580地址指向的指令一致,但执行结果不一致!


2021-8-21 11:44
0
雪    币: 458
活跃值: (1882)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
18
```
如果是,直接返回这个key-value就行,只需将返回值写入到RAX中,同时设置RIP为RET指令的值,退出这个函数。不能在fabonacci函数内直接跳转到RET,因为这条指令已经被HOOK了,所以我们跳转到main中的ret。
如果dict中没有出现参数和对应的值,将参数添加到dict中。
```

这里我感觉可以跳到fabonacci函数内的ret啊,因为这是fabonacci函数返回的,为什么要跳到main函数的ret
2021-9-26 16:06
0
雪    币: 185
活跃值: (262)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
task2的目的是啥啊
2023-4-26 13:57
0
游客
登录 | 注册 方可回帖
返回
//