-
-
[原创] VMP攻略笔记
-
发表于: 10小时前 227
-
前言
本篇笔记记录的是一次围绕 VMP 样本的完整逆向实战:从入口定位、混淆对抗、执行路径还原,到关键机制理解并在AI的加持下实现虚拟化加固。项目地址:e82K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9P5s2A6Q4x3X3c8B7K9h3q4F1k6r3q4F1i4K6u0r3g2X3#2H3f1s2u0G2K9X3g2U0N6l9`.`.
逆向分析环境
Python版本:3.11
IDA版本:9.2
Frida版本:hluda-server-16.0.10
样本:金罡大佬同款
虚拟机定位
VMP的入口函数特征确实不太好找,但根据 VMP 开发的常见思路,虚拟机入口函数一般都会被 Wrapper 函数包装调用,这些 Wrapper 函数一般拥有如下的特征,我们可以据此推测一些比较可疑的VMP入口地址,再配合 Frida 具体定位
- 规模:指令数量极少,一般少于 20 条指令
- 调调用函数数量限制:仅存在一条跳转指令
- 调用函数方式限制:存在一条固定目标跳转指令(BL跳转指令)
- 返回方式:以 ret 结束
- 尾指令数量:CALL和RET之间的指令数量一般不会太多,一般应小于3
- 返回结果:CALL 之后不修改 X0/W0
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | # Wrapper 函数特征# 1. 规模:指令数量极少,一般少于 20 条指令# 2. 调调用函数数量限制:仅存在一条跳转指令# 3. 调用函数方式限制:存在一条固定目标跳转指令(BL跳转指令)# 4. 返回方式:以 ret 结束# 5. 尾指令数量:CALL和RET之间的指令数量一般不会太多,一般应小于3# 6. 返回结果:CALL 之后不修改 X0/W0import ida_funcsimport ida_uaimport idautilsimport idaapiimport ida_idp# =============================================================================# 配置# =============================================================================MAX_INSNS = 30 # wrapper 最大指令数(对应特征 1:规模)MIN_WRAPPER_COUNT = 15 # 至少几个 wrapper 指向同一 VM Entry# =============================================================================# 工具函数(规模 / 调用关系 / 输出 检查用)# =============================================================================def iter_func_insns(func): """遍历函数内指令(IDA 9.2:func_item_iterator_t)""" fii = ida_funcs.func_item_iterator_t() if not fii.set(func): return for ea in fii.code_items(): yield eadef is_call_insn(ea): """是否为 call 指令(用于检查“仅一次 call”)""" insn = ida_ua.insn_t() if ida_ua.decode_insn(insn, ea): return ida_idp.is_call_insn(insn) return Falsedef writes_return_reg(ea): """是否存在写入X0/W0寄存器 """ insn = ida_ua.insn_t() if ida_ua.decode_insn(insn, ea) == 0: return False if not ida_idp.has_insn_feature(insn.itype, ida_idp.CF_CHG1): return False op0 = insn.ops[0] if op0.type != ida_ua.o_reg: return False width = ida_ua.get_dtype_size(op0.dtype) reg_name = ida_idp.get_reg_name(op0.reg, width) return reg_name in ("X0", "W0")# 获取函数中所有CALL指令def get_call_list(func): call_list = [] for ea in iter_func_insns(func): if is_call_insn(ea): call_list.append(ea) return call_list# 获取 CALL 指令后面的所有指令(不含 call 本身)def get_call_tail(func): call_list = get_call_list(func) if not call_list: return [] tail_insns = [] for ea in iter_func_insns(func): if ea > call_list[0]: tail_insns.append(ea) return tail_insnsdef analyze_wrapper(func): """若 func 符合 Wrapper 特征,返回其唯一调用的 VM Entry 地址;否则返回 None。""" insns = list(iter_func_insns(func)) # 1. 规模:指令数量极少 if len(insns) == 0 or len(insns) > MAX_INSNS: return None # 2. 调用函数数量限制:仅存在一条跳转指令 call_list = get_call_list(func) if len(call_list) != 1: return None # 3. 调用函数方式限制:存在一条固定目标跳转指令(BL跳转指令) targets = list(idautils.CodeRefsFrom(call_list[0], False)) if len(targets) != 1: return None # 4. 返回方式:以 ret 结束 if not idaapi.is_ret_insn(insns[-1]): return None # 5. 尾指令数量:CALL和RET之间的指令数量应小于5 tail_insns = get_call_tail(func) if len(tail_insns) == 0 or len(tail_insns) > 3: return None # 6. 返回结果:CALL 之后不修改 X0/W0 for ea in tail_insns: if writes_return_reg(ea): return None callee_func = ida_funcs.get_func(targets[0]) if not callee_func: # 目标不在任何函数内(如 thunk)则排除 return None return callee_func.start_eadef find_vm_entry_candidates(): wrapper_map = {} for func_ea in idautils.Functions(): func = ida_funcs.get_func(func_ea) if not func: continue vm_entry = analyze_wrapper(func) if not vm_entry: continue wrapper_map.setdefault(vm_entry, []).append(func.start_ea) print("\n====== VM Entry Candidates ======") for vm_entry, wrappers in wrapper_map.items(): if len(wrappers) >= MIN_WRAPPER_COUNT: print(f"\n[+] VM Entry Candidate: {hex(vm_entry)} ({len(wrappers)} wrappers)") for w in wrappers: print(f" wrapper: {hex(w)}")def main(): find_vm_entry_candidates()main() |
混淆对抗
BL混淆还原
防守方设计了大量的 BL 混淆代码,这里没啥说的,直接上罡佬的脚本,执行后可以去除 BL 混淆,注意要重新加载分析文件,Apply patches to input file --> 退出IDA ---- 重新加载so
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 | import idautilsimport idcimport idaapifrom keystone import * # pip3 install keystone-enginedef get_insn_const(addr): op_val = None if idc.print_insn_mnem(addr) in ['MOV', 'LDR']: op_val = idc.get_operand_value(addr, 1) if op_val > 0x1000: # 可能是间接引用 op_val = idc.get_wide_dword(op_val) else: raise Exception(f"error ops const: {addr}") return op_valdef get_patch_data(addr): addr_list = [] for bl_insn_addr in idautils.XrefsTo(addr): bl_insn_addr = bl_insn_addr.frm # print(f'L1 {hex(bl_insn_addr)}:') for xref_addr_l2 in idautils.XrefsTo(bl_insn_addr): # print(f'\tL2 {hex(xref_addr_l2.frm)}:') index = get_insn_const(xref_addr_l2.frm - 4) const_table_start = bl_insn_addr + 4 offset = idaapi.get_dword(const_table_start + index * 4) link_target = const_table_start + offset addr_list.append({"bl_insn_addr": bl_insn_addr, "patch_addr": xref_addr_l2.frm, "index": index, "offset": offset, "link_target": link_target}) return addr_listdef print_patch_data(patch_data): for item in patch_data: print( f"bl_insn_addr: {item["bl_insn_addr"]:#x}, patch_addr: {item["patch_addr"]:#x}, index: {item["index"]}, offset: {item["offset"]:#x}, link_target: {item["link_target"]:#x}")def patch_insns(patch_data): index = 0 for item in patch_data: ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN) asm = f'B {item["link_target"]:#x}' print(f'patch addr {item["patch_addr"]:#x}: {asm}') encoding, count = ks.asm(asm, as_bytes=True, addr=item["patch_addr"]) print(encoding) for i in range(4): idc.patch_byte(item["patch_addr"] + i, encoding[i]) index += 1 # if index == 1: # break def start(): modify_x30_func_address = 0x25D00 patch_data = get_patch_data(modify_x30_func_address) print_patch_data(patch_data) patch_insns(patch_data)start() |
F5 干扰处理
防守方设计了干扰 F5 的代码,但不过是迷惑 IDA 的障眼法,我们调整一下重定位表,执行后就可以 F5 了
直接对 sub_138518 函数直接 F5 会提示
13CEF4: invalid basic block此时我们查看 13CEF4 会发现,IDA 将 13CEF4 识别成代码块了
1.text:000000000013CEF40E000000dword_13CEF4 DCD0xE; DATA XREF: .data.rel.ro:off_1F15E0↓o12345678910.data.rel.ro:1F15E0A896130000000000off_1F15E0 DCQ loc_1396A8 ; DATA XREF: sub_138518+1498↑o.data.rel.ro:1F15E0; sub_138518+14C0↑o ....data.rel.ro:1F15E80095130000000000DCQ loc_139500.data.rel.ro:1F15F0F4 CE130000000000DCQ dword_13CEF4.data.rel.ro:1F15F80094130000000000DCQ loc_139400.data.rel.ro:1F16002896130000000000DCQ loc_139628.data.rel.ro:1F160820B0130000000000DCQ loc_13B020.data.rel.ro:1F1610D4 A7130000000000DCQ loc_13A7D4.data.rel.ro:1F1618F4 CF130000000000DCQ loc_13CFF4.data.rel.ro:1F16200097130000000000DCQ dword_139700我们可以将 1F15F0 中的地址指向一个空白的代码段然后将 13CEF4 的值直接写入,随后即可成功执行 F5 反汇编
123456789101112defval(value, size):returnvalue.to_bytes(size, byteorder='little', signed=False)defraw_patch(ea, data):importidaapifori, binenumerate(data):idaapi.put_byte(ea+i, b)written=ida_bytes.get_bytes(ea,len(data))print(f"验证写入: {written.hex()}")raw_patch(0x1F15F0, val(0x144218,4))raw_patch(0x144218, val(0xe,4))
BR 跳转还原
防守方在开发时大量的使用跳转表的结构,编译之后就形成了类似 BR 的混淆代码,虽然我们确实可以找到跳转表,但 IDA 经常无法识别跳转表或者识别跳转表错误或者是以函数指针的形式调用,导致 F5 看不全代码,我们可以分析一下跳转表的逻辑,然后构造 IDA 易于识别的跳转逻辑
在 IDA 中模糊匹配无法正确建立switch case 表的 BR X8/9/10 跳转块
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170defsearch_wildcard_ex(hex_str1,*args):"""search_wildcard_ex(hex_str1,gap1, hex_str2,gap2, hex_str3,...)gap 语义:前一个 pattern 结束地址 与 下一个 pattern 起始地址 之间允许的最大字节数(不包含 pattern 自身)"""importida_bytesimportida_idaimportida_idaapi# -# 1. 参数校验# -iflen(args)%2!=0:raiseValueError("arguments must be (gap, hex_str) pairs")patterns=[(hex_str1,None)]foriinrange(0,len(args),2):patterns.append((args[i+1], args[i]))# -# 2. 工具函数# -defcompile_pat(hex_str):cpv=ida_bytes.compiled_binpat_vec_t()base=ida_ida.inf_get_min_ea()err=ida_bytes.parse_binpat_str(cpv, base, hex_str,16)iferr:raiseRuntimeError("binpat compile failed: %s"%hex_str)returncpvdefpat_len_from_hex(hex_str):# "?? 7A ?? F8" -> 4returnlen(hex_str.strip().split())# -# 3. 编译 pattern# -pats=[]forhex_str, gapinpatterns:cpv=compile_pat(hex_str)pat_len=pat_len_from_hex(hex_str)pats.append((hex_str, cpv, pat_len, gap))start=ida_ida.inf_get_min_ea()end=ida_ida.inf_get_max_ea()# -# 4. anchor 搜索# -results=[]addr=startwhileTrue:addr, _=ida_bytes.bin_search(addr, end,pats[0][1],ida_bytes.BIN_SEARCH_FORWARD)ifaddr==ida_idaapi.BADADDR:breakcur_ea=addrcur_len=pats[0][2]matched=Truechain=[addr]# 本条匹配链:各 pattern 的起始地址# -# 5. 逐段验证# -foriinrange(1,len(pats)):_, cpv, pat_len, gap=pats[i]# gap = 前一个结束 与 下一个起始 之间的最大字节数(含下一段起始)# 下一段起始可在 [prev_end, prev_end+gap],故搜索范围需覆盖到 prev_end+gap+pat_len(右开)search_start=cur_ea+cur_lensearch_end=cur_ea+cur_len+gap+pat_lenea2, _=ida_bytes.bin_search(search_start,search_end,cpv,ida_bytes.BIN_SEARCH_FORWARD)ifea2==ida_idaapi.BADADDR:matched=Falsebreakchain.append(ea2)cur_ea=ea2cur_len=pat_lenifmatched:results.append(chain)addr+=1# 允许重叠ifnotresults:print("[-] no match")else:match_map={chain[0]: chain[1:]forchaininresults}lines=[" {}: [{}]".format(hex(k),", ".join(hex(x)forxinv))fork, vinsorted(match_map.items())]print("match_map = {")print(",\n".join(lines))print("}")returnresults# .text:000000000013A5B4 08 B9 00 11 ADD W8, W8, #0x2E ; '.'# .text:000000000013A5B8 E8 5A 68 F8 LDR X8, [X23,W8,UXTW#3]# .text:000000000013A5BC F9 03 1F 2A MOV W25, WZR# .text:000000000013A5C0 F3 03 00 32 MOV W19, #1# .text:000000000013A5C4 F5 63 08 A9 STP X21, X24, [SP,#0x120+var_A0]# .text:000000000013A5C8 E8 7A 68 F8 LDR X8, [X23,X8,LSL#3]# .text:000000000013A5CC E0 7B 3B A9 STP X0, X30, [SP,#0x120+var_170]# .text:000000000013A5D0 00 01 1F D6 BR X8 ; apply_bit_mask compare_float_double## .text:0000000000139DD0 29 B9 00 11 ADD W9, W9, #0x2E ; '.'# .text:0000000000139DD4 E9 5A 69 F8 LDR X9, [X23,W9,UXTW#3]# .text:0000000000139DD8 E9 7A 69 F8 LDR X9, [X23,X9,LSL#3]# .text:0000000000139DDC E0 7B 3B A9 STP X0, X30, [SP,#0x120+var_170]# .text:0000000000139DE0 20 01 1F D6 BR X9## .text:000000000013A548 4A B9 00 11 ADD W10, W10, #0x2E ; '.'# .text:000000000013A54C EA 5A 6A F8 LDR X10, [X23,W10,UXTW#3]# .text:000000000013A550 EA 7A 6A F8 LDR X10, [X23,X10,LSL#3]# .text:000000000013A554 E0 7B 3B A9 STP X0, X30, [SP,#0x120+var_170]# .text:000000000013A558 40 01 1F D6 BR X10## .text:000000000013A184 08 B9 00 11 ADD W8, W8, #0x2E ; '.'# .text:000000000013A188 E8 5A 68 F8 LDR X8, [X23,W8,UXTW#3]# .text:000000000013A18C 5A 17 00 11 ADD W26, W26, #5# .text:000000000013A190 F5 03 13 AA MOV X21, X19# .text:000000000013A194 FC 03 18 AA MOV X28, X24# .text:000000000013A198 E8 7A 68 F8 LDR X8, [X23,X8,LSL#3]# .text:000000000013A19C F8 03 09 AA MOV X24, X9# .text:000000000013A1A0 F3 03 1A 2A MOV W19, W26# .text:000000000013A1A4 E0 7B 3B A9 STP X0, X30, [SP,#0x120+var_170]# .text:000000000013A1A8 00 01 1F D6 BR X8search_wildcard_ex("?? B9 00 11 ?? 5A ?? F8",12,"?? 7A ?? F8",12,"?? 01 1F D6")-----match_map={0x139dd0: [0x139dd8,0x139de0],0x13a184: [0x13a198,0x13a1a8],0x13a548: [0x13a550,0x13a558],0x13a5b4: [0x13a5c8,0x13a5d0],0x13a914: [0x13a928,0x13a930],0x13afc8: [0x13afd0,0x13afd8],0x13b00c: [0x13b014,0x13b01c],0x13b6cc: [0x13b6d4,0x13b6dc],0x13bce0: [0x13bce8,0x13bcf0],0x13bed4: [0x13bedc,0x13bee4],0x13c214: [0x13c21c,0x13c224],0x13d024: [0x13d02c,0x13d034],0x13d07c: [0x13d084,0x13d08c]}利用 frida 批量 hook 建立 x23 和 x8/9/10 的映射表
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165let trace_map =newMap();function_py_str(s) {return'"'+ String(s).replace(/\\/g, "\\\\").replace(/"/g,'\\"') +'"';}functiondump_as_python_dict(trace_map) {if(!trace_map ||typeoftrace_map.forEach !=="function") {console.log("{}");return;}let lines = [];trace_map.forEach(function(node, pc_hex) {let br_reg = (node && node.br_reg) ? node.br_reg :"";let values = (node && node.values) ? node.values :newMap();let innerParts = [];values.forEach(function(val, xn_1) {if(Array.isArray(val)) {let listStr ="["+ val.map(function(s) {return_py_str(s); }).join(", ") +"]";innerParts.push(_py_str(xn_1) +": "+ listStr);}else{innerParts.push(_py_str(xn_1) +": "+ _py_str(val));}});let valuesStr ="{"+ innerParts.join(", ") +"}";lines.push(" "+ _py_str(pc_hex) +": {\"br_reg\": \""+ br_reg +"\", \"values\": "+ valuesStr +"}");});console.log("{\n"+ lines.join(",\n") +"\n}");}functionhook_libmtguard_by_offset(base) {// [[0x139dd0, 0x139dd8, 0x139de0], [0x13a184, 0x13a198, 0x13a1a8], [0x13a548, 0x13a550, 0x13a558], [0x13a5b4, 0x13a5c8, 0x13a5d0], [0x13a914, 0x13a928, 0x13a930], [0x13afc8, 0x13afd0, 0x13afd8], [0x13b00c, 0x13b014, 0x13b01c], [0x13b6cc, 0x13b6d4, 0x13b6dc], [0x13bce0, 0x13bce8, 0x13bcf0], [0x13bed4, 0x13bedc, 0x13bee4], [0x13c214, 0x13c21c, 0x13c224], [0x13d024, 0x13d02c, 0x13d034], [0x13d07c, 0x13d084, 0x13d08c]]let hook_addr_map =newMap();hook_addr_map.set(0x139dd0,"x9");hook_addr_map.set(0x13a184,"x8");hook_addr_map.set(0x13a548,"x10");hook_addr_map.set(0x13a5b4,"x8");hook_addr_map.set(0x13a914,"x8");hook_addr_map.set(0x13afc8,"x8");hook_addr_map.set(0x13b00c,"x8");hook_addr_map.set(0x13b6cc,"x8");hook_addr_map.set(0x13bce0,"x8");hook_addr_map.set(0x13bed4,"x8");hook_addr_map.set(0x13c214,"x8");hook_addr_map.set(0x13d024,"x8");hook_addr_map.set(0x13d07c,"x8");hook_addr_map.forEach(function(br_reg_name, off) {let addr = base.add(off)Interceptor.attach(addr, {onEnter() {let changed =false;let pc_offset =this.context.pc.sub(base);let pc_offset_hex ="0x"+ pc_offset.toString(16);console.log("pc_offset_hex =", pc_offset_hex);let reg =this.context[br_reg_name];let xn_value_1 = reg.toInt32();let xn_value_1_hex ="0x"+ xn_value_1.toString(16);console.log("xn_value_1_hex =", xn_value_1_hex);let xn_value_2 = xn_value_1 + 0x2e;let xn_value_2_hex ="0x"+ xn_value_2.toString(16);console.log("xn_value_2_hex =", xn_value_2_hex);let xn_value_3 =this.context.x23.add(xn_value_2 * 8).readU64();let xn_value_3_hex ="0x"+ xn_value_3.toString(16);console.log("xn_value_3_hex =", xn_value_3_hex);let xn_value_4 =this.context.x23.add(xn_value_3 * 8).readU64();let xn_value_4_hex ="0x"+ xn_value_4.toString(16);console.log("xn_value_4_hex =", xn_value_4_hex);let xn_value_5 = ptr(xn_value_4).sub(base);// 有符号差值let xn_value_5_hex ="0x"+ (xn_value_5 >>> 0).toString(16);// 按无符号显示偏移console.log("xn_value_5_hex =", xn_value_5_hex);if(!trace_map.has(pc_offset_hex)) {trace_map.set(pc_offset_hex, {br_reg: br_reg_name,values:newMap()});changed =true;}let pc_node = trace_map.get(pc_offset_hex);if(!pc_node.values.has(xn_value_1_hex)) {pc_node.values.set(xn_value_1_hex, [xn_value_3_hex, xn_value_5_hex]);changed =true;}if(!changed)return;dump_as_python_dict(trace_map);console.log("======================");}});});}functionhook_linker_load() {vardlopen = Module.findExportByName(null,"dlopen");varandroid_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");if(dlopen) {console.log("[+] dlopen @", dlopen);Interceptor.attach(dlopen, {onEnter:function(args) {varname = args[0].readCString();if(name && name.indexOf(".so") > -1) {// console.log("dlopen --> " + name);}}});}else{console.log("[-] dlopen not found");}if(android_dlopen_ext) {console.log("[+] android_dlopen_ext @", android_dlopen_ext);Interceptor.attach(android_dlopen_ext, {onEnter:function(args) {this.soname = args[0].readCString();// console.log("android_dlopen_ext --> " + this.soname);},onLeave:function() {if(!this.soname)return;if(this.soname.indexOf("libxxguard.so") === -1) {return;}varbase = Module.findBaseAddress("libxxguard.so");if(!base) {base = Module.findBaseAddress(this.soname);}if(!base) {console.log("[-] libxxguard.so base not found yet");return;}console.log("[+] libxxguard.so base =", base);console.log("[+] libxxguard.so pid =", Process.id);hook_libmtguard_by_offset(base);}});}else{console.log("[-] android_dlopen_ext not found");}}functionmain() {hook_linker_load();}setImmediate(main);批量 patch BR 跳转,执行后此时 IDA 已经可以较好的反汇编生成伪 C 代码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899defset_asm(addr:int, asm:str)->int:"""在给定地址 addr 处写入一条 ARM64 汇编指令 asm(单行字符串)。返回实际汇编出的指令条数(通常为 1)。"""# 懒加载依赖,便于拷贝到其他工程中使用importida_bytesimportida_uaimportida_kernwinimportida_idaapifromkeystoneimportKs, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN# 处理可能出现的 loc_xxxxx:将其替换为对应的绝对地址 0xXXXXXXXXif"loc_"inasm:parts=asm.split()fori, tokeninenumerate(parts):iftoken.startswith("loc_"):ea=ida_kernwin.str2ea(token)ifea==ida_idaapi.BADADDR:raiseRuntimeError(f"Cannot resolve {token}")parts[i]=f"0x{ea:X}"asm=" ".join(parts)# Keystone 汇编ks=Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)encoding, count=ks.asm(asm, addr)code=bytes(encoding)# 以十六进制显示编码,例如 "70 BD FA 17"hex_encoding=" ".join(f"{b:02X}"forbinencoding)print(f"[+] Encoding: {hex_encoding}")# Patch 到 IDAida_bytes.patch_bytes(addr, code)# 强制 IDA 重新反汇编ea=addrend=addr+len(code)whileea < end:ida_ua.create_insn(ea)ea+=4print(f"[+] Patched {count} ARM64 instructions at {hex(addr)}")returncountdeffind_empty_space(size:int)->int:"""在当前二进制的 .text 段中查找一块大小为 size 且全部为 0x00 的空闲空间,返回 0x10 对齐的起始地址,找不到则返回 BADADDR。"""importida_bytesimportida_idaapiimportida_segment# 只在 .text 段中查找,和注释含义保持一致seg=ida_segment.get_segm_by_name(".text")ifnotseg:returnida_idaapi.BADADDRstart=seg.start_eaend=seg.end_ea# 从第一个 0x10 对齐的地址开始,每次步进 0x10,直接在该对齐处读 size 字节检查是否全 0ea=(start+0xF) & ~0xFwhileea+size <=end:data=ida_bytes.get_bytes(ea, size)ifdataisnotNoneandall(b==0forbindata):returneaea+=0x10returnida_idaapi.BADADDRmatch_map={0x139dd0: [0x139dd8,0x139de0],......}trace_map={"0x139dd0": {"x9": {"0xc":"0x139628","0xf":"0x13a934","0x13":"0x13cd74"}},......}forpc_offset, nodeintrace_map.items():forreg, infoinnode.items():print(f"reg:{reg}")br_reg_id=int(reg.replace("x", ""))print(f"br_reg_id:{br_reg_id}")empty_space=find_empty_space((len(info)*2+1)*4)set_asm(int(pc_offset,16),"NOP")set_asm(int(pc_offset,16)+4,"NOP")set_asm(match_map[int(pc_offset,16)][0],"NOP")set_asm(match_map[int(pc_offset,16)][1],"B "+hex(empty_space))index=0forkey, valueininfo.items():print(f"reg:{reg} key:{key} addr:{value}")set_asm(empty_space+4*index, f"CMP W{br_reg_id}, #{key}")index+=1set_asm(empty_space+4*index, f"B.EQ {value}")index+=1set_asm(empty_space+4*index,"RET")print("================================================")
代码跟踪工具
Trace工具
没什么说的,直接上追佬的工具,配合葫芦佬的 frida 一键trace
0a0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7K9i4q4A6N6e0t1H3x3U0u0Q4x3V1k6$3L8g2)9J5k6s2c8J5j5h3y4W2i4K6u0V1M7X3g2D9k6h3q4K6k6b7`.`.
16fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5P5Y4A6Z5k6i4W2S2L8X3N6Q4x3V1k6K6N6s2u0G2L8X3N6d9i4K6u0V1k6Y4u0A6k6r3q4Q4x3X3c8S2L8X3c8J5L8$3W2V1
Trace查看工具
强推 010Editor16,秒开超大文本,支持智能高亮
Trace分析工具
我自己拿 AI 写了一个简单的 UI 分析工具,主要是用来快速追溯寄存器的赋值情况(010Editor来回翻太痛苦了)
项目地址:a12K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9P5s2A6Q4x3X3c8B7K9h3q4F1k6r3q4F1i4K6u0r3g2s2u0S2j5$3g2m8L8X3q4D9P5i4A6W2M7R3`.`.

