这篇文章是在Servex.exe专杀工具写完之后的一些小小感想,关于PE感染和修复,大牛飘过~

  自某日不小心中了Serverx.exe病毒,电脑中大部分EXE文件被感染,系统盘system32目录下的病毒文件Serverx.exe总也杀不掉,因为即使删掉了,每次运行了被感染的程序之后还会再次生成。(关于此毒的详细资料,有兴趣的直接在百度搜索"Serverx.exe"或"愤怒天使"即可)在网上求助无果,遂自己动手,研究了一下这个病毒的感染方式,最终找到了恢复被感染文件的方法,下面将此过程分享给大家。

遇到这种感染型的病毒,恢复时需要解决几个问题。
第一个问题,感染后的文件能否恢复?

那么我们从被感染后的EXE运行情况来看看。以调用Winrar解压缩为例,可以正常实现解压过程。但在解压后就应退出的Winrar仍在运行。如果用Process Explorer观察进程树的话,会发现是Winrar本身又重新运行了一个Winrar进程来完成解压缩功能,而解压完成后,第一个运行的Winrar仍在继续运行,用FileMon监视的话会发现它有大量的文件访问操作,从访问的目录顺序和规律来看是在遍历并寻找EXE进行感染。而这两个Winrar的路径是一样的,也就是说实现感染功能的和实现解压功能的是同一个程序,解压功能仍然是被感染后的Winrar完成的,这说明一个问题:感染后的文件功能没有被破坏!这非常重要,也是因为程序的功能没有被破坏才有下面的恢复过程。

第二个问题,此病毒是何种感染方式?
典型的感染方式有“夹心饼”式的捆绑感染,替换资源或添加资源式的感染,准确说这些不算是“正宗”的PE感染。其它典型的方式有加新区段,写入shellcode(这里把病毒自己注入的代码也称为shellcode),然后跳回原入口。还有一些高级的感染方式,不一一列举。
为了分析这个病毒的感染方式,我将感染前后的Winrar.exe进行对比,首先会发现文件大小要增加,再用PEID(或其它同类工具)查看一下相关数据,首先发现入口所在区段不一样了,由.text段变成了最后的.rsrc段,入口处代码也不一样了。既然感染跟区段有关,那么就来看一下感染前后区段数据的变化:
感染前: 

感染后: 

可以看到,区段数并没有增加,仍为7个,但是最后一个区段的虚拟大小(VirtualSize)和原始大小(RawSize)都有所增加,且段属性标记也被修改了。这样我们对此病毒的感染方式有所了解了,就是增加最后一个段的大小,写入shellcode,并修改段属性为可执行,并不是某些人分析的增加新区段方式。具体地把一些数据列出来并进行计算(所有数据都是Winrar.exe的):
感染前文件大小:929792 Bytes   感染后:974742 Bytes  增加:44950=0xAF96
感染前最后一个节的数据:
VirtualSize:0x00032000   RawSize:0x00031200   Flags:0x40000040
感染后最后一个节的数据:
VirtualSize:0x0003C196   RawSize:0x0003C196   Flags:0xE0000040
增加的大小:VirtualSize:0x0000A196  RawSize:0x0000AF96
经过计算,最后一个节的RawSize增加的大小刚好等于文件增加的大小,这个用WinHex打开感染前后的文件比较一下就可以证实,病毒确实是在原始文件末的地方开始写入shellcode的。对PE知识的了解使我们知道节表中的RawSize对应于这个节在文件中实际的大小,我们更证实了之前的想法。可知病毒的感染方法是这样的:根据shellcode计算需要的空间大小nSize,然后将文件增加nSize,写入shellcode,然后修正PE头中最后一个区段的原始大小、虚拟大小,修改区段属性
为可执行,因为映像大小被修改,还要修正IMAGE_OPTIONAL_HEADER结构中的SizeOfImage。Shellcode中必然是做了些坏事然后跳回到原入口处继续执行。这是我们目前分析得到的信息。

第三个问题,要恢复被感染的文件,需要修复哪些数据?
结合前面分析出的病毒的感染过程,要修复感染后的文件,至少要知道以下几个信息,同时也是需要修复的数据。
(1)  感染前的文件大小。前面已经知道,病毒直接从文件末开始写入shellcode,要去掉病毒写入的内容,就必须知道感染前原文件大小。
(2)  感染前最后一个区段的RawSize。因为RawSize增加的大小等于文件增加的大小,所以知道了感染前文件大小也就是RawSize的大小。
(3)  感染前最后一个区段的VirtualSize。在获取正确的RawSize之后就可以根据对齐来计算VirtuslSize,也就是说第二个问题解决了,这个问题也就解决了。
(4)  感染前的映像和。在获取最后一个区段正确的VirtualSize之后就可以计算映像大小之和,第三个问题解决了,这个问题也就解决了。
(5)  感染前的入口点。这个关键数据的重要性不需多说吧?前四个信息是一环扣一环,所以总的来说,要获取的关键数据只有两个,感染前的文件大小和感染之前的入口点

