病毒编程技术-3

* API函数地址的获取

        在能够正确重定位之后,病毒就可以运行自己代码了。但是这还远远不够,要搜索文件、读写文件、进行进程枚举等操作总不能在有Win32 API的情况下自己用汇编完全重新实现一套吧,那样的编码量过大而且兼容性很差。Win9X/NT/2000/XP/2003系统都实现了同一套在各个不同的版本上都高度兼容的Win32 API,因此调用系统提供的Win32 API实现各种功能对病毒而言就是自然而然的事情了。
  所以接下来要解决的问题就是如何动态获取Win32 API的地址。最早的PE病毒采用的是预编码的方法,比如Windows 2000中CreateFileA的地址是0x7EE63260,那么就在病毒代码中使用call [7EE63260h]调用该API,但问题是不同的Windows版本之间该API的地址并不完全相同,使用该方法的病毒可能只能在Windows 2000的某个版本上运行。因此病毒作者自然而然地回到PE结构上来探求解决方法,我们知道系统加载PE文件的时候,可以将其引入的特定DLL中函数的运行时地址填入PE的引入函数表中,那么系统是如何为PE引入表填入正确的函数地址的呢?答案是系统解析引入DLL的导出函数表,然后根据名字或序号搜索到相应引出函数的的RVA(相对虚拟地址),然后再和模块在内存中的实际加载地址相加,就可以得到API函数的运行时真正地址。在研究操作系统是如何实现动态PE文件链接的过程中,病毒作者找到了以下两种解决方案:

A)在感染PE文件的时候,可以搜索宿主的函数引入表的相关地址,如果发现要使用的函数已经被引入,则将对该API的调用指向该引入表函数地址,若未引入,则修改引入表增加该函数的引入表项,并将对该API的调用指向新增加的引入函数地址。这样在宿主程序启动的时候,系统加载器已经把正确的API函数地址填好了,病毒代码即可正确地直接调用该函数。

B)系统可以解析DLL的导出表,自然病毒也可以通过这种手段从DLL中获取所需要的API地址。要在运行时解析搜索DLL的导出表,必须首先获取DLL在内存中的真实加载地址,只有这样才能解析从PE的头部信息中找到导出表的位置。应该首先解析哪个DLL呢?我们知道Kernel32.DLL几乎在所有的Win32进程中都要被加载,其中包含了大部分常用的API,特别是其中的LoadLibrary和GetProcAddress两个API可以获取任意DLL中导出的任意函数,在迄今为止的所有Windows平台上都是如此。只要获取了Kernel32.DLL在进程中加载的基址,然后解析Kernel32.DLL的导出表获取常用的API地址,如需要可进一步使用Kernel32.DLL中的LoadLibrary和GetProcAddress两个API更简单地获取任意其他DLL中导出函数的地址并进行调用。

* 获取Kernel32.DLL基址

  获取Kernel32.DLL基址的方法很多,最常见的一种是搜索法,如果已知Kernel32.DLL加载的大致地址,那么可由该地址向高地址或低地址进行搜索可以找到其基址。另外一种方法是搜索NT PEB结构中的模块列表获取Kernel32.DLL的准确加载基址。下面看一下具体的实现代码:

方法1:暴力搜索获取Kernel32.DLL的基址

         最初的病毒是指定一个大致的加载地址,比如根据实验在9X下其加载地址是0xBFF70000;在Windows 2000下加载基址是0x77E80000;在XP和2003下其加载基址是0x77E60000,因此在NT系统下就可以从0x77e00000开始向高地址搜索,在9X下可以从0xBFF00000开始向高地址搜索,如果搜索到Kernel32.DLL的加载地址,其头部一定是“MZ”标志,由模块起始偏移0x3C的双字确定的PE头部标志必然是“PE”标志,因此可根据这两个标志判断是否找到了模块加载地址,也许有人认为该方法不可靠,因为如果恰好有某段数据符合这两个特征,那么找到的基址可能就是错误的,但经实验证明,该判断方法非常可靠,基本不会出现错误。有一点需要注意的是,在所有版本的Windows系统下Kernel32.DLL的加载基址都是按照0x10000对齐的,根据这一特点可以不必逐字节搜索,按照64K对齐的边界地址搜索即可。
       从大致的一个地址开始搜索Kernel32.DLL基址可能会出现读写到未映射内存区域的情况,因此需要和SEH配合使用。如果有在各个版本下准确获取Kernel32.DLL中某地址的通用方法,那么就可以更可靠地从该地址开始向低地址搜索,显然会更加通用。事实上,这种方法是存在的。在系统加载PE文件跳转到PE入口点第一条指令的时候,堆栈顶保存的就是Kernel32.DLL中的某个地址,Elkern中采用的就是这种方法:
_start:
        pushfd ;If some flags,especial DF,changed,some APIs can crash down!!!
        pushad
_start_@1 equ $
        ;......
        mov ebx,[esp+9*4]   ;前面已经由pushfd和pushad压入了9个双字
        and ebx,0ffe00000h  ;该地址为Kernel32.dll模块下方的某个地址
                ;先减去0x100000确保该地址处于Kernel32.dll的下方
                ;向高地址搜索如果将来Windows的发行版本中Kernel32.dll
                ;大小和代码结构发生变化,该方法可能无效

  ebx中现在已经是Kernel32.DLL基址之前某个地址了,后续代码可以向高地址搜索其基址。该方法有一个缺点,就是必须明确知道程序入口的堆栈指针值,或间接可计算出该值,对于那些在程序入口获取控制权的病毒代码而言,是可以的,但对于采用EPO技术的病毒而言,该方法则不适用。事实上还有另外一种更加通用的方法,我们知道在Win32程序执行过程中fs段寄存器的基址总是指向进程的TEB,TEB的第一个成员指向SEH链表,该链表每个节点都是一个EXCEPTION_REGISTRATION结构,该结构定义如下:
