序言

最近在学习如何使用自动化脚本解除OLLVM控制流平坦化的混淆时,遇到了一个难题。对于真实块的执行顺序与上下文存在关联时,如何找到真实块间的执行顺序,然后恢复控制流?

所幸已经有前辈给出了答案!

2022祥云杯CTF 中babyparser 的题解

通过unicorn 模拟执行解决。但是没学习过unicorn 的使用,于是有了这篇文章。

unicorn 简介与安装

简介

Unicorn是一个基于Qemu的轻量级的多平台、多架构的 CPU 模拟器框架。

具备如下优点:

  • 多架构:ARM, ARM64 (ARMv8), m68k, MIPS, PowerPC, RISC-V, S390x (SystemZ), SPARC, TriCore & x86 (include x86_64)
  • 干净/简单/轻量级/直观的架构中立 API
  • 对Windows和**nix系统(已确认包含Mac OSX, Linux,* BSD & Solaris)的原生支持
  • 以纯 C 语言实现,绑定了 Crystal、Clojure、Visual Basic、Perl、Rust、Ruby、Python、Java、.NET、Go、Delphi/Free Pascal、Haskell、Pharo 和 Lua。
  • 使用JIT编译技术,实现高性能
  • 考虑了多线程安全

conda 安装

# conda 创建虚拟环境
conda create -n unicorn_ollvm python=3.9

# 安装 unicorn 模块
pip install unicorn -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

unicorn 基础知识简记

虚拟内存

Unicorn 采用虚拟内存机制,使得虚拟CPU的内存与真实CPU的内存隔离。

# unicorn 操作内存的api

uc_mem_map(address, size)
uc_mem_read(address, size)
uc_mem_write(addreee, code)

使用uc_mem_map映射内存的时候,address 与 size 都需要与0x1000对齐,也就是0x1000的整数倍,否则会报UC_ERR_ARG 异常。

Hook 机制

Unicorn的Hook机制为编程控制虚拟CPU提供了便利。
Unicorn 支持多种不同类型的Hook。

# 指令执行类
UC_HOOK_INTR
UC_HOOK_INSN
UC_HOOK_CODE
UC_HOOK_BLOCK

# 内存访问类
UC_HOOK_MEM_READ
UC_HOOK_MEM_WRITE
UC_HOOK_MEM_FETCH
UC_HOOK_MEM_READ_AFTER
UC_HOOK_MEM_PROT
UC_HOOK_MEM_FETCH_INVALID
UC_HOOK_MEM_INVALID
UC_HOOK_MEM_VALID

# 异常处理类
UC_HOOK_MEM_READ_UNMAPPED
UC_HOOK_MEM_WRITE_UNMAPPED
UC_HOOK_MEM_FETCH_UNMAPPED

调用hook_add函数可添加一个Hook。Unicorn的Hook是链式的,而不是传统Hook的覆盖式,也就是说,可以同时添加多个同类型的Hook,Unicorn会依次调用每一个handler。

实战学习

[翻译]Unicorn引擎教程-外文翻译-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

task1

该任务是hxp CTF 2017上的一个叫做Fibonacci的例子。

由于程序使用递归进行,导致flag的print越来越慢,我们需要使用unicorn 模拟执行这一过程,将flag快速print。

main() Fibonacci()
image image

part1-unicorn 模拟该程序

脚本如下:【都有注释】

image

运行效果:【成功模拟执行,但是print速度比原程序还慢】

image

part2-模拟加速

上面步骤,已经可以成功模拟程序的执行了。

那么如何提高模拟运行效率呢?【】

涉及到算法中的记忆化搜索 - 知乎 (zhihu.com)

fibonacci() 这个函数有两个参数,分别存于EDI、RSI中,返回值写到RAX中,同时RSI的值只会为0或1。

image

由于是递归调用fibonacci()函数,那么当其参数固定时返回值也是确定的,所以可以考虑用dp数组保存这两个参数是否被访问过,来形成记忆化搜索。

具体可以使用dict保存这些成对的值:

  • 在函数开始的时候,检查参数对应的值是否已经被dict记录
    • 如果是,直接返回这个key-value就行,只需将返回值写入到RAX中,同时设置RIPRET指令的值,退出这个函数。不能在fabonacci函数内直接跳转到RET,因为这条指令已经被HOOK了,所以我们跳转到main中的ret
    • 如果dict中没有出现参数和对应的值,将参数添加到dict中。
  • 当退出函数的时候,保存返回值。可以从我们的栈结构中读取参数和返回值。

