恶意代码的亲密接触之文件搜索和API导址
在
前一篇文章中介绍了病毒的相关基础知识后,从本文开始我们就要深入病毒内部,开始看一些具体的病毒编码片断,在本文中我们会看到API函数搜索以及文件搜索的技术,为后面更加深入的探讨打下良好的基础,如果你已经准备好了,就让作者带你进入这无所不用其极的病毒技术世界吧。
获取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中采用的就是这种方法:
ebx 中现在已经是Kernel32.DLL 基址之前某个地址了,后续代码可以向高地址搜索其基址。该方法有一个缺点,就是必须明确知道程序入口的堆栈指针值,或间接可计算出该值,对于那些在程序入口获取控制权的病毒代码而言,是可以的,但对于采用EPO 技术的病毒而言,该方法则不适用。事实上还有另外一种更加通用的方法,我们知道在Win32 程序执行过程中fs 段寄存器的基址总是指向进程的TEB,TEB 的第一个成员指向SEH 链表,该链表每个节点都是一个EXCEPTION_REGISTRATION 结构,该结构定义如下:
在Windows 下SEH 链表最后一个成员的handler 指向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基址的相关代码如下:
方法2:搜索PEB 的相关结构获取Kernel32.DLL 的基址
前述TEB偏移0x30处,亦即FS:[0x30]地址处保存着一个重要的指针,该指针指向PEB(进程环境块),PEB成员很多,这里并不介绍PEB的详细结构。我们只需要知道PEB结构的偏移0xC处保存着另外一个重要指针ldr,该指针指向PEB_LDR_DATA 结构:
该结构的后三个成员是指向LDR_MODULE链表结构中相应三条双向链表头的指针,分别是按照加载顺序、在内存中的地址顺序和初始化顺序排列的模块信息结构的指针。
LDR_MODULE结构如下所示:
Peb->Ldr->InInitializationOrderModuleList指向按照初始化顺序排序的第一个L D R _ M O D U L E 节点的InInitializationOrderModuleList成员的指针,在WinNT平台(不包含Win9X)下,该链表头节点的LDR_MODULE结构包含的是NTDLL.DLL的相关信息,而链表的下一个节点所包含的就是Kernel32.dll 相关的信息了,该节点LDR_MODULE结构中的BaseAddress 不正是我们所苦苦寻找的吗。注意InInitializationOrderModuleList 是LDR_MODULE的第3个成员,因此要获取BaseAddress 的地址,只需将其指针加8 再derefrence即可。因此下面的汇编代码即可获取Kernel32.DLL的基址:
该方法在所有的Windows NT(包括Windows 2003 SP1和Windows XP SP2)操作系统上都是有效的,唯一的缺憾是由于PEB结构不同,该方
法在Win9X系统上无效。听起来可能比较费解,还是用一张图更加清晰一些(见图6)。
获取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指向按照初始化顺序排序的第一个L D R _ M O D U L E 节点的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 成员的指针 93 2005.08 程序员 lodsd ;获取双向链表当前节点后继的指针 mov ebx, dword ptr [eax+08h] ;取其基地址,该结构当前包含的是 ;kernel32.dll 相关的信息 |
该方法在所有的Windows NT(包括Windows 2003 SP1和Windows XP SP2)操作系统上都是有效的,唯一的缺憾是由于PEB结构不同,该方
法在Win9X系统上无效。听起来可能比较费解,还是用一张图更加清晰一些(见图6)。
图6:利用PEB 搜索kernel32.dll 基地址的过程 |