查看原文
其他

Unicorn引擎教程(一)

2018-02-13 zplusplus 看雪学院

在该篇教程中,你将通过实际操作来学习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@1
  int v4; // ebx@1
  signed __int64 v5; // r8@2
  char v6; // r9@3
  __int64 v7; // r8@3
  char v8; // cl@3
  __int64 v9; // r9@5
  int a2a; // [sp+Ch] [bp-1Ch]@3
 
  v3 = &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@1
  unsigned int v3; // er12@3
  unsigned int result; // eax@3
  unsigned int v5; // edx@3
  unsigned int v6; // esi@3
  unsigned int v7; // edx@4
 
  v2 = 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 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)


这里,我只添加了一些常用的在之后会用到的一些函数

 

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 = 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)


可以开始模拟和执行代码了,但是需要先知道从哪开始从哪结束。

 

我们可以从地址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.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)


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 = 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 = [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 = 0x0000000000400670
FIBONACCI_END = [0x00000000004006F1, 0x0000000000400709]
 
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 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)
 
    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 RDI
        r_rsi = mu.reg_read(UC_X86_REG_RSI)         # Read the second argument which is a reference
        arg1 = u32(mu.mem_read(r_rsi, 4))           # Read the second argument from reference
 
        if (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 register
            mu.mem_write(r_rsi, p32(ret_ref))       # Set retun value through reference
            mu.reg_write(UC_X86_REG_RIP, 0x400582)  # Set RIP to point at RET instruction. We want to return from fibonacci function
 
        else:
            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 function
 
        ret_rax = mu.reg_read(UC_X86_REG_RAX)       # Read the return value that is stored in RAX
        ret_ref = u32(mu.mem_read(r_rsi,4))         # Read the return value that is passed reference
        d[(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,

更多干货等着你~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存