最近学习unicorn,看到一位大佬在AndroidNativeEmu上把 X-Gorgen的计算跑通了,听说这个版本的leviathan函数很复杂,准备动手看看复杂在哪里,一眼就看到了JNI_OnLoad里的花指令。抱着学习unicorn的目的,本文记录下如何使用unicorn去掉libcms.so的花指令。
本文的涉及到2个文件,libcms.so请从4.0.0版本的抖音app里解压出来,unflower_cms.py 位于该gist下:https://gist.github.com/LeadroyaL/80a5f6fbb83ee1c102c860aaf2bc594d
一、初步分析
随手下载了4.0.0的抖音,混淆还在,leviathan函数也在,本文就用它来演示。
先看两段汇编,demo1和demo2,都是PUSH起手,连续操作2个和PC相关的MOV,之后进行简单的运算和跳转,最后POP收尾,根据预期,花指令运行时不大依赖上下文,非常适合unicorn模拟执行。
dmeo1
1 2 3 4 5 6 7 8 9 10 11 12
| .text:0007862A LDR R0, =loc_5D45C .text:0007862C MOVW R1, #0xFFF <font color=red>.text:00078630 PUSH {R0-R3}</font> <font color=red>.text:00078632 MOV R0, PC</font> <font color=red>.text:00078634 MOV R1, PC</font> .text:00078636 SUBS R2, R1, R0 .text:00078638 SUBS R2, R2, #2 .text:0007863A CMP R2, #0 .text:0007863C BGT loc_78662 .text:0007863E B loc_78640 ----- <font color=red>.text:00078686 POP {R0-R3}</font>
|
demo2
.text:000786B6 PUSH {R0-R3,R7}
.text:000786B8 MOV R0, PC
.text:000786BA MOV R1, PC
.text:000786BC SUBS R2, R1, R0
.text:000786BE SUBS R3, R2, #2
.text:000786C0 CMP R3, #0
.text:000786C2 BGT loc_786E8
.text:000786C4 B loc_786C6
.text:000786CE PUSH {R1}
.text:000786E4 POP {R1}
.text:000786E6 B loc_7870A
.text:0007870A POP {R0-R2}
.text:0007870C POP {R3,R7}
二、整体思路:从PUSH进入,向后执行,尝试找到栈平衡的时刻,按照预期,这个时刻寄存器的内容与开始时是完全相同的。
三、详细思路
1、使用python3和相关组件: unicorn keystone-engine(这个不要用pip装) capstone pyelftools
2、使用pyelftools找到连接时视图,即节表,根据节表找到.text的范围。使用keystone反汇编.text节的内容,找到我们需要的格式,这里有个坑点,keystone反汇编是存在范围限制的,因此每次只反1000字节,防止超出部分被截断,而且往前稍微扫一点,防止寻找的内容被拆到两次扫描中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
filename = "./libcms.so" fd = open(filename, 'rb') elf = ELFFile(fd) sh_offset = elf.get_section_by_name(".text").header['sh_offset'] sh_size = elf.get_section_by_name(".text").header['sh_size'] fd.seek(sh_offset) text_data = fd.read(sh_size)
cs = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
entries = [] step = 1000 for i in range(0, len(text_data), step): _i = max(0, i - 10) g = cs.disasm(text_data[_i:_i + step], 0) while True: try: ins = next(g) assert isinstance(ins, CsInsn) if ins.mnemonic != 'push': continue ins2 = next(g) assert isinstance(ins2, CsInsn) if not ins2.mnemonic.startswith('mov') or not ins2.op_str.endswith('pc'): continue ins3 = next(g) assert isinstance(ins3, CsInsn) if not ins3.mnemonic.startswith('mov') or not ins3.op_str.endswith('pc'): continue entries.append((_i + sh_offset + ins.address, ins.op_str)) except StopIteration: break print(entries)
|
3、使用pyelftools找到运行时视图,即段表。根据段表将library正确加载到内存中。这里的坑点在于内存要对齐,否则无法进行mmap操作。
2019年9月15日13:56:16更新:以及另一个坑点,p_flags和uc_prot是不一致的,不能直接使用。
1 2 3 4 5 6 7 8 9 10 11 12
| 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;
PF_X=0x1 PF_W=0x2 PF_R=0x4
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| def align(addr, size, align): fr_addr = addr // align * align to_addr = (addr + size + align - 1) // align * align return fr_addr, to_addr - fr_addr
load_base = 0 emu = Uc(UC_ARCH_ARM, UC_MODE_LITTLE_ENDIAN) load_segments = [x for x in elf.iter_segments() if x.header.p_type == 'PT_LOAD'] for segment in load_segments: fr_addr, size = align(load_base + segment.header.p_vaddr, segment.header.p_memsz, segment.header.p_align) emu.mem_map(fr_addr, size, pflags2prot(segment.header.p_flags)) emu.mem_write(load_base + segment.header.p_vaddr, segment.data())
|
4、使用unicorn进行模拟执行,根据寻找到的特征,找到对应的真实代码的位置。这里判断的原则有两个,一是栈平衡,二是被保护的寄存器被完整地还原。这里也有一个坑点,在PUSH指令执行时,也会满足栈平衡的条件,会导致程序刚执行第一句就退出了,需要额外判断一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| STACK_ADDR = 0x7F000000 STACK_SIZE = 1024 * 1024 start_addr = None
def hook_code(mu: Uc, address, size, user_data): if mu.reg_read(UC_ARM_REG_PC) != start_addr and mu.reg_read(UC_ARM_REG_SP) == STACK_ADDR + STACK_SIZE: emu.emu_stop()
emu.mem_map(STACK_ADDR, STACK_SIZE) emu.hook_add(UC_HOOK_CODE, hook_code)
_to_reg_id = { "r0": UC_ARM_REG_R0, "r1": UC_ARM_REG_R1, "r2": UC_ARM_REG_R2, "r3": UC_ARM_REG_R3, "r4": UC_ARM_REG_R4, "r5": UC_ARM_REG_R5, "r6": UC_ARM_REG_R6, "r7": UC_ARM_REG_R7, "r8": UC_ARM_REG_R8, "r9": UC_ARM_REG_R9, "r10": UC_ARM_REG_R10, "r11": UC_ARM_REG_R11, "r12": UC_ARM_REG_R12, "r13": UC_ARM_REG_R13, "r14": UC_ARM_REG_R14, "r15": UC_ARM_REG_R15, "lr": UC_ARM_REG_LR, "pc": UC_ARM_REG_PC, "sp": UC_ARM_REG_SP, "sb": UC_ARM_REG_SB, "sl": UC_ARM_REG_SL, "fp": UC_ARM_REG_FP, "ip": UC_ARM_REG_IP, }
ret = [] MAGIC32 = 0x12345678 for push_entry, push_regs in entries: emu.reg_write(UC_ARM_REG_SP, STACK_ADDR + STACK_SIZE) print("Emulate arm code start", hex(push_entry)) start_addr = push_entry for r in push_regs.strip('{}').replace(' ', '').split(','): emu.reg_write(_to_reg_id[r], MAGIC32) emu.emu_start(push_entry + 1, 0, 0, 100) print("Emulation arm code done") changed = False for r in push_regs.strip('{}').replace(' ', '').split(','): if emu.reg_read(_to_reg_id[r]) != MAGIC32: changed = True break stop_addr = emu.reg_read(UC_ARM_REG_PC) if not changed: print("Match:", start_addr, stop_addr) ret.append((start_addr, stop_addr)) else: print("Cannot handle:", start_addr)
|
5、使用capstone和ida的API,将PUSH语句改写为跳转语句,刚好会覆盖掉原有的PUSH和MOV语句,跳转的目标是POP后栈平衡的那条语句、而非POP语句,刚好也就是我们的stop_addr。
1 2 3 4 5 6 7
| for start, stop in ret: ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB) a = ks.asm("B.W $+" + str(stop - start)) PatchByte(start, a[0][0]) PatchByte(start + 1, a[0][1]) PatchByte(start + 2, a[0][2]) PatchByte(start + 3, a[0][3])
|
完整代码见gist,可直接运行。
四、验证成果
找台root的手机,把libcms.so丢进去,覆盖原先的文件,发现抖音仍然可以正常使用,说明patch的确实没问题!
参考链接:
http://www.unicorn-engine.org/docs/tutorial.html
https://bbs.pediy.com/thread-253868.htm
https://zhuanlan.zhihu.com/p/33087450