hook_code函数中添加

stack =  []
dp = {}
FIBONACCI_ENTRY = 0x400670
FIBONACCI_END = [0x4006F1, 0x400709]

image

再次unicorn 模拟执行,很快得到了flag。

image

【注意:由于此时fibonacci这个函数还在被hook中,所以不能跳转到该函数本身的ret指令。因为这样会触发 hook函数中elif address in FIBONACCI_END:的判断条件,导致stack失衡】

task2

分析下面的shellcode

image

hint

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

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

part1-disasm

Ubuntu 下使用pwntools 的disasm ,对shellcode 进行反汇编得到。

image

part2-unicorn 模拟执行

image

执行得到

image

调用号15对应 int chmod(const char *filename,int mode)

调用号1对应void exit(int status)

part3-获取完整信息

那么知道是对 chmodexit 的系统调用。同时结合汇编一共用到了 eax, ebx, ecx, edx 四个寄存器存储对应调用的参数,可以再次修改 hook 函数对其进行判断输出详细信息。

image

模拟执行

image

可以知道,shellcode对/etc/shadow设置666权限,也就是【所有者、用户组、其他】都具有可读写权限

image

part4-capstone

使用capstone 反汇编引擎 print 完整汇编代码。

# 添加
from capstone import *

md = Cs(CS_ARCH_X86, CS_MODE_32)

# def hookcode() 中增加
for code in md.disasm(machine_code,address):
	print("     0x%x:\t%s\t%s" % (code.address, code.mnemonic, code.op_str))

完整汇编代码很长,就不贴出了。

image

task3

image

代码使用的编译选项:gcc function.c -m32 -o function

任务:如何使用unicorn 模拟执行使得函数super_function()的返回值为1

image

image

一个显然的思路,使用unicorn在调用super_function()时将参数修改。

我们知道stdcall 函数调用约定,参数从右往左依次入栈。栈空间结构如下:

image

part1-unicorn-hook参数

image

.text:000005CA E8 AC FF FF FF call super_function处,进行参数hook。

image

task4

hint:目标架构不是x86而是小端ARM32

  • 函数的第一个参数通过R0传递(UC_ARM_REG_R0)
  • 返回值同样在R0
  • 函数的第二个参数通过R1传递(UC_ARM_REG_R1)
  • 可以通过mu = Uc (UC_ARCH_ARM,UC_MODE_LITTLE_ENDIAN)来初始化Unicorn。
a@x:~/Desktop/unicorn_engine_lessons$ file task4
task4: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=3dbf508680ba3d023d3422025954311e1d8fb4a1, not stripped

ida查看程序,发现和task1类似都是递归。

part1 - unicorn 模拟执行

程序的分析和task1差不多,直接贴脚本。

image

image

得到正确答案:2635833876

unicorn 小抄

from unicorn import * - 加载Unicorn库。包含一些函数和基本的常量。

from unicorn.x86_const import* - 加载 X86 和X64架构相关的常量

unicorn 模块中的常量

