首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >使用 Unicorn 模拟器运行具有不同 CPU 架构的代码

使用 Unicorn 模拟器运行具有不同 CPU 架构的代码

作者头像
Khan安全团队
发布于 2022-01-17 15:58:19
发布于 2022-01-17 15:58:19
4.1K0
举报
文章被收录于专栏:Khan安全团队Khan安全团队

Unicorn 是一个轻量级的多平台、多架构 CPU 仿真器框架™ -官网。它有什么用处?我用它来跟踪和分析 iOS arm64 二进制文件中严重混淆和深度嵌套的代码部分。所以它可以是一个非常好的工具来帮助进行一些动态代码分析。您可以运行具有不同目标架构的代码并立即观察结果。

演示应用

这是我为这个演示制作的一个非常基本的应用程序。如您所见,它要求用户提供密钥并将其与预定义的 XOR 加密密钥进行比较。如果它们匹配,我们会打印“成功”消息,否则会打印“错误键”消息。

代码语言:javascript
复制
mbp:~ ./demo
Enter key:
AAAAAAAAAA
Wrong key.

源代码:

代码语言:javascript
复制
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define KEY_LEN 11

const char enc_key[] = { 0x32, 0x24, 0x22, 0x33, 0x24, 0x35, 0x1e, 0x2a, 0x24, 0x38, 0x41 }; // "secret_key" xor 0x41

int check_key(char *key) {
    char dec_key[KEY_LEN];
    for (int i=0; i<KEY_LEN; i++) {
        dec_key[i] = enc_key[i] ^ 0x41;
    }
    return strcmp(dec_key, key);
}

int main(int argc, char* argv[]) {
    printf("Enter key:\n");
    char key[KEY_LEN];
    scanf("%10s", key);
    if (check_key(key) == 0) {
        printf("Success!\n");
    } else {
        printf("Wrong key.\n");
    }
    return 0;
}

arm64为了展示仿真的强大功能,我将使用 iOS SDK将其编译为二进制文件。我的主机是x86_64Intel Mac。编译需要 Xcode。(实际上,iOS 等目标平台并不重要,因为我们正在模拟 CPU,而不是使用二进制加载器、动态链接器等的整个平台。但理论上,在生成的汇编代码中,调用约定可能因平台而异.)

代码语言:javascript
复制
mbp:~ clang demo.c -o demo -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk -fno-stack-protector

我添加了-fno-stack-protector禁用堆栈金丝雀的选项,只是为了让这个演示更容易一些。

如果一切正常,结果将如下所示,功能齐全的 iOS arm64 二进制文件:

代码语言:javascript
复制
mbp:~ file demo
demo: Mach-O 64-bit executable arm64

一些组装

这是check_key函数的反汇编(如 所示objdump)

