虚拟分支中最明显的一致性是PUSHVSP的使用。当两个加密值位于VSP + 0
、 和的堆栈上时,将执行此虚拟指令VSP + 8
。这些加密值使用给定块的最后一个LCONSTDW值解密。因此,可以基于这两个一致性创建一个非常小的算法。算法的第一部分将简单地使用std::find_if
反向迭代器来定位给定代码块中的最后一个LCONSTDW。该 DWORD 值将被解释为用于解密两个分支的加密相对虚拟地址的 XOR 密钥。std::find_if
现在执行第二个步骤来定位PUSHVSPvirtual 指令,当执行时,两个加密的相对虚拟地址将位于堆栈上。该算法将每条PUSHVSP指令的顶部两个堆栈值解释为加密的相对虚拟地址,并对最后一个LCONSTDW值应用 XOR 运算。
std::optional< jcc_data > get_jcc_data( vm::ctx_t &vmctx, code_block_t &code_block )
{
// there is no branch for this as this is a vmexit...
if ( code_block.vinstrs.back().mnemonic_t == vm::handler::VMEXIT )
return {};
// find the last LCONSTDW... the imm value is the JMP xor decrypt key...
// we loop backwards here (using rbegin and rend)...
auto result = std::find_if( code_block.vinstrs.rbegin(), code_block.vinstrs.rend(),
[]( const vm::instrs::virt_instr_t &vinstr ) -> bool {
auto profile = vm::handler::get_profile( vinstr.mnemonic_t );
return profile && profile->mnemonic == vm::handler::LCONSTDW;
} );
jcc_data jcc;
const auto xor_key = static_cast< std::uint32_t >( result->operand.imm.u );
const auto &last_trace = code_block.vinstrs.back().trace_data;
// since result is already a variable and is a reverse itr
// i'm going to be using rbegin and rend here again...
//
// look for PUSHVSP virtual instructions with two encrypted virtual
// instruction rva's ontop of the virtual stack...
result = std::find_if(
code_block.vinstrs.rbegin(), code_block.vinstrs.rend(),
[ & ]( const vm::instrs::virt_instr_t &vinstr ) -> bool {
if ( auto profile = vm::handler::get_profile( vinstr.mnemonic_t );
profile && profile->mnemonic == vm::handler::PUSHVSP )
{
const auto possible_block_1 = code_block_addr( vmctx,
vinstr.trace_data.vsp.qword[ 0 ] ^ xor_key ),
possible_block_2 = code_block_addr( vmctx,
vinstr.trace_data.vsp.qword[ 1 ] ^ xor_key );
// if this returns too many false positives we might have to get
// our hands dirty and look into trying to emulate each branch
// to see if the first instruction is an SREGQ...
return possible_block_1 > vmctx.module_base &&
possible_block_1 < vmctx.module_base + vmctx.image_size &&
possible_block_2 > vmctx.module_base &&
possible_block_2 < vmctx.module_base + vmctx.image_size;
}
return false;
} );
// if there are not two branches...
if ( result == code_block.vinstrs.rend() )
{
jcc.block_addr[ 0 ] = code_block_addr( vmctx, last_trace );
jcc.has_jcc = false;
jcc.type = jcc_type::absolute;
}
// else there are two branches...
else
{
jcc.block_addr[ 0 ] = code_block_addr( vmctx,
result->trace_data.vsp.qword[ 0 ] ^ xor_key );
jcc.block_addr[ 1 ] = code_block_addr( vmctx,
result->trace_data.vsp.qword[ 1 ] ^ xor_key );
jcc.has_jcc = true;
jcc.type = jcc_type::branching;
}
return jcc;
}
注意:虚拟分支所依赖的底层标志不是使用该算法提取的。这是该算法的不利方面之一。
VMProfiler Qt是一个基于 C++ Qt 的小型 GUI,允许检查虚拟指令跟踪。这些跟踪是通过VMEmu生成的,包含每个虚拟指令的所有信息。GUI 包含一个窗口,用于显示虚拟寄存器值、本机寄存器值、虚拟堆栈、虚拟指令、可扩展虚拟分支,最后是一个包含所有虚拟机处理程序及其本机指令和转换的选项卡。
VMProfiler CLI是一个命令行项目,用于演示所有 VMProfiler 功能。该项目仅包含一个文件 ( main.cpp ),但是对于那些有兴趣继承 VMProfiler 作为其代码库的人来说,这是一个很好的参考。
Usage: vmprofiler-cli [options...]
Options:
--bin, --vmpbin unpacked binary protected with VMProtect 2
--vmentry, --entry rva to push prior to a vm_entry
--showhandlers show all vm handlers...
--showhandler show a specific vm handler given its index...
--vmp2file path to .vmp2 file...
--showblockinstrs show the virtual instructions of a specific code block...
--showallblocks shows all information for all code blocks...
--devirt lift to VTIL IR and apply optimizations, then display the output...
-h, --help Shows this page
VMEmu是一个基于独角兽引擎的项目,它模拟虚拟机处理程序以随后解密虚拟指令操作数。VMEmu继承了VMProfiler,它有助于确定给定的代码块中是否有虚拟 JCC。VMEmu目前不支持转储模块,因为“转储模块”可以有多种形式。转储模块没有一种标准文件格式,因此对转储模块的支持将与另一个基于独角兽引擎的项目一起提供,以生成标准转储格式。
Usage: vmemu [options...]
Options:
--vmentry relative virtual address to a vm entry... (Required)
--vmpbin path to unpacked virtualized binary... (Required)
--out output file name for trace file... (Required)
-h, --help Shows this page
为了静态解密虚拟指令操作数,首先必须了解这些操作数是如何加密的。VMProtect 2 用于加密虚拟指令操作数的算法可以表示为数学公式。
\text{令} F_n \text{ 是一个加密函数并且} T_{m,F_n} \text{表示} m\text{函数的第一个变换} F_n:让 Fn 是一个加密函数和 T米,˚Fn表示 函数F 的第m个变换 n:F_0(e, o) = T_{4, F_0} \circ T_{3, F_0}\circ T_{2, F_0} \circ T_{1, F_0} \circ T_{0, F_0}(e, o)F_0(ē ,_o )=吨_4 、_F_0∘吨3 、_F_0∘吨2 , _F_0∘吨1 , _F_0∘吨0 , _F_0(ē ,_o )G_0(e, o) = T_{1, F_0}(F_0(e, o), e)G_0(ē ,_o )=吨_1 , _F_0( _F_0(ē ,Ø ),Ë )\text{因此:}因此:\text{key}_{n+1} = G_n(\text{key}_n, \text{operand}_n)钥匙_n + 1=Gn(键n,操作数n)\text{operand}_{n+1} = F_n(\text{key}_n, \text{operand}_n)操作数n + 1=Fn(键n,操作数n)\text{此外:}此外:T_{m, F_n} \text{ 映射到给定的 vm::transformation::type 使得 } T_{0, F_n} = \text {vm::transform::type::generic\textunderscore0},吨米,˚Fn 映射到给定的 vm::transformation::type 使得 T_0 , _Fn=vm::transform::type::generic_0 ,T_{1, F_n} = \text{ vm::transform::type::rolling\textunderscore key }, ..., T_{6, F_n} = \text{ vm::transform::type::update\textunderscore key }吨_1 , _Fn= vm::transform::type::rolling_key ,…,吨_6 、_Fn= vm::transform::type::update_key
考虑上图,操作数的解密只是函数的逆 FF. 这个逆生成到原生 x86_64 指令中,并嵌入到每个虚拟机处理程序以及 calc_jmp 中。人们可以通过在 C/C++ 中重新实现这些指令来简单地模拟这些指令,但是我对这些指令的实现只是为了加密,而不是为了解密。相反,在这种情况下首选使用unicorn-engine,因为通过简单地模拟这些虚拟机处理程序,将生成解密的操作数。
理解没有运行时值可能会影响操作数的解密,因此可以忽略无效的内存访问。然而,运行时间值可以改变哪些虚拟指令块被解密,因此需要在执行分支虚拟指令之前保存模拟 CPU 的上下文。这将允许在分支指令之前恢复仿真 CPU 的状态,但另外改变仿真 CPU 将采用的分支,允许静态地完全解密所有虚拟指令块。
重申一下,unicorn-engine 的使用是为了计算 F(e, o)F ( e ,o ) 和 G(e, o)G ( e ,o ) 在哪里 电子电子 采用本地寄存器的形式 RBXRBX, ○○ 采用本地寄存器的形式 雷克斯ř甲X, 和 T_{m, F_n}吨米,˚Fn 采取变换的形式 米米日。
此外,不仅可以使用 unicorn-engine 获得解密的操作数,而且可以为每条虚拟指令对虚拟堆栈的视图进行快照。这允许算法利用堆栈上的值。对本机 WinAPI 的调用是在虚拟机之外完成的,但极少数情况除外,例如 VMProtect 2 打包程序虚拟机处理程序LoadLibrary
使用指向RCX
.
查看所有代码路径非常重要。考虑最基本的情况,即检查参数以查看它是否为 nullptr。
auto demo(int* a)
{
if (!a)
return {};
// more code down here
}
在无法看到所有代码路径的情况下分析上述代码会导致一些无用的东西。因此,查看虚拟机内的所有分支是重中之重。在本节中,我将详细介绍虚拟分支如何在 VMProtect 2 虚拟机内部工作,以及我设计用于识别和分析所有路径的算法。
首先,并非所有代码块都以分支虚拟指令结束。有些以虚拟机退出或绝对跳转结束。因此需要一种算法来确定给定的虚拟指令块是否会分支。为了产生这样的算法,需要对虚拟机分支机制有深入的了解,特别是如何将原生 JCC转换为虚拟指令。
考虑可能受影响的本地 ADD指令的标志位。标志OF
、SF
、ZF
、AF
、CF
和PF
都可能受到影响,具体取决于计算。本机分支是通过JCC 指令完成的,这些指令取决于一个或多个特定标志的状态。
test rax, rax
jz branch_1
图 2。
考虑图 2,了解JZ
如果ZF
设置了标志,本机指令将跳转到“branch_1” 。可以以这样一种方式重新实现图 2,即只能使用本机 JMP指令和一些其他数学和堆栈操作。将分支指令的数量减少为单个本机 JMP指令。
考虑到本机 TEST指令AND
对两个操作数按位执行,相应地设置标志,并忽略AND
结果。可以简单地用一些堆栈操作和本机 AND指令替换本机 TEST指令。
0: 50 push rax
1: 48 21 c0 and rax,rax
4: 9c pushf
5: 48 83 24 24 40 and QWORD PTR [rsp],0x40
a: 48 c1 2c 24 03 shr QWORD PTR [rsp],0x3
f: 58 pop rax
10: ff 34 25 00 00 00 00 push branch_1
17: ff 34 25 00 00 00 00 push branch_2
1e: 48 8b 04 04 mov rax,QWORD PTR [rsp+rax*1]
22: 48 83 c4 10 add rsp,0x10
26: 48 89 44 24 f8 mov QWORD PTR [rsp-0x8],rax
2b: 58 pop rax
2c: ff 64 24 f0 jmp QWORD PTR [rsp-0x10]
图 3. 注意:这里没有使用 bittest/test,因为它是通过 AND 和 SHR 实现的。
尽管将单个指令转换为多个指令可能会适得其反,最终需要更多的工作,但事实并非如此,因为这些指令将在其他方向重用。使用上述汇编代码模板可以非常简单地重新实现所有 JCC 指令。即使在这样的转移指令的JRCXZ
,JECXZ
和JCXZ
指令可以通过简单地交换来实现RAX
与RCX
/ EAX
/CX
在上面的例子。
图 3 虽然在原生 x86_64 中,但提供了 VMProtect 2 如何在虚拟机内部进行分支的可靠示例。但是,VMProtect 2 通过数学混淆添加了额外的混淆。首先,压入堆栈的两个地址都是加密的相对虚拟地址。这些地址通过 XOR 解密。尽管 XOR、SUB 和其他数学运算本身被混淆到 NAND 运算中。
; push encrypted relative virtual addresses onto the stack...
LCONSTQ 0x19edc194
LCONSTQ 0x19ed8382
PUSHVSP
; calculate which branch will be executed, then read its encrypted address on the stack...
LCONSTBZXW 0x3
LCONSTBSXQ 0xbf
LREGQ 0x80
NANDQ
SREGQ 0x68
SHRQ
SREGQ 0x70
ADDQ
SREGQ 0x48
READQ
; clear the stack of encrypted addresses...
SREGQ 0x68
SREGQ 0x70
SREGQ 0x90
; put the selected branch encrypted address back onto the stack...
LREGQ 0x68
LREGQ 0x68
; xor value on top of the stack with 59f6cb36
LCONSTDW 0xa60934c9
NANDDW
SREGQ 0x48
LCONSTDW 0x59f6cb36
LREGDW 0x68
NANDDW
SREGQ 0x48
NANDDW
SREGQ 0x90
SREGQ 0x70
; removed virtual instructions...
; …
; load the decrypted relative virtual address and jmp...
LREGQ 0x70
JMP
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有