原创
VMProtect 3.x- 如何对vmp静态分析(3)
24天前阅读 4.2K0
图 4。
如前所述,VMProtect 2 使用 XOR 操作解密并随后加密推送到堆栈上的相关虚拟地址。特定加密相对虚拟地址的选择是通过移动给定标志使其值为 0 或 8 来完成的。然后,添加VSP
到结果移位计算加密的相对虚拟地址所在的地址。
#define FIRST_CONSTANT a60934c9 #define SECOND_CONSTANT 59f6cb36 unsigned int jcc_decrypt(unsigned int encrypted_rva) { unsigned int result = ~encrypted_rva & ~encrypted_rva; result = ~result & ~FIRST_CONSTANT; result = ~(~encrypted_rva & ~SECOND_CONSTANT) & ~result; return result; }
图 5. 注意:注意FIRST_CONSTANT
和SECOND_CONSTANT
互为倒数。
VMAssembler – 概述
VMAssembler是一个虚拟指令汇编器项目,最初被认为是一个笑话。不管它对任何事物的重要性,它都是一个有趣的项目,它可以让个人更加熟悉 VMProtect 2 的功能。VMAssembler使用LEX(https://en.wikipedia.org/wiki/Lex_(software%29)和[YACC](https://en.wikipedia.org/wiki/Yacc)来解析文本文件以获取标签和虚拟指令标记。然后它根据通过命令行指定的特定虚拟机对这些虚拟指令进行编码和加密。最后生成一个 C++ 头文件,其中包含组装的虚拟指令以及原始的 VMProtect 二进制文件。
VMAssembler – 汇编器阶段
VMAssembler使用LEX(https://en.wikipedia.org/wiki/Lex_(software%29)和[YACC](https://en.wikipedia.org/wiki/Yacc)来解析文本文件以获取虚拟指令名称和立即数。VMAssembler 有四个主要阶段,词法分析和解析,虚拟指令编码,虚拟指令加密,最后是 C++ 代码生成。
VMAssembler – 第一阶段,词法分析和解析
词法分析和标记解析本身是两个阶段,但是我将这些阶段称为一个阶段,因为这些阶段是由 C++ 管理的数据结构。
VMAssembler 的第一阶段几乎完全由LEX(https://en.wikipedia.org/wiki/Lex_(software%29)和[YACC](https://en.wikipedia.org/wiki/Yacc)处理。文本被转换为表示虚拟指令的 C++ 结构。这些结构被称为_vinstr_meta和_vlable_meta。然后第二阶段使用这些结构来验证虚拟指令的存在,以及将这些更高级别的虚拟指令表示编码为解密的虚拟操作数。
VMAssembler – 第二阶段,虚拟指令编码
组装的虚拟指令编码阶段还验证每个虚拟标签的所有虚拟指令的存在。这是通过将分析的 vm 处理程序名称与虚拟指令名称标记进行比较来完成的。如果不存在虚拟指令,则组装将停止。
if ( !parse_t::get_instance()->for_each( [ & ]( _vlabel_meta *label_data ) -> bool { std::printf( "> checking label %s for invalid instructions... number of instructions = %dn", label_data->label_name.c_str(), label_data->vinstrs.size() ); const auto result = std::find_if( label_data->vinstrs.begin(), label_data->vinstrs.end(), [ & ]( const _vinstr_meta &vinstr ) -> bool { std::printf( "> vinstr name = %s, has imm = %d, imm = 0x%pn", vinstr.name.c_str(), vinstr.has_imm, vinstr.imm ); for ( auto &vm_handler : vmctx->vm_handlers ) if ( vm_handler.profile && vm_handler.profile->name == vinstr.name ) return false; std::printf( "[!] this vm protected file does not have the vm handler for: %s...n", vinstr.name.c_str() ); return true; } ); return result == label_data->vinstrs.end(); } ) ) { std::printf( "[!] binary does not have the required vm handlers...n" ); exit( -1 ); }
一旦验证了所有虚拟指令 IL,就可以开始对这些虚拟指令进行编码。在整个编码和加密过程中,虚拟指令指针前进的顺序很重要。方向决定了操作数和虚拟指令的顺序。
VMAssembler – 第三阶段,虚拟指令加密
就像汇编的第二阶段一样,第三阶段也必须考虑虚拟指令指针前进的方式。这是因为操作数必须按照基于 VIP 前进方向的顺序进行加密。上一个操作数加密产生的加密密钥用于下一个操作数的起始加密密钥,详见“VMEmu-Unicorn Engine,操作码的静态解密”。
这个阶段会做 F^{-1}(e, o)F− 1(ē ,o ) 和 G^{-1}(e, o)G− 1(ē ,o )对于每个标签的每个虚拟指令操作数。最后,计算从 vm_entry 到第一条虚拟指令的第一个操作数的相对虚拟地址,然后使用用于将相对虚拟地址解密为虚拟指令本身的逆变换进行加密。您可以在上一篇文章的vm_entry 部分中找到有关这些转换的更多详细信息。
VMAssembler – 第四阶段,C++ 头文件生成
第四阶段是虚拟指令汇编的最后阶段。在此阶段生成 C++ 代码。该代码是完全自包含的并且与环境无关。但是,当前的实现有一些限制。最明显的是需要一个 RWX(读、写和可执行)部分。如果要在 Windows 内核驱动程序中使用此生成的 C++ 代码,则该驱动程序将不支持 HVCI 系统。此外,截至 2021 年 6 月 19 日,MSVC 无法编译生成的头文件,因为无论出于何种原因,原始模块的静态初始化程序导致编译器挂起。如果要使用从 VMAssembler 生成的头文件进行编译,则必须使用 clang-cl。
VMAssembler – 示例
使用VMAssembler生成 C++ 头文件后,您现在可以将其包含到您的项目中,并使用任何非 MSVC 的编译器进行编译,因为 MSVC 编译器出于某种原因无法处理包含受保护二进制文件的如此大的静态初始值设定项,clang-但是 cl 处理它。您定义的每个标签都将插入到vm::calls
枚举中。每个枚举条目的值是标签虚拟指令的加密相对虚拟地址。
namespace vm { enum class calls : u32 { get_hello = 0xbffd6fa5, get_world = 0xbffd6f49, }; // // ... // template < calls e_call, class T, class... Ts > auto call( const Ts... args ) -> T { static auto __init_result = gen_data.init(); __vmcall_t vmcall = nullptr; for ( auto idx = 0u; idx < sizeof( call_map ) / sizeof( _pair_t< u8, calls > ); ++idx ) if ( call_map[ idx ].second == e_call ) vmcall = reinterpret_cast< __vmcall_t >( &gen_data.__vmcall_shell_code[ idx ] ); return reinterpret_cast< T >( vmcall( args... ) ); } }
您现在可以通过简单地将vm::calls
枚举条目和标签返回类型指定为模板化参数来从 C++ 代码中调用任何标签。
#include <iostream> #include "test.hpp" int main() { const auto hello = vm::call< vm::calls::get_hello, vm::u64 >(); const auto world = vm::call< vm::calls::get_world, vm::u64 >(); std::printf( "> %s %sn", ( char * )&hello, (char*)&world ); }
输出
> hello world
VTIL – 入门
目前在 github 上的 VTIL 项目有一些不为人知的需求和依赖项,它们没有被子模块化。我创建了一个VTIL 分支,它是子模块的基石和顶点,并描述了必须应用于继承 VTIL 的项目的 Visual Studios 配置。VTIL使用concept
关键字等C++ 2020特性,因此必须使用最新的Visual Studios(2019),不支持vs2017。如果您在非 Windows/非视觉工作室环境中进行编译,则可以忽略最后一句话。
git clone --recursive https://githacks.org/_xeroxz/vtil.git
注意:也许这会成为 VTIL-Core 中的一个分支,如果是这样,如果/当发生这种情况时,您应该参考官方的 VTIL-Core 存储库。
编译 VTIL 的另一个要求是您必须NOMINMAX
在包含 Windows.h 之前定义宏,因为std::numeric_limits具有静态成员函数(max 和 min)。这些静态成员函数名称被视为最小/最大宏,因此会导致编译错误。
#define NOMAXMIN #include <Windows.h>
最后一个要求与导致堆栈溢出的动态初始值设定项有关。为了使包含 VTIL 的已编译可执行文件不会立即崩溃,您必须增加初始堆栈大小。我将我的设置为 4MB 只是为了预防,因为我在 VMProfiler 中有大量动态初始化程序。
Linker->System->Stack Reserve Size/Stack Commit Size, set both to 4194304
VTIL – 基本块
vtil::optimizer::apply_all对vtil::basic_block对象进行操作,该对象可以通过调用vtil::basic_block::begin来构造。一个vtil :: basic_block包含VTIL指令结束与分支指令或列表vexit。要添加链接到现有基本块的新基本块,您可以调用vtil::basic_block::fork。
// Creates a new block connected to this block at the given vip, if already explored returns nullptr, // should still be called if the caller knowns it is explored since this function creates the linkage. // basic_block* basic_block::fork( vip_t entry_vip ) { // Block cannot be forked before a branching instruction is hit. // fassert( is_complete() ); // Caller must provide a valid virtual instruction pointer. // fassert( entry_vip != invalid_vip ); // Invoke create block. // auto [blk, inserted] = owner->create_block( entry_vip, this ); return inserted ? blk : nullptr; }
注意:vtil::basic_block::fork
将断言is_complete以确保您的基本块在分叉之前以分支指令结束。
一旦创建了基本块,就可以开始将https://docs.vtil.org/ 中记录的 VTIL 指令附加到基本块对象。对于每个定义的 VTIL 指令,使用“WRAP_LAZY”宏创建一个模板函数。您现在可以在您的虚拟机处理程序提升器中轻松“emplace_back”任何 VTIL 指令。
// Generate lazy wrappers for every instruction. // #define WRAP_LAZY(x) template<typename... Tx> basic_block* x( Tx&&... operands ) { emplace_back( &ins:: x, std::forward<Tx>( operands )... ); return this; } WRAP_LAZY( mov ); WRAP_LAZY( movsx ); WRAP_LAZY( str ); WRAP_LAZY( ldd ); WRAP_LAZY( ifs ); WRAP_LAZY( neg ); WRAP_LAZY( add ); WRAP_LAZY( sub ); WRAP_LAZY( div ); WRAP_LAZY( idiv ); WRAP_LAZY( mul ); WRAP_LAZY( imul ); WRAP_LAZY( mulhi ); WRAP_LAZY( imulhi ); WRAP_LAZY( rem ); WRAP_LAZY( irem ); WRAP_LAZY( popcnt ); WRAP_LAZY( bsf ); WRAP_LAZY( bsr ); WRAP_LAZY( bnot ); WRAP_LAZY( bshr ); WRAP_LAZY( bshl ); WRAP_LAZY( bxor ); WRAP_LAZY( bor ); WRAP_LAZY( band ); WRAP_LAZY( bror ); WRAP_LAZY( brol ); WRAP_LAZY( tg ); WRAP_LAZY( tge ); WRAP_LAZY( te ); WRAP_LAZY( tne ); WRAP_LAZY( tle ); WRAP_LAZY( tl ); WRAP_LAZY( tug ); WRAP_LAZY( tuge ); WRAP_LAZY( tule ); WRAP_LAZY( tul ); WRAP_LAZY( js ); WRAP_LAZY( jmp ); WRAP_LAZY( vexit ); WRAP_LAZY( vemit ); WRAP_LAZY( vxcall ); WRAP_LAZY( nop ); WRAP_LAZY( sfence ); WRAP_LAZY( lfence ); WRAP_LAZY( vpinr ); WRAP_LAZY( vpinw ); WRAP_LAZY( vpinrm ); WRAP_LAZY( vpinwm ); #undef WRAP_LAZY
VTIL – VMProfiler 提升
以虚拟机处理程序提升器LCONSTQ 为例。Lifter 只需添加一个 VTIL 推送指令,该指令将一个 64 位值推送到堆栈上。请注意vtil::operand创建 64 位立即数操作数的用法。
vm::lifters::lifter_t lconstq = { // push imm<N> vm::handler::LCONSTQ, []( vtil::basic_block *blk, vm::instrs::virt_instr_t *vinstr, vmp2::v3::code_block_t *code_blk ) { blk->push( vtil::operand( vinstr->operand.imm.u, 64 ) ); } };
VMProfiler 简单地遍历给定块的所有虚拟指令并应用提升器。一旦所有代码块都用完,就会调用vtil::optimizer::apply_all。这是当前 VTIL 的高潮,因为其中一些优化通道针对基于堆栈处理的混淆。在 vmprofiler 中对 VTIL 进行子建模的目的是为了这些优化,因为自己编程这些需要几个月的研究。编译器优化是它自己的一个领域,很有趣,但我目前没有时间去研究,所以 VTIL 就足够了。
结论 – 最后的话和未来的工作
尽管我在 VMProtect 2 上做了很多工作,但我努力的主要成功确实是静态地发现所有虚拟分支并生成清晰的 IL。另外在做这一切的,有据可查的,开源的,这可以进一步通过其他研究者继承C ++库。我不会考虑我所做的任何接近“成品”或可以这样呈现的工作,这只是朝着去虚拟化的正确方向迈出的一步。最后一句话的最后一句话引导我进入下一点。
在与我的 VMProtect 2 工作有关的所有文档和文章中,都避免了去虚拟化,因为对我而言,这一直超出了项目的范围。考虑到我是一名孤独的研究人员,虚拟机架构的许多方面无法由一个人在有意义的时间内解决。例如,当一条指令没有被 VMProtect 2 虚拟化时,就会发生 vmexit 并且原始指令在虚拟机之外执行。这意味着如果我想看到一个完整的例程,它需要我在虚拟机之外跟踪代码执行,因此 VMEmu 需要更多的开发时间来支持这样的事情。我对这些项目进行编程的方式允许多个工程师在给定时间处理代码库,
此外,去虚拟化需要转换回原生 x86_64。为了做到这一点,每个虚拟机处理程序都必须进行分析,每个虚拟机处理程序都必须为其定义一个 VTIL 提升器,并且每个 VTIL 指令都必须映射到本机指令。至少这似乎是我目前所拥有的知识水平所必需的,很可能有一种更优雅的方式来解决这个问题,而我此时只是忘记了。因此我对去虚拟化的结论是:这不是一个人的工作,因此我的项目的目标从来不是去虚拟化,它一直是虚拟指令的 IL 视图,VTIL 提供去混淆伪代码。仅 IL 就足以让一个专注的人开始研究,VTIL 伪代码使我们其他人更容易。VMProfiler Qt 与目前存在的 IDA Pro 相结合,可用于分析受 VMProtect 2 保护的二进制文件。它可能不是初学者友好的解决方案,但在我看来,它就足够了。
我必须指出,假设私人实体为 VMProtect 2 提供了全面的解决方案并不是一件容易的事。我可以想象一个比我更熟练的个人团队日复一日地致力于去虚拟化会产生什么. 最重要的是,考虑到 VMProtect 2 已公开的时间长度,这些私人实体有足够的时间来创建此类工具。
结论 – 未来的工作
最后,在我对 VMProtect 2 的研究过程中,有一种微妙的冲动想要以开源的方式自己重新实现一些混淆和虚拟机功能,以更好地传达 VMProtect 2 的功能。 然而,经过深思熟虑,它会更多创建一个混淆框架可以相对轻松地创建这些想法。一个处理代码分析以及文件格式解析、解构和重建的框架。比 LLVM 优化通道低的级别,但级别足够高,以至于使用此框架的程序员只需要自己编写混淆算法,甚至不必知道底层文件格式。该框架将仅支持单一 ISA,即 x86。
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 yunjia_community@tencent.com 删除。
我来说两句
0 条评论
请登录后查看评论内容