首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
1
1
[原创]基于 Trace 的 VMP 还原实践文档
发表于: 1小时前
64
[原创]基于 Trace 的 VMP 还原实践文档
一只鸭子
1小时前
64
# 基于 Trace 的 VMP 还原实践文档 ## 零、前提 > **作者:** 人生导师 > **日期:** 2026 年 09 月 20 日 星期日 20:37:28 CST 这玩意适合想尝试 VMP 但是自己没什么思路,没那么强的逆向功底去啃静态字节码与 handler 的一个小实现 但是还是要求有一些逆向功底的,可以自己做到简单的内存布局分析,当然也可以直接让模型给你跑,得到产物后对照 ## 一、问题定义 VMP 逆向的传统路径很重,并且现在的方法大多数也还是在延续 Unpacking Virtualization Obfuscators(WOOT 2009)的六步法则 1. 逆向 VM 解释器(找 dispatcher/handler) 2. 找虚拟化代码的控制流入口 3. 写一个字节码反汇编器 4. 反汇编出 VM 字节码(得到 IR) 5. 做编译器式优化(去掉 VM 的样板开销) 6. 重新生成可执行代码 当然,这六步不好吗?当然很好但是这对于当今日益复杂与庞大的现代 VMP 来说几乎是不够用的,啃完现在版本,下一个版本 handler 又发生变化了,还需要再去做,这是和厂家进行时间竞赛,也是折磨自己,所以我们要想一种更符合现在所使用技术栈的方法,从汇编与地址热点来看它们有什么特点,现在做逆向,trace 几乎已经是标配。unidbg 模拟执行也好、真机跑也罢,最终都会拿到一份几十万甚至上千万条指令的执行记录。当然了古法思路依旧是我们坚实的后盾,依旧是不可丢弃的 可以发现的:VMP handler 的结构极为规范——每条 handler 都有固定的入口模式、固定的指令序列、固定的出口跳转。而 trace 本身就是把动态执行"拍平"成静态文本的过程。 所以:**那为什么不直接解析 trace 来还原 VMP 执行逻辑?** trace 已经记录了所有执行结果。你只需要知道"这段指令是哪个 handler",而不需要完全搞懂 handler 内部的每一条运算。本质上,这是把 "逆向虚拟机保护" 降维成 "填空游戏"——工具问"这是啥 handler?",你答"STORE_REG",工具说"好的继续"。 --- ## 二、核心洞察:Dispatch 模式是固定的 最初的想法是用正则匹配 handler 开头几条指令的特征序列。但实际实现中发现了一个更简单的方案。 VMP 的 dispatch 模式是固定的: ```asm LDR X8, [X24, X8, LSL#3] ; 从 dispatch table 加载下一个 handler 地址 BR X8 ; 间接跳转到 handler ; ──────────── 边界 ──────────── [handler入口...] ; 这里就是 handler 的起始地址 ``` 这意味着**不需要匹配 handler 内部的任何指令模式**。只需要我们多分析和多看热点地址,首先可以看出来最常调用的 handler 或者函数,就可以通过这块内容进行向上或者向下的寻找执行,在国内 VMP 中这还是非常非常简单的,寄存器使用的都很标准 然后就可以开始进行跳转的匹配了,这里说的是链式结构,看跳转前的寄存器是不是某个我们分析得到的寄存器来源,比如 x25 寄存器是跳转的来源,那么就可以写一个匹配,对全部 br 后寄存器进行追踪看看它的来源是不是 x25,如果是那么就符合所需结构,就判断 br 后是一个 handler,这时候就有一个问题,第一个 br 前呢? 要回答这个问题就要想一下 VM 的前缀操作和尾部操作,一个被加固的内容肯定不是直接就开始 VM 化操作了,前面会有混淆、花指令、参数处理然后才是 VM,所以尽可能的排除这些内容,专心在 VM 本身,当然了也会有小小的瑕疵比如 VM 的入口和出口,这就需要简单分析一下了,明确标记出来从什么时候 VM 开始从什么时候结束 --- ## 三、系统设计:像 VM 解释器一样执行 ### 3.1 解释器模式 设计理念:**不一次性扫描整个 trace,而是像 VMP 解释器一样逐条推进。** 每切块一次,说明走了一条 handler,这时候就应该进行一次还原,这时候就会有 3-4 条分歧路存在 1. 普通 handler,这没什么好说的,可以直接还原 2. 控制流,这块是难点也不是难点 3. 复合 handler,这块也是比较难的,不过也还可以走普通 handler 的方法 4. CALL 类,这玩意也可以说是中等级别,这玩意就需要看你的目标要求是什么了 ## 四、handler 核心 这块我们来分别说一下前面所说的四类内容,不过还是在此之前说一下前置思路吧 ### 0. 影子轨道与二代 IR 在这玩意之前也有一个前置条件,你需要理解 P-code 或者 VEX 或者任何一种 IR 形式,我推荐是这两种,其中 P-code 发过看雪 影子轨道意思是汇编为骨架,填充后的内容是影子,序列号就是我自己定的 trace 唯一索引,也是行号 以下是我 trace 格式间隔是 tab 符号 ``` 12 0x7769b02368 0xa2368 "mov x8, sp" {"SP":"0x77845ff550"} {"X8":"0x77845ff550"} {} 13 0x7769b0236c 0xa236c "stur x8, [x29, #-0x10]" {"X8":"0x77845ff550","FP":"0x77845fffa0"} {} {} 14 0x7769b02370 0xa2370 "sub x9, x8, #0x10" {"X8":"0x77845ff550"} {"X9":"0x77845ff540"} {} 15 0x7769b02374 0xa2374 "sub x8, x8, #0x10" {"X8":"0x77845ff550"} {"X8":"0x77845ff540"} {} 16 0x7769b02378 0xa2378 "stur x8, [x29, #-0x98]" {"X8":"0x77845ff540","FP":"0x77845fffa0"} {} {} 17 0x7769b0237c 0xa237c "mov sp, x9" {"X9":"0x77845ff540"} {"SP":"0x77845ff540"} {} 18 0x7769b02380 0xa2380 "mov x8, sp" {"SP":"0x77845ff540"} {"X8":"0x77845ff540"} {} 19 0x7769b02384 0xa2384 "sub x9, x8, #0x10" {"X8":"0x77845ff540"} {"X9":"0x77845ff530"} {} 1a 0x7769b02388 0xa2388 "sub x10, x8, #0x10" {"X8":"0x77845ff540"} {"X10":"0x77845ff530"} {} 1b 0x7769b0238c 0xa238c "stur x8, [x29, #-0x18]" {"X8":"0x77845ff540","FP":"0x77845fffa0"} {} {} 1c 0x7769b02390 0xa2390 "mov sp, x9" {"X9":"0x77845ff530"} {"SP":"0x77845ff530"} {} 1d 0x7769b02394 0xa2394 "sub x8, sp, #0x20" {"SP":"0x77845ff530"} {"X8":"0x77845ff510"} {} ``` #### 影子轨道 两代 IR,同源同 varnode: - 第一代 = 原始汇编 → SLEIGH → P-code。语义完整的"真身",静态骨架,不产值。 - 第二代 = 第一代 + trace 值 → varnode 折叠 → P-code。带具体值的"影子",做向导、穿透。 关键认知:值来自 trace(动态执行),不是第一代自己算出来的。第一代不执行不产值,只回答"这段指令长啥样";trace 回答"这次跑起来值是啥"。两代是同一批指令的两个视图,别混成一个东西。 对齐(影子身份):两代从同一批字节、同一套 SLEIGH 提升,unique varnode 对同一条指令分配是确定性的,所以共享同一套 varnode 命名空间。影子身份靠这个,不靠"两代汇编都合法"。PC 锚点用 RVA(trace 第三列),不能用绝对地址(ASLR 漂移)。 折叠的本质:varnode = (space, offset, size),折叠 = 把 varnode 从 register space 踢到 const space。分两步: 1. 替换:源 varnode 换成 trace 里的常量值。 2. 常量折叠:代数化简,能算的算出来,中间 unique 变死代码消掉。 unique 生命周期只在单条指令内,trace 不记它,只能靠"输入寄存器折成常量后化简"消掉。顺序:先折输入,再化简。 折叠边界(trace 给了啥才能折啥): - 读列(源寄存器值)→ 折源 - 写列(目标寄存器值)→ 折目标 - 内存地址 → 值没记 → LOAD/STORE 只能靠"输出寄存器已知"后向折,没法前向折"这地址里是啥" varnode 红利:SLEIGH 白送寄存器重命名 + 值编号。汇编里 x10 复用、内存别名、条件码这些最脏的隐式数据流,提升时全显式化,每个值一个 varnode,跨指令 def-use 现成。这条 def-use = DFG 骨架,直接接"DFG + 反向污点"那套。所以 P-code 不是"任意的"——换个没 varnode / 显式 def-use 的 IR,这层得自己重写。 两个坑: 1. flags/NZCV 没记(cmp 写列空,csel 读列不记 NZCV)。控制流穿透(cbz/tbz)会卡。 2. 内存地址 → 值没记(第 7 列空)。控制流穿透够用,追内存 key 不够。 难点答案: - 代入多少/怎么代 → 自己解析并构造原有 P-code 结构字典,整个 handler 作为一个列表,列表内是字典,将实际值记录进字典内,后续算法根据这个字典进行,这样就可以做到不影响原有骨架而带有具体值了 OK,这点就到这里了,也是非常非常简单 --- ### 1. 普通 handler 这块根据多次 VM 的存取值,以及 VM 的内存站位,进行判断它的主要寄存器是什么,普通寄存器、浮点 32 位、浮点 64 位等等,这时候就有了锚点了,就可以开始写图算法了也可以说是反向污点,从最终操作到来源进行记录,同时得益与 IR 我们也还可以直接拿具体执行的操作码,最终还原得到伪汇编或者自己定义的一套结构化 IR ### 2. 控制流 handler 对于这个方案来说,这东西和上面的一样,不过这块的锚点在于 IP 或者字节码的指针操作,当然这玩意也会有特征的,当你在进行上一步还原以后发现得到的东西具体是比较相关的,这时候我推荐是原样记录,因为方案局限性 ### 3. 复合 handler 这块的恶心点在于它有很多锚点,会进行很多次操作,不过总体思路还是一样的,肯定还是找最终值到谁了,怎么来的、为什么这样来 注意点就是确定好当前处理 handler 的 IO 边界,不然就会出现层次泄漏导致最终 IR 与实际不符合 ### 4. CALL 类 handler 这东西是 handler 内封装出来对原始函数进行调用的,这块也是一个阴间,可以看需求吧。 > 在 VMP 语境下,handler 块本身是连续且地址确定的。BL/BLR 的目标地址可以直接从指令中解出,结合已知的 handler 布局,就能确定被调函数的入口。如果被调函数也是 handler 块的一部分,它的边界由块布局给出,而不是由 BL 的返回地址给出。 1. 只记录所调用的具体函数偏移,不记录任何其他有利信息比如`@CALL{Addr}` 2. 记录具体函数内的函数调用列表以及深度,类似与 Linux 上 tree 的样子,不过只记录具体函数调用列表,再深不会记录 3. 根据上下文对函数进行数据流方面的参数、地址提取以实现`@CALL{Addr[arg1,arg2,arg3]}` 其他的方式暂时没有想到,期待补充,来写一下折叠的思路,根据 Arm64 的规范可知,每条汇编是 4 字节,可以理由这个规律进行函数调用的识别,找到`bl`之后的内容并且找到这条跳转下一条指令的地址这时候我们就把一个函数包含起来了,这时候就考虑做调用栈的归纳,然后就可以实现上面所说展示内容了 ### 5. 输出 这块就是一个序列化输出了,我推荐直接抄一下你所还原平台的汇编操作码以及操作符,在此基础上进行叠加序列化,这里不要做任何你以为的高级操作,比如代数化简,IR 是为了保证原有语义的正确性的,可以在后续操作中进行但是不能在这种级别产物进行 ### 6. handler 总结以及 IR 后续 至此,我们的 handler 方面的任务算是差不多了,接下来就该是处理我们的后续 IR 了,当然不会在这里直接写处理,这里只写一些简单的东西 - 直接继续使用 P-code 继续向上进行反编译,这里就是直接使用了 Ghidra 了因为是开源的,也好接入 - 产出为 LLVM IR 这个思路是后面直接编译成一个 so 文件,在 ida,Ghidra,BN 等等里面直接反编译用 - 自己实现后续的 CFG,DFG,SSA 等等算法内容,自己去写一套还原到伪 C 的逻辑,最难但是也最挑战性 ### 7. VMP 自调用 这玩意肯定是熟悉的,我搞的 VMP 只有一个没有自调用,其他的全部都有,有的一套字节码还调用两次,他奶奶的都没看明白这玩意想干啥 这在前面进行 handler 切割的时候做,VM 的出、入口表明出来,然后搞一个栈结构进行出入的总结与收敛 - **栈存储**:BL 的返回地址(`current_offset + 4`) - **返回检测**:当前 offset 匹配栈顶 → 出栈 - **栈不为空时**:handler 入口和 BR 指令放行,其余折叠 - **inst_count** 只统计非折叠部分,500 条限制更准确 - 支持任意深度调用嵌套 - 基于 offset 匹配,不依赖 RET 指令(trace 中可能看不到) 这个 BL 折叠逻辑本质上是"回型嵌套"思路在 VMP 场景的实现——动态栈驱动、自底向上、容错性强。 ## 五、人与模型 这时候人的工作已经做的差不多了,剩下的可以自己写也可以交给模型,IDA、Ghidra 这种就是负责对照值和 VM 构造的工具了,这玩意可算降级成了工具而不是主战场了 ## 六、总结 经过上述我们的描述和理解,相信你也是一个非常非常成熟的 VMP 逆向还原选手了,只要你认认真真按我的思路走就可以得到一份还原后的 VMP,如果你有自己的思考和反思那么更好了,也欢迎和我进行交流(此时此刻,我的脑中响起了新闻联播的结束音乐,感觉任务结束了)
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
1
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
mb_hcadvceu
非常支持你的观点!
1分钟前
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
一只鸭子
雪 币:
331
活跃值:
(915)
能力值:
( LV3,RANK:20 )
在线值:
发帖
15
回帖
57
粉丝
74
关注
私信
一只鸭子
2
楼
**OnePanada-Sec 招新啦**
**招新要求**
- 热爱网络安全,喜欢 CTF;
- 拥有 CTF 比赛经验,有较好比赛成绩的;
- 乐于奉献、热爱分享,愿意提升自己同时帮助他人;
- 时间允许参加各类赛事,服从战队管理与安排;
- 各类比赛获奖者、能力出众者视情况考量;
- 未参与其他高校联队;
- 大一同学视情况放宽资历要求。
**联系方式**
请将个人简历发送至以下邮箱:
简历邮箱:2638726415@qq.com
1小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
一只鸭子
15
发帖
57
回帖
20
RANK
关注
私信
他的文章
[原创]基于 Trace 的 VMP 还原实践文档
0
[讨论]逆向死没死
191
[原创] 七神VMP真正的分析与还原
5141
[原创] 用 P-code 做 VMP 还原:一份实战向的 IR 入门
3064
[讨论]模型的理解
355
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部