UC_API_MAJOR                UC_ERR_VERSION              UC_MEM_READ                 UC_PROT_ALL
UC_API_MINOR                UC_ERR_WRITE_PROT           UC_MEM_READ_AFTER           UC_PROT_EXEC
UC_ARCH_ARM                 UC_ERR_WRITE_UNALIGNED      UC_MEM_READ_PROT            UC_PROT_NONE
UC_ARCH_ARM64               UC_ERR_WRITE_UNMAPPED       UC_MEM_READ_UNMAPPED        UC_PROT_READ
UC_ARCH_M68K                UC_HOOK_BLOCK               UC_MEM_WRITE                UC_PROT_WRITE
UC_ARCH_MAX                 UC_HOOK_CODE                UC_MEM_WRITE_PROT           UC_QUERY_MODE
UC_ARCH_MIPS                UC_HOOK_INSN                UC_MEM_WRITE_UNMAPPED       UC_QUERY_PAGE_SIZE
UC_ARCH_PPC                 UC_HOOK_INTR                UC_MILISECOND_SCALE         UC_SECOND_SCALE
UC_ARCH_SPARC               UC_HOOK_MEM_FETCH           UC_MODE_16                  UC_VERSION_EXTRA
UC_ARCH_X86                 UC_HOOK_MEM_FETCH_INVALID   UC_MODE_32                  UC_VERSION_MAJOR
UC_ERR_ARCH                 UC_HOOK_MEM_FETCH_PROT      UC_MODE_64                  UC_VERSION_MINOR
UC_ERR_ARG                  UC_HOOK_MEM_FETCH_UNMAPPED  UC_MODE_ARM                 Uc
UC_ERR_EXCEPTION            UC_HOOK_MEM_INVALID         UC_MODE_BIG_ENDIAN          UcError
UC_ERR_FETCH_PROT           UC_HOOK_MEM_PROT            UC_MODE_LITTLE_ENDIAN       arm64_const
UC_ERR_FETCH_UNALIGNED      UC_HOOK_MEM_READ            UC_MODE_MCLASS              arm_const
UC_ERR_FETCH_UNMAPPED       UC_HOOK_MEM_READ_AFTER      UC_MODE_MICRO               debug
UC_ERR_HANDLE               UC_HOOK_MEM_READ_INVALID    UC_MODE_MIPS3               m68k_const
UC_ERR_HOOK                 UC_HOOK_MEM_READ_PROT       UC_MODE_MIPS32              mips_const
UC_ERR_HOOK_EXIST           UC_HOOK_MEM_READ_UNMAPPED   UC_MODE_MIPS32R6            sparc_const
UC_ERR_INSN_INVALID         UC_HOOK_MEM_UNMAPPED        UC_MODE_MIPS64              uc_arch_supported
UC_ERR_MAP                  UC_HOOK_MEM_VALID           UC_MODE_PPC32               uc_version
UC_ERR_MODE                 UC_HOOK_MEM_WRITE           UC_MODE_PPC64               unicorn
UC_ERR_NOMEM                UC_HOOK_MEM_WRITE_INVALID   UC_MODE_QPX                 unicorn_const
UC_ERR_OK                   UC_HOOK_MEM_WRITE_PROT      UC_MODE_SPARC32             version_bind
UC_ERR_READ_PROT            UC_HOOK_MEM_WRITE_UNMAPPED  UC_MODE_SPARC64             x86_const
UC_ERR_READ_UNALIGNED       UC_MEM_FETCH                UC_MODE_THUMB              
UC_ERR_READ_UNMAPPED        UC_MEM_FETCH_PROT           UC_MODE_V8                 
UC_ERR_RESOURCE             UC_MEM_FETCH_UNMAPPED       UC_MODE_V9

unicorn.x86_const中的常量

UC_X86_REG_EAX
UC_X86_REG_RIP
UC_X86_REG_RAX

常用函数示例

mu = Uc(arch,mode) - 获取Uc实例。在这里指定目标架构,例如:

  • mu = Uc(UC_ARCH_X86,UC_MODE_64) - 获取X86-64架构的实例。
  • mu = Uc(UC_ARCH_X86,UC_MODE_32) - 获取X86-32架构的实例。

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 命令追踪

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(向Unicorn引擎中),我们定义的函数会在执行每一条命令之前被执行。参数含义如下:

  • Uc实例
  • 指令的地址
  • 指令的长度
  • 用户定义数据(通过hook_add()函数传递)

hook的类型

C++
// All type of hooks for uc_hook_add() API.
typedef enum uc_hook_type {
    // Hook all interrupt/syscall events
    UC_HOOK_INTR = 1 << 0,
    // Hook a particular instruction - only a very small subset of instructions supported here
    UC_HOOK_INSN = 1 << 1,
    // Hook a range of code
    UC_HOOK_CODE = 1 << 2,
    // Hook basic blocks
    UC_HOOK_BLOCK = 1 << 3,
    // Hook for memory read on unmapped memory
    UC_HOOK_MEM_READ_UNMAPPED = 1 << 4,
    // Hook for invalid memory write events
    UC_HOOK_MEM_WRITE_UNMAPPED = 1 << 5,
    // Hook for invalid memory fetch for execution events
    UC_HOOK_MEM_FETCH_UNMAPPED = 1 << 6,
    // Hook for memory read on read-protected memory
    UC_HOOK_MEM_READ_PROT = 1 << 7,
    // Hook for memory write on write-protected memory
    UC_HOOK_MEM_WRITE_PROT = 1 << 8,
    // Hook for memory fetch on non-executable memory
    UC_HOOK_MEM_FETCH_PROT = 1 << 9,
    // Hook memory read events.
    UC_HOOK_MEM_READ = 1 << 10,
    // Hook memory write events.
    UC_HOOK_MEM_WRITE = 1 << 11,
    // Hook memory fetch for execution events
    UC_HOOK_MEM_FETCH = 1 << 12,
    // Hook memory read events, but only successful access.
    // The callback will be triggered after successful read.
    UC_HOOK_MEM_READ_AFTER = 1 << 13,
} uc_hook_type;

