找回密码
 开放注册

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

搜索
查看: 1025|回复: 0

反病毒引擎设计之虚拟机查毒篇『C』

[复制链接]

739

主题

468

回帖

4307

牛毛

论坛管理员

狼群

积分
4347
发表于 2008-1-20 20:03:50 | 显示全部楼层 |阅读模式
2.3虚拟机实现技术详解

有了前面关于加密变形病毒的介绍,现在我们知道动态特征码扫描技术的关键就在于必须得到病毒体解密后的明文,而得到明文产生的时机就是病毒自身解密代码解密的完毕。目前有两种方法可以跟踪控制病毒的每一步执行,并能够在病毒循环解密结束后从内存中读出病毒体明文。一种是单步和断点跟踪法,和目前一些程序调试器相类似;另一种方法当然就是虚拟执行法。下面分别分析单步和断点跟踪法和虚拟执行法的技术细节。

单步跟踪和断点是实现传统调试器的最根本技术。单步的工作原理很简单:当CPU在执行一条指令之前会先检查标志寄存器,如果发现其中的陷阱标志被设置则会在指令执行结束后引发一个单步陷阱INT1H。至于断点的设置有软硬之分,软件断点是指调试器用一个通常是单字节的断点指令(CC,即INT3H)替换掉欲触发指令的首字节,当程序执行至断点指令处,默认的调试异常处理代码将被调用,此时保存在栈中的段/偏移地址就是断点指令后一字节的地址;而硬件断点的设置则利用了处理器本身的调试支持,在调试寄存器(DR0--DR4)中设置触发指令的线形地址并设置调试控制寄存器(DR7)中相关的控制位,CPU会在预设指令执行时自动引发调试异常。而Windows本身又提供了一套调试API,使得调试跟踪一个程序变得非常简单:调试器本身不用接挂默认的调试异常处理代码,而只须调用WaitForDebugEvent等待系统发来的调试事件;调试器可利用GetThreadContext挂起被调试线程获取其上下文,并设置上下文中的标志寄存器中的陷阱标志位,最后通过SetThreadContext使设置生效来进行单步调试;调试器还可通过调用两个功能强大的调试API--ReadProcessMemory和WriteProcessMemory来向被调试线程的地址空间中注入断点指令。根据我逆向后的分析结果,VC++的调试器就是直接利用这套调试API写成的。使用以上的调试技术既然可以写出像VC++那样功能齐全的调试器,那么没有理由不能将之运用于病毒代码的自动解密上。最简单的最法:创建待查可执行文件为调试器的调试子进程,然后用上述方法对其进行单步跟踪,每当收到具有EXCEPTION_SINGLE_STEP异常代码的事件时就可以分析该条以单步模式执行的指令,最后当判断病毒的整个解密过程结束后即可调用ReadProcessMemory读出病毒体明文。

用单步和断点跟踪法的唯一一点好处就在于它不用处理每条指令的执行--这意味着它无需编写大量的特定指令处理函数,因为所有的解密代码都交由CPU去执行,调试器不过是在代码被单步中断的间隙得到控制权而已。但这种方法的缺点也是相当明显的:其一容易被病毒觉察到,病毒只须进行简单的堆栈检查,或直接调用IsDebugerPresent就可确定自己正处于被调试状态;其二由于没有相应的机器码分析模块,指令的译码,执行完全依赖于CPU,所以将导致无法准确地获取指令执行细节并对其进行有效的控制。;其三单步和断点跟踪法要求待查可执行文件真实执行,即其将做为系统中一个真实的进程在自己的地址空间中运行,这当然是病毒扫描所不能允许的。很显然,单步和断点跟踪法可以应用在调试器,自动脱壳等方面,但对于查毒却是不合适的。

而使用虚拟执行法的唯一一点缺点就在于它必须在内部处理所有指令的执行--这意味着它需要编写大量的特定指令处理函数来模拟每种指令的执行效果,这里根本不存在何时得到控制权的问题,因为控制权将永远掌握在虚拟机手中。用软件方法模拟CPU并非易事,需要对其机制有足够的了解,否则模拟效果将与真实执行相去甚远。举两个例子:一个是病毒常用的乘法后ASCII调整指令AAM,这条指令因为存在未公开的行为从而常常被病毒用来考验虚拟机设计的优劣。通常情况下AAM是双字节指令,操作码为D4 0A(其实0A隐含代表了操作数10);但也可作为单字节指令明确地指定第二字节除数为任意8位立即数,此时操作码仅为D4。虚拟机必需考虑到后一种指定除数的情况来保证模拟结果的正确性;还有一个例子是关于处理器响应中断的方式,即CPU在刚打开中断后将不会马上响应中断,而必须隔一个指令周期。如果虚拟机没有考虑到该机制则很可能虚拟执行流程会与真实情况不符。但虚拟执行的优点也是很明显的,同时它正好填补了单步和断点跟踪法所力不能及的方面:首先是不可能被病毒觉察到,因为虚拟机将在其内部缓冲区中为被虚拟执行代码设立专用的堆栈,所以堆栈检查结果与实际执行无二(不会向堆栈中压入单步和断点中断时的返回地址);其次由于虚拟机自身完成指令的解码和地址的计算,所以能够获取每条指令的执行细节并加以控制;最后,最为关键的一条在于虚拟执行确实做到了“虚拟”执行,系统中不会产生代表被执行者的进程,因为被执行者的寄存器组和堆栈等执行要素均在虚拟机内部实现,因而可以认为它在虚拟机地址空间中执行。鉴于虚拟执行法诸多的优点,所以将其运用于通用病毒体解密上是再好不过的了。