struct EXCEPTION_REGISTRATION{
    struct EXCEPTION_REGISTRATION *prev;
 void* handler;
};
在Windows下SEH链表最后一个成员的handler指向Kernel32.DLL中函数UnhandledExceptionFilter的起始地址,利用这一特性我们可以写出更通用的代码:
        xor     esi,esi
        lods    dword [fs:esi];取得SEH链表的头指针
      @@:
        inc     eax             ;是否是最后一个SEH节点,检查prev是否为0xFFFFFFFF
        je      @F
        dec     eax
        xchg    esi,eax        
        LODSD                   ;下一个SEH节点
        jmp     near @B
      @@:
        LODSD                   ;取得Kernel32.dll中UnhandledExceptionFilter的地址

         在有的病毒直接以0x7FFDE000作为TEB的指针值,其原因在于在Windows 2003 SP1、Windows XP SP2以前的NT类系统上,该值是固定的,这样的确可以节省一两个字节。但是在Windows 2003 SP1、Windows XP SP2中,情况已经发生了变化,出于安全性的考虑,Windows系统开始动态映射TEB了,也就是说,指向TEB的指针值不再固定,因此这种硬编码的方法也就走到了尽头。此时可以按照前面的方法向低地址搜索判断直到找到Kernel32.dll的基址为止。Elkern中判断是否找到了Kernel32.dll基址的相关代码如下:
search_api_addr_@1:
        add ebx,10000h
        jz short search_api_addr_seh_restore
        cmp word ptr [ebx],'ZM'   ;是否是MZ标志
        jnz short search_api_addr_@1
        mov eax,[ebx+3ch]
        add eax,ebx
        cmp word ptr [eax],'EP'   ;是否具有PE标志
        jnz short search_api_addr_@1
   ;找到了kernel32.dll的基址

方法2:搜索PEB的相关结构获取Kernel32.DLL的基址

        前述TEB偏移0x30处,亦即FS:[0x30]地址处保存着一个重要的指针,该指针指向PEB(进程环境块),PEB成员很多,这里并不介绍PEB的详细结构。我们只需要知道PEB结构的偏移0xC处保存着另外一个重要指针ldr,该指针指向PEB_LDR_DATA结构:
typedef struct _PEB_LDR_DATA
{
                                                          
  ULONG             Length;                              // +0x00  
  BOOLEAN           Initialized;                         // +0x04  
  PVOID             SsHandle;                            // +0x08  
  LIST_ENTRY        InLoadOrderModuleList;   // +0x0c  
  LIST_ENTRY        InMemoryOrderModuleList;          // +0x14  
  LIST_ENTRY        InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA;                        // +0x24      
  该结构的后三个成员是指向LDR_MODULE链表结构中相应三条双向链表头的指针,分别是按照加载顺序、在内存中的地址顺序和初始化顺序排列的模块信息结构的指针。LDR_MODULE结构如下所示:
typedef struct _LDR_MODULE
{
    LIST_ENTRY        InLoadOrderModuleList;             // +0x00
    LIST_ENTRY        InMemoryOrderModuleList;          // +0x08
    LIST_ENTRY        InInitializationOrderModuleList; // +0x10
    PVOID             BaseAddress;                        // +0x18
    PVOID             EntryPoint;                         // +0x1c
    ULONG             SizeOfImage;                        // +0x20
    UNICODE_STRING    FullDllName;                       // +0x24
    UNICODE_STRING    BaseDllName;                       // +0x2c
    ULONG             Flags;                              // +0x34
    SHORT             LoadCount;                          // +0x38
    SHORT             TlsIndex;                           // +0x3a
    LIST_ENTRY        HashTableEntry;                    // +0x3c
    ULONG             TimeDateStamp;                      // +0x44
                                                          // +0x48
} LDR_MODULE, *PLDR_MODULE;

  Peb->Ldr->InInitializationOrderModuleList指向按照初始化顺序排序的第一个LDR_MODULE节点的InInitializationOrderModuleList成员的指针,在WinNT平台(不包含Win9X)下,该链表头节点的LDR_MODULE结构包含的是NTDLL.DLL的相关信息,而链表的下一个节点所包含的就是Kernel32.dll相关的信息了,该节点LDR_MODULE结构中的BaseAddress不正是我们所苦苦寻找的吗。注意InInitializationOrderModuleList是LDR_MODULE的第3个成员,因此要获取BaseAddress的地址,只需将其指针加8再derefrence即可。因此下面的汇编代码即可获取Kernel32.DLL的基址:

 mov eax, dword ptr fs:[30h]     ;获取PEB基址
     mov eax, dword ptr [eax+0ch] ;获取PEB_LDR_DATA结构指针
     mov esi, dword ptr [eax+1ch] 
     ;获取InInitializationOrderModuleList链表头第一个LDR_MODULE节点
     InInitializationOrderModuleList成员的指针
    lodsd                 ;获取双向链表当前节点后继的指针
  mov ebx, dword ptr [eax+08h] ;取其基地址,该结构当前包含的是
                     ;kernel32.dll相关的信息

        该方法在所有的Windows NT(包括Windows 2003 SP1和Windows XP SP2)操作系统上都是有效的,唯一的缺憾是由于PEB结构不同,该方法在Win9X系统上无效。听起来可能比较费解,还是用一张图更加清晰一些:


图6 利用PEB搜索kernel32.dll基地址的过程

你可能感兴趣的:(病毒编程技术-3)