关键函数的声明等

uc_open

C
/*
 Create new instance of unicorn engine.
 
 @arch: architecture type (UC_ARCH_*)
 @mode: hardware mode. This is combined of UC_MODE_*
 @uc: pointer to uc_engine, which will be updated at return time
 
 @return UC_ERR_OK on success, or other value on failure (refer to uc_err enum
   for detailed error).
*/
UNICORN_EXPORT
uc_err uc_open(uc_arch arch, uc_mode mode, uc_engine **uc);

@arch:架构类型(UC_ARCH_
@mode:硬件模式。 这是结合 UC_MODE_

@uc:指向uc_engine的指针,返回时会更新
@return UC_ERR_OK 成功,或其他值失败(参考 uc_err 枚举详细错误)。

uc_mem_map

C
/*
 Map memory in for emulation.
 This API adds a memory region that can be used by emulation.
 
 @uc: handle returned by uc_open()
 @address: starting address of the new memory region to be mapped in.
    This address must be aligned to 4KB, or this will return with UC_ERR_ARG error.
 @size: size of the new memory region to be mapped in.
    This size must be a multiple of 4KB, or this will return with UC_ERR_ARG error.
 @perms: Permissions for the newly mapped region.
    This must be some combination of UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC,
    or this will return with UC_ERR_ARG error.
 
 @return UC_ERR_OK on success, or other value on failure (refer to uc_err enum
   for detailed error).
*/
UNICORN_EXPORT
uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);

映射内存以进行仿真。
该API添加了一个可供仿真使用的内存区域。
参数:
@uc: uc_open() 返回的句柄
@address:要映射到的新内存区域的起始地址。
该地址必须与 4KB 对齐,否则将返回 UC_ERR_ARG 错误。
@size:要映射到的新内存区域的大小。
此大小必须是 4KB 的倍数,否则将返回 UC_ERR_ARG 错误。
@perms:新映射区域的权限。
这必须是 UC_PROT_READ | 的某种组合。 UC_PROT_WRITE | UC_PROT_EXEC,
否则这将返回 UC_ERR_ARG 错误。
@return UC_ERR_OK 成功,或其他值失败(参考 uc_err 枚举详细错误)。

最后一个参数是uc_prot的枚举值:

C
typedef enum uc_prot {
   UC_PROT_NONE = 0,
   UC_PROT_READ = 1,
   UC_PROT_WRITE = 2,
   UC_PROT_EXEC = 4,
   UC_PROT_ALL = 7,
} uc_prot;

uc_mem_write

C
/*
 Write to a range of bytes in memory.
 
 @uc: handle returned by uc_open()
 @address: starting memory address of bytes to set.
 @bytes:   pointer to a variable containing data to be written to memory.
 @size:   size of memory to write to.
 
 NOTE: @bytes must be big enough to contain @size bytes.
 
 @return UC_ERR_OK on success, or other value on failure (refer to uc_err enum
   for detailed error).
*/
UNICORN_EXPORT
uc_err uc_mem_write(uc_engine *uc, uint64_t address, const void *bytes, size_t size);

写入内存中的字节范围。
@uc: uc_open() 返回的句柄
@address:要设置的字节的起始内存地址。
@bytes:指向包含要写入内存的数据的变量的指针。
@size:要写入的内存大小。
注意:@bytes 必须足够大以包含 @size 字节。
@return UC_ERR_OK 成功,或其他值失败(参考 uc_err 枚举详细错误)

uc_reg_write

C
/*
 Write to register.
 
 @uc: handle returned by uc_open()
 @regid:  register ID that is to be modified.
 @value:  pointer to the value that will set to register @regid
 
 @return UC_ERR_OK on success, or other value on failure (refer to uc_err enum
   for detailed error).
*/
UNICORN_EXPORT
uc_err uc_reg_write(uc_engine *uc, int regid, const void *value);

