本篇介绍
unicorn是一个轻量级,多平台,多架构的CPU模拟器框架。使用Unicorn可以模拟执行执行,并且也支持指令级别的hook。本篇看下unicorn的用法。
模拟指令介绍
代码如下:
#include <iostream>
#include <unicorn/unicorn.h>
// code to be emulated
#define X86_CODE32 "\x41\x4a" // INC ecx; DEC edx
// memory address where emulation starts
#define ADDRESS 0x1000000
int main(int argc, char **argv, char **envp)
{
uc_engine *uc;
uc_err err;
int r_ecx = 0x1234; // ECX register
int r_edx = 0x7890; // EDX register
std::cout<<"Emulate i386 code"<<std::endl;
// Initialize emulator in X86-32bit mode
err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc);
if (err != UC_ERR_OK) {
std::cout<<"Failed on uc_open() with error returned:"<<err<<std::endl;
return -1;
}
// map 2MB memory for this emulation
uc_mem_map(uc, ADDRESS, 2 * 1024 * 1024, UC_PROT_ALL);
// write machine code to be emulated to memory
if (uc_mem_write(uc, ADDRESS, X86_CODE32, sizeof(X86_CODE32) - 1)) {
std::cout<<"Failed to write emulation code to memory, quit!"<<err<<std::endl;
return -1;
}
std::cout<<"Before Emulation done"<<std::endl;
std::cout<<">>> ECX = "<<std::hex << r_ecx << std::endl;
std::cout<<">>> EDX = "<<std::hex<< r_edx<< std::endl;
// initialize machine registers
uc_reg_write(uc, UC_X86_REG_ECX, &r_ecx);
uc_reg_write(uc, UC_X86_REG_EDX, &r_edx);
// emulate code in infinite time & unlimited instructions
err=uc_emu_start(uc, ADDRESS, ADDRESS + sizeof(X86_CODE32) - 1, 0, 0);
if (err) {
std::cout<<"Failed on uc_emu_start() with error returned"<< err << uc_strerror(err) << std::endl;
}
// now print out some registers
std::cout<<"Emulation done. Below is the CPU context"<<std::endl;
uc_reg_read(uc, UC_X86_REG_ECX, &r_ecx);
uc_reg_read(uc, UC_X86_REG_EDX, &r_edx);
std::cout<<">>> ECX = "<<std::hex << r_ecx << std::endl;
std::cout<<">>> EDX = "<<std::hex<< r_edx<< std::endl;
uc_close(uc);
return 0;
}
执行结果如下:
Emulate i386 code
Before Emulation done
>>> ECX = 1234
>>> EDX = 7890
Emulation done. Below is the CPU context
>>> ECX = 1235
>>> EDX = 788f
从上面的例子总结如下:
uc_err uc_open(uc_arch arch, uc_mode mode, uc_engine **uc);
负责创建新的 Unicorn实例。
uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);
为模拟映射一块内存,地址首地址和长度都需要是0x1000的整倍数,在模拟过程中,左右的CPU操作都只能访问此内存。
uc_err uc_mem_write(uc_engine *uc, uint64_t address, const void *bytes, size_t
size);
在内存中写入一段字节码。
uc_err uc_reg_write(uc_engine *uc, int regid, const void *value);
将值写入寄存器。
uc_err uc_emu_start(uc_engine *uc, uint64_t begin, uint64_t until, uint64_t
timeout, size_t count);
在指定的时间内模拟机器码。
uc_err uc_reg_read(uc_engine *uc, int regid, void *value);
读取寄存器的值。
hook指令介绍
unicorn也支持多种类型的hook,类型如下:
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,
// Hook on new edge generation. Could be useful in program analysis.
//
// NOTE: This is different from UC_HOOK_BLOCK in 2 ways:
// 1. The hook is called before executing code.
// 2. The hook is only called when generation is triggered.
UC_HOOK_EDGE_GENERATED = 1 << 15,
// Hook on specific tcg op code. The usage of this hook is similar to
// UC_HOOK_INSN.
UC_HOOK_TCG_OPCODE = 1 << 16,
} uc_hook_type;
看一个例子如下:
#include <string.h>
#include "unicorn/unicorn.h"
int syscall_abi[] = {
UC_X86_REG_RAX, UC_X86_REG_RDI, UC_X86_REG_RSI, UC_X86_REG_RDX,
UC_X86_REG_R10, UC_X86_REG_R8, UC_X86_REG_R9
};
uint64_t vals[7] = { 200, 10, 11, 12, 13, 14, 15 };
void* ptrs[7];
void uc_perror(const char* func, uc_err err)
{
fprintf(stderr, "Error in %s(): %s\n", func, uc_strerror(err));
}
#define BASE 0x10000
// mov rax, 100; mov rdi, 1; mov rsi, 2; mov rdx, 3; mov r10, 4; mov r8, 5; mov r9, 6; syscall
#define CODE "\x48\xc7\xc0\x64\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc6\x02\x00\x00\x00\x48\xc7\xc2\x03\x00\x00\x00\x49\xc7\xc2\x04\x00\x00\x00\x49\xc7\xc0\x05\x00\x00\x00\x49\xc7\xc1\x06\x00\x00\x00\x0f\x05"
void hook_code(uc_engine *uc, uint64_t address, uint32_t size,
void *user_data)
{
printf("HOOK_CODE: 0x%" PRIx64 ", 0x%x\n", address, size);
}
int main() {
int i;
uc_hook sys_hook;
uc_err err;
uc_engine* uc;
for (i = 0; i < 7; i++) {
ptrs[i] = &vals[i];
}
if ((err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc))) {
uc_perror("uc_open", err);
return 1;
}
printf("reg_write_batch({200, 10, 11, 12, 13, 14, 15})\n");
if ((err = uc_reg_write_batch(uc, syscall_abi, ptrs, 7))) {
uc_perror("uc_reg_write_batch", err);
return 1;
}
memset(vals, 0, sizeof(vals));
if ((err = uc_reg_read_batch(uc, syscall_abi, ptrs, 7))) {
uc_perror("uc_reg_read_batch", err);
return 1;
}
printf("reg_read_batch = {");
for (i = 0; i < 7; i++) {
if (i != 0) printf(", ");
printf("%" PRIu64, vals[i]);
}
printf("}\n");
// syscall
printf("\n");
printf("running shellcode\n");
if ((err = uc_hook_add(uc, &sys_hook, UC_HOOK_CODE, hook_code, NULL, 1, 0))) {
uc_perror("uc_hook_add", err);
return 1;
}
if ((err = uc_mem_map(uc, BASE, 0x1000, UC_PROT_ALL))) {
uc_perror("uc_mem_map", err);
return 1;
}
if ((err = uc_mem_write(uc, BASE, CODE, sizeof(CODE) - 1))) {
uc_perror("uc_mem_write", err);
return 1;
}
if ((err = uc_emu_start(uc, BASE, BASE + sizeof(CODE) - 1, 0, 0))) {
uc_perror("uc_emu_start", err);
return 1;
}
memset(vals, 0, sizeof(vals));
if ((err = uc_reg_read_batch(uc, syscall_abi, ptrs, 7))) {
uc_perror("uc_reg_read_batch", err);
return 1;
}
printf("reg_read_batch = {");
for (i = 0; i < 7; i++) {
if (i != 0) printf(", ");
printf("%" PRIu64, vals[i]);
}
printf("}\n");
return 0;
}
对应的输出如下:
reg_write_batch({200, 10, 11, 12, 13, 14, 15})
reg_read_batch = {200, 10, 11, 12, 13, 14, 15}
running shellcode
HOOK_CODE: 0x10000, 0x7
HOOK_CODE: 0x10007, 0x7
HOOK_CODE: 0x1000e, 0x7
HOOK_CODE: 0x10015, 0x7
HOOK_CODE: 0x1001c, 0x7
HOOK_CODE: 0x10023, 0x7
HOOK_CODE: 0x1002a, 0x7
HOOK_CODE: 0x10031, 0x2
reg_read_batch = {100, 1, 2, 3, 4, 5, 6}
此时每个指令执行时,都会被hook到。
看下如下函数:
uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...);
注册hook事件的回调,当hook事件被触发将会进行回调。
unicorn与Android
再看一个unicorn调试Android的例子,看看如何在unicorn中模拟arm环境,执行Android上的so。首先写一个helloworld。
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
使用交叉编译成android上的bin文件。查看file信息:
a.bin: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped
我目前在macos上,肯定是执行不了这个bin,那接下来就需要让unicorn加载该代码,也就是把整个so都读到内存中。这儿需要考虑到代码里调用了printf这样libc的函数,unicorn里是访问不到的,那就需要跳过该地址,具体操作就是遇到执行printf的指令,也就是lr,那就直接修改pc为下一条指令。
这儿会有一个小问题,让unicorn从哪儿开始执行呢?肯定不是从so的0地址开始执行,entry_point算是一个地址,可是我们更希望是从main开始执行,那如何做到呢?其实很简单,找一个反编译软件看下就可以了,我这边查到的结果如下:
这样偏移就有了。接下来为了让指令显示更清晰下,就顺便反编译了下,这儿借助capstone就做到了。最后的执行结果如下:
code is 0xd10083ff
0x0: sub sp, sp, #0x20
op_count: 3
operands[0].type: REG = sp
operands[0].access: WRITE
operands[1].type: REG = sp
operands[1].access: READ
operands[2].type: IMM = 0x20
operands[2].access: READ
Registers read: sp
Registers modified: sp
HOOK_CODE: 0x65c, 0x4
code is 0xa9017bfd
0x0: stp x29, x30, [sp, #0x10]
op_count: 3
operands[0].type: REG = fp
operands[0].access: READ
operands[1].type: REG = lr
operands[1].access: READ
operands[2].type: MEM
operands[2].mem.base: REG = sp
operands[2].mem.disp: 0x10
operands[2].access: READ | WRITE
Registers read: fp lr sp
HOOK_CODE: 0x660, 0x4
code is 0x910043fd
0x0: add x29, sp, #0x10
op_count: 3
operands[0].type: REG = fp
operands[0].access: WRITE
operands[1].type: REG = sp
operands[1].access: READ
operands[2].type: IMM = 0x10
operands[2].access: READ
Registers read: sp
Registers modified: fp
HOOK_CODE: 0x664, 0x4
code is 0x2a1f03e8
0x0: mov w8, wzr
op_count: 2
operands[0].type: REG = w8
operands[0].access: WRITE
operands[1].type: REG = wzr
operands[1].access: READ
Registers read: wzr
Registers modified: w8
HOOK_CODE: 0x668, 0x4
code is 0xb9000be8
0x0: str w8, [sp, #8]
op_count: 2
operands[0].type: REG = w8
operands[0].access: READ
operands[1].type: MEM
operands[1].mem.base: REG = sp
operands[1].mem.disp: 0x8
operands[1].access: READ | WRITE
Registers read: w8 sp
HOOK_CODE: 0x66c, 0x4
code is 0xb81fc3bf
0x0: stur wzr, [x29, #-4]
op_count: 2
operands[0].type: REG = wzr
operands[0].access: READ
operands[1].type: MEM
operands[1].mem.base: REG = fp
operands[1].mem.disp: 0xfffffffc
operands[1].access: READ | WRITE
Registers read: wzr fp
HOOK_CODE: 0x670, 0x4
code is 0xf0ffffe0
0x0: adrp x0, #0xfffffffffffff000
op_count: 2
operands[0].type: REG = x0
operands[0].access: WRITE
operands[1].type: IMM = 0xfffffffffffff000
operands[1].access: READ
Registers modified: x0
HOOK_CODE: 0x674, 0x4
code is 0x91142000
0x0: add x0, x0, #0x508
op_count: 3
operands[0].type: REG = x0
operands[0].access: WRITE
operands[1].type: REG = x0
operands[1].access: READ
operands[2].type: IMM = 0x508
operands[2].access: READ
Registers read: x0
Registers modified: x0
HOOK_CODE: 0x678, 0x4
code is 0x94000015
0x0: bl #0x54
op_count: 1
operands[0].type: IMM = 0x54
operands[0].access: READ
Registers modified: lr
HOOK_CODE: 0x67c, 0x4
skip it
code is 0xb9400be0
0x0: ldr w0, [sp, #8]
op_count: 2
operands[0].type: REG = w0
operands[0].access: WRITE
operands[1].type: MEM
operands[1].mem.base: REG = sp
operands[1].mem.disp: 0x8
operands[1].access: READ
Registers read: sp
Registers modified: w0
HOOK_CODE: 0x680, 0x4
code is 0xa9417bfd
0x0: ldp x29, x30, [sp, #0x10]
op_count: 3
operands[0].type: REG = fp
operands[0].access: WRITE
operands[1].type: REG = lr
operands[1].access: WRITE
operands[2].type: MEM
operands[2].mem.base: REG = sp
operands[2].mem.disp: 0x10
operands[2].access: READ
Registers read: sp
Registers modified: fp lr
HOOK_CODE: 0x684, 0x4
code is 0x910083ff
0x0: add sp, sp, #0x20
op_count: 3
operands[0].type: REG = sp
operands[0].access: WRITE
operands[1].type: REG = sp
operands[1].access: READ
operands[2].type: IMM = 0x20
operands[2].access: READ
Registers read: sp
Registers modified: sp
HOOK_CODE: 0x688, 0x4
最后看下完整代码:
#include <string.h>
#include <stdlib.h>
#include "unicorn/unicorn.h"
#include <capstone/platform.h>
#include <capstone/capstone.h>
#define BASE 0x0
static csh handle;
uint64_t skip_list[] = {0x67c,};
void uc_perror(const char* func, uc_err err)
{
fprintf(stderr, "Error in %s(): %s\n", func, uc_strerror(err));
}
static void print_string_hex(const char *comment, unsigned char *str, size_t len)
{
unsigned char *c;
printf("%s", comment);
for (c = str; c < str + len; c++) {
printf("0x%02x ", *c & 0xff);
}
printf("\n");
}
static void print_insn_detail(cs_insn *ins)
{
cs_arm64 *arm64;
int i;
cs_regs regs_read, regs_write;
unsigned char regs_read_count, regs_write_count;
unsigned char access;
// detail can be NULL if SKIPDATA option is turned ON
if (ins->detail == NULL)
return;
arm64 = &(ins->detail->arm64);
if (arm64->op_count)
printf("\top_count: %u\n", arm64->op_count);
for (i = 0; i < arm64->op_count; i++) {
cs_arm64_op *op = &(arm64->operands[i]);
switch(op->type) {
default:
break;
case ARM64_OP_REG:
printf("\t\toperands[%u].type: REG = %s\n", i, cs_reg_name(handle, op->reg));
break;
case ARM64_OP_IMM:
printf("\t\toperands[%u].type: IMM = 0x%" PRIx64 "\n", i, op->imm);
break;
case ARM64_OP_FP:
#if defined(_KERNEL_MODE)
// Issue #681: Windows kernel does not support formatting float point
printf("\t\toperands[%u].type: FP = <float_point_unsupported>\n", i);
#else
printf("\t\toperands[%u].type: FP = %f\n", i, op->fp);
#endif
break;
case ARM64_OP_MEM:
printf("\t\toperands[%u].type: MEM\n", i);
if (op->mem.base != ARM64_REG_INVALID)
printf("\t\t\toperands[%u].mem.base: REG = %s\n", i, cs_reg_name(handle, op->mem.base));
if (op->mem.index != ARM64_REG_INVALID)
printf("\t\t\toperands[%u].mem.index: REG = %s\n", i, cs_reg_name(handle, op->mem.index));
if (op->mem.disp != 0)
printf("\t\t\toperands[%u].mem.disp: 0x%x\n", i, op->mem.disp);
break;
case ARM64_OP_CIMM:
printf("\t\toperands[%u].type: C-IMM = %u\n", i, (int)op->imm);
break;
case ARM64_OP_REG_MRS:
printf("\t\toperands[%u].type: REG_MRS = 0x%x\n", i, op->reg);
break;
case ARM64_OP_REG_MSR:
printf("\t\toperands[%u].type: REG_MSR = 0x%x\n", i, op->reg);
break;
case ARM64_OP_PSTATE:
printf("\t\toperands[%u].type: PSTATE = 0x%x\n", i, op->pstate);
break;
case ARM64_OP_SYS:
printf("\t\toperands[%u].type: SYS = 0x%x\n", i, op->sys);
break;
case ARM64_OP_PREFETCH:
printf("\t\toperands[%u].type: PREFETCH = 0x%x\n", i, op->prefetch);
break;
case ARM64_OP_BARRIER:
printf("\t\toperands[%u].type: BARRIER = 0x%x\n", i, op->barrier);
break;
}
access = op->access;
switch(access) {
default:
break;
case CS_AC_READ:
printf("\t\toperands[%u].access: READ\n", i);
break;
case CS_AC_WRITE:
printf("\t\toperands[%u].access: WRITE\n", i);
break;
case CS_AC_READ | CS_AC_WRITE:
printf("\t\toperands[%u].access: READ | WRITE\n", i);
break;
}
if (op->shift.type != ARM64_SFT_INVALID &&
op->shift.value)
printf("\t\t\tShift: type = %u, value = %u\n",
op->shift.type, op->shift.value);
if (op->ext != ARM64_EXT_INVALID)
printf("\t\t\tExt: %u\n", op->ext);
if (op->vas != ARM64_VAS_INVALID)
printf("\t\t\tVector Arrangement Specifier: 0x%x\n", op->vas);
if (op->vector_index != -1)
printf("\t\t\tVector Index: %u\n", op->vector_index);
}
if (arm64->update_flags)
printf("\tUpdate-flags: True\n");
if (arm64->writeback)
printf("\tWrite-back: %s\n", arm64->post_index ? "Post" : "Pre");
if (arm64->cc)
printf("\tCode-condition: %u\n", arm64->cc);
// Print out all registers accessed by this instruction (either implicit or explicit)
if (!cs_regs_access(handle, ins,
regs_read, ®s_read_count,
regs_write, ®s_write_count)) {
if (regs_read_count) {
printf("\tRegisters read:");
for(i = 0; i < regs_read_count; i++) {
printf(" %s", cs_reg_name(handle, regs_read[i]));
}
printf("\n");
}
if (regs_write_count) {
printf("\tRegisters modified:");
for(i = 0; i < regs_write_count; i++) {
printf(" %s", cs_reg_name(handle, regs_write[i]));
}
printf("\n");
}
}
printf("\n");
}
void hook_code(uc_engine *uc, uint64_t address, uint32_t size,
void *user_data)
{
uint32_t code;
if(uc_mem_read(uc,address, &code, sizeof(code))) {
printf("Failed to read emulation code to memory, quit!\n");
return;
}
printf("code is 0x%x\n", code);
cs_insn *insn;
int count = cs_disasm(handle, (const uint8_t*)(&code), size, 0x00, 0, &insn);
if (count) {
size_t j;
for (j = 0; j < count; j++) {
printf("0x%" PRIx64 ":\t%s\t%s\n", insn[j].address, insn[j].mnemonic, insn[j].op_str);
print_insn_detail(&insn[j]);
}
//printf("0x%" PRIx64 ":\n", insn[j-1].address + insn[j-1].size);
// free memory allocated by cs_disasm()
cs_free(insn, count);
} else {
printf("ERROR: Failed to disasm given code!\n");
}
printf("HOOK_CODE: 0x%" PRIx64 ", 0x%x\n", address, size);
uint64_t next_addr = address + size;
for (int i = 0; i < sizeof(skip_list); i++) {
if (address == skip_list[i]) {
printf("skip it\n");
uc_reg_write(uc, UC_ARM64_REG_PC, &next_addr);
}
}
}
bool init_capstone() {
cs_err err = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle);
if (err) {
printf("Failed on cs_open() with error returned: %u\n", err);
return false;
}
cs_option(handle, CS_OPT_DETAIL, CS_OPT_ON);
return true;
}
int main() {
int i;
uc_hook sys_hook;
uc_err err;
uc_engine* uc;
char *data = NULL;
if ((err = uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc))) {
uc_perror("uc_open", err);
return 1;
}
if (!init_capstone()) {
goto end;
}
FILE *ptr = fopen("./a.bin", "rb");
if (ptr == NULL) {
printf("open fail\n");
return 1;
}
fseek(ptr, 0l, SEEK_END);
int file_size = ftell(ptr);
fseek(ptr, 0l, SEEK_SET);
if (file_size <= 0) {
printf("invalid file size\n");
goto end;
}
data = (char *)malloc(file_size);
if (data == NULL) {
printf("malloc fail\n");
goto end;
}
int read_size = fread(data, 1, file_size, ptr);
if (read_size != file_size) {
printf("read fail\n");
goto end;
}
if ((err = uc_mem_map(uc, BASE, 8 * 0x1000, UC_PROT_ALL))) {
uc_perror("uc_mem_map", err);
return 1;
}
if ((err = uc_mem_write(uc, BASE, data, read_size))) {
uc_perror("uc_mem_write", err);
return 1;
}
uint64_t stack_top = BASE + read_size + 0x1000 -0x8;
uc_reg_write(uc, UC_ARM64_REG_SP, &stack_top);
uint64_t start_addr = BASE + 0x65c;
uint64_t end_addr = BASE + 0x68c;
if ((err = uc_hook_add(uc, &sys_hook, UC_HOOK_CODE, hook_code, NULL, start_addr, end_addr))) {
uc_perror("uc_hook_add", err);
return 1;
}
if ((err = uc_emu_start(uc, start_addr, end_addr, 0, 0))) {
uc_perror("uc_emu_start", err);
return 1;
}
end:
if (ptr != NULL) {
fclose(ptr);
ptr = NULL;
}
if (data != NULL) {
free(data);
data = NULL;
}
return 0;
}
这样就大概介绍了下unicorn的使用,还是很有趣的。