背景
众所周知,在使用 IDA 进行 iOS 逆向工程时,仅仅依赖静态分析很难直接确定方法的 Callers,借助于 Decompiler 和 IDA 自己的分析能力仅能分析出非常有限的 objc_msgSend 交叉引用。目前一般的解法是借助于动态调试的 backtrace 或是 IDA 脚本进行交叉引用的重建。
因此笔者做了一个工具,该工具支持加载 Mach-O 文件的关键信息,并结合 capstone 静态分析 + unicorn 动态分析结合的方式去模拟执行所有的 objc method,并尽可能建立起方法间的交叉引用,最后生成一个 IDA Script,用于将分析结果导入到 IDA 中。
项目地址: github.com/Soulghost/i…
Show Case
话不多说,我们先来看一下分析的效果,以 WeChat 为例,通过笔者开发的扫描工具 iblessing 可以模拟 Mach-O 的 class 加载、dyld 符号地址解析、local 符号解析、静态内存映射等过程,并启动多个 unicorn 实例去模拟执行所有的 objc 方法,并生成 objc_msgSend 的交叉引用报告:
> iblessing -m scan -i objc-msg-xref -f WeChat -d 'antiWrapper=1'
☠️
██╗██████╗ ██╗ ███████╗███████╗███████╗██╗███╗ ██╗ ██████╗
██║██╔══██╗██║ ██╔════╝██╔════╝██╔════╝██║████╗ ██║██╔════╝
██║██████╔╝██║ █████╗ ███████╗███████╗██║██╔██╗ ██║██║ ███╗
██║██╔══██╗██║ ██╔══╝ ╚════██║╚════██║██║██║╚██╗██║██║ ██║
██║██████╔╝███████╗███████╗███████║███████║██║██║ ╚████║╚██████╔╝
╚═╝╚═════╝ ╚══════╝╚══════╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
[***] iblessing iOS Security Exploiting Toolkit Beta 0.1.1 (http://blog.asm.im)
[***] Author: Soulghost (高级页面仔) @ (https://github.com/Soulghost)
[*] set output path to /opt/one-btn/tmp/apps/WeChat/Payload
[*] input file is WeChat
[+] detect mach-o header 64
[+] detect litten-endian
[*] !!! Notice: enter anti-wrapper mode, start anti-wrapper scanner
[*] start Symbol Wrapper Scanner
[*] try to find wrappers for_objc_msgSend
[*] Step1. find __TEXT,__text
[+] find __TEXT,__text at 0x100004000
[+] mapping text segment 0x100000000 ~ 0x107cb0000 to unicorn engine
[*] Step 2. scan in __text
[*] start disassembler at 0x100004000
[*] / 0x1069d986c/0x1069d9874 (100.00%)
[*] reach to end of __text, stop
[+] anti-wrapper finished
[*] start ObjcMethodXrefScanner Exploit Scanner
[*] Step 1. realize all app classes
[*] realize classes 14631/14631 (100.00%)
[+] get 667318 methods to analyze
[*] Step 2. dyld load non-lazy symbols
[*] Step 3. track all calls
[*] progress: 667318 / 667318 (100.00%)
[*] Step 4. serialize call chains to file
[*] saved to /opt/one-btn/tmp/apps/WeChat/Payload/WeChat_method-xrefs.iblessing.txt
> ls -alh WeChat_method-xrefs.iblessing.txt
-rw-r--r-- 1 soulghost wheel 63M Jul 23 14:46 WeChat_method-xrefs.iblessing.txt
报告的格式是方法信息的简单序列化,包含了方法地址、方法签名、前序调用和后续调用的信息:
> head WeChat_method-xrefs.iblessing.txt
iblessing methodchains,ver:0.2;
chainId,sel,prefix,className,methodName,prevMethods,nextMethods
182360,0x1008a0ab8,+[A8KeyControl initialize],+,A8KeyControl,initialize,[],[4429#0x1008a1064@4376#0x1008a1050@13769#0x1008a10d0]
182343,0x1008a0ad0,+[A8KeyControl_QueryStringTransferCookie initialize],+,A8KeyControl_QueryStringTransferCookie,initialize,[],[4429#0x1008a1064@4376#0x1008a1050@13769#0x1008a10d0]
145393,0x1008c2220,+[A8KeyResultCookieWriter initWithDomain:weakWebView:andCompleteBlock:],+,A8KeyResultCookieWriter,initWithDomain:weakWebView:andCompleteBlock:,[145386#0x10036367c],[]
145396,0x1008c3df8,+[A8KeyResultCookieWriter setA8KeyCookieExpireTime:],+,A8KeyResultCookieWriter,setA8KeyCookieExpireTime:,[145386#0x1003636e8],[]
145397,0x1008c27e8,+[A8KeyResultCookieWriter writeCompleteMarkerCookieValue:forKey:],+,A8KeyResultCookieWriter,writeCompleteMarkerCookieValue:forKey:,[145386#0x10036380c],[]
253456,0x0,+[AAOperationReq init],+,AAOperationReq,init,[253455#0x1039a9d30],[]
253457,0x0,+[AAOperationReq setBaseRequest:],+,AAOperationReq,setBaseRequest:,[253455#0x1039a9d8c],[]
186847,0x0,+[AAOperationRes length],+,AAOperationRes,length,[186845#0x10342aa54],[]
报告可以通过两种方式进行可视化,其一是通过 iblessing 自带的 local query server:
另一种则是通过 iblessing 的 generator 组件生成导入 XREFs 的 IDA Script:
> iblessing -m generator -i ida-objc-msg-xref -f WeChat_method-xrefs.iblessing.txt
[*] set output path to /opt/one-btn/tmp/apps/WeChat/Payload
[*] input file is WeChat_method-xrefs.iblessing.txt
[*] start IDAObjMsgXREFGenerator
[*] load method-chain db for version iblessing methodchains,ver:0.2;
[*] table keys chainId,sel,prefix,className,methodName,prevMethods,nextMethods
[-] bad line 104467,0x0,+[TPLock P, ],+,TPLock,P, ,[104426#0x1043b9904],[]
[-] bad line 114905,0x0,?[0x108ce1578 (,],?,0x108ce1578,(,,[114900#0x1011e8c68],[]
[-] bad line 104464,0x0,?[? P, ],?,?,P, ,[104426#0x1043b98a8],[]
[-] bad line 139234,0x0,?[? X [-] bad line ],?,?,X
[-] bad line ,[139205#0x1013c222c],[]
[+] load storage from disk succeeded!
[*] Generating XREF Scripts ...
[*] saved to /opt/one-btn/tmp/apps/WeChat/Payload/WeChat_method-xrefs.iblessing.txt_ida_objc_msg_xrefs.iblessing.py
> ls -alh WeChat_method-xrefs.iblessing.txt_ida_objc_msg_xrefs.iblessing.py
-rw-r--r-- 1 soulghost wheel 23M Jul 23 16:16 WeChat_method-xrefs.iblessing.txt_ida_objc_msg_xrefs.iblessing.py
生成的 Script 内容为:
def add_objc_xrefs():
ida_xref.add_cref(0x10036367c, 0x1008c2220, XREF_USER)
ida_xref.add_cref(0x1003636e8, 0x1008c3df8, XREF_USER)
ida_xref.add_cref(0x10036380c, 0x1008c27e8, XREF_USER)
ida_xref.add_cref(0x103add16c, 0x700006e187a8, XREF_USER)
ida_xref.add_cref(0x102cbee0c, 0x101143ee8, XREF_USER)
ida_xref.add_cref(0x10085c92c, 0x1005e9360, XREF_USER)
ida_xref.add_cref(0x10085c8bc, 0x1005e9274, XREF_USER)
ida_xref.add_cref(0x10085c8dc, 0x1005e92bc, XREF_USER)
ida_xref.add_cref(0x10085c8cc, 0x1005e9298, XREF_USER)
# ...
if __name__ == '__main__':
add_objc_xrefs()
导入 IDA 后再查看交叉引用的效果:
原理
要分析 objc_msgSend 动态调用,核心原理主要有两点:
- 正确解析 external 的 objc_msgSend 符号,从而识别出
bl _objc_msgSend
调用; - 模拟加载 objc classes, 这里需要模拟 objc runtime 做一下 class realize;
- 通过模拟执行尽确定
bl _objc_msgSend
的关键入参 x0 和 x1。
0x01 确定 objc_msgSend 符号地址
在 aarch64 下,_objc_msgSend
需要通过一个 text stub 去取位于 __DATA,__la_symbol_ptr
的 _objc_msgSend
指针,因此我们只要记录下 dynamic symbol table 中的 text stub 地址即可在遇到 bl 时正确识别该符号:
; void *objc_msgSend(void *, const char *, ...)
_objc_msgSend
NOP
LDR X16, =__imp__objc_msgSend
BR X16 ; __imp__objc_msgSend
; End of function _objc_msgSend
因此这里的关键是如何正确的加载 dynamic symbol table,这里我们可以参考 fishhook 中的相关处理,主要步骤有:
- 从
__LINKEDIT
段确定linkedit_base
,随后即可基于此加载 symtab, strtab 和 dynamic symtab; - 随后我们遍历 indirect symtab,它只存储了 indirect symbol 的 index,我们需要根据 Mach-O ABI 按照一定的规则找到每个符号所在的 section,再根据 section 计算出符号的实际地址,根据 index 去查 symtab 和 strtab 确定符号的名称;
- 建立符号地址与名称的索引,以便后续查询。
关键代码如下,摘自 github.com/Soulghost/i…
void SymbolTable::buildDynamicSymbolTable(std::vector<struct section_64 *> sectionHeaders, uint8_t *data, uint64_t nSymbols, uint8_t *mappedData) {
uint32_t *dyTableEntries = (uint32_t *)data;
for (size_t i = 0; i < nSymbols; i++) {
struct section_64 *symSect = nullptr;
for (size_t j = sectionHeaders.size() - 1; j >= 0; j--) {
struct section_64 *sectHeader = sectionHeaders[j];
// only search for lazy symbol sections
uint32_t flags = sectHeader->flags;
if ((flags & SECTION_TYPE) != S_SYMBOL_STUBS &&
(flags & SECTION_TYPE) != S_LAZY_SYMBOL_POINTERS &&
(flags & SECTION_TYPE) != S_LAZY_DYLIB_SYMBOL_POINTERS &&
(flags & SECTION_TYPE) != S_NON_LAZY_SYMBOL_POINTERS) {
continue;
}
// find symbol's section by index range
uint32_t startIndex = sectHeader->reserved1;
if (startIndex > i) {
continue;
}
symSect = sectHeader;
break;
}
uint32_t symIdx = dyTableEntries[i];
if (symSect == nullptr) {
cout << termcolor::red;
cout << "Error: cannot find dynamic symbol section at index " << symIdx;
cout << termcolor::reset << endl;
exit(1);
}
uint32_t pointerSize = symSect->reserved2 > 0 ? symSect->reserved2 : 8;
uint64_t pointerAddr = symSect->addr + (i - symSect->reserved1) * pointerSize;
// build symbol
Symbol *lazySymbol = new Symbol();
if ((symIdx & (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) == 0) {
// stubs
if (symIdx >= symbolTable.size()) {
cout << termcolor::red;
cout << "Error: symbol index out of bound, check if buildSymbolTable has been executed";
cout << termcolor::reset << endl;
continue;
}
lazySymbol->name = symbolTable.at(symIdx).first;
lazySymbol->info = symbolTable.at(symIdx).second;
symbolMap.insert(pointerAddr, lazySymbol);
name2symbol[lazySymbol->name].pushBack(lazySymbol);
// ...
}
0x02 加载 Objc Class
这里的加载包括两部分:
- 从
__objc_classlist
加载 App 内部的类; - 模拟 dyld eachBind 来加载外部类,例如 Foundation 和 UIKit 中的类。
关于 Class Realize
对于 class realize,主要是为了递归建立类对象的内存结构,并构建起 method 和 ivar 的索引,这部分代码可以直接参考开源的 objc runtime 中的 realizeClassWithoutSwift 实现,这里的逻辑比较冗长就不贴了,在 iblessing 中对应的代码在 github.com/Soulghost/i… 。 这里说两个坑,一个是注意 Swift 下的 class_ro_t 的对齐问题,另一个是内存读取的安全性问题,目前 iblessing 中是直接读取 mapped file,有 segment fault 风险,后续打算借助 unicorn 虚拟内存来读写,来防止 segment fault 导致进程崩溃。
关于模拟 dyld bind
这一块主要是顺着 dyld_info
去执行各种绑定指令,这里归功于 dyld 良好的代码设计,我们可以直接从中拷贝 void ImageLoaderMachOCompressed::eachBind(const LinkContext& context, bind_handler handler)
的代码来完成这些 non-lazy 符号的绑定:
bool DyldSimulator::eachBind(uint8_t *mappedData, std::vector<struct segment_command_64 *> segmentHeaders, dyld_info_command *dyldinfo, DyldBindHandler handler) {
uint32_t bind_off = dyldinfo->bind_off;
uint32_t bind_size = dyldinfo->bind_size;
const uint8_t * const bind_start = mappedData + bind_off;
const uint8_t * const bind_end = bind_start + bind_size;
const uint8_t * p = bind_start;
bool done = false;
uint64_t libraryOrdinal = 0;
const char *symbolName = NULL;
uint8_t symbolFlags = 0;
uint8_t type = 0;
uint64_t addend = 0;
uint32_t segmentIndex = 0;
uint64_t segmentEndAddress = 0;
uint64_t address = 0;
while (!done && (p < bind_end)) {
uint8_t immediate = *p & BIND_IMMEDIATE_MASK;
uint8_t opcode = *p & BIND_OPCODE_MASK;
++p;
switch (opcode) {
case BIND_OPCODE_DONE:
done = true;
printf("[+] bind info parsed done\n");
break;
case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM:
libraryOrdinal = immediate;
printf("[+] set dylib ordinal to %lld\n", libraryOrdinal);
break;
case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:
libraryOrdinal = read_uleb128(p, bind_end);
break;
case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM:
// the special ordinals are negative numbers
if ( immediate == 0 )
libraryOrdinal = 0;
else {
int8_t signExtended = BIND_OPCODE_MASK | immediate;
libraryOrdinal = signExtended;
}
break;
case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM:
symbolName = (char*)p;
symbolFlags = immediate;
while (*p != '\0')
++p;
++p;
break;
case BIND_OPCODE_SET_TYPE_IMM:
type = immediate;
break;
case BIND_OPCODE_SET_ADDEND_SLEB:
addend = read_sleb128(p, bind_end);
break;
case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segmentIndex = immediate;
if ( segmentIndex >= segmentHeaders.size() )
printf("[-]BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB has segment %d which is too large (0..%lu)\n",
segmentIndex, segmentHeaders.size() - 1);
address = segmentHeaders[segmentIndex]->vmaddr + read_uleb128(p, bind_end);
segmentEndAddress = address + segmentHeaders[segmentIndex]->vmsize;
break;
case BIND_OPCODE_ADD_ADDR_ULEB:
address += read_uleb128(p, bind_end);
break;
case BIND_OPCODE_DO_BIND:
if ( address >= segmentEndAddress ) {
printf("[-] address exceeded segment range\n");
return false;
}
printf("[+] bind symbol %s at 0x%llx, and address + 8\n", symbolName, address);
handler(address, type, symbolName, symbolFlags, addend, libraryOrdinal, "");
address += sizeof(intptr_t);
break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB:
if ( address >= segmentEndAddress ) {
printf("[-] address exceeded segment range\n");
return false;
}
printf("[+] bind symbol %s at 0x%llx, and address + offset\n", symbolName, address);
handler(address, type, symbolName, symbolFlags, addend, libraryOrdinal, "");
address += read_uleb128(p, bind_end) + sizeof(intptr_t);
break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED:
if ( address >= segmentEndAddress ) {
printf("[-] address exceeded segment range\n");
return false;
}
printf("[+] bind symbol %s at 0x%llx, and address + immediate * 8 + 8 (scaled)\n", symbolName, address);
handler(address, type, symbolName, symbolFlags, addend, libraryOrdinal, "");
address += immediate*sizeof(intptr_t) + sizeof(intptr_t);
break;
case BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB:
uint64_t count = read_uleb128(p, bind_end);
uint64_t skip = read_uleb128(p, bind_end);
for (uint32_t i=0; i < count; ++i) {
if ( address >= segmentEndAddress )
if ( address >= segmentEndAddress ) {
printf("[-] address exceeded segment range\n");
return false;
}
printf("[+] bind symbol %s at 0x%llx, and address + immediate * 8 + 8 (scaled)\n", symbolName, address);
handler(address, type, symbolName, symbolFlags, addend, libraryOrdinal, "");
address += skip + sizeof(intptr_t);
}
break;
}
}
return true;
}
0x03 Objc 方法模拟执行
这里的模拟执行是基于 unicorn 的,这里的操作主要包括:
- 创建 engine,构建虚拟内存分段,随后将 Mach-O 文件映射进来;
- 遍历所有 realize 的 class 找到所有 objc method;
- 对于每个 objc method,我们都单独启动一次 unicorn engine 进行模拟,其中初始化的入参 x0 = self or Class, x1 = SEL,同时为他们提供一个全新的堆栈环境;
- 在模拟执行过程中跳过所有分支逻辑,这里注意要规避循环块;
- 当遇到
bl _objc_msgSend
的逻辑时,分析 x0 和 x1 的值并建立当前方法和被调用方法之间的链接。
上面的描述中省略了一些细节,下面我们分别来说明。
1. 创建 Engine 和构建虚拟内存
这里针对每个 unicorn engine 构建了 12 GB 的虚拟内存,其中前 4GB 为 PAGE_ZERO,后面为正常的堆栈:
// mapping 12GB memory region, first 4GB is PAGEZERO
// ALL 0x000000000 ~ 0x300000000
// PAGE_ZERO 0x000000000 ~ 0x100000000
// HEAP 0x100000000 ~ 0x300000000
// STACK ? ~ 0x300000000
随后将 Mach-O 文件映射进来,需要注意的是这里的映射方式是不完整的,有一些段需要逐段映射才能得到正确结果,但对于当前的模拟执行要求而言不必去做逐段映射:
uint64_t unicorn_vm_size = 12L * 1024 * 1024 * 1024;
uint64_t unicorn_vm_start = 0;
assert(uc_mem_map(uc, unicorn_vm_start, unicorn_vm_size, UC_PROT_ALL) == UC_ERR_OK);
// FIXME: failed condition
assert(uc_mem_write(uc, vm->vmaddr_base, vm->mappedFile, vm->mappedSize) == UC_ERR_OK);
// setup default thread state
assert(uc_context_alloc(uc, &ctx) == UC_ERR_OK);
uint64_t unicorn_sp_start = 0x300000000;
uc_reg_write(uc, UC_ARM64_REG_SP, &unicorn_sp_start);
// set FPEN on CPACR_EL1
uint32_t fpen;
uc_reg_read(uc, UC_ARM64_REG_CPACR_EL1, &fpen);
fpen |= 0x300000; // set FPEN bit
uc_reg_write(uc, UC_ARM64_REG_CPACR_EL1, &fpen);
uc_context_save(uc, ctx);
这里我们还同时设置了 FPEN 以便能正确执行 SIMD 的相关操作,这里的 uc_context
作为一个初始状态的快照,可以在每次启动 unicorn engine 之前重置虚拟内存。
2. 找到所有 Objc Method
这一步在有了 realized class list 的情况下是非常简单地,只需要顺着 isa 聚合所有 method 即可,这里就不赘述了。
3. 模拟执行方法
在模拟执行之前,我们要为 engine 设置 3 个 hook,一个指令 hook 和 2 个内存 hook,其中指令 hook 用于分析指令并协助 engine 做出下一步的决策;内存 hook 主要是处理遇到内存问题后能继续执行:
static void mem_hook_callback(uc_engine *uc, uc_mem_type type, uint64_t address, int size, int64_t value, void *user_data) {
}
static bool mem_exception_hook_callback(uc_engine *uc, uc_mem_type type, uint64_t address, int size, int64_t value, void *user_data) {
return true;
}
// add hooks
uc_hook_add(uc, &insn_hook, UC_HOOK_CODE, (void *)insn_hook_callback, NULL, 1, 0);
uc_hook_add(uc, &mem_hook, UC_HOOK_MEM_VALID, (void *)mem_hook_callback, NULL, 1, 0);
uc_hook_add(uc, &memexp_hook, UC_HOOK_MEM_INVALID, (void *)mem_exception_hook_callback, NULL, 1, 0);
模拟执行的核心逻辑位于 insn_hook_callback 中,我们稍后重点讲解。在设定完这些基础信息后,我们就可以通过 method 取到 IMP,设定好 engine 环境开始执行:
ObjcMethod *m = context->methods[i];
context->currentMethod = m;
uc_context_restore(context->engine, context->defaultContext);
context->lastPc = 0;
// init x0 as classref
uint64_t selfTrickAddr = ((uint64_t)m->classInfo) | SelfInstanceTrickMask;
uc_reg_write(context->engine, UC_ARM64_REG_X0, &selfTrickAddr);
// init x1 as SEL, faked as self class info
uint64_t selfSELAddr = ((uint64_t)m->classInfo) | SelfSelectorTrickMask;
uc_reg_write(context->engine, UC_ARM64_REG_X1, &selfSELAddr);
uc_err err = uc_emu_start(context->engine, m->imp, 0, 0, 0);
if (err != UC_ERR_OK) {
// printf("\t[*] uc error %s\n", uc_strerror(err));
// assert(0);
}
uc_emu_stop(context->engine);
4. 处理指令
这里我们会借助 insn_hook_callback 去处理指令,这里我们会借助 capstone 将机器码进行反汇编,随后根据当前 PC 和指令的内容作出决策。
避免陷入循环
由于我们选择跳过所有分支逻辑,可能会错误的陷入死循环,这里我们需要不断记录上一次的 PC 值 lastPC,如果发现新 PC 的值小于上次的 PC,则说明遇到了回跳的循环,我们选择跳到 lastPC + 4 的地址继续执行:
// detect loop
if (address <= ctx->lastPc) {
uint64_t pc = ctx->lastPc + size;
assert(uc_reg_write(uc, UC_ARM64_REG_PC, &pc) == UC_ERR_OK);
// ...
}
// ...
ctx->lastPc = address;
找到返回指令
对于 objc 方法的 IMP,它的返回指令不一定是 RET
,还可能是:
B _objc_retainAutoreleaseReturnValue
B _objc_retainAutoreleasedReturnValue
; ...
等等一系列指令,因此我们封装了方法来检测方法返回:
bool ARM64Runtime::isRET(cs_insn *insn) {
const char *mnemonic = insn[0].mnemonic;
if (strcmp(mnemonic, "ret") == 0) {
return true;
}
if (strcmp(insn[0].mnemonic, "b") == 0 ||
strncmp(insn[0].mnemonic, "b.", 2) == 0) {
uint64_t pc = insn[0].detail->arm64.operands[0].imm;
SymbolTable *symtab = SymbolTable::getInstance();
Symbol *symbol = symtab->getSymbolByAddress(pc);
if (symbol == nullptr) {
return false;
}
const char *fname = symbol->name.c_str();
if (strcmp(fname, "_objc_retainAutoreleaseReturnValue") == 0 ||
strcmp(fname, "_objc_retainAutoreleasedReturnValue") == 0 ||
strcmp(fname, "_objc_autoreleaseReturnValue") == 0 ||
strcmp(fname, "_objc_unsafeClaimAutoreleasedReturnValue") == 0 ||
strcmp(fname, "_objc_retain") == 0 ||
strcmp(fname, "_objc_storeWeak") == 0 ||
strcmp(fname, "_objc_storeStrong") == 0 ||
strcmp(fname, "_objc_loadWeakRetained") == 0 ||
strcmp(fname, "_objc_release") == 0 ||
strcmp(fname, "_objc_destroyWeak") == 0 ||
strcmp(fname, "_objc_copyWeak") == 0) {
return true;
}
}
return false;
}
跳过分支逻辑
由于我们只关心 objc_msgSend 调用,因此可以忽略掉其他的分支和条件分支逻辑继续执行,但这样会错过一些 path,有待后续优化:
// split at condition branch
// FIXME: skip now
if (strcmp(insn->mnemonic, "cbz") == 0 ||
strcmp(insn->mnemonic, "cbnz") == 0) {
// always jump to next ins
uint64_t pc = address + size;
assert(uc_reg_write(uc, UC_ARM64_REG_PC, &pc) == UC_ERR_OK);
}
// skip branches
if (strncmp(insn->mnemonic, "b.", 2) == 0 ||
strncmp(insn->mnemonic, "bl.", 3) == 0) {
// always jump to next ins
uint64_t pc = address + size;
assert(uc_reg_write(uc, UC_ARM64_REG_PC, &pc) == UC_ERR_OK);
}
探测 objc_msgSend 并追踪
这里需要注意的是有时候 objc_msgSend
是以 b
而非 bl
的形式调用的,例如:
__text:0000000106B52674 MOV X3, X2
__text:0000000106B52678 ADRP X8, #selRef_sgm_showAlertViewWithTitle_message_cancelButtonTitle_otherButtonTitles_dismissed_canceled_@PAGE
__text:0000000106B5267C LDR X1, [X8,#selRef_sgm_showAlertViewWithTitle_message_cancelButtonTitle_otherButtonTitles_dismissed_canceled_@PAGEOFF]
__text:0000000106B52680 ADRP X2, #stru_10706CF58@PAGE
__text:0000000106B52684 ADD X2, X2, #stru_10706CF58@PAGEOFF
__text:0000000106B52688 ADRP X4, #stru_1070BD238@PAGE ; "知道了"
__text:0000000106B5268C ADD X4, X4, #stru_1070BD238@PAGEOFF ; "知道了"
__text:0000000106B52690 MOV X5, #0
__text:0000000106B52694 MOV X6, #0
__text:0000000106B52698 MOV X7, #0
__text:0000000106B5269C B objc_msgSend
我们的探测代码也非常简单,主要依赖于前面对 dynamic symbol table 的建立:
// record objc_msgSend, skip all bl
if (strcmp(insn->mnemonic, "b") == 0 ||
strcmp(insn->mnemonic, "bl") == 0) {
uint64_t pc = insn[0].detail->arm64.operands[0].imm;
bool isMsgSendOrWrapper = false;
Symbol *symbol = symtab->getSymbolByAddress(pc);
if (symbol && strcmp(symbol->name.c_str(), "_objc_msgSend") == 0) {
uint64_t x0 = 0, x1 = 0;
uint64_t x0 = 0, x1 = 0;
if (uc_reg_read(uc, UC_ARM64_REG_X0, &x0) == UC_ERR_OK &&
uc_reg_read(uc, UC_ARM64_REG_X1, &x1) == UC_ERR_OK) {
trackCall(uc, ctx->currentMethod, x0, x1);
}
}
// jump to next ins
pc = address + size;
assert(uc_reg_write(uc, UC_ARM64_REG_PC, &pc) == UC_ERR_OK);
}
5. 分析结果
到这里我们已经获取到了 x0 和 x1,其中 x1 作为 SEL 其获取方法很简单,绝大多数情况下可以从虚拟内存中的 __objc_selrefs
中取到,但也不免有一些动态合成的 SEL 需要依赖模拟执行才能获取到,由于目前的模拟执行没有链接和执行外部符号的能力,无法处理这类 SEL,因此需要针对这类 SEL 做一个排除:
解析 SEL
// read sel
const char *detectedSEL = "?";
if (x1) {
if (x1 & SelfSelectorTrickMask) {
detectedSEL = currentMethod->name.c_str();
} else {
detectedSEL = vm2->readString(x1, 255);
}
}
这里的 x1 在模拟执行启动时会写入用 SelfSelectorTrickMask 标记的值,用来指示是否复用了方法入参的 SEL,如果不是则尝试在虚拟内存中读取,这里的 vm2 也是一个 unicorn 实例,采用了逐段映射的方法构建起虚拟内存,来保证实现 Mach-O 文件加载后的正确寻址:
char* VirtualMemoryV2::readString(uint64_t address, uint64_t limit) {
char *charBuf = (char *)malloc(limit);
uint64_t offset = 0;
uint64_t unPrintCount = 0;
bool ok = true;
while (offset < limit && (ok = (uc_mem_read(uc, address + offset, charBuf + offset, sizeof(char))) == UC_ERR_OK)) {
if (charBuf[offset] == 0) {
break;
}
if (!(charBuf[offset] >= 0x20 && charBuf[offset] <= 0x7E)) {
unPrintCount++;
if (unPrintCount > 10) {
ok = false;
break;
}
}
offset++;
}
if (!ok) {
free(charBuf);
return NULL;
}
charBuf[offset] = 0;
return charBuf;
}
解析 x0
对于 x0,情况就比较多了,主要有:
- x0 = self
- x0 = Class
- x0 = instance
对于前两种情况非常好处理,第一种可以类似 SEL 做一个 SelfInstanceTrickMask 标记来判断,而 Class 的情况模拟执行能够正确解析,此时的 x0 就是类对象指针,拿着它去已加载的 objc class 索引表中查询即可。
对于 x0 = instance 的情况,目前 iblessing 只支持来自 ivar 的,即将支持局部 allocate 的。对于 ivar 的支持,这里有一些 trick,首先在 class 加载时会记下一份 ivar getter 的索引表,当我们遇到 ivar 的 getter 时会用 IvarInstanceTrickMask 去标记 x0,那么此后如果是对 ivar instance 进行的 objc_msgSend 调用我们就能正确的检测:
// eval ivar method and mask x0
if (detectedClassInfo && detectedSEL) {
ObjcClassRuntimeInfo *ivarClassInfo = rt->evalReturnForIvarGetter(detectedClassInfo, detectedSEL);
if (ivarClassInfo) {
// FIXME: ivar class addr trick mask
uint64_t encodedTrickAddr = ivarClassInfo->address | IvarInstanceTrickMask;
rt->ivarInstanceTrickAddress2RuntimeInfo[encodedTrickAddr] = ivarClassInfo;
uc_reg_write(uc, UC_ARM64_REG_X0, &encodedTrickAddr);
}
}
// detect ivar method
uint64_t addr = x0;
if (rt->ivarInstanceTrickAddress2RuntimeInfo.find(addr) != rt->ivarInstanceTrickAddress2RuntimeInfo.end()) {
// -[self.ivar foo]
detectedClassInfo = rt->ivarInstanceTrickAddress2RuntimeInfo[addr];
methodPrefix = "-";
}
目前未能支持 allocate 方式的主要原因是 iblessing 的一部分虚拟内存是直接在当前进程内模拟的,没有基于 unicorn 做虚拟化,从而导致内存异常会直接崩溃,下面一步需要将所有的内存模拟都迁移为 unicorn 方式才能安全的探测 allocated instance。
分析速度优化
对于单 unicorn 实例似乎只能跑满 1 个 Core,因此在这里的分析中使用了多个 unicorn 实例并发执行来提升了分析速度,这里的改造思路是将所有的 oc 方法分成 N 组,每组独占一个 unicorn 实例然后并发执行,unicorn 实例以自身为索引查询索引表获取执行上下文,当检测到 objc_msgSend
需要写入交叉引用表时用锁保护临界区:
#define ThreadCount 8
class EngineContext {
public:
int identifer;
uc_engine *engine;
uint64_t lastPc;
uc_context *defaultContext;
ObjcMethod *currentMethod;
vector<ObjcMethod *> methods;
};
static map<uc_engine *, EngineContext *> engineContexts;
static pthread_mutex_t globalMutex;
uc_engine* createEngine(int identifier) {
VirtualMemory *vm = VirtualMemory::progressDefault();
uc_engine *uc;
uc_context *ctx;
uc_err err = uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc);
if (err) {
printf("\t[-] error: %s\n", uc_strerror(err));
return NULL;
}
// add hooks
uc_hook_add(uc, &insn_hook, UC_HOOK_CODE, (void *)insn_hook_callback, NULL, 1, 0);
uc_hook_add(uc, &mem_hook, UC_HOOK_MEM_VALID, (void *)mem_hook_callback, NULL, 1, 0);
uc_hook_add(uc, &memexp_hook, UC_HOOK_MEM_INVALID, (void *)mem_exception_hook_callback, NULL, 1, 0);
// mapping 12GB memory region, first 4GB is PAGEZERO
// ALL 0x000000000 ~ 0x300000000
// PAGE_ZERO 0x000000000 ~ 0x100000000
// HEAP 0x100000000 ~ 0x300000000
// STACK ? ~ 0x300000000
uint64_t unicorn_vm_size = 12L * 1024 * 1024 * 1024;
uint64_t unicorn_vm_start = 0;
assert(uc_mem_map(uc, unicorn_vm_start, unicorn_vm_size, UC_PROT_ALL) == UC_ERR_OK);
// FIXME: failed condition
assert(uc_mem_write(uc, vm->vmaddr_base, vm->mappedFile, vm->mappedSize) == UC_ERR_OK);
// setup default thread state
assert(uc_context_alloc(uc, &ctx) == UC_ERR_OK);
uint64_t unicorn_sp_start = 0x300000000;
uc_reg_write(uc, UC_ARM64_REG_SP, &unicorn_sp_start);
// set FPEN on CPACR_EL1
uint32_t fpen;
uc_reg_read(uc, UC_ARM64_REG_CPACR_EL1, &fpen);
fpen |= 0x300000; // set FPEN bit
uc_reg_write(uc, UC_ARM64_REG_CPACR_EL1, &fpen);
uc_context_save(uc, ctx);
// build context
EngineContext *engineCtx = new EngineContext();
engineCtx->identifer = identifier;
engineCtx->engine = uc;
engineCtx->defaultContext = ctx;
engineCtx->lastPc = 0;
engineCtx->currentMethod = NULL;
engineContexts[uc] = engineCtx;
return uc;
}
void trace_all_methods(vector<uc_engine *> engines, vector<ObjcMethod *> &methods, uint64_t cursor) {
// split methods by engines
uint64_t groupCount = engines.size();
uint64_t methodCount = methods.size();
if (methodCount < groupCount) {
groupCount = methodCount;
}
uint64_t groupCap = methodCount / groupCount;
curCount = 0;
totalCount = methodCount;
// create global lock
pthread_mutexattr_t attr = {0};
assert(pthread_mutexattr_init(&attr) == 0);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
assert(pthread_mutex_init(&globalMutex, &attr) == 0);
// create threads
vector<pthread_t> threads;
size_t startIdx = 0;
for (size_t i = 0; i < groupCount; i++) {
EngineContext *ctx = engineContexts[engines[i]];
auto endIt = __builtin_expect(i == groupCount - 1, false) ? methods.end() : methods.begin() + startIdx + groupCap;
vector<ObjcMethod *> workMethods(methods.begin() + startIdx, endIt);
ctx->methods = workMethods;
startIdx += groupCap;
pthread_t thread;
assert(pthread_create(&thread, nullptr, pthread_uc_worker, (void *)ctx) == 0);
threads.push_back(thread);
}
for (pthread_t t : threads) {
pthread_join(t, NULL);
}
// ...
}
一些细节
上面主要介绍了整个交叉引用分析的核心逻辑,其中还有不少细节,例如从实例到类方法的调用可能是通过如下方式进行的:
[[instance class] foo];
对于这类调用,在汇编层面会有两种表达:
- 普通的
objc_msgSend
调用,其中 SEL = "class"; - 优化的
objc_opt_class
调用,入参为实例,出参为类对象。
因此我们需要分别模拟这两种情况下的结果从而得到正确的返回值供后续分析。其他细节这里就不展开了,欢迎大家去源码里挑猫饼和找答案 github.com/Soulghost/i… 。
总结
目前整个交叉引用分析器已经有了不错的分析能力,但对于反射、创建和入参的分析能力还非常有限,但用作 iOS App 辅助逆向分析上已经有不错的效果。相比于直接编写 IDA Script,ibleesing 有更快的执行速度和更加自由的玩法,目前刚刚开源,处于 beta 阶段,欢迎大家试用。
宣传时间
本文介绍的 objc_msgSend 交叉引用分析只是 ibleesing 的一个模块,此外它还包含了 App 信息收集, symbol wrapper 检测与转换, Class 引用扫描等功能,今后将不断迭代以提升其分析能力,助力 iOS 逆向工程相关工作。
欢迎大家 Star: github.com/Soulghost/i…