引言在阅读本文前建议研究以下二进制反混淆的社区研究- https://arxiv.org/pdf/1909.01752- https://github.com/Colton1skees/Dna/pull/8- https://github.com/JonathanSalwan/VMProtect-devirtualization- https://github.com/NaC-L/Mergen- https://www.youtube.com/watch?v3LtwqJM3Qjg- https://github.com/backengineering/vmp2- https://back.engineering/blog/17/05/2021/- https://www.youtube.com/watch?vvYAJCfafYTY- https://www.youtube.com/watch?vKYQOtGiH9pQ- https://github.com/r3bb1t/bin_lift- https://nac-l.github.io/2025/01/25/lifting_0.html- https://blog.thalium.re/posts/llvm-powered-devirtualization/- https://github.com/avast/retdec- https://github.com/ergrelet/themida-unmutate- https://github.com/lifting-bits/remill本文展示了对 CodeVirtualizer/Themida 保护代码的去虚拟化过程此技术几乎适用于所有基于虚拟机的混淆器稍作修改就能支持不同混淆器。以下是可简化的混淆器列表- https://vmpsoft.com/- https://www.oreans.com/themida.php- https://github.com/vxlang/vxlang-page- https://github.com/snowsnowsnows/EagleVM- https://github.com/dmaivel/covirt- https://github.com/noahware/binprotectThemida 架构分析Themida 的虚拟机架构与 VMProtect 主要区别在于支持嵌套虚拟化因其虚拟机上下文和虚拟栈存在于二进制文件本身而非本地栈。本文不深入探讨架构因其与去虚拟化方法关系不大。重要的特定于虚拟机的组件是虚拟分支和 VMEXIT 行为将在后续章节介绍。如需全面分析 Themida 架构可参考此研究。给明智者的警告不建议采用将虚拟机处理程序与 x86 指令进行模式匹配的方法。曾有人尝试但这种方法无法扩展。保护程序供应商对处理程序布局、操作码表或调度逻辑的小更改可能破坏工具。本文方法减少对特定于虚拟机知识的依赖能在各种 Themida 版本中发挥作用。研究虚拟机架构有价值可了解内部结构为引导符号执行引擎做决策。绝大多数去虚拟化工作通过通用优化完成处理控制流特别是虚拟分支和虚拟化调用时才需要特定于虚拟机的知识。引导式符号执行核心是将本地指令提升为可处理的中间表示IR通过优化解决未知分支目标时具体化控制流推动提升。Back Engineering Labs 维护的二进制提升和重新编译引擎 BLARE2 有自定义静态单赋值SSAIR支持 AMD64 和 ARM64具备完整过程系统、优化器、指令选择器、寄存器分配器和链接器。BLARE2 可将优化后的 IR 降级回本地代码并重新插入二进制文件生成与原始代码几乎 1:1 的输出。遵循本文技术用 Triton 或基于 LLVM 的提升器如 Remill 也能实现大部分功能它们能生成干净的优化 IR但后端让 LLVM 生成紧凑、行为良好的本地代码并重新插入较难。提升从所有寄存器和标志符号化开始指令反汇编并提升直到无法确定下一个指令指针。情况取决于控制流指令提升后的 ret 意味着最后存储到 RSP 的值是下一个 IP。地址无法具体化可能是优化不够深入或分支有多个实际目标如虚拟化的 JCC。具体化栈指针符号执行开始时除栈指针赋予具体初始值外所有寄存器和标志符号化。这是设计选择非严格要求。保持 RSP 具体现有加载/存储传播机制可处理栈访问调整栈指针的算术运算可常量折叠。另一种选择是保持 RSP 符号化编写专门栈传播逻辑但在去虚拟化背景下工作量大且无实际收益。这种方法代价是不支持动态栈分配的函数如 alloca 或编译器生成的变长数组因栈偏移量非静态可知。实践中易被虚拟化的函数中动态分配的栈帧不常见具体 RSP 的简单性值得这种限制。优化简化 Themida 或 VMP 的虚拟化无需详尽的编译器优化一组小的优化过程运行到收敛足以瓦解虚拟机框架。以下介绍各优化过程及对去虚拟化的贡献。这些优化过程相互关联。从内存加载的字节码提升为常量使围绕它的解密算术运算折叠产生具体处理程序索引解决处理程序表查找暴露下一个处理程序地址为常量。各过程为下一个过程提供输入虚拟机框架逐渐瓦解。常量提升与内存建模内存加载的数据常用于间接跳转计算VM 字节码是重要例子。提升器遇到从字节码地址加载数据时需将值提升为常量供其他优化过程处理。字节码加载提升后可对解码算术运算常量折叠。处理程序解密逻辑、操作码表索引、VPC 更新数学运算等逐渐折叠直到只剩具体处理程序地址用该地址继续提升。BLARE2 中的加载存储传播逻辑可配置程序员可指定二进制文件中可安全提升的内存范围避免 VM 私有常量提升触及用户数据。该过程跟踪存储操作若对地址 0x5000 存储随后从 0x5000 加载会转发存储的 SSA 值而非从原始映像读取。传播按字节级别建模可处理重叠存储操作。有两种失败模式需注意从加载前写入的地址提升会产生错误结果需存储跟踪过度提升会破坏原始程序语义可配置范围策略区分。常量折叠表达式操作数为已知常量时可用结果替换表达式。如 10 10 可写成 20适用于加法、减法等运算。去虚拟化中此过程需运行到收敛。一次折叠使表达式为常量可能让之前非常量的表达式变为常量实现多次折叠。各优化过程相互依赖使虚拟机框架瓦解。字节码解码算术运算折叠处理程序表索引具体化调度逻辑消失。死存储消除广泛应用死存储消除不安全未使用的存储操作可能有实际副作用。这里安全是因为针对的存储操作限于 VM 私有内存。Themida 用自己的部分存储虚拟机上下文、虚拟栈和相关框架原始程序不会观察到这些内存。提升到 VMEXIT 时从恢复函数角度只触及 Themida 部分的存储操作可证明无用可删除。跳过此过程有代价VM 处理程序通过上下文和虚拟栈交换状态不消除存储操作会使 IR 像 VM 解释器。结合死依赖分析过程可使 IR 更像函数。指令组合指令组合通过识别代数恒等式和合并有可知结果的操作简化表达式目标是将 IR 简化为保留原始语义的最小表达式。这些恒等式对去虚拟化重要VM 处理程序有很多噪声如相互抵消的算术运算、冗余掩码操作和恒等乘法。指令组合运行到收敛会消除这些噪声简化表达式用于常量折叠和分支折叠。提升后复杂的表达式应用规则后通常简化为常量。分支折叠前面优化完成后标志计算应解析为常量或未定义。依赖常量标志的分支有静态可知的目标可消除不透明的分支目标。VMEXIT 行为提升开始时栈指针具体化初始 RSP 值已知。提升器遇到 RSP 为 initRSP - 0x10 的返回指令时是 VMEXIT-CALL。Themida 和 VMP 都用此模式调用目标放在 RSP 处返回地址放在 RSP 0x8 处。此时在 IR 中发出调用操作原始代码执行间接调用时调用目标可能符号化需明确处理。这种模式不符合 CET 规范因栈上的返回地址不是由 vmenter 存根放置的。并非每个 VMEXIT 都是调用与初始 RSP 的偏移量可区分它们。返回到原始函数尾声、不支持的指令退出和类似调用的退出会产生不同的栈指针值控制流匹配逻辑用这些差异正确分类每个退出。虚拟化控制流提升过程需记录每个虚拟指令指针。原因是发现回边时需将它们识别为循环而非无限展开。不跟踪 VIP 会使虚拟化循环展开IR 膨胀。对于 VMProtect跟踪 VIP 简单字节码编码下一个处理程序地址间接跳转计算中最后一次模块加载的值是当前的 VIP。在 BLARE2 中从持有跳转或返回目标的 SSA 值反向遍历 IR 有向无环图DAG找到最后一次加载得到 VIP。Themida 处理虚拟化条件分支的方法与 VMP 不同这是少数需要特定于虚拟机知识的地方。前面内容通用适用于 VMP 和其他基于 VM 的保护程序但 VJCC 处理程序是 Themida 特有的结构。在 Themida 的 VJCC 处理程序中条件先评估结果写入 VM 上下文中的 branch_taken_flag。处理程序结束设置该标志后VIP 才前进。这意味着符号执行在 VIP 解析前会遇到分支分叉需探索两条路径通过处理程序底部的条件 VPC 更新逻辑跟踪每条路径的正确 VIP不能用 VMP 的简单启发式方法。必须跟踪 branch_taken_flag 直到 VIP 分叉处。死依赖分析过程降级前需知道从虚拟化区域出来后哪些寄存器和标志活跃。方法是收集 VM 退出后立即被本地代码覆盖的寄存器和标志集合。被覆盖的内容从 VM 角度无用无需计算。没有此过程符号表达式会在 IR 中悬空。以 ZF 为例若符号化且依赖 VM 内部计算即使下一条本地指令覆盖它IR 也会保留 ZF 的完整表达式树。栈指针重写过程提升开始时栈指针具体化简化后的 IR 有针对常量栈地址的加载和存储操作。降级前此过程将这些访问重写为相对于 RSP 的形式。需验证没有栈引用为负数否则意味着访问到红色区域。中间表示IR降级有干净的 IR 后下一步将其降级回原始指令集架构ISA。降级包括指令选择、寄存器分配、汇编以及将恢复的代码重新链接到二进制文件中由 BLARE2 处理。降级过程关键约束是寄存器压力。若寄存器分配器溢出去虚拟化后的代码会有自己的栈帧与原始函数的栈帧并存导致 IDA、Binary Ninja 等工具误读去虚拟化区域内调用的栈传递参数。目标是生成接近原始代码的可执行输出能在反汇编器中干净加载。这也是对基于 LLVM 的去虚拟化框架在该问题上持怀疑态度的原因。让 LLVM 生成紧凑、行为良好的本地代码并重新插入二进制文件是个项目。虽能从提升管道得到可读的 LLVM IR但要得到干净、可重新插入的本地代码需与 LLVM 框架斗争。结果以下是两张图片第一张显示虚拟化前 IDA 中的原始函数第二张显示去虚拟化后的同一函数。虚拟化之前 IDA 中的原始代码去虚拟化之后的代码去虚拟化后的输出与原始代码功能 1:1 对应。后端选择了不同的指令和寄存器这是寄存器分配器和指令选择器的不同选择结果但关键是没有寄存器溢出。恢复的代码紧凑干净无多余栈帧或伪影。重要的是去虚拟化后的代码不仅结构相似而且可执行。恢复的函数可作为本地代码运行能在反汇编器中干净加载行为与原始实现相同。对于有兴趣进一步分析或验证的人将提供 GitHub 仓库包含原始二进制文件、虚拟化后的二进制文件和去虚拟化后的二进制文件。仓库链接https://github.com/backengineering/themida-devirt阻止符号执行为让混淆器击败符号执行需防止间接跳转目标具体化。一种方法是将分支目标编码为包含不透明值的多变量布尔代数MBA表达式防止常量折叠减少表达式。但现在用此演示中描述的技术可简化用于隐藏分支目标的 MBA 表达式。存在更强大的技术使符号执行不可行CodeDefender 在其更高级别的保护层级中实现了这些技术具体细节超出本文范围。标签- 混淆- Themida相关文章Theodosius - Jit 链接器、符号映射器和混淆器- IDontCode- Windows现有的软件保护框架通常在较小的编译级别范围内运行。最高级别的混淆通常直接作用于源代码源到源次高级别是 LLVM IR通过优化过程最常见的第三级别是作用于本地二进制映像二进制到二进制。Ring-1.io 的反混淆与分析- IDontCode, noahware, Eggsy, AVX- Windows作为这项研究的一部分部分反混淆了 ring-1.io 使用的多个 Themida 保护的二进制文件包括其 UEFI 引导加载器植入程序。恢复了几个关键函数以便对植入程序的行为进行静态分析。这项工作揭示了有意设计来抵抗检查的机制包括虚拟化辅助钩子、执行重定向和内核操作技术。VMProtect 2 - 第二部分完整静态分析- IDontCode- Windows本文的目的是详细阐述上一篇题为《VMProtect 2 - 虚拟机架构的详细分析》的文章中披露的工作并纠正一些错误。此外本文将主要关注利用上一篇文章中披露的知识创建静态分析工具……
Themida 静态去虚拟化全揭秘:通用优化瓦解虚拟机框架,代码恢复 1:1 可执行
发布时间:2026/6/7 8:52:04
引言在阅读本文前建议研究以下二进制反混淆的社区研究- https://arxiv.org/pdf/1909.01752- https://github.com/Colton1skees/Dna/pull/8- https://github.com/JonathanSalwan/VMProtect-devirtualization- https://github.com/NaC-L/Mergen- https://www.youtube.com/watch?v3LtwqJM3Qjg- https://github.com/backengineering/vmp2- https://back.engineering/blog/17/05/2021/- https://www.youtube.com/watch?vvYAJCfafYTY- https://www.youtube.com/watch?vKYQOtGiH9pQ- https://github.com/r3bb1t/bin_lift- https://nac-l.github.io/2025/01/25/lifting_0.html- https://blog.thalium.re/posts/llvm-powered-devirtualization/- https://github.com/avast/retdec- https://github.com/ergrelet/themida-unmutate- https://github.com/lifting-bits/remill本文展示了对 CodeVirtualizer/Themida 保护代码的去虚拟化过程此技术几乎适用于所有基于虚拟机的混淆器稍作修改就能支持不同混淆器。以下是可简化的混淆器列表- https://vmpsoft.com/- https://www.oreans.com/themida.php- https://github.com/vxlang/vxlang-page- https://github.com/snowsnowsnows/EagleVM- https://github.com/dmaivel/covirt- https://github.com/noahware/binprotectThemida 架构分析Themida 的虚拟机架构与 VMProtect 主要区别在于支持嵌套虚拟化因其虚拟机上下文和虚拟栈存在于二进制文件本身而非本地栈。本文不深入探讨架构因其与去虚拟化方法关系不大。重要的特定于虚拟机的组件是虚拟分支和 VMEXIT 行为将在后续章节介绍。如需全面分析 Themida 架构可参考此研究。给明智者的警告不建议采用将虚拟机处理程序与 x86 指令进行模式匹配的方法。曾有人尝试但这种方法无法扩展。保护程序供应商对处理程序布局、操作码表或调度逻辑的小更改可能破坏工具。本文方法减少对特定于虚拟机知识的依赖能在各种 Themida 版本中发挥作用。研究虚拟机架构有价值可了解内部结构为引导符号执行引擎做决策。绝大多数去虚拟化工作通过通用优化完成处理控制流特别是虚拟分支和虚拟化调用时才需要特定于虚拟机的知识。引导式符号执行核心是将本地指令提升为可处理的中间表示IR通过优化解决未知分支目标时具体化控制流推动提升。Back Engineering Labs 维护的二进制提升和重新编译引擎 BLARE2 有自定义静态单赋值SSAIR支持 AMD64 和 ARM64具备完整过程系统、优化器、指令选择器、寄存器分配器和链接器。BLARE2 可将优化后的 IR 降级回本地代码并重新插入二进制文件生成与原始代码几乎 1:1 的输出。遵循本文技术用 Triton 或基于 LLVM 的提升器如 Remill 也能实现大部分功能它们能生成干净的优化 IR但后端让 LLVM 生成紧凑、行为良好的本地代码并重新插入较难。提升从所有寄存器和标志符号化开始指令反汇编并提升直到无法确定下一个指令指针。情况取决于控制流指令提升后的 ret 意味着最后存储到 RSP 的值是下一个 IP。地址无法具体化可能是优化不够深入或分支有多个实际目标如虚拟化的 JCC。具体化栈指针符号执行开始时除栈指针赋予具体初始值外所有寄存器和标志符号化。这是设计选择非严格要求。保持 RSP 具体现有加载/存储传播机制可处理栈访问调整栈指针的算术运算可常量折叠。另一种选择是保持 RSP 符号化编写专门栈传播逻辑但在去虚拟化背景下工作量大且无实际收益。这种方法代价是不支持动态栈分配的函数如 alloca 或编译器生成的变长数组因栈偏移量非静态可知。实践中易被虚拟化的函数中动态分配的栈帧不常见具体 RSP 的简单性值得这种限制。优化简化 Themida 或 VMP 的虚拟化无需详尽的编译器优化一组小的优化过程运行到收敛足以瓦解虚拟机框架。以下介绍各优化过程及对去虚拟化的贡献。这些优化过程相互关联。从内存加载的字节码提升为常量使围绕它的解密算术运算折叠产生具体处理程序索引解决处理程序表查找暴露下一个处理程序地址为常量。各过程为下一个过程提供输入虚拟机框架逐渐瓦解。常量提升与内存建模内存加载的数据常用于间接跳转计算VM 字节码是重要例子。提升器遇到从字节码地址加载数据时需将值提升为常量供其他优化过程处理。字节码加载提升后可对解码算术运算常量折叠。处理程序解密逻辑、操作码表索引、VPC 更新数学运算等逐渐折叠直到只剩具体处理程序地址用该地址继续提升。BLARE2 中的加载存储传播逻辑可配置程序员可指定二进制文件中可安全提升的内存范围避免 VM 私有常量提升触及用户数据。该过程跟踪存储操作若对地址 0x5000 存储随后从 0x5000 加载会转发存储的 SSA 值而非从原始映像读取。传播按字节级别建模可处理重叠存储操作。有两种失败模式需注意从加载前写入的地址提升会产生错误结果需存储跟踪过度提升会破坏原始程序语义可配置范围策略区分。常量折叠表达式操作数为已知常量时可用结果替换表达式。如 10 10 可写成 20适用于加法、减法等运算。去虚拟化中此过程需运行到收敛。一次折叠使表达式为常量可能让之前非常量的表达式变为常量实现多次折叠。各优化过程相互依赖使虚拟机框架瓦解。字节码解码算术运算折叠处理程序表索引具体化调度逻辑消失。死存储消除广泛应用死存储消除不安全未使用的存储操作可能有实际副作用。这里安全是因为针对的存储操作限于 VM 私有内存。Themida 用自己的部分存储虚拟机上下文、虚拟栈和相关框架原始程序不会观察到这些内存。提升到 VMEXIT 时从恢复函数角度只触及 Themida 部分的存储操作可证明无用可删除。跳过此过程有代价VM 处理程序通过上下文和虚拟栈交换状态不消除存储操作会使 IR 像 VM 解释器。结合死依赖分析过程可使 IR 更像函数。指令组合指令组合通过识别代数恒等式和合并有可知结果的操作简化表达式目标是将 IR 简化为保留原始语义的最小表达式。这些恒等式对去虚拟化重要VM 处理程序有很多噪声如相互抵消的算术运算、冗余掩码操作和恒等乘法。指令组合运行到收敛会消除这些噪声简化表达式用于常量折叠和分支折叠。提升后复杂的表达式应用规则后通常简化为常量。分支折叠前面优化完成后标志计算应解析为常量或未定义。依赖常量标志的分支有静态可知的目标可消除不透明的分支目标。VMEXIT 行为提升开始时栈指针具体化初始 RSP 值已知。提升器遇到 RSP 为 initRSP - 0x10 的返回指令时是 VMEXIT-CALL。Themida 和 VMP 都用此模式调用目标放在 RSP 处返回地址放在 RSP 0x8 处。此时在 IR 中发出调用操作原始代码执行间接调用时调用目标可能符号化需明确处理。这种模式不符合 CET 规范因栈上的返回地址不是由 vmenter 存根放置的。并非每个 VMEXIT 都是调用与初始 RSP 的偏移量可区分它们。返回到原始函数尾声、不支持的指令退出和类似调用的退出会产生不同的栈指针值控制流匹配逻辑用这些差异正确分类每个退出。虚拟化控制流提升过程需记录每个虚拟指令指针。原因是发现回边时需将它们识别为循环而非无限展开。不跟踪 VIP 会使虚拟化循环展开IR 膨胀。对于 VMProtect跟踪 VIP 简单字节码编码下一个处理程序地址间接跳转计算中最后一次模块加载的值是当前的 VIP。在 BLARE2 中从持有跳转或返回目标的 SSA 值反向遍历 IR 有向无环图DAG找到最后一次加载得到 VIP。Themida 处理虚拟化条件分支的方法与 VMP 不同这是少数需要特定于虚拟机知识的地方。前面内容通用适用于 VMP 和其他基于 VM 的保护程序但 VJCC 处理程序是 Themida 特有的结构。在 Themida 的 VJCC 处理程序中条件先评估结果写入 VM 上下文中的 branch_taken_flag。处理程序结束设置该标志后VIP 才前进。这意味着符号执行在 VIP 解析前会遇到分支分叉需探索两条路径通过处理程序底部的条件 VPC 更新逻辑跟踪每条路径的正确 VIP不能用 VMP 的简单启发式方法。必须跟踪 branch_taken_flag 直到 VIP 分叉处。死依赖分析过程降级前需知道从虚拟化区域出来后哪些寄存器和标志活跃。方法是收集 VM 退出后立即被本地代码覆盖的寄存器和标志集合。被覆盖的内容从 VM 角度无用无需计算。没有此过程符号表达式会在 IR 中悬空。以 ZF 为例若符号化且依赖 VM 内部计算即使下一条本地指令覆盖它IR 也会保留 ZF 的完整表达式树。栈指针重写过程提升开始时栈指针具体化简化后的 IR 有针对常量栈地址的加载和存储操作。降级前此过程将这些访问重写为相对于 RSP 的形式。需验证没有栈引用为负数否则意味着访问到红色区域。中间表示IR降级有干净的 IR 后下一步将其降级回原始指令集架构ISA。降级包括指令选择、寄存器分配、汇编以及将恢复的代码重新链接到二进制文件中由 BLARE2 处理。降级过程关键约束是寄存器压力。若寄存器分配器溢出去虚拟化后的代码会有自己的栈帧与原始函数的栈帧并存导致 IDA、Binary Ninja 等工具误读去虚拟化区域内调用的栈传递参数。目标是生成接近原始代码的可执行输出能在反汇编器中干净加载。这也是对基于 LLVM 的去虚拟化框架在该问题上持怀疑态度的原因。让 LLVM 生成紧凑、行为良好的本地代码并重新插入二进制文件是个项目。虽能从提升管道得到可读的 LLVM IR但要得到干净、可重新插入的本地代码需与 LLVM 框架斗争。结果以下是两张图片第一张显示虚拟化前 IDA 中的原始函数第二张显示去虚拟化后的同一函数。虚拟化之前 IDA 中的原始代码去虚拟化之后的代码去虚拟化后的输出与原始代码功能 1:1 对应。后端选择了不同的指令和寄存器这是寄存器分配器和指令选择器的不同选择结果但关键是没有寄存器溢出。恢复的代码紧凑干净无多余栈帧或伪影。重要的是去虚拟化后的代码不仅结构相似而且可执行。恢复的函数可作为本地代码运行能在反汇编器中干净加载行为与原始实现相同。对于有兴趣进一步分析或验证的人将提供 GitHub 仓库包含原始二进制文件、虚拟化后的二进制文件和去虚拟化后的二进制文件。仓库链接https://github.com/backengineering/themida-devirt阻止符号执行为让混淆器击败符号执行需防止间接跳转目标具体化。一种方法是将分支目标编码为包含不透明值的多变量布尔代数MBA表达式防止常量折叠减少表达式。但现在用此演示中描述的技术可简化用于隐藏分支目标的 MBA 表达式。存在更强大的技术使符号执行不可行CodeDefender 在其更高级别的保护层级中实现了这些技术具体细节超出本文范围。标签- 混淆- Themida相关文章Theodosius - Jit 链接器、符号映射器和混淆器- IDontCode- Windows现有的软件保护框架通常在较小的编译级别范围内运行。最高级别的混淆通常直接作用于源代码源到源次高级别是 LLVM IR通过优化过程最常见的第三级别是作用于本地二进制映像二进制到二进制。Ring-1.io 的反混淆与分析- IDontCode, noahware, Eggsy, AVX- Windows作为这项研究的一部分部分反混淆了 ring-1.io 使用的多个 Themida 保护的二进制文件包括其 UEFI 引导加载器植入程序。恢复了几个关键函数以便对植入程序的行为进行静态分析。这项工作揭示了有意设计来抵抗检查的机制包括虚拟化辅助钩子、执行重定向和内核操作技术。VMProtect 2 - 第二部分完整静态分析- IDontCode- Windows本文的目的是详细阐述上一篇题为《VMProtect 2 - 虚拟机架构的详细分析》的文章中披露的工作并纠正一些错误。此外本文将主要关注利用上一篇文章中披露的知识创建静态分析工具……