字符串加密
sub_25CDC这个函数就是字符串解密函数,byte_218F01是标记有没有解密的

字符串解密函数就是一个异或,参数第一个存放解密后字符串,第二个是加密的字符串,第三个是密钥,第四个是长度

字符串还是挺多的,需要批量处理一下
解密算法已经知道了,直接从解密函数调用前的10个指令开始模拟执行到解密函数,得到解密函数的4个参数
找解密函数调用处的前10个指令时,需要判断一下是不是bxx之类的分支指令,是的话就不往前找了,避免模拟执行跑飞
然后根据参数解密出字符串,patch到第一个参数的地址
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | import flare_emufrom idc import *from idaapi import *import idaapiimport ida_allinsfrom ida_ida import *from idautils import *import unicornfrom keystone import *from unicorn import arm64_constmy_EH = flare_emu.EmuHelper()def emu_last_x_insn(ea, insn_cnt=10, skip_ea=True, stop_in_brach=True, reg_info={}): count = 0 curr_addr = ea while count < insn_cnt: curr_addr = prev_head(curr_addr, 0) insn = idaapi.insn_t() length = idaapi.decode_insn(insn, curr_addr) if stop_in_brach: if (insn.get_canon_feature() & CF_JUMP) != 0 or (insn.get_canon_feature() & CF_CALL) != 0 or insn.itype in [ ARM_b, ARM_bl, ARM_br, ARM_bx, ARM_blx1, ARM_blx2, ARM_cbz, ARM_cbnz, ARM_blr, ARM_tbz, ARM_tbnz, ARM_ldrpc, ARM_ret] : curr_addr = next_head(curr_addr, BADADDR) break count += 1 start_addr = curr_addr end_addr = ea my_EH.emulateRange(start_addr, end_addr, registers=reg_info, count=insn_cnt) return my_EH.ucdef is_valid_address(addr, segment_name = ".data"): seg = get_segm_by_name(segment_name) return addr >= seg.start_ea and addr <= seg.end_eadef is_valid_string(string): for c in string: if c not in string.printable: return False return Truedef dec_str(): func_addr = 0x25CDC all_used_addr = list(CodeRefsTo(func_addr, False)) error_process_addr = [] for used_addr in all_used_addr: try: uc_stata = emu_last_x_insn(used_addr, reg_info={"X0": 0, "X1": 0, "X2": 0, "X3": 0}) x0 = uc_stata.reg_read(arm64_const.UC_ARM64_REG_X0) x1 = uc_stata.reg_read(arm64_const.UC_ARM64_REG_X1) x2 = uc_stata.reg_read(arm64_const.UC_ARM64_REG_X2) x3 = uc_stata.reg_read(arm64_const.UC_ARM64_REG_X3) out_addr, enc_addr, key, lens = x0, x1, x2, x3 if is_valid_address(enc_addr): curr_str_bytes = [] for i in range(lens): curr_char = get_wide_byte(enc_addr + i) curr_char ^= key key += 3 key %= 256 curr_str_bytes.append(curr_char) curr_str = bytearray(curr_str_bytes).decode('utf-8') if is_valid_address(out_addr): for k in range(lens): patch_byte(out_addr + k, curr_str_bytes[k]) create_strlit(out_addr,0,STRTYPE_C) print(f"patched {hex(out_addr)} with {curr_str}") else: error_process_addr.append(used_addr) else: error_process_addr.append(used_addr) except Exception as e: error_process_addr.append(used_addr) print() for addr in error_process_addr: print(hex(addr),end=' ') |
最后会有十几个字符串参数识别有问题,手动处理一下就可以

如果是把字符串解密函数直接内联到使用字符串的函数代码中可能混淆效果好一些
控制流混淆
直接看JNI_Onload,先是给W0赋了个值

然后来到0x24980调用sub_25d0c

函数从x30加上w0<<2的内存位置取了一个dword,然后加到x30

函数调用时会把返回地址放到link register(x30),所以取数据的位置就是0x24980下面
上面是取了[0x24984 + 2<<2],也就是0x78

加到x30之后ret来到0x0249FC

查看0x24980的地方发现还有另外几个,并且sub_25D0C也不止有0x24980调用
可以猜测是每个函数都有有类似0x24980这种类似分发器的东西,通过上面的运算分发到函数内的基本块上


还是要用脚本批量处理一下
找到BL sub_25d0c的所有调用处,然后每个调用处向前找3条指令开始模拟执行,找到取数据时的下标,取出对应的偏移加回去得到正确的跳转位置,然后用keystone汇编一下指令,再patch回BL sub_25d0c的调用处
当然也不一定非要去模拟执行,这个so中BL sub_25d0c的所有调用处的前一条指令刚好都是赋值下标的指令,也就是LDR W0, xxx ,所以直接匹配也是可以

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 | def jump_deobfuscator(): sp_modify_func = 0x25D0C all_used_addr = list(CodeRefsTo(sp_modify_func, False)) error_process_addr = [] for used_addr in all_used_addr: data_addr = used_addr + 4 for xref_addr in list(CodeRefsTo(used_addr, False)): insn = idaapi.insn_t() length = idaapi.decode_insn(insn, xref_addr) assert(insn.itype == ARM_b) try: uc_stata = emu_last_x_insn(xref_addr, insn_cnt=2, reg_info={"X0": 0xffffffff, "X1": 0, "X2": 0, "X3": 0}) x0 = uc_stata.reg_read(arm64_const.UC_ARM64_REG_X0) data_offset = x0 if (data_offset == 0xffffffff): error_process_addr.append([used_addr, xref_addr]) continue jump_addr = get_wide_dword(data_addr + (data_offset * 4)) + data_addr if (not is_valid_address(jump_addr, ".text")): error_process_addr.append([used_addr, xref_addr]) continue ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) assembly_code = f"b {hex(jump_addr)}" encoding, count = ks.asm(assembly_code, as_bytes=True, addr=xref_addr) for k in range(4): patch_byte(xref_addr + k, encoding[k]) except Exception as e: error_process_addr.append([used_addr, 0]) print() for addr in error_process_addr: print(f"{hex(addr[0])} {hex(addr[1])}") |
patch完成之后,应用到so文件,重新用ida打开就可以看到函数都可以正常分析了


JNINativeInterface重映射
看JNI_Onload

sub_24A74里分配了一些空间,然后调用sub_24D20

sub_24D20中对JNINativeInterface里的函数全部重新映射了一遍,我们直接建个新的struct映射一下就好

最后映射完会发现4个reserve没有映射,所以出现了部分方法出现2次,这里直接把其中一个方法加个前缀保留

导入

修改类型重新反编译