第四个问题,如何获取感染前的文件大小?
并不是所有程序都像Winrar这样有备份,可以进行对比分析。最一般的情况,只有一个感染后的EXE文件,如何获取感染前的文件大小?而且这个方法还必须要通用,因为将来我们要修复的是所有被感染的EXE文件。这个问题我想了很久,后来想到一个办法,病毒是从原文件末开始写入shellcode的,又观察了几个感染后的程序,可以看到入口代码都是一样的,也就是说写入的shellcode都一样,没有变形处理等。基于病毒固定的shellcode特征,而且是从文件末开始写入shellcode的,我们可以从文件最后将前搜索匹配此shellcode特征,找到的shellcode的起始位置也就是原文件结束的位置,此时距文件头的偏移就是原来文件的大小了!
用OllyDbg载入感染后的Winrar.exe,来获取一下shellcode特征:
00524200    60                   pushad
00524201    78 03              js short 有毒_Win.00524206
00524203    79 01              jns short 有毒_Win.00524206
00524205  ^ EB E8             jmp short 有毒_Win.005241EF
00524207    74 11              je short 有毒_Win.0052421A
就取其前8字节作为特征,定义如下:
char Signature[]="\x60\x78\x03\x79\x01\xEB\xE8\x74";
搜索的代码比较简单,CreateFile打开目标文件然后CreateFileMapping,MapViewOfFile进行映射,从后向前使用memcmp进行比较即可。代码如下:
void SearchCode(char *buf,DWORD dwBufSize)
 {
  //第一个参数即映射后的内存地址,第二个参数为映射的内存大小
  int i=0;
  //SIGNATURELEN即前面定义的特征码长度,即8
  char *p=buf+dwBufSize-SIGNATURELEN;
  //输出内存基址
  printf("p=0x%08X\n",p);
  do 
  {
    if (memcmp(p,Signature,SIGNATURELEN)==0)
    {
      //若匹配,则输出当前指针位置,距开始的偏移,距结束的偏移
printf("[%d]Found Signatue 0x%08X offset=0x%08X size=0x%08X\n",i,p,p-buf,buf+dwBufSize-p);
      i++;
    }
    p--;
  } while(p!=buf);
  printf("Search OK!\n");
}
运行结果如下:
 
来分析一下这个结果吧!
在感染后的Winrar中,我们逆向搜索到了十处匹配的特征码,在最后一处,距文件起始处的偏移为0x000E3000,换成十进制就是929972,如果你对这个数还有印象的话,就会知道,这就是感染前的文件大小!而距结尾处的偏移为0x0000AF96,这个就是文件增加的大小,也是最后一个区段的RawSize增加的大小!这些激动人心的数据不仅证实了我们前面所有关于感染方式的论证是正确的,而且获取原始文件大小的问题也得到了解决!

第五个问题,如何获取原始入口点?
前面列出了修复文件所需的五个数据,而第四个问题的解决也使前四个数据都有了着落。病毒一定在shellcode中办完自己的事后以某种方式跳回原入口点,保证原程序的功能不变。如何获取原入口点呢?静态分析已经不能解决问题了,我们来跟踪一下病毒注入的shellcode吧。再次拿出Ollydbg,载入感染后的Winrar.exe,仔细跟踪。(如果不小心跑飞了要立即结束!否则可能会有文件被感染)如果说前面的那些根据病毒外在表现的分析是黑盒分析的话,现在要进行的就是白盒分析了!
刚开始,shellcode搜索MZ标志寻找kernel32.dll的基址,然后获取LoadLibrary()和GetProcAddress()的地址,之后加载用到的dll并获取相关函数地址,算是比较常规的过程啊。这些过程进程完之后,病毒做的第一件事,就是调用CreateMutex,创建互斥体“Angry Angel v3.0”,看来这也是此病毒被称为“愤怒天使”的原因了。

然后RtlGetLastWin32Error(),判断是否为ERROR_ALREADY_EXISTS,若存在则直接int 3,若不存在则继续执行。为什么要int 3呢,有玄机,等会儿再看。继续执行,就GetCommandLine(),然后再WinExec()执行之。这也就是前面提到感染后的程序运行时会有两个进程的原因了。WinExec之后,调用GetSystemDirectory()获取系统目录,然后连接上病毒名Serverx.exe,判断文件是否存在,存在则写注册表自启动项。后面的过程我就不再分析了,大致还有向Explorer.exe进程注入代码用于进程保护,另外开启线程进行文件感染等,不再多说,多说就偏题了,我们现在关心的问题是如何获取原入口点啊。也就是被病毒WinExec方式运行的进程,又如何实现原来的功能,也就是如何跳回到原入口点。把上面的过程再走一遍,因为还是这个程序,还要走这个过程,但是在RtlGetLastWin32Error()时就有差别了,由于刚才病毒已经创建了互斥体,这时就会得到最终错误码为ERROR_ALREADY_EXISTS,然后,病毒执行int 3,而不是想像中的jmp XXXXXXXX。Int 3会怎样?会引发一个异常,于是想到病毒可能是在异常处理中跳回了原入口。异常处理的详细过程不多说(其实我也只了解大概),大致就是依次调用SHE链中注册的异常处理过程。在跟踪中发现,在10次中断在CreatMutex之后,再int 3执行ZwContinue()就会跳回到原入口。为什么是10次呢?稍后再说。反正我们知道最后一次int 3之后会跳回原入口,目前只关心最后一次int 3之后执行ZwContinue时做了什么?

