一般来说还原的话,要先能够像fkvmp这种插件先能从指令流中把虚拟机执行的流程弄出来,
弄出来之后分静态和动态两种方法,静态分析的缺点在于它本身就是堆栈机,堆栈这种本身就是一种动态的思想,静态无法获得堆栈信息,
但是他那个vJcc,也就是分支跳转,是在堆栈中各种运算出来的。如果是动态的话,也就是用unicorn或者triton什么玩意的仿真执行的引擎,
有可能执行的时候堆栈中的数和文件在真机下运行的堆栈情况不同,导致vJcc跳转目的地不对(这个我目前没找到问题在哪)。
```
vm-entry 到vm-exit的流程,这是第一步要获得的。
[vip 14000ace9]SREGQ
[vip 14000ace7]LCONSTDWSXQ
[vip 14000ace2]ADDQ
[vip 14000ace1]SREGQ
[vip 14000acdf]SREGQ
[vip 14000acdd]SREGQ
[vip 14000acdb]SREGQ
[vip 14000acd9]SREGQ
[vip 14000acd7]SREGQ
[vip 14000acd5]SREGQ
[vip 14000acd3]SREGQ
[vip 14000acd1]SREGQ
[vip 14000accf]SREGQ
[vip 14000accd]SREGQ
[vip 14000accb]SREGQ
[vip 14000acc9]SREGQ
[vip 14000acc7]SREGQ
[vip 14000acc5]SREGQ
[vip 14000acc3]SREGQ
[vip 14000acc1]SREGQ
[vip 14000acbf]SREGQ
[vip 14000acbd]SREGQ
[vip 14000acbb]SREGQ
[vip 14000acb9]SREGQ
[vip 14000acb7]LCONSTQ
[vip 14000acae]LREGQ
[vip 14000acac]ADDQ
[vip 14000acab]SREGQ
[vip 14000aca9]LCONSTQ
[vip 14000aca0]LREGQ
[vip 14000ac9e]ADDQ
[vip 14000ac9d]SREGQ
[vip 14000ac9b]LREGQ
[vip 14000ac99]LREGQ
[vip 14000ac97]LREGQ
[vip 14000ac95]LREGQ
[vip 14000ac93]LCONSTQ
[vip 14000ac8a]LREGQ
[vip 14000ac88]ADDQ
[vip 14000ac87]SREGQ
[vip 14000ac85]SREGQ
[vip 14000ac83]LREGDW
[vip 14000ac81]LREGDW
[vip 14000ac7f]LREGQ
[vip 14000ac7d]LCONSTQ
[vip 14000ac74]ADDQ
[vip 14000ac73]SREGQ
[vip 14000ac71]WRITEDW
[vip 14000ac70]SREGDW
[vip 14000ac6e]LCONSTBSXDW
[vip 14000ac6c]SREGDW
[vip 14000ac6a]LREGQ
[vip 14000ac68]LREGQ
[vip 14000ac66]LREGQ
[vip 14000ac64]LREGQ
[vip 14000ac62]LREGQ
[vip 14000ac60]LREGQ
[vip 14000ac5e]LREGQ
[vip 14000ac5c]LREGQ
[vip 14000ac5a]LREGQ
[vip 14000ac58]LREGQ
[vip 14000ac56]LREGQ
[vip 14000ac54]LREGQ
[vip 14000ac52]LREGQ
[vip 14000ac50]LREGQ
[vip 14000ac4e]LREGQ
vm-exit
```
其次为每个handler写lifter,lifter的作用就是生成同样作用的IR,我是用的llvm的ir,git上面有个东西叫vtil,他好像也能生成ir,还有能专门针对堆栈机的优化,最后代码还原的效果直接取决于优化的,但是他的项目没文档,c++新特性还用的多,代码看都看不懂。
比如下面这个lifter,
```
lifters addq
{
vm::handler::ADDQ,
[](_cvmp2& vmp2, std::variant<uint64_t, uint32_t, uint16_t, uint8_t> param1)
{
//mov rax1, [rbp+0]
auto ptr1 = vmp2.builder.CreateIntToPtr(vmp2.stack,PointerType::getInt64PtrTy(vmp2.context));
auto dqValue1 = vmp2.builder.CreateLoad(Type::getInt64Ty(vmp2.context),ptr1);
//mov rax2,[rbp+8]
auto ptr2 = vmp2.builder.CreateIntToPtr(vmp2.builder.CreateAdd(vmp2.stack, vmp2.builder.getInt64(8)), PointerType::getInt64PtrTy(vmp2.context));
auto dqValue2 = vmp2.builder.CreateLoad(Type::getInt64Ty(vmp2.context),ptr2);
//add rax2,rax1
auto sum = vmp2.builder.CreateAdd(dqValue2, dqValue1);
vmp2.builder.CreateStore(sum, ptr2);
vmp2.builder.CreateStore(vmp2.builder.getInt64(0), ptr1);
}
};
```
他的作用就是模仿vmp的堆栈机的流程。
```
这个lifter 写的ir编译成x86是下面这样,熟悉vmp的一眼就能看出来就是add那个handler
mov rcx,rsp
mov rax, [rcx]
add rax, [rcx+8]
mov [rcx+8], rax
mov qword ptr [rcx], 0(flags) 因为我是动态搞的,分支由模拟器决定,不需要flags,写0就行
mov rsp,rcx
```
上图中rsp+xx是对应某个虚拟reg,不加优化编译出来还是堆栈机的流程,人还是看不懂,IDA也看不懂,下图是F5的,明显不行。
llvm具体优化有几个,都是干什么的,我不清楚,我把官网那个例子的优化加进去。
```
fpm->add(llvm::createDeadStoreEliminationPass());
fpm->add(llvm::createGVNPass());
fpm->add(llvm::createPromoteMemoryToRegisterPass());
fpm->add(llvm::createInstructionCombiningPass());
fpm->add(llvm::createReassociatePass());
fpm->add(llvm::createCFGSimplificationPass());
fpm->add(llvm::createCorrelatedValuePropagationPass());
fpm->run(*main);
```
有些优化会乱搞cfg,因为我要内联汇编,他会把汇编位置乱移,会有这个问题。
应该是某个优化会把vreg转化成真实x86的reg,具体怎么对应的我不知道。llvm优化的还不太好,人还是看不懂,F5还有IDA的优化,
上图是还原出来的,真实的未被虚拟的代码是下图。IDA能发现v3也就是eax是未知的,因为eax是由上面那个IsDebuggerPresent返回的,调用导入函数要先vm-exit,再vm-entry另一个指令流,我是直接从下一个指令流还原的,所以eax是未知的,所以这里由涉及到多个basic block的合并
而且顺序的代码很好还原,那种多重if的很容易出错,flags很难处理,basicblock也很难合并,这里开始我就没研究了,也木有这个水平了
附件是还原的,F5可以看看。
参考
https://back.engineering/17/05/2021/ by _xeroxz 最基础的识别handler的轮子我就是抄他的,有需要的话要扩展,他的也不全
VMProtect 学习 by OoWoodOne
Samuel Chevet - SecurityDay2015 - Inside VMProtect
VMProtect的逆向分析和静态还原 pdf 09软件峰会
VMProtect逆向分析 by oolff2
asia-18-Blazytko-Breaking-State-Of-The-Art-Binary-Code-Obfuscation-Via-Program-Synthesis-wp
https://slidesplayer.com/slide/14781905/
https://github.com/aobfucated/vmp2-devirtualization 帖子中所有涉及代码基本都在这里
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课