逆向VMP3.X虚拟化进阶(一)

本文默认你已阅读上篇《逆向VMP3.X虚拟化入门》

本文绝大部分内容来自于Can Bölük的VTIL项目及他的文章 https://blog.can.ac/2020/04/11/writing-an-optimizing-il-compiler-for-dummies-by-a-dummy/

不要分析VMP

分析VMP的实现已经被无数前辈们实践尝试过了。

他们大抵是像本系列文章入门篇一样,通过软件自动化分析得到机器码对应的虚拟机启动与退出,每一种虚拟机虚拟指令(VPUSH等),虚拟指令的控制流。

这样的做法,在VMP1.0还能还原到汇编代码,在VMP2.0还能还原出虚拟机虚拟指令,可是到了VMP3.X,就连虚拟指令的识别也变得十分困难了。

我们遗漏了什么?

不仅仅是虚拟机中插入了大量的混淆代码,使得一个虚拟指令可以变为无数种机器码的组合

VMP同时加密了虚拟机的ip寄存器,使你无法轻易得到被虚拟程序的控制流

最困难的还在于,VMP的虚拟寄存器并不是固定不变的(寄存器轮转),VMP运行过程中,会轮换虚拟寄存器与原始真实寄存器的对应关系。

push   rax
pop    rax

编译到VMP后,说不定就成了

vpush  reg0
vpop   reg1

不要分析VMP

VMP的虚拟机结构太过于复杂,这个版本的分析结论也不一定能带到下一个版本。但是虚拟化的代码有时候又是我们逆向过程中的必经之路,如果不分析VMP实现,我们能做什么?

将虚拟机与虚拟程序一起优化

说到底,VMP的虚拟化技术是混淆技术的一种,返璞归真,我们不要考虑VMP的虚拟机与虚拟指令,就把它视为一体,视为一般的机器码,我们要去优化它。

中间语言(Intermediate Language)

观察下面的伪代码

push rax
add [rsp], 1
pop rbx

我们人类可以轻易的发现 其将rax+1赋值给了rbx,同时影响了eflags。

但是你如何通过代码描述这件事情?

我们至少需要为每一句汇编指令描述它的语义。

不过描述语义后,我们如何把代码组织起来,形成代码块与控制流呢?

汇编语言本身存在其局限性,我们需要一门新的语言,这门语言,需要语义简单,方便操作,方便组织。这类语言就叫做中间语言(IL),我们需要把汇编语句转移为IL,同时,IL也需要可以转移到汇编,这个转移过程叫做:lifting (著名编译器LLVM编译过程就是把原始语言如C++编译到其定义的中间语言IR,在中间语言进行优化等过程,最后再编译到目标语言如汇编)

静态单赋值(Static single assignment)

考虑下面的伪代码

a = 1
b = a
a = 2
b = a
return b

a = 1
b = a 
c = 2
d = c
return d

很显然,两端代码返回值相同,但是编写程序分析到这一点,明显第二段代码更简单,第一段代码还需要考虑a和b是否被覆盖,而由于第二段代码中,d只被赋值一次,所以其非常容易被追踪。

这就是静态单赋值(SSA),它能够方便我们的化简过程(等等)。

这样。我们lifting后的中间语言,不要再是栈,堆,寄存器的架构,而是一个个变量的赋值。

组织代码

由于SSA,我们的中间语言的每一个表达式,都可以用目标=A 运算符 B来表达,如v = 1 + a

A和B可以是表达式或立即数

这样我们就可以用代码来表达一个表达式

struct expression
{
  ?int??_t?                    value;

  operator_id                  op;
  shared_reference<expression> lhs; //一元运算时为空
  shared_reference<expression> rhs;
  ...
};

这个表达式以二叉树的形式组织起来。当一元运算时,lhs为空。(如a++或++a就是一元运算)

二叉树形式表现 (t0|t1)&~(t0&t1)

这个方法也可以用于描述控制流,如:

if(a)
  return 1;
return 2;

变为中间语言

c0 = (bool)a;
t0 = a ? 1 : 0;
c1 = !(bool)a;
t1 = c1 ? 2: 0;
return t0 + t1;

于是现在我们可以成功的把我们的中间语言组织起来。

现在我们将汇编语句lifting到我们的中间语言后,就可以开始真正的代码化简(反VMP)过程。

发表评论

电子邮件地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据