先了解一下ZwContinue()这个函数,从WRK中找到对应的NtContinue原型如下:
 NTSTATUS
 NtContinue (
    IN PCONTEXT ContextRecord,
    IN BOOLEAN TestAlert
   )
NtContinue用于恢复一个线程的执行。第一个参数是一个指向线程上下文信息的结构体指针,里面有所有的线程环境信息,包括通用寄存器、段寄存器、浮点寄存器等。当然,还有我们想要的EIP。此时调用的代码可以这么表示:
NtContinue(0x0012FBBC,0);
结合CONTEXT的结构(详细结构可参见WRK),可以知道异常恢复后的EIP,那么Ctrl+G,填入EIP,F2下断,再F8单步,果然在这里断了下来,再F7单步几次,看到如下代码:
 
真是令人兴奋啊!这个0x00401000正是感染前程序的入口点!
综合前面得到的信息,来理一下思路吧。前面说搜索shellcode时搜索到了10处,而对CreateMutex下断时也断下来10次,int 3也执行了10次。这只能说明,病毒将同一段shellcode写入了10次!而每一段shellcode即将结束时int 3引发异常,在异常中进入了第二段同样的shellcode,一直到第10次int 3时才跳回程序原入口处继续执行。最后一段shellcode的起始地址是0x00524200,而push/ret距起始的偏移为0x0052421A-0x00524200=0x1A,而下一次跳转的地址偏移就是0x1B处的DWORD!来检验下我们的计算结果吧。Ctrl+F2重新载入,Ctrl+G跳到距入口也就是第一段shellcode起始地址0x0052E007偏移为0x1A的地方,可以看到:
 
再跳到这个0x0052CE78处看看:

和第一段shellcode的代码是一模一样的,只是地址不同而已。这时,我们可以得出感染后EXE的执行过程,简单画个图表示一下(很粗糙哈):

其中每个灰色方块代表一段shellcode,而前面斜线部分才是真正的原始文件。病毒以最后一段shellcode的起点为入口,每段shellcode结束时都跳入到前一段,最后一次跳回原始入口点,执行原程序的功能。理清之后,要获取每一次转移后的地址就容易了,在距每段shellcode开头偏移0x1B处取一个DWORD的值,就是下一次转移的目标!
把我们前面的SearchCode函数改进一下,增加识别转移地址的功能,新函数如下:


void SearchCode(char *buf,DWORD dwBufSize)
{
  DWORD offset=0;
  int i=0;
  char *p=buf+dwBufSize-SIGNATURELEN;
  printf("p=0x%08X\n",p);
  do 
  {
    if (memcmp(p,Signature,SIGNATURELEN)==0)
    {
      printf("[%d]Found Signatue 0x%08X Entry=0x%08X 

offset=0x%08X size=0x%08X\n",i,p,*(DWORD*)(p+0x1B),p-buf,buf+dwBufSize-p);
      trueFileSize=p-buf;
      entryNow=*(DWORD*)(p+0x1B);
      i++;
    }
    p--;
  } while(p!=buf);
  printf("Search OK!\n");
}
其中的Entry就是转移目标了!
输出结果如下:
 
最后一次搜索到的就是原始的入口点了(如果从前往后搜索的话就是第一次),

第五个问题解决!
好了,五个问题都解决了,可以写修复工具了!修复过程如下:
(1)  通过在感染后的文件中搜索shellcode得到原始入口点,原文件大小;
(2)  根据当前文件大小,得出增加部分的大小ExtraSize;
(3)  根据ExtraSize修正最后一个区段的RawSize,当前值减去多的部分即可得正确RawSize;
(4)  由正确的RawSize和内存对齐大小,计算正确的VirtualSize;
(5)  根据PE头的大小和所有区段映像大小,计算总的SizeofImage;
(6)  原始入口点减去映像基址即得AddressofEntryPoint;
(7)  将文件大小减小ExtraSize,设置结束标记。
这就是完整的修复方案!限于篇幅,具体代码不直接贴出来了,请在随文代码中查看。

修复后Winrar.exe与感染前相比,仅有3个字节的差异!差异主要体现在程序的校验和和最后一个区段的属性。校验和只有SYS才会用到,对于病毒感染的EXE和SCR格式来说并不重要,因此不再修正。而最后一个区段多了执行属性,考虑到一些加壳后的程序的特殊性,不再修正这个值。修复效果堪称完美!将以上代码整理一下,再加上遍历EXE和SCR、判断是否感染、修复,就成了一个实用的专杀工具!至此,Serverx.exe出炉了!

初写此程序时感觉还有些难,毕竟是从感染的逆方向入手,没写过也没把握。就像破解一个程序,要了解作者的思路和方法,才能找到对策。
现在再看,好像一切都是那么清晰,就像对着答案看题目一样,不过当初做题时不容易啊~~文中如有错误,还请大家指出~