一. DOS头
原来为DOS系统使用,现在只需要记住如下两项。
1、DOC头:
WORD e_magic * "MZ标记" 用于判断是否为可执行文件.
DWORD e_lfanew; * PE头相对于文件的偏移,用于定位PE文件
如上图所示,DOS头一共40H个字节,即64个字节。
结尾处4个字节指向了标准PE头的位置:D8。在这中间的文字是编译器加入的一些描述信息。这些字节是可以修改的。
二. NT头
NT头中包括了标准PE头和可选PE头。
struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature; //E8的位置,也就是DOS头中e_lfanew指向的位置。
0x04 _IMAGE_FILE_HEADER FileHeader;
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};
三. 标准PE头
标准PE头一共20个字节。
2、标准PE头:
WORD Machine; * 程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器
WORD NumberOfSections; * 文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值.
DWORD TimeDateStamp; * 时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的.
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; * 可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h 大小可以自定义.
WORD Characteristics; * 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1
将可以修改的位置改为F
如果Machine清零会提示版本过低,无法在此电脑上运行。
NumberOfSections:清零或者改为不正确的值仍然无法运行。
SizeOfOptionalHeader,Characteristics均不可清零。
Characteristics 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1。
(此表只描述了15位的信息。不确定是否16位全部表示信息。)
用鼠标选中的部分就是标准PE头,一共20个字节。打开的这个软件的标准PE头的位置和前一个就不同了,DOS的末尾将指向了100H这个位置。
四. 可选PE头
可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h 大小可以自定义.
3、可选PE头:
WORD Magic; * 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;* 所有代码节的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfInitializedData;* 已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfUninitializedData;* 未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
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 Win32VersionValue;
DWORD SizeOfImage;* 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍
DWORD SizeOfHeaders;* 所有头+节表按照文件对齐后的大小,否则加载会出错
DWORD CheckSum;* 校验和,一些系统文件有要求.用来判断文件是否被修改. 每两个字节作为一个数,循环相加
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;* 初始化时保留的堆栈大小
DWORD SizeOfStackCommit;* 初始化时实际提交的大小
DWORD SizeOfHeapReserve;* 初始化时保留的堆大小
DWORD SizeOfHeapCommit;* 初始化时实践提交的大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;* 目录项数目
_IMAGE_DATA_DIRECTORY DataDirectory[16];
最后一项为数据目录,一共16个,第16个未定义。
将校验和的最高位改成11 22这样就没事,改为FF AA这样程序将会无法启动。
五. 节表
每个节表有28H个字节,即40个字节。在所有节表的末尾编译器还会添加40个0,标志着整个结构的结束。
#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;
1. 节表的名字, 8个字节。
2. Misc 双字 是该节在没有对齐前的真实尺寸,该值可以不准确。
3. VirtualAddress 节区在内存中的偏移地址。加上ImageBase才是在内存中的真正地址.
4. SizeOfRawData 节在文件中对齐后的尺寸.
5. PointerToRawData 节区在文件中的偏移.
6. PointerToRelocations 在obj文件中使用 对exe无意义
7. PointerToLinenumbers 行号表的位置 调试的时候使用
8. NumberOfRelocations 在obj文件中使用 对exe无意义
9. NumberOfLinenumbers 行号表中行号的数量 调试的时候使用
10、Characteristics 节的属性
六. 导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 未使用
WORD MinorVersion; // 未使用
DWORD Name; // 指向该导出表文件名字符串
DWORD Base; // 导出函数起始序号
DWORD NumberOfFunctions; // 所有导出函数的个数
DWORD NumberOfNames; // 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
NumberOfFunctions:大于等于导出函数的个数。因为这个值 = .def末尾序号 - 首个序号,如果自定义的序号不是连续的,那么这个值自然会大于导出函数的个数。
末尾三个值都是RVA地址,可以写一个RVA ---> FOA的转换函数。AddressOfNames表里的地址也是RVA的,同样需要转换。
Base:为导出函数的其实序号,即给出的序号中第一个、最小那一个。
找(导出)函数的两种方式:
1. 通过名字导出 AddressOfNames表中获得序号,找对应位置的AddressOfOrdinals位置的值,这个值就指向了AddressOfFunctions表的下标,这个位置存储的就是名字对应的函数地址。
2. 通过序号导出 这里所说的序号也是就.def文件中定义的序号。序号 - 第一个序号 = AddressOfFunctions表的下标。
注意:所有的地址都是RVA都需要转换为FOA。
导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字已0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
加载前:
加载后:
加载前后不同,要用INT表查IAT表。
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; 函数地址表索引,不是导出序号
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
重定位表
1、通过IMAGE_DATA_DIRECTORY结构的VirtualAddress
属性 找到第一个IMAGE_BASE_RELOCATION
2、判断一共有几块数据:
最后一个结构的VirtualAddress与SizeOfBlock都为0
3、具体项 宽度:2字节
也就是这个数据
内存中的页大小是1000H 也就是说2的12次方 就可以表示
一个页内所有的偏移地址 具体项的宽度是16字节 高四位
代表类型:值为3 代表的是需要修改的数据 值为0代表的是
用于数据对齐的数据,可以不用修改.也就是说 我们只关注
高4位的值为3的就可以了.
4、VirtualAddress 宽度:4字节
当前这一个块的数据,每一个低12位的值+VirtualAddress 才是
真正需要修复的数据的RVA
真正的RVA = VirtualAddress + 具体项的低12位
5、SizeOfBlock 宽度:4字节
当前块的总大小
具体项的数量 = (SizeOfBlock - 8)/2
绑定导入表
就是提前写好导入表,不用加载dll的时候现修改。用绑定导入表验证是否有绑定。
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
OffsetModuleName地址不是RVA,而是第一个DESCRIPTOR的值+OffsetModuleName。这么设计也许是为了每个结构节省2字节的内存吧。
之后看情况/心情更新