目录
初识PE文件格式
DOS
Dos header
比较重要的有e_magic和e_lfanew两个属性。
其中e_magic属性用来判断这个文件是否为PE文件
e_lfnaw属性用来确定NT header相对于整个文件头的偏移量
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Dos Stub(存根)
如果在 MS-DOS 中执行文件,则会调用存根程序。它通常显示合适的消息;但是,任何有效的 MS-DOS 应用程序都可以是存根程序。
NT Header
整个NT文件头由三个部分组成
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
Signature
这个参数是一个标志。在一个有效的PE文件里, Signature字段被设置为 00004550h。
FileHeader
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //标志着运行平台
WORD NumberOfSections; //节的数量
DWORD TimeDateStamp; //This represents the date and time the image was created by the linker.
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;//可选头的大小
WORD Characteristics; //说明了文件的可写、可读属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
OptionalHeader
下面列出了其中几个比较重要的属性
typedef struct _IMAGE_OPTIONAL_HEADER64 {
DWORD AddressOfEntryPoint;//指出文件被执行时的入口地址,注意这里是一个RVA地址
ULONGLONG ImageBase; //程序的首选装载地址
DWORD SectionAlignment; //内存中的区块的对齐大小
DWORD FileAlignment; //文件中的区块的对齐大小
DWORD SizeOfImage; //映像装入内存后的总尺寸
WORD DllCharacteristics; // DllMain()函数何时被调用
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//数据目录表。这个字段是一个指针,它由16个相同的 IMAGE_DATA_DIRECTORY结构组成。
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //虚拟地址(RVA)
DWORD Size; //目录大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中每个目录有特殊的含义,比较重要的有:
- 第0个 导出表
- 第1个 导入表
- 第5个 基址重定位
- 第12个 IAT表
Section Table
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //名字填充
union {
DWORD PhysicalAddress; //文件地址
DWORD VirtualSize; //加载到内存中的节的总大小
} Misc;
DWORD VirtualAddress;
//加载到内存中的部分的第一个字节的地址,相对于映像基。对于对象文件,这是在应用重定位之前的第一个字节的地址。
DWORD SizeOfRawData; //在磁盘上的初始化的数据大小
DWORD PointerToRawData; //COFF 文件中第一页的文件指针。
DWORD Characteristics; //说明了节区的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Import Table
想要找到导入表,我们首先找到Optional Header中的DataDirectory。
其次找到对应目录中的VirtualAddress。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //指向INT的RVA
} DUMMYUNIONNAME;
DWORD Name; //指向导入映像文件的名称
DWORD FirstThunk; // 指向IAT的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;
想要找到一个导入函数,我们首先通过OriginalFirstThunk找到我们存放Image_Thunk_Data,随后进一步找到我们需要的导入函数。
INT和IAT之间的区别
注意上图PE文件加载前,IAT表和INT表的完全相同的,所以此时IAT表也可以判断函数导出序号,或指向函数名字结构体。
而在加载后,差别就是IAT表发生变化,系统会先根据结构体变量Name加载对应的dll(拉伸),读取dll的导出表,对应原程序的INT表,匹配dll导出函数的地址,返回其地址,贴在对应的IAT表上,挨个修正地址(也就是GetProcAddress的功能)。
IMAGE_IMPORT_BY_NAME
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1]; //function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
开始我们注入导入表的旅程吧
首先我们需要明确我们要做什么:
我们要将给定的DLL添加到一个应用程序中,并且当这个应用程序启动时,它会自动感染当前目录下的所有应用程序并将DLL文件分别添加到所有应用程序的导入表中。
根据我们上面提到的基础知识,我们首先需要指导一个导入表加载到IAT中的过程。
所以如果我们想要添加dll到导入表内,我们需要修改IAT和INT表、添加一个IMAGE_IMPORT_DESCRIPTOR和IMAGE_IMPORT_BY_NAME。
想要添加东西我们就必须要思考,是在原来的节区中添加还是新添加一个节区来存放我们需要的内容。最终我们选择新加一个节区,这里考虑到原来的节区可能存放的东西比较乱,不如我们新建一个节区比较好。
我们回顾一下我们整个程序需要修改的地方:
-
我们新添加一个节区头,肯定要修改NT头中Optional Header中的SizeOfImage
-
我们需要在原来的sectionHeader后面添加上我们新的文件头,然后把原来导入表所在的节区的节区头完全拷入到我们新建的节区头中。
-
添加新的节区头之后,我们肯定要把指向原来导入表的值修改为指向我们新加节头的地址。这里主要修改可选头中的DataDirectory[1]中的虚拟地址
-
修改完节点头之后,我们需要在整个文件后面添加新的节区。
在这个新的节区中,我们主要添加7个部分:
1.一个映像导入描述符。需要修改这个描述符的INT和IAT地址。
2.一个空白的描述符,来说明描述符结束。
3.一个INT。修改其AddressOfData属性,使其指向新添加得到IMAGE_IMPORT_BY_NAME
4.一个空白的INT。表明INT结束。
5.一个IAT,使其指向新添加的IMAGE_IMPORT_BY_NAME。
6.一个空白的IAT,表明IAT结束。
7.一个IMAGE_IMPORT_BY_NAME,修改其Name,Hint属性
有几个需要值得注意的地方:
- 新添加节的属性一定要包含IMAGE_SCN_MEM_WRITE属性。
- 注意区分32位和64位程序。