PE文件格式
PE文件是指32位可执行文件,也称PE32;64位的可执行文件称为PE+或PE32+,是PE文件的一种扩展形式
种类 | 主扩展名 |
---|---|
可执行系列 | EXE 或 SCR |
驱动程序系列 | SYS 或 VXD |
库系列 | DLL 或 OCX 或 CPL 或 DRV |
对象文件系列 | OBJ |
PE文件基本结构
1、从DOS头(DOS header)到节区头(Section header)是PE头部分,其下的节区合称PE体,文件中使用偏移(offset),内存中使用VA(Virtual Address 虚拟地址)来表示位置。
2、PE头与各节区的尾部都存在一个区域,称为NULL填充(NULL padding)。
VA & RVA
1、VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address 相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址。其中VA与RVA满足:RVA + ImageBase = VA
2、PE头内部信息大都以RVA形式存在。
RVA to RAW(内存地址与文件偏移地址的映射)
1、PE文件加载到内存时,每个节区都要准确完成内存地址与文件偏移间的映射,一般称为RVA to RAW,方法如下:
(1)查找RVA所在节区,即节区头的虚拟地址VirtualAddress
(2)使用简单的公式计算文件偏移(RAW)
2、根据IMAGE_SECTION_HEADER结构体,换算公式:
RAW - PointerToRawData = RVA - VirtualAddress,其中PointerToRawData是磁盘上的PE文件节区偏移地址,VirtualAddress是内存上的PE文件节区相对基准位置ImageBase偏移地址。
案例:
若RVA = 5000时,则RAW = 5000(RVA) - 1000(VirtualAddress) + 400(PointerToRawData)
PE文件头
DOS头
typedef struct _IMAGE_DOS_HEADER{
WORD e_magic; //DOS signature:4D5A("MZ")
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_oemind;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; //offset to NT(IMAGE_NT_HEADER) header
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_DOS_HEADER结构体大小为64(0x3CH)字节,其中e_magic指示DOS签名,e_lfanew指示NT头的偏移。
DOS存根
DOS存根(stub)在DOS头下方,是个可选项,且大小不固定,由代码与数据混合而成,灵活使用该特性可以在一个可执行文件中创建另一个文件,在DOS与Windows中都能运行(在DOS环境中运行16位DOS代码,在Windows环境中运行32Windows代码)。
NT头
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif
typedef struct _IMAGE_NT_HEADERS{
WORD Signature; //PE Signature:50450000("PE"00)
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEADERS32结构体由3个成员组成,第一个成员为签名Signature,值位50450000H,另外两个成员分别为文件头(File Header)与可选头(Option Header)结构体。
IMAGE_FILE_HEADER FileHeader文件头
typedef struct _IMAGE_FILE_HEADER{
WORD Machine;//每个CPU都拥有唯一的Machine码,兼容32位Intel x86芯片的Machine码为14C
WORD NumberOfSections;//指出文件中存在的节区数量,该值必须大于0
DWORD TimeDateStamp;//用于记录编译器创建此文件的时间
DWORD PointerToSymbolTable;//COFF符号指针,程序调试信息
DWORD NumberOfSymbols;//符号数
WORD SizeOfOptionalHeader;//指出IMAGE_OPTIONAL_HEADER32结构体的大小,PE+与PE对应的结构体大小不一样
WORD Charateristics;//标识文件的属性,是否是可运行、是否为DLL文件等信息,以bit OR形式组合
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
//Charateristics
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 //Relocation info stripped from file
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 //File is executable
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 //Line numbers stripped from file
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 //Local symbols stripped from file
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 //Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 //App can handle >2Gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 //Byte of machine word are reversed
#define IMAGE_FILE_32BIT_MACHINE 0x0100 //32 bit word machine
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 //Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 //If iamge is on removable media,copy and run from the swap file
#define IMAGE_FILE_RNET_RUN_FROM_SWAP 0x0800 //If image is on net,copy and run from the swap file
#define IMAGE_FILE_SYSTEM 0x1000 //System file
#define IMAGE_FILE_DLL 0x2000 //File is a DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 //File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED 0x8000 //Byte of machine word are reversed
IMAGE_OPTIONAL_HEADER32 OptionalHeader 可选头
typedef struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;//数据的起始地址(RVA)
DWORD Size;//对应的数据大小
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_OPTIONAL_HEADER{
WORD Magic;//为IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10B;为IMAGE_OPTIONAL_HEADER64结构体时,Magic码为20B
BYTE MajorLinkerVersion;//创建文件连接器的主板本号
BYTE MinorLinkerVersion;//创建文件连接器的次板本号
DWORD SizeOfCode;//所有具有IMAGE_SCN_CNT_CODE属性的节的总大小
DWORD SizeOfInitializedData;//所有包含已初始化数据的节的总大小
DWORD SizeOfUninitialiedData;//所有包含未初始化数据的节的总大小
//这个值总是0,因为连接器把未初始化的数据附加到常规数据节的末尾
DWORD AddressOfEntryPoint;//持有EP的RVA值,指出程序最先执行的代码起始地址
DWORD BaseOfCode;//第一个执行代码相对于ImageBase的位置
DWORD BaseOfData;
DWORD ImageBase;//PE文件被加载到内存时,指出文件的优先装入地址,EXE文件的ImageBase值为00400000H,DLL文件的ImageBase值为10000000
//执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase + AddressOfEntryPoint。
DWORD SectionAlignment;//指定节区在内存中的最小单位
DWORD FileAlignment;//指定节区在磁盘文件中的最小单位
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;//指定PE Image在虚拟内存中所占空间的大小
DWORD SizeOfHeader;//指出整个PE头的大小,该值必须是FileAlignment的整数倍
DWORD CheckSum;
WORD Subsystem;//区分系统驱动文件(*.sys)与普通的可执行文件(*.exe或*.dll)
//1:Driver文件,如ntfs.sys 2:GUI文件,如notepad.exe 3:CUI文件,如cmd.exe
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD NumberOfRvaAndSizes;//指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//每项数组都有不同的定义
}IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
//DataDirectory数组
DataDirectory[0] = EXPORT Directory
DataDirectory[1] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
DataDirectory[3] = EXCEPTION Directory
DataDirectory[4] = SECURITY Directory
DataDirectory[5] = BASERELOC Directory
DataDirectory[6] = DEBUG Directory
DataDirectory[7] = COPYRIGHT Directory
DataDirectory[8] = GLOBALPTR Directory
DataDirectory[9] = TLS Directory
DataDirectory[A] = LOAD_CONFIG Directory
DataDirectory[B] = BOUND_IMPORT Directory
DataDirectory[C] = IAT Directory
DataDirectory[D] = DELAY_IMPORT Directory
DataDirectory[E] = COM_DESCRIPTOR Directory
DataDirectory[F] = Reserved Directory
节区头
//每个节区头大小为40字节
#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;//内存中节区起始地址(RVA)
DWORD SizeOfRawData;//磁盘文件中节区所占大小
DWORD PointerToRawData;//磁盘文件中节区起始位置
DWORD PointerToRelocations;//段重定位表在文件中的位置
DWORD PointerToLineNumbers;//段的行号表在文件中的位置
WORD NumberOfRelocations;//
WORD NumberOfLineNumbers;//
DWORD Characteristics;//节区属性(bit OR)
}IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
//Charateristics
#define IMAGE_SCN_CNT_CODE 0x00000020 //Section contains code
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 //Section contains initialized data
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 //Section contains uninitialized data
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 //Section is executable
#define IMAGE_SCN_MEN_READ 0x40000000 //Section is readable
#define IMAGE_SCN_MEM_WRITE 0x80000000 //Section is writable
1、VirtualAddress与PointerToRawData可以不带任何值,分别由(IMAGE_OPTIONAL_HEADER32)中的SectionAlignment与File Alignment确定。
2、Name成员不一定需要以NULL结束,也不一定是ASCII值
PE文件格式的应用
1、加载DLL的方式有两种:一种是“显式链接”(Explicit Linking),程序使用DLL是加载,使用完毕后释放内存;另一种是“隐式加载”(Implicit Linking),程序开始时一同加载DLL,程序终止时再释放占用内存。IAT提供的机制与隐式链接有关。
2、
IAT导入地址表
1、IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构有关。
2、程序编译时,并不知道该程序要运行在那种Windows(7、XP),哪种语言(ENG、JPN),哪种服务包(ServicePack)下,因此,内核重要的DLL(如kernel32.dll)的版本不一样,对应的DLL中API函数地址也不相同;为了确保在所有环境中都能正常调用DLL中的API函数,编译器要预先保存API函数实际地址的位置,并记下CALL DWORD PTR DS:[Address]指令形式,其中Address是IAT数组中的某一项地址,执行文件时,PE装载器将对应的API函数地址写到Address位置上。
3、DLL加载时,无法保证被加载到PE头指定的ImageBase地址处,需要DLL重定位(Windows系统的DLL文件拥有自身固定的ImageBase)。
4、EXE文件拥有独立的虚拟空间地址,因此能够准确加载到自身的ImageBase中。
5、微软制作服务包过程中重建相关系统文件,此时会硬编入准确地址(普通的DLL实际地址不会被硬编码到IAT中,通常带有与INT相同的值)。
//IMAGE_IMPORT_DESCRIPTOR结构体记录着PE文件要导入哪些库文件
//导入多少个库,就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成数组,且结构体数组最后以NULL结构体结束
//IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值就是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;//INT(Import Name Table) address(RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;//Library name string address(RVA)
DWORD FirstThunk;//IAT(Import Address Table) address(RVA)
}IMAGE_IMPORT_DESCRIPTOR;
//OriginalFirstThunk、FirstThunk指向的IAT、INT为长整形(4个字节数据类型)数组,以NULL结束
//INT数组中各元素的值是IMAGE_IMPORT__BY_NAME结构体指针
//IAT数组是4个字节的指针数组,指向IMAGE_THUNK_DATA类型,以NULL结束,指向DLL中的API函数地址
//INT数组和IAT数组大小应相同
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;//ordinal address(RVA),指向序号地址
//指向库中函数固有编号地址,在Ordinal序号的后面为函数名称字符串(以'\0'结束)
BYTE Name[1];//function name string,基本为0000H形式存储
}IMAGE_IMPORT__BY_NAME, *PIMAGE_IMPORT__BY_NAME;
//有多少个函数被导入,就有多少个元素,以NULL结尾
typedef struct _IMAGE_THUNK_DATA32{
union{
DWORD ForwarderString; //一个RVA地址,指向forward string
DWORD Function; //被导入的函数的入口地址
DWORD Ordinal; //该函数的序号
DWORD AddressOfData; //一个RVA地址,指向IAMGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;
图中,INT与IAT的各元素指向相同地址,但是实际很多情况下它们指向的地址不一致的。
IAT每个元素对应一个被导入的符号,元素的值在不同情况有不同的含义,在动态链接器刚完成映射还没有开始重定位和符号解析时,IAT中的元素值表示相对应的导入符号的序号或者符号名,当动态链接器完成该模块的链接时,元素值被动态链接器改写成该符号的真正地址。如若元素值最高位为1,则低31位值就是导入符号的序号值,如若没有,那么元素的值指向IMAGE_IMPORT__BY_NAME结构体的RVA
PE装载器把导入函数输入至IAT的顺序
- 读取IMAGE_IMPORT_DESCRIPTOR结构体的Name成员,获取库名称字符串(如"kernel32.dll")
- 装载相应库–>LoadLibrary(“kernel32.dll”)
- 读取IMAGE_IMPORT_DESCRIPTOR结构体的OriginalFirstThunk成员,获取INT地址
- 逐一读取INT数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
- 使用IMAGE_IMPORT_BY_NAME的Hint(Ordinal)或Name成员,获取相应函数的起始地址–>GetProcAddress(“GetCurrentThreadld”)从EAT中获取实际地址
- 读取IMAGE_IMPORT_DESCRIPTOR结构体的FirstThunk(IAT)成员,获取IAT数组地址
- 将第5步获得的函数地址输入相应IAT数组中
- 重复步骤4~7,直到INT结束(遇到NULL)
EAT导出地址表
1、只有通过EAT才能准确求得从相应库中导出函数的起始地址。
2、PE文件内的特定结构体(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明EAT导出地址表的IMAGE_EXPORT_DIRECTORY结构体。
3、用来说明IAT导入地址表的IMAGE_EXPORT_DIRECTORY结构体以数组形式存在,且拥有多个成员。
4、从库中获取函数地址的API为GetProcAddress()函数,该API引用EAT来获取指定API的地址。
//IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress的值就是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址
//结构体数组可能同时包含 一个EAT的IMAGE_EXPORT_DIRECTORY结构体 和 多个IAT的IMAGE_EXPORT_DIRECTORT结构体,且结构体数组最后以NULL结构体结束
//
typedef struct _IMAGE_EXPORT_DIRECTORY{
DWORD Charateristics;
DWORD TimeDateStamp;//creation time date stamp,创建时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;//address of library file name,库文件名称地址
DWORD Base;//ordinal base
DWORD NumberOfFunctions;//numbers of functions,实际导出函数的数量
DWORD NumberOfNames;//number of names,导出函数有具体名称的函数个数
DWORD AddressOfFunctions;//address of function start address array,导出函数地址数组(RVA)
//4个字节数据数组,保存函数起始地址,用于填充IAT数组内容
//数组元素个数 == NumberOfFunctions
DWORD AddressofNames;//address of function name string array,函数名称地址数组(RVA)
//4个字节指针数组,数组元素个数 == NumberOfNames
DWORD AddressOfNameOrdinals;//address of ordinal array,序号地址数组(RVA)
//2个字节数据数组,数组元素个数 == NumberOfNames
}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
GetProcAddress()操作原理
9. 利用AddressOfNames成员转到“函数名称数组”
10. “函数名称数组”中存储着字符串地址,通过比较(strcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index)
11. 利用AddressOfNameOrdinals成员,转到ordinal数组
12. 在ordinal数组中通过name_index查找相应的ordinal值
13. 利用addressOfFunctions成员转到“函数地址数组(EAT)
14. 在“函数地址数组”中将刚刚获得的ordinal用作数组索引,获得指定函数的起始地址
15. 指定函数的实际地址 = DLL的实际加载基址 +DLL指定函数的起始地址
**提示:**对于没有函数名称的导出函数,可以通过Ordinal查找到它们的地址。从Ordinal值重减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为“函数地址数组”的索引,即可查找到相应的函数地址。
TLS(Thread Local Storage 线程局部存储)回调函数
1、TLS回调函数的运行要先于EP代码的执行,常用于反调试;
2、TLS是各线程的独立的数据存储空间;
3、TLS回调函数,每当创建/终止进程的线程时会自动调用执行的函数,创建/终止进程的主线程也会自动调用回调函数,且其调用执行先于EP代码;
//IMAGE_OPTIONAL_HEADER32.DataDirectory[9].VirtualAddress的值就是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址
typedef struct _IMAGE_TLS_DIRECTORY{
DWORD StarAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex;
DWORD AddressOfCallBacks;//指向含有TLS回调函数地址(VA)的数组,该数组以NULL结束
//进程启动运行时,(执行EP代码前)系统会一一调用存储在该数组中的函数
DWORD SizeOfZeroFill;
DWORD Characteristics;
}IMAGE_TLS_DIRECTORY, *PIMAGE_TLS_DIRECTORY;
//TLS回调函数的定义,与DLL中的DllMain()函数定义类似
typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) (PVOID DllHandle, DWORD Reason, PVOID Reserved);
//回调函数的Reason表示调用TLS回调函数的原因
#define DLL_PROCESS_ATTACH 1 //进程的主线程调用main()函数前,已经注册的TLS回调函数会先被调用执行
#define DLL_THREAD_ATTACH 2 //main()函数开始调用执行,创建用户线程前,TLS回调函数会被再次调用执行
#define DLL_THREAD_DETACH 3 //线程函数执行完毕,TLS回调函数被调用执行
#define DLL_PROCESS_DETACH 0 //main()函数终止时,TLS回调函数最后一次被调用执行
TEB(Thread Environment Block 线程环境块)
1、TEB指线程环境块,包含进程中运行线程的各种信息,进程中的每个进程都对应一个TEB结构体;
2、在不同Window OS下,TEB结构体的成员不一样;
3、offset 0x00 是NtTib成员结构体,意为“线程信息块”
4、offset 0x30 是ProcessEnvironmentBlock成员,指向PEB(Process Environment Block进程环境块),每个进程对应一个PEB结构体。
//TEB访问方法
//1、Ntdll.NtCurrentTeb()用来返回当前线程的TEB结构体的地址;
//2、FS段寄存器用来指示当前线程的TEB结构体,是根据持有SDT的索引,其实SDT位于内核内存区域,其地址存储在特殊的寄存器GDTR(Global Descriptor Table Resigter全局描述符表寄存器)中。
//FS:[0x00] = SEH起始地址
//FS:[0x18] = TEB起始地址
//FS:[0x00] = PEB起始地址
typedef struct _TEB{
BYTE Reserved1[1952];
PVOID Reserved2[412];
PVOID TlsSlots[64];
BYTE Reserved3[8];
PVOID Reserved4[26];
PVOID ReservedForOle;
PVOID Reserved5[4];
PVOID TlsExpansionSlots;
}TEB, *PTEB;
typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;//指向_EXCEPTION_REGISTRATION_RECORD结构体组成的链表
//用于Window OS的SEH
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union{
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
}NT_TIB;
typedef NT_TIB *PNT_TIB;
SDT索引TEB结构体
PEB(Process Environment Block 线程环境块)
1、PEB是存放进程信息的结构体,尺寸非常大;
2、在不同Window OS下,PEB结构体的成员不一样;
3、offset 0x02 BeingDebugged:Uchar 指示该进程是否正在调试状态
4、offset 0x08 ImageBaseAddress:Ptr32 Void 进程被加载的ImageBase
5、offset 0x0C Ldr:Ptr32 _PEB_LDR_DATA 指向_PEB_LDR_DATA结构体指针,当模块DLL加载到进程后,通过PEB.Ldr成员可以直接获取该模块的加载地址
6、offset 0x18 ProcessHeap:Ptr32 Void 应用于反调试技术
7、offset 0x68 NtGlobalFlag:Uint4B 应用于反调试技术
//PEB访问方法
//1、直接获取PEB地址:MOV EAX, DWORD PTR FS:[30]
//2、先获取TEB地址,再通过ProcessEnvironmentBlock成员(+30H偏移)获取PEB地址
//FS:[0x00] = SEH起始地址
//FS:[0x18] = TEB起始地址
//FS:[0x30] = PEB起始地址
typedef struct _PEB{
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
BYTE Reserved4[104]
PVOID Reserved5[52];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
}PEB, *PPEB;
//双向链表结构体
typedef struct _LIST_ENTRY{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
}LIST_ENTRY, *pLIST_ENTRY;
//每个加载到进程中的DLL模块都有与之对应的_LDR_DATA_TABLE_ENTRY结构体,这些结构体相互链接,最终形成_LIST_ENTRY双向链表
typedef struct _LDR_DATA_TABLE_ENTRY{
PVOID Reserved1[2];
_LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
Unicode_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved[3];
union{
ULONG CheckSum;
PVOID Reserved6;
}
ULONG TimeDateStamp;
}LDR_DATA_TABLE_ENTRY, *pLDR_DATA_TABLE_ENTRY;
//_PEB_LDR_DATA结构体
typedef struct _PEB_LDR_DATA{
UINT Length;
BYTE Initialized;
PVOID Sshandle;
_LIST_ENTRY InLoadOrderModuleList;
_LIST_ENTRY InMemoryOrderModuleList;
_LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
BYTE ShutdownInProgress;
PVOID ShutdownThreadId;
}PEB_LDR_DATA, *pPEB_LDR_DATA;
SEH异常处理机制
1、进程正常运行时,若发生异常,OS会委托进程处理,若进程代码中存在具体的异常处理(SEH异常处理)代码,则能顺利处理相关异常,程序继续运行,但如果进程内部没有实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行。
2、进程调试运行时,若被调试进程内部发生异常,OS会首先把异常抛给调试进程处理,调试器几乎拥有被调试者的所有权限,不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限(也就是说被调试者内部发生的所有异常都由调试器处理),所以调试过程中发生的所有异常都要先交给调试器管理(被调试者的SEH依据优先顺序推给调试器)。被调试者发生异常时,调试器会暂停运行,必须处理异常,完成后继续调试,可以通过直接修改异常代码、寄存器、内存等。
3、将异常抛给被调试者处理:如果被调试者内部存在SEH(异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理,与正常运行时的异常处理方式一样。
4、OS默认的异常处理机制:若调试器与被调试者都无法处理或故意不处理当前发生的异常,则OS的默认异常处理机制会处理它,终止被调试进程,同时结束调试。
5、SEH大量应用于压缩器、保护器、恶意程序(Malware),用来反调试。
//操作系统定义的异常
#define EXCEPTION_DATATYPE_MISALIGNMENT (0x80000002)
#define EXCEPTION_BREAKPOINT (0x80000003)//CPU尝试运行设置断点的代码发生异常,
//设置断点命令的汇编指令为INT3,机器指令为0xCC
//CPU运行代码,若遇到汇编指令INT3,触发EXCEPTION_BREAKPOINT异常
#define EXCEPTION_SINGLE_STEP (0x80000004)//CPU进入单步模式后,每执行一条指令引发异常
#define EXCEPTION_ACCESS_VIOLATION (0x80000005)//试图访问不存在或不具访问权限的内存区域
#define EXCEPTION_IN_PAGE_ERROR (0x80000006)
#define EXCEPTION_ILLEGAL_INSTRUCTION (0x8000001D)//CPU遇到无法解析的指令时引发异常
#define EXCEPTION_NONCONTINUABLE_EXCEPTION (0x80000025)
#define EXCEPTION_INVALID_DISPOSITION (0x80000026)
#define EXCEPTION_ARRAY_BOUNDS_EXCEEDED (0x8000008C)
#define EXCEPTION_FLT_DENORMAL_OPERAND (0x8000008D)
#define EXCEPTION_FLT_DIVIDE_BY_ZERO (0x8000008E)
#define EXCEPTION_FLT_INEXACT_RESULT (0x8000008F)
#define EXCEPTION_FLT_INVALID_OPERATION (0x80000090)
#define EXCEPTION_FLT_OVERFLOW (0x80000091)
#define EXCEPTION_FLT_STACK_CHECK (0x80000092)
#define EXCEPTION_FLT_UNDERFLOW (0x80000093)
#define EXCEPTION_INT_DIVIDE_BY_ZEARO (0x80000094)//除法运算,分母为0时引发异常
#define EXCEPTION_INT_OVERFLOW (0x80000095)
#define EXCEPTION_PRIV_INSTRUCTION (0x80000096)
#define EXCEPTION_STACK_OVERFLOW (0x800000FD)
//SEH以链形式存在,第一个异常处理器中若未处理相关异常,就会被传递到下一个异常处理器,直到得到处理
typedef struct _EXCEPTION_REGISTERATION_RECORD{
PEXCEPTION_REGISTERATION_RECORD Next;//指向下一个_EXCEPTION_REFISTERATION_RECORD结构体
//若Next = 0xFFFFFFFF,则表示链表最后一个节点
PEXCEPTION_DISPOSITION Handler;//异常处理函数(异常处理器)
}EXCEPTION_REGISTERATION_RECORD, *PEXCEPTION_REGISTERATION_RECORD;
//异常处理器函数定义,是一个回调函数,返回PEXCEPTION_DISPOSITION枚举类型
PEXCEPTION_DISPOSITION _except_handler(
EXCEPTION_RECORD *pRecord,
EXCEPTION_REGISTERATION_RECORD *pFrame,
CONTEXT *pContext,
PVOID pValue);
//EXCEPTION_RECORD结构体
#define EXCEPTION_MAXIMUM_PARAMETERS 15
typedef struct _EXCEPTION_RECORD{
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]
}EXCEPTION_RECORD, *PEXCEPTION_RECORD;
//CONTEXT结构体的定义,用来备份CPU寄存器的值。每个线程内部都拥有1个CONTEXT结构体
//CPU暂时离开当前线程运行其他线程时,CPU寄存器的值就会保存到当前线程的CONTEXT结构体中,当CPU再次运行该线程时,使用CONTEXT中的值恢复CPU寄存器的值,并从之前暂停处继续运行。
#define MAXIMUM_SUPPORTED_EXTENSION 512
struct CONTEXT{
DWORD ContextFlags;
DWORD Dr0; //0x04h
DWORD Dr1; //0x08h
DWORD Dr2; //0x0Ch
DWORD Dr3; //0x10h
DWORD Dr6; //0x14h
DWORD Dr7; //0x18h
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs; //0x8Ch
DWORD SegFs; //0x90h
DWORD SegEs; //0x94h
DWORD SegDs; //0x98h
DWORD Edi; //0x9Ch
DWORD Esi; //0xA0h
DWORD Ebx; //0xA4h
DWORD Edx; //0xA8h
DWORD Ecx; //0xACh
DWORD Eax; //0xB0h
DWORD Ebp; //0xB4h
DWORD Eip; //0xB8h
DWORD SegCs; //0xBCh
DWORD EFlags; //0xC0h
DWORD Esp; //0xC4h
DWORD SegSs; //0xC8h
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION ];
}
//PROCESS_INFOMATION结构体定义
typedef struct _PROCESS_INFOMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFOMATION, *LPROCESS_INFOMATION;
//PEXCEPTION_DISPOSITION枚举类型
typedef enum _EXCEPTION_DISPOSITION{
ExceptionContinueExecution = 0, //继续执行异常代码
ExceptionContinueSearch = 1, //运行下一个异常处理器
ExceptionNestedException = 2, //在OS内部使用
ExceptionCollidedUnwind = 3 //在OS内部使用
}EXCEPTION_DISPOSITION;
- 异常发生时,执行异常代码的线程就会中断运行,转而运行SEH(异常处理器/异常处理函数),此时OS会把线程的CONTEXT结构体的指针传递给异常处理函数的相应参数,在CONTEXT结构体的Eip成员(偏移量:B8),将会被设置为其他地址,然后返回异常处理函数,这样,被暂停的线程就会执行新设置的EIP地址处的代码。
- 异常处理器处理异常后返回ExceptionContinueExecution(0),从发生异常的代码处继续运行,若当前异常处理器无法处理异常,则返回ExceptionContinueSearch(1),将异常派送到SEH链的下一个异常处理器。
//SEH安装方法,也就是将自身的EXCEPTION_REGISTERATION_RECORD结构体链接到EXCEPTION_REGISTERATION_RECORD结构体链表中
//在C语言中使用__try、__except、__finally关键字就可以向代码添加SEH
//在汇编语言中添加SEH的方法
PUSH @MyHandler //异常处理函数
PUSH DWORD PTR FS:[0] //SEH链头
MOV DWORD PTR FS:[0], ESP //添加链表
PE重定位操作原理
PE重定位的基本操作原理
- 在应用程序中查找硬编码的地址位置,可以通过基址重定位表查找
- 读取值后,减去ImageBase(VA --> RVA)
- 加上实际加载地址(RVA --> VA)
PE加载器通过对一个IMAGE_BASE_RELOCATION结构体(位于节区.reloc)所有的TypeOffset重复上述处理,根据实际加载的内存地址修正后,将得到的值覆盖到同一位置。
基址钟定位表
基址重定位表地址位于PE头的DataDirectory数组的第六个元素(数组索引为5):IMAGE_OPTIONAL_HEADER32.DataDirectory[5].VirtualAddress的值就是IMAGE_BASE_RELOCATION结构体数组的起始地址
IMAGE_BASE_RELOCATION结构体
//若TypeOffset值为0,则表明一个IMAGE_BASE_RELOCATION结构体结束
//重定位表数组以NULL结构体结束
typedef struct _IMAGE_BASE_RELOCATION{
DWORD VirtualAddress;//基准地址,指示TypeOffset数组偏移地址的基准地址(起始地址)
DWORD SizeOfBlock;//指重定位块的大小,以数组形式存在,块末端显示为0
// WORD TypeOffset[1];//该数组不是结构体成员,注释形式存在
//表示该结构体之下会出现WORD类型数组,并且该数组元素的值就是硬编码在程序中的地址偏移
//TypeOffset值为2个字节,是由4位Type和12位的Offset合成
//其中高4位,PE文件常见值为3(IMAGE_REL_BASE_HIGHLOW),64位的PE+常见值为A(IMAGE_REL_BASE_DIR64)
//TypeOffset低12位是真正的位移,基于VirtualAddress的偏移
//程序中硬编码地址的偏移(RVA) = VirtualAddress + (TypeOffset & 0x0FFF)
}IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;
#define IMAGE_REL_BASE_ABSOLUTE 0
#define IMAGE_REL_BASE_HIGH 1
#define IMAGE_REL_BASE_LOW 2
#define IMAGE_REL_BASE_HIGHLOW 3
#define IMAGE_REL_BASE_HIGHADJ 4
#define IMAGE_REL_BASE_MIPS_JMPADDR 5
#define IMAGE_REL_BASE_MIPS_JMPADDR16 9
#define IMAGE_REL_BASE_IA64_IMM64 9
#define IMAGE_REL_BASE_DIR64 10
基址重定位表的分析方法
1、根据重定位表,查找程序中需要重定位的位置RVA = VirtualAddress + (TypeOffset & 0x0FFF)
2、读取硬编码地址的值后,减去ImageBase值(VR --> RVA)
3、加上实际加载地址(RVA --> VA)
4、将得到的值覆盖到重定位的位置上
5、对一个IMAGE_BASE_RELOCATION结构体的所有TypeOffset重复步骤1~4,直至TypeOffset的值为0
6、对PE文件中的所有IMAGE_BASE_RELOCATION结构体(衔接上一个结构体的TypeOffset的后面),重复步骤1~5,直至结构体为NULL(即最后一个结构体的TypeOffset为0时,后面连续有8个字节为0)
工具
PEView.exe
PEView.exe是一个分析PE文件的应用程序
UPack
Upack(Ultimate PE压缩器)是一款PE文件的运行时压缩器
Stud_PE
Stud_PE是一款分析PE文件的应用软件:http://www.cgsoftlabs.ro
DebugView
可以用来捕获并显示系统中运行的进程输出的所有调试字符串:https://technet.microsoft.com/en-us
PE Tools
一款功能强大的PE文件编辑工具,具有进程内存转储、PE文件头编辑、PE重建等功能,并且支持插件,带有插件编写示例:http://petools.org
CFF Explorer(重点使用)
提供多样化的功能,并且支持PE32+文件格式:https://ntcore.com,还提供PE编辑器、PE重建、RVA<–>RAW转换器、反汇编等综合功能。