@uc: uc_open() 返回的句柄
@regid:要修改的注册ID。
@value:指向将设置为注册@regid 的值的指针
@return UC_ERR_OK 成功,或其他值失败(参考 uc_err 枚举详细错误)。

uc_hook_add

C
/*
 Register callback for a hook event.
 The callback will be run when the hook event is hit.
 
 @uc: handle returned by uc_open()
 @hh: hook handle returned from this registration. To be used in uc_hook_del() API
 @type: hook type, refer to uc_hook_type enum
 @callback: callback to be run when instruction is hit
 @user_data: user-defined data. This will be passed to callback function in its
      last argument @user_data
 @begin: start address of the area where the callback is in effect (inclusive)
 @end: end address of the area where the callback is in effect (inclusive)
   NOTE 1: the callback is called only if related address is in range [@begin, @end]
   NOTE 2: if @begin > @end, callback is called whenever this hook type is triggered
 @...: variable arguments (depending on @type)
   NOTE: if @type = UC_HOOK_INSN, this is the instruction ID (ex: UC_X86_INS_OUT)
 
 @return UC_ERR_OK on success, or other value on failure (refer to uc_err enum
   for detailed error).
*/
UNICORN_EXPORT
uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
        void *user_data, uint64_t begin, uint64_t end, ...);

为钩子事件注册回调
当钩子事件被命中时,回调将运行

@uc: uc_open() 返回的句柄
@hh:从这个注册函数返回的钩子句柄。用于 uc_hook_del() API
@type:钩子类型,参考uc_hook_type枚举
@callback:当指令被命中时运行的回调
@user_data:用户定义的数据。这将传递给回调函数的最后一个参数@user_data
@begin:回调生效区域的起始地址(含)
@end:回调生效区域的结束地址(含)
注意 1:仅当相关地址在 [@begin, @end] 范围内时才会调用回调
注意 2:如果 @begin > @end,每当触发此钩子类型时都会调用回调
@…:可变参数(取决于@type)
注意:如果@type = UC_HOOK_INSN,这是指令ID(例如:UC_X86_INS_OUT)
@return UC_ERR_OK 成功,或其他值失败(参考 uc_err 枚举详细错误)。

C
// All type of hooks for uc_hook_add() API.
typedef enum uc_hook_type {
    // Hook all interrupt/syscall events
    UC_HOOK_INTR = 1 << 0,
    **// Hook a particular instruction - only a very small subset of instructions supported here
    UC_HOOK_INSN = 1 << 1,**
    // Hook a range of code
    UC_HOOK_CODE = 1 << 2,
    // Hook basic blocks
    UC_HOOK_BLOCK = 1 << 3,
    // Hook for memory read on unmapped memory
    UC_HOOK_MEM_READ_UNMAPPED = 1 << 4,
    // Hook for invalid memory write events
    UC_HOOK_MEM_WRITE_UNMAPPED = 1 << 5,
    // Hook for invalid memory fetch for execution events
    UC_HOOK_MEM_FETCH_UNMAPPED = 1 << 6,
    // Hook for memory read on read-protected memory
    UC_HOOK_MEM_READ_PROT = 1 << 7,
    // Hook for memory write on write-protected memory
    UC_HOOK_MEM_WRITE_PROT = 1 << 8,
    // Hook for memory fetch on non-executable memory
    UC_HOOK_MEM_FETCH_PROT = 1 << 9,
    **// Hook memory read events.
    UC_HOOK_MEM_READ = 1 << 10,**
    // Hook memory write events.
    UC_HOOK_MEM_WRITE = 1 << 11,
    // Hook memory fetch for execution events
    UC_HOOK_MEM_FETCH = 1 << 12,
    // Hook memory read events, but only successful access.
    // The callback will be triggered after successful read.
    UC_HOOK_MEM_READ_AFTER = 1 << 13,
    // Hook invalid instructions exceptions.
    UC_HOOK_INSN_INVALID = 1 << 14,
} uc_hook_type;

uc_hook_add的第三个参数的值是2,即(uc_hook_type)UC_HOOK_INSN, 钩住一个特定的指令——这里只支持非常小的指令子集

如果第三个参数的值是0x400,说明这是钩取的内存读 // Hook memory read events.

反正这个函数的关键就在第三个参数

最后一个参数,就像函数声明中说的一样,如果@type = UC_HOOK_INSN,这是指令ID(例如:UC_X86_INS_OUT,就是一些X86的指令)。