
Unicorn 是一个轻量级的多平台、多架构 CPU 仿真器框架™ -官网。它有什么用处?我用它来跟踪和分析 iOS arm64 二进制文件中严重混淆和深度嵌套的代码部分。所以它可以是一个非常好的工具来帮助进行一些动态代码分析。您可以运行具有不同目标架构的代码并立即观察结果。
这是我为这个演示制作的一个非常基本的应用程序。如您所见,它要求用户提供密钥并将其与预定义的 XOR 加密密钥进行比较。如果它们匹配,我们会打印“成功”消息,否则会打印“错误键”消息。
mbp:~ ./demo
Enter key:
AAAAAAAAAA
Wrong key.源代码:
#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,而不是使用二进制加载器、动态链接器等的整个平台。但理论上,在生成的汇编代码中,调用约定可能因平台而异.)
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 二进制文件:
mbp:~ file demo
demo: Mach-O 64-bit executable arm64这是check_key函数的反汇编(如 所示objdump)
mbp:~ objdump --disassemble-symbols=_check_key demo0000000100007e78 <_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:
mbp:~ python3 -m venv .venv/ && source .venv/bin/activate
(.venv) mbp:~ pip install unicorn capstone hexdumpCapstone 是一个多架构的反汇编框架。我将使用它即时反汇编和记录指令。
这是一个完全工作的模拟器代码。让我们部分地回顾它。
#!/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)让我们分解一下。
BASE_ADDR- 我们的二进制文件将被加载的地址。BASE_SIZE- 应该足以容纳整个二进制文件。HEAP_ADDR和STACK_ADDR- 具有任意大小的堆和堆栈地址0x21000。如果我们在仿真期间耗尽了堆或堆栈内存(并且可能崩溃),我们总是可以增加这些值并重新启动仿真。Unicorn 是一个 CPU 模拟器。它不会动态增加我们的堆栈或堆。这就是操作系统的工作。*_ARCH_ARM64使用架构和UC_MODE_ARM模式初始化 Unicorn 和 Capstone 引擎。demo二进制文件并将其写入映射内存BASE_ADDR。UC_HOOK_CODE用来挂钩每条指令,反汇编和打印hook_code功能。有多个可用的钩子:内存读/写钩子、CPU 中断钩子(我用这个来跟踪syscalls)等。ret指令。到那时我们可能会停止仿真,如果我们对单个函数的仿真感兴趣,这会很有帮助。check_key函数接受一个参数,该参数通过思想x0寄存器传递。在这里,我们通过将AAAAAAAAAA(10 * A) 写入堆并将指向堆开始的指针放入x00x100007e78是check_key开始的地址,也是我们要开始仿真的地址。0x100007ed8是strcmp我们希望仿真结束的地址。x0并x1转储相应地址的内存。在这里我们可以看到模拟器成功运行。我们的secret_key价值被倾销到控制台中!
(.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 删除。