通常,虚拟机的设计方案可以采取以下三种之一:自含代码虚拟机(SCCE),缓冲代码虚拟机(BCE),有限代码虚拟机(LCE)。

自含代码虚拟机工作起来象一个真正的CPU。一条指令取自内存,由SCCE解码,并被传送到相应的模拟这条指令的例程,下一条指令则继续这个循环。虚拟机会包含一个例程来对内存/寄存器寻址操作数进行解码,然后还会包括一个用于模拟每个可能在CPU上执行的指令的例程集。正如你所想到的,SCCE的代码会变的无比的巨大而且速度也会很慢。然而SCCE对于一个先进的反病毒软件是很有用的。所有指令都在内部被处理,虚拟机可以对每条指令的动作做出非常详细的报告,这些报告和启发式数据以及通用清除模块将相互参照形成一个有效的反毒系统。同时,反病毒程序能够最精确地控制内存和端口的访问,因为它自己处理地址的解码和计算。

缓冲代码虚拟机是SCCE的一个缩略版,因为相对于SCCE它具有较小的尺寸和更快的执行速度。在BCE中,一条指令是从内存中取得的,并和一个特殊指令表相比较。如果不是特殊指令,则它被进行简单的解码以求得指令的长度,随后所有这样的指令会被导入到一个可以通用地模拟所有非特殊指令的小过程中。而特殊指令,只占整个指令集的一小部分,则在特定的小处理程序中进行模拟。BCE通过将所有非特殊指令用一个小的通用的处理程序模拟来减少它必须特殊处理的指令条数,这样一来它削减了自身的大小并提高了执行速度。但这意味着它将不能真正限制对某个内存区域,端口或其他类似东西的访问,同时它也不可能生成如SCCE提供的同样全面的报告。

有限代码虚拟机有点象用于通用解密的虚拟系统所处的级别。LCE实际上并非一个虚拟机,因为它并不真正的模拟指令,它只简单地跟踪一段代码的寄存器内容,也许会提供一个小的被改动的内存地址表,或是调用过的中断之类的东西。选择使用LCE而非更大更复杂的系统的原因,在于即使只对极少数指令的支持便可以在解密原始加密病毒的路上走很远,因为病毒仅仅使用了INTEL指令集的一小部分来加密其主体。使用LCE,原本处理整个INTEL指令集时的大量花费没有了,带来的是速度的巨大增长。当然,这是以不能处理复杂解密程序段为代价的。当需要进行快速文件扫描时LCE就变的有用起来,因为一个小型但象样的LCE可以用来快速检查执行文件的可疑行为,反之对每个文件都使用SCCE算法将会导致无法忍受的缓慢。当然,如果一个文件看起来可疑,LCE还可以启动某个SCCE代码对文件进行全面检查。

下面开始介绍32位自含代码虚拟机w32encode(w32encode.cpp,Tw32asm.h,Tw32asm.cpp做为查毒引擎的一部分和其它搜索清除模块联编为Rsengine.dll)的程序结构和流程。由于这是一个设计完备且复杂的大型商用虚拟机,其中不可避免地包含了对某些特定病毒的特定处理,为了使虚拟机模型的结构清晰脉络分明,分析时我将做适当的简化。

w32encode的工作原理很简单:它首先设置模拟寄存器组(用一个DWORD全局变量模拟真实CPU内部的一个寄存器,如ENEAX)的初始值,初始化执行堆栈指针(虚拟机用内部的一个数组static int STACK[0x20]来模拟堆栈)。然后进入一个循环,解释执行指令缓冲区ProgBuffer中的头256条指令,如果循环退出时仍未发现病毒的解密循环则可由此判定非加密变形病毒,若发现了解密循环则调用EncodeInst函数重复执行循环解密过程,将病毒体明文解密到DataSeg1或DataSeg2中。相关部分代码如下:W32Encode0中总体流程控制部分代码:

for (i=0;i<0x100;i++) //首先虚拟执行256条指令试图发现病毒循环解密子 { if (InstLoc>=0x280) return(0); if (InstLoc+ProgSeekOff>=ProgEndOff) return(0); //以上两条判断语句检查指令位置的合法性 saveinstloc(); //存储当前指令在指令缓冲区中的偏移 HasAddNewInst=0; if (!(j=parse())) //虚拟执行指令缓冲区中的一条指令 return(0); //遇到不认识的指令时退出循环 if (j==2) //返回值为2说明发现了解密循环 break; } if (i==0x100) //执行过256条指令后仍未发现循环则退出 return(0); PreParse=0; ProcessInst(); if (!EncodeInst()) //调用解密函数重复执行循环解密过程 return(0); jmp中判定循环出现部分代码: if ((loc>=0)&&(loc<InstLoc)) //若转移后指令指针小于当前指令指针则可能出现循环 if (!isinstloc(loc)) //在保存的指令指针数组InstLocArray中查找转移后指 ...... //令指针值,如发现则可判定循环出现 else { ...... return(2); //返回值2代表发现了解密循环 }