关键分析整理
VmState 数据结构与管理机制
我们可以简单的考虑一个问题,VMP 的原理是将编码的数据转换为自己能理解的 vmstate 然后再进行执行,并且每个被VMP保护的函数都对应一个 vmstate,那么vmp会每次执行的时候都重新解码一次构造一个新的 vmstate 么?换句话说如果我们自己来设计一个虚拟机我们会怎么做?显然是利用红黑树来管理所有的 vmstate,并且本次样本也是这么干的,此处可以说英雄所见略同。
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 | struct VmState{ FunctionType *function_list; __int64 register_count; __int64 type_count; void *register_list; void *type_list; void *inst_list; void *param_list;};struct VmStateTreeNode{ char _color_pad[8]; struct VmStateTreeNode *parent; struct VmStateTreeNode *left; struct VmStateTreeNode *right; unsigned __int64 key; VmState *value;};struct VmStateTree{ VmStateTreeNode *root; union { struct { VmStateTreeNode *sentinel_left; VmStateTreeNode *sentinel_parent; VmStateTreeNode *sentinel_right; void *sentinel_key; VmState *sentinel_value; }; char sentinel_area[40]; }; pthread_mutex_t mutex;}; |
ByteCode与ReTable的重定位机制
我们从设计一个虚拟机来考虑一个问题,如果将虚拟机所有的代码都放在一个BYTECODE数据块中是否可行,理论上来讲一定是可行的,但这会导致我们几乎需要实现完整的linker,因为虚拟机中解释执行到 BL 这类指令时虚拟机需要自己计算重定位,这除了增加我们的开发难度以外几乎没有任何正向收益。所以简单的办法是除了BYTECODE数据块以外,我们还需要重构一份重定位表,让系统的 Linker 帮我们计算,而我们只需要在 ByteCode 里直接取 ReTable[id] 就行了。
ByteCode解析机制
解析每组 BYTECODE 时都采用了如下的结构体进行解析
1 2 3 4 5 6 7 8 | struct ByteCodeReader{ void *buffer_ptr; __int64 buffer_size; __int64 cached_bits; unsigned int bit_count; __int64 read_pos;}; |
解码按 6 bit 为一单元消费,其中最高位为继续标志位,当前 cached_bits 不足 6 bit 时触发补读,补读会使 byteCodeReader 从 buffer 再读 8 字节(不足 8 字节则读到末尾)做 bit 拼接,再继续;若仍不足则解码失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | type_tag = bit_stream_read((__int64)byteCodeReader, 6u); //1.读取6位type_tag_1 = type_tag; //2.初始化结果if ( (type_tag & 0x20) != 0 ) //3.检查bit5(继续标志位){ type_tag_1 = type_tag & 0x1F; //4.提取低5位数据/5。位移量初始化为5 v88 = 5; //5.位移量初始化为5 do { v89 = bit_stream_read((__int64)byteCodeReader, 6u); //6. 读下一个6位 type_tag_1 |= (v89 & 0x1F) << v88; //7.拼接低5位数据 v88 += 5; } while ( (v89 & 0x20) != 0 ); //9.检查继续标志} //10.type_tag_1 现在包含完整的VLE解码值 |
不定参数与ABI机制
反汇编中出现的大量类似如下的代码,本质应该还是标准API va_arg、va_copy 在编译器优化后的展开形式,这里提供一份分析用的代码,可以自行编译调试一下,还是挺有意思的。
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 | struct __va_list_tag { void *__stack; void *__gr_top; void *__vr_top; int __gr_offs; int __vr_offs;};void test_va(int a, int b, int c, int d,int e, int f, int g, int h,...) { va_list ap; va_start(ap, h); struct __va_list_tag *va = (struct __va_list_tag *)≈ __va_list_tag* caller_context = new __va_list_tag(); memcpy(caller_context, &ap, sizeof(__va_list_tag)); for(int i = 0; i < 3; i++){ double* stack = nullptr; int vr_offs = caller_context->__vr_offs; if ( (int)vr_offs < 0 && (caller_context->__vr_offs = vr_offs + 16, (int)vr_offs + 16 <= 0)) { stack = (double *)((char *)caller_context->__vr_top + vr_offs); } else { stack = (double *)caller_context->__stack; caller_context->__stack = (char *)caller_context->__stack + 8; } double v213 = *stack; LOGD("v213:%f", v213); uint64_t stack_args_value = 0; int* stack_args_1 = nullptr; int __gr_offs = caller_context->__gr_offs; if ( (int)__gr_offs < 0 && (caller_context->__gr_offs = __gr_offs + 8, (int)__gr_offs + 8 <= 0) ) { stack_args_value = *(int *)((char *)caller_context->__gr_top + __gr_offs); } else { stack_args_1 = (int *)caller_context->__stack; caller_context->__stack = (char *)caller_context->__stack + 8; stack_args_value = *stack_args_1; } LOGD("stack_args_value:%lu", stack_args_value); } return;}test_va(1,2,3,4,5,6,7,8,1.25, 2.5);test_va(1,2,3,4,5,6,7,8,100, 1.3, 300); |
符号与 RTTI 泄露信息
通过 .data.rel.ro 段中的 vtable 与 RTTI 符号,可以还原出大量关键信息
1 2 3 4 5 6 7 8 9 10 | .data.rel.ro:00000000001F1340 ; `vtable for'jg_vmp::Type.data.rel.ro:00000000001F1340 00 00 00 00 _ZTVN6jg_vmp4TypeE DCQ 0 ; DATA XREF: decode_type+DF0↑o.data.rel.ro:00000000001F1340 00 00 00 00 ; decode_type+EDC↑o ....data.rel.ro:00000000001F1340 ; offset to this.data.rel.ro:00000000001F1348 78 13 1F 00… DCQ _ZTIN6jg_vmp4TypeE ; `typeinfo for'jg_vmp::Type.data.rel.ro:00000000001F1350 2C ED 13 00…off_1F1350 DCQ nullsub_5.data.rel.ro:00000000001F1358 64 DB 13 00… DCQ j_j_.free_3.data.rel.ro:00000000001F1360 68 DB 13 00… DCQ sub_13DB68.data.rel.ro:00000000001F1368 74 DB 13 00… DCQ sub_13DB74.data.rel.ro:00000000001F1370 38 E0 13 00… DCQ sub_13E038 |
- 类型系统完整暴露,可明确识别虚拟机内部的类型体系,包括:
jg_vmp::Type(基类)IntegerType / StructType / PointerType / FunctionType / ArrayType / VectorType(派生类)
继承关系可准确还原,RTTI 使用
__si_class_type_info,表明所有类型均统一继承自jg_vmp::Type可完整重建类型继承树虚函数接口与调用约定泄露,vtable 中保存了虚函数数量和顺序以及派生类对基类虚函数的覆盖关系
VM 类型解析与执行逻辑被轻易推断,vtable / RTTI 被
decode_type与vm_interpreter大量使用
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 | enum TypeKind : unsigned __int32{ VoidType = 0x0, FloatType = 0x2, DoubleType = 0x3, IntegerType = 0xB, FunctionType = 0xC, StructType = 0xD, ArrayType = 0xE, PointerType = 0xF, VectorType = 0x10,};struct /*VFT*/ Type_vtbl{ void (*_destructor)(); void (*_operator_delete)(); void (*_clone)(); void (*_get_type_name)(); TypeKind (*_get_type_size)();};struct Type{ Type_vtbl *vtable; TypeKind kind; char field1; char field2; char field3; char field4;}; |
虚拟寄存器机制
- 寄存器数量动态生成,解码时从 bytecode 里读出,再按这个数量分配 RegManager。同一套解释器可跑“不同寄存器规模”的 bytecode。
- 堆指针标志,每个槽 24 字节,除存值/指针外,还有一个布尔类型的堆指针标志。若某条指令(如 ALLOC)在某个寄存器上标记了堆指针标志,则退出时由 VM 对该寄存器中的指针做释放。
- 虚拟寄存器初始化,当前指令的参数个数大于0时,会从 vava(VaStruct / va_list) 里按 ABI 取参,依次写入参数个数的寄存器,还会从 bytecode 取参并设置返回值类型。
- 缓存的不是“执行后的状态”,当指令没有参数时,每次执行都从初始寄存器缓存中拷贝一份新的虚拟寄存器内存用于执行,当指令存在参数时则从 va(VaStruct / va_list) 里按 ABI 取参,即初始化的寄存器缓存永远不用于执行,只作为拷贝的源数据。
基于逆向的自研 VMP 项目
前面做逆向时,我们已经把样本里的关键机制摸得比较清楚了:状态缓存、字节码组织、调用桥接、参数传递、寄存器槽管理。所以我们自己实现一个虚拟化加固,但路线选择上,我没有走 LLVM IR,而是选择了汇编语义翻译。虽然实现起来更困难,但更易于理解VMP核心机制。
项目地址:ee9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9P5s2A6Q4x3X3c8B7K9h3q4F1k6r3q4F1i4K6u0r3g2X3#2H3f1s2u0G2K9X3g2U0N6l9`.`.
加固流程
离线阶段的核心目标是: 将原始函数逻辑转化为虚拟执行载荷,并重构目标库的调用路径,实现执行权转移。
1. 导出函数编码产物
对选定的目标函数进行指令抽取与翻译处理:
- 解析函数控制流结构(BasicBlock / CFG)
- 将原生 ARM64 指令翻译为虚拟指令序列
- 生成函数级 VM 编码载荷
每个函数最终形成:
1 | [函数标识] + [虚拟指令流] + [元数据] |
构成独立的“函数执行单元”。
2. 构建扩展载荷容器
将所有函数执行单元汇总,组织为统一的扩展容器结构:
- 函数索引表(symbolKey → payloadOffset)
- 共享分支地址映射表
- 外部调用桥接信息
- 运行时所需元数据
容器结构写入扩展载荷库尾部区域,形成独立逻辑区块。
3. 嵌入运行时宿主库
将构建完成的扩展载荷整体嵌入运行时宿主库(VM Engine)尾部:
- 保持单一 so 交付形态
- 避免额外文件依赖
- 降低部署复杂度
最终形成:
1 | [VM Engine 本体] + [扩展载荷容器] |
实现单库集成模型。
4. 接管目标库导出符号
对目标库进行符号层重构:
- 修改导出符号指向
- 更新符号表与哈希布局(这里主要参考vmprotect-3.5.1的实现)
- 将原函数入口重定向至统一接管入口
调用路径被收口为:
1 | 原始导出函数 → 接管分发层 → 虚拟执行 |
虚拟执行流程
运行时阶段的核心目标是:在保持外部调用行为不变的前提下,完成载荷装载、链接恢复与虚拟执行调度。
1. 读取嵌入载荷并交由自定义链接器处理
- 定位宿主库尾部嵌入的扩展载荷
- 直接在内存中解析(这里主要参考 soLoader)
- 将载荷恢复为可执行内存映像
3. 预热函数缓存
对扩展容器进行解析:
- 构建函数索引缓存
- 恢复函数执行元数据
- 挂载共享分支地址表
- 建立 symbolKey → payload 映射关系
5. 路由到目标虚拟函数
当导出函数被调用时:
- 通过 symbolKey + 模块编号定位目标函数
- 构造虚拟执行上下文
- 装配参数与寄存器模型
6. 解释执行与桥接调用
虚拟机进入执行循环:
- 逐条解析虚拟指令
- 模拟寄存器与栈环境
- 控制流跳转通过内部映射表完成
结语
这份笔记既是样本分析记录,也是工程实践说明。希望它能为同样在做 VMP 逆向或虚拟化保护研究的同学提供些许帮助
参考资料
https://bbs.kanxue.com/thread-286441.htm
580K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1j5H3P5r3q4U0j5h3u0Q4x3X3g2G2M7X3N6Q4x3V1k6T1K9h3c8S2M7$3y4A6i4K6u0r3N6X3#2H3M7X3!0@1k6h3y4@1i4K6u0V1x3#2)9J5k6e0g2Q4x3X3f1I4
e10K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0L8%4W2n7k6h3q4F1e0h3W2D9K9%4S2Q4x3V1k6K6L8@1I4G2j5h3c8W2M7R3`.`.
赞赏
- [原创] VMP攻略笔记 228
- [原创]Android TEE 密钥认证证书数据结构学习笔记 38907
- [原创]Overt安全风控检测工具深度解析 8835
- [原创] OLLVM 攻略笔记 36409
- [原创]Pixel6 降级记录 9061