代码语言:javascript
复制
mbp:~ objdump --disassemble-symbols=_check_key demo
代码语言:javascript
复制
0000000100007e78 <_check_key>:
100007e78: sub  sp, sp, #48
100007e7c: stp  x29, x30, [sp, #32]
100007e80: add  x29, sp, #32
100007e84: stur x0, [x29, #-8]
100007e88: str  wzr, [sp, #8]
100007e8c: ldr  w8, [sp, #8]
100007e90: subs w8, w8, #11
100007e94: b.ge 0x100007ed0 <_check_key+0x58>
100007e98: ldrsw    x9, [sp, #8]
100007e9c: adrp x8, 0x100007000 <_check_key+0x24>
100007ea0: add  x8, x8, #3972
100007ea4: ldrsb    w8, [x8, x9]
100007ea8: mov  w9, #65
100007eac: eor  w8, w8, w9
100007eb0: ldrsw    x10, [sp, #8]
100007eb4: add  x9, sp, #13
100007eb8: add  x9, x9, x10
100007ebc: strb w8, [x9]
100007ec0: ldr  w8, [sp, #8]
100007ec4: add  w8, w8, #1
100007ec8: str  w8, [sp, #8]
100007ecc: b    0x100007e8c <_check_key+0x14>
100007ed0: ldur x1, [x29, #-8]
100007ed4: add  x0, sp, #13
100007ed8: bl   0x100007f78 <_strcmp+0x100007f78>
100007edc: ldp  x29, x30, [sp, #32]
100007ee0: add  sp, sp, #48
100007ee4: ret

我们将尝试模拟这段代码,而不是进行静态分析,以获取与enc_key用户输入进行比较的密钥的值。

如果我使用调试器,我通常会尝试在地址处放置一个断点0x100007ed8——一个strcmp实际执行字符串比较和分析寄存器的函数调用。但是在这里,我们正在分析不同目标架构的二进制文件,我们不能直接运行或调试它。

我们知道strcmp需要两个参数。根据arm64 调用 convetion前 8 个参数通过寄存器传递x0- x7。

正如我们在调用之前看到的那样strcmp,我们有ldur x1, [x29, -8]一条指令从内存中加载一个值,该值x29寄存器指向递减的8寄存器x1,并将add x0, sp, #13其添加13到sp(堆栈指针)值并将其存储到x0. 根据调用约定,这些应该是上面源代码中我们dec_key和变量的地址。key

让我们在模拟器中运行这段代码,x0并x1在strcmp调用之前转储内容。无论如何,我们不会将 C 运行时库加载到我们的模拟器中,因此strcmp不会指向真正的函数,因此不会工作。此外,它还需要重新绑定一些函数存根,这超出了本文的范围。

模拟器

创建一个新的虚拟环境,使用以下命令安装所有依赖项pip:

代码语言:javascript
复制
mbp:~ python3 -m venv .venv/ && source .venv/bin/activate
(.venv) mbp:~ pip install unicorn capstone hexdump

Capstone 是一个多架构的反汇编框架。我将使用它即时反汇编和记录指令。

这是一个完全工作的模拟器代码。让我们部分地回顾它。

代码语言:javascript
复制
#!/usr/bin/env python3

from hexdump import hexdump
from unicorn import *
from unicorn.arm64_const import *
from capstone import *

# 1
BASE_ADDR = 0x1_0000_0000 # base address
BASE_SIZE = 100 * 1024 # enough memory to fit the binary image

HEAP_ADDR = 0x5_0000_0000 # arbitrary address
HEAP_SIZE = 0x21_000 # some default heap size

STACK_ADDR = 0x9_0000_0000 # arbitrary address
STACK_SIZE = 0x21_000 # some default stack size
STACK_TOP = STACK_ADDR + STACK_SIZE # stack grows downwards

# 6
def hook_code(uc, address, size, user_data):
    code = BINARY[address-BASE_ADDR:address-BASE_ADDR+size]
    for i in md.disasm(code, address):
        print("0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))
        # stop emulation when function returns
        if i.mnemonic == "ret":
            uc.emu_stop()
    return True


try:
    # 2
    print("[+] Init")
    md = Cs(CS_ARCH_ARM64, UC_MODE_ARM)
    mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

    # 3
    print("[+] Create memory segments")
    mu.mem_map(BASE_ADDR, BASE_SIZE)
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    mu.mem_map(HEAP_ADDR, HEAP_SIZE)

    # 4
    print("[+] Load and map binary")
    BINARY = open("./demo", "rb").read()
    mu.mem_write(BASE_ADDR, BINARY)

    # 5
    print("[+] Add hooks")
    mu.hook_add(UC_HOOK_CODE, hook_code)

    # 7
    print("[+] Setup stack pointer")
    mu.reg_write(UC_ARM64_REG_SP, STACK_TOP)

    # 8
    # write our input to heap
    mu.mem_write(HEAP_ADDR, b"A" * 10)
    mu.reg_write(UC_ARM64_REG_X0, HEAP_ADDR)

    # 9
    print("[+] Start emulation")
    start_addr = 0x1_0000_7e78 # check_key
    end_addr = 0x1_0000_7ed8 # strcmp
    mu.emu_start(start_addr, end_addr)

    # 10
    # print x0 and x1 values
    print("[+] x0: 0x%x" % (mu.reg_read(UC_ARM64_REG_X0)))
    hexdump(mu.mem_read(mu.reg_read(UC_ARM64_REG_X0), 16))

    print("[+] x1: 0x%x" % (mu.reg_read(UC_ARM64_REG_X1)))
    hexdump(mu.mem_read(mu.reg_read(UC_ARM64_REG_X1), 16))  

    print("[+] Done")
except UcError as err:
    print("[E] %s" % err)

让我们分解一下。

  1. 在这里,我设置了我们将在仿真中使用的基本内存段的地址。BASE_ADDR- 我们的二进制文件将被加载的地址。BASE_SIZE- 应该足以容纳整个二进制文件。HEAP_ADDR和STACK_ADDR- 具有任意大小的堆和堆栈地址0x21000。如果我们在仿真期间耗尽了堆或堆栈内存(并且可能崩溃),我们总是可以增加这些值并重新启动仿真。Unicorn 是一个 CPU 模拟器。它不会动态增加我们的堆栈或堆。这就是操作系统的工作。
  2. *_ARCH_ARM64使用架构和UC_MODE_ARM模式初始化 Unicorn 和 Capstone 引擎。
  3. 创建我们的三个内存段:主二进制文件、堆和具有相应大小的堆栈。
  4. 读取我们编译的 arm64demo二进制文件并将其写入映射内存BASE_ADDR。
  5. 设置挂钩。在这里,我UC_HOOK_CODE用来挂钩每条指令,反汇编和打印hook_code功能。有多个可用的钩子:内存读/写钩子、CPU 中断钩子(我用这个来跟踪syscalls)等。
  6. 我们的钩子函数,它使用 Capstone 反汇编代码,它也检查我们是否到达了一条ret指令。到那时我们可能会停止仿真,如果我们对单个函数的仿真感兴趣,这会很有帮助。
  7. 设置堆栈指针的初始值,随着堆栈向下增长,该指针应指向堆栈顶部。
  8. 我们的check_key函数接受一个参数,该参数通过思想x0寄存器传递。在这里,我们通过将AAAAAAAAAA(10 * A) 写入堆并将指向堆开始的指针放入x0
  9. 开始仿真。0x100007e78是check_key开始的地址,也是我们要开始仿真的地址。0x100007ed8是strcmp我们希望仿真结束的地址。
  10. 仿真结束后,我们要检查地址x0并x1转储相应地址的内存。

输出

在这里我们可以看到模拟器成功运行。我们的secret_key价值被倾销到控制台中!

代码语言:javascript
复制
(.venv) mbp:~ ./demo_emu.py
[+] Init
[+] Map memory
[+] Load and map binary
[+] Add hooks
[+] Setup stack pointer
[+] Starting at: 0x100007e78
[+] x0: 0x900020fdd
00000000: 73 65 63 72 65 74 5F 6B  65 79 00 00 00 00 00 05  secret_key......
[+] x1: 0x500000000
00000000: 41 41 41 41 41 41 41 41  41 41 00 00 00 00 00 00  AAAAAAAAAA......
[+] Done

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 演示应用
  • 一些组装
  • 模拟器
  • 输出
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档