parse中虚拟执行每条指令的过程较复杂一些:通常parse会从取得指令缓冲区ProgBuffer中取得当前指令的头两个字节(包括了全部操作码)并根据它们的值调用相应的指令处理函数。例如当第一个字节等于0F并且第二个字节位与BE后等于BE时,可判定此指令为movszx并同时调用movszx进行处理。当执行进入特定指令的处理函数中时,首先要通过判断寻址方式(调用modregrm或modregrm1)确定指令长度并将控制权交给saveinst函数。saveinst在保存该指令的相关信息后会调用真正指令执行函数W32ExecuteInst。这个函数和parse非常相似,它从SaveInstBuf1中取得当前指令的头两个字节并根据它们的值调用相应的指令模拟函数以完成一条指令的执行。相关部分代码如下:



C++代码
W32ExecuteInst中指令分遣部分代码:  
  
if ((c&0xf0)==0x50)  
  
{if (ExecutePushPop1(c)) //模拟push和pop  
  
return(gotonext());  
  
return(0);  
  
}  
  
if (c==0x9c)  
  
{if (ExecutePushf()) //模拟pushf  
  
return(gotonext());  
  
return(0);  
  
}  
  
if (c==(char)0x9d)  
  
{if (ExecutePopf()) //模拟popf  
  
return(gotonext());  
  
return(0);  
  
}  
  
if ((c==0xf)&&((c2&0xbe)==0xbe))  
  
{if (i=ExecuteMovszx(0)) //模拟movszx  
  
return(gotonext());  
  
return(0);  
  
}  
  
  
2.4虚拟机代码剖析   
  
总体流程控制和分遣部分的相关代码,在上一章中都已分析过了。下面分析具体的特定指令模拟函数,这才是虚拟机的精华之所在。我将指令分成不依赖标志寄存器和依赖标志寄存器两大类分别介绍:   
  
2.4.1不依赖标志寄存器指令模拟函数的分析   
  
push和pop指令的模拟:   
  
  
static int ExecutePushPop1(int c)  
  
{  
  
if (c<=0x57)  
  
{if (StackP<0) //入栈前检查堆栈缓冲指针的合法性  
  
return(0);  
  
}  
  
else  
  
if (StackP>=0x40) //出栈前检查堆栈缓冲指针的合法性  
  
return(0);  
  
if (c<=0x57) {  
  
StackP--;  
  
ENESP-=4; //如果是入栈指令则在入栈前减少堆栈指针  
  
}  
  
switch (c)  
  
{case 0x50:STACK[StackP]=ENEAX; //模拟push eax  
  
break;  
  
......  
  
case 0x5f:ENEDI=STACK[StackP]; //模拟push edi  
  
break;  
  
}  
  
if (c>=0x58) {  
  
StackP++;  
  
ENESP+=4; //如果是出栈指令则在出栈后增加堆栈指针   
  
}  
  
return(1);  
  
}  
  
  
2.4.2依赖标志寄存器指令模拟函数的分析   
  
CW32Asm类中cmp指令的模拟:   
  
  
void CW32Asm:: cmpw(int c1,int c2)  
  
{  
  
char FlgReg;  
  
__asm {  
  
mov eax,c1 //取得第一个操作数  
  
mov ecx,c2 //取得第二个操作数  
  
cmp eax,ecx //比较  
  
lahf //将比较后的标志结果装入ah  
  
mov FlgReg,ah //保存结果在局部变量FlgReg中  
  
}  
  
FlagReg=FlgReg; //保存结果在全局变量FlagReg中  
  
}  
  
CW32Asm类中jnz指令的模拟:  
  
  
  
int CW32Asm::JNE()  
  
{int i;  
  
char FlgReg=FlagReg; //用保存的FlagReg初始化局部变量FlgReg  
  
__asm  
  
{  
  
mov ah,FlgReg //设置ah为保存的模拟标志寄存器值  
  
pushf //保存虚拟机自身当前标志寄存器  
  
sahf //将模拟标志寄存器值装入真实标志寄存器中  
  
mov eax,1  
  
jne l //执行jnz  
  
popf //恢复虚拟机自身标志寄存器  
  
xor eax,eax  
  
l:  
  
popf //恢复虚拟机自身标志寄存器   
  
mov i,eax  
  
}  
  
return(i); //返回值为1代表需要跳转
您需要登录后才可以回帖 登录 | 开放注册

本版积分规则

帮助|Archiver|小黑屋|通信管理局专项备案号:[2008]238号|NB5用户社区 ( 皖ICP备08004151号;皖公网安备34010402700514号 )

GMT+8, 2025-1-11 14:11 , Processed in 0.115451 second(s), 23 queries , Yac On.

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表