本文是参考【保姆级教程】16 节吃透 Windows PE 文件格式!从解析到 Hook 攻防全覆盖 进行的个人总结
想要进一步学习的师傅很推荐去看看原视频以及该师傅的其它课程
DOS头
dos头中有两个字段是最关键的
e_magic :标识这是一个有效的 DOS 可执行文件格式。一般展现为MZ(4D 5A)的形式
e_lfanew:PE Header(NT Headers)在整个文件中的偏移位置。
如图所示
文件在硬盘和运行过程中不太一样,他在运行中的对齐值会比在文件中大
DOS Stub:当一个 PE 文件被老式 DOS 系统运行时,DOS Stub 负责显示一段提示;现代 Windows 加载 PE 时基本不执行它。
PE头/NT头部 1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS { DWORD Signature;//PE标志 IMAGE_FILE_HEADER FileHeader;//文件头 IMAGE_OPTIONAL_HEADER32 OptionalHeader;//可选 PE 头 }IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
标准PE头 1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_FILE_HEADER { WORD Machine;//程序允许的CPU型号如果为0表示能在任何CPU上允许 0x14C 386及后续 WORD NumberOfSections;//文件中存在的区段的数量 DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader;//可选 PE头的大小,32 位 pe文件默认 E0,64 位默认F0 WORD Characteristics;//文件属性,每个位有不同的含义。 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可选PE头 在x86平台下的 OPTIONAL_HEADER32 结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 typedef struct _IMAGE_OPTIONAL_HEADER32 { // 标准域 WORD Magic; //表示32位/64位PE文件 32位(0x10B) BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; //所有代码段的总大小,按照FileAlignment对齐 DWORD SizeOfInitializedData; //已初始化数据大小,按照FileAlignment对齐 DWORD SizeOfUninitializedData; //未初始化节数据大小,按照FileAlignment对齐 DWORD AddressOfEntryPoint; //程序执行入口OEP(偏移值) DWORD BaseOfCode; DWORD BaseOfData; // NT附加域 DWORD ImageBase; //程序载入基地址 // 4字节 DWORD SectionAlignment; //内存的对齐值 DWORD FileAlignment; //文件中的对齐值 WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; //文件在内存中的大小,按SectionAlignment;对齐 DWORD SizeOfHeaders; //DOS头+NT头+区段头,按SectionAlignment;对齐 DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; //数据目录表的数量16 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
实际SizeOfCode,SizeOfInitializedData,SizeOfUninitializedData; 只是作为展示,改变他们的值不影响程序运行
一般是第一行代码的执行地址,即odb拖入停下的地址,但是由于动态基址,编译时基址不会按照两值相加
DllCharacteristics中的DLLCHARACTERISTICS_DYNAMIC_BASE记录基址是否改变
可选PE头的大小不固定,32位的话时e0,64位的大小是f0。其它几个块都是固定的
区段头 区段头的个数是由区段决定的,区段头可看作是区段的脑袋,区段头存放的是下面区段的头部信息
如区段位置,区段大小等
区段头(Section Header)->一个“说明书/目录项”
区段(Section)->真正的代码、数据等内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//区段的名字,最长8字节,此处和字符串不一样,不会/0 结尾 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; //该区段在内存中的真实大小(未对齐) DWORD VirtualAddress;// 区段在内存中的偏移位置,+ImageBase为真实地址 DWORD SizeOfRawData;// 区段在文件中对齐后的大小 DWORD PointerToRawData;// 区段的在文件中的偏移位置 DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;//区段属性,是否可读可写可执行 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
C 字符串要求:字符 + 字符 + 字符 + \0
但 PE 的 Name[8] 是:8 个字节的固定长度字段,**协议规定这 8 个字节就是区段名称字段,并不要求一定以 \0 结尾。**也就是说只有8个字符空位,没有位置放置\0
Name[8] 更准确地说是:8 字节定长字符数组(fixed-length character field)
DOS Header、NT Header、Section Header构成:PE Headers ,其在文件和人内存中的偏移没有变化.他通过字段SizeOfHeaders对齐
当我们点击exe程序去运行时,会先复制文件对齐的各个部分,接着按内存对齐,增大区段头和区段1 之间的灰色部分让PE头+灰色的大小为0x1000,之后扩展每个区段间的白色部分,让各个区段大小以及开始地址都是FileAlignment的整数倍
数据目录表 一个 IMAGE_DATA_DIRECTORY 结构体数组,数组中的每一个元素描述一个 PE 数据目录。
这里以导入表为例
这个并不是导出表,只是存放导出表的位置和大小
size是表的大小,但是修改不影响
1 2 3 4 5 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress;//RVA 内存中导出表相对于PE映像基址(ImageBase)的偏移。 // FOA+FileBuff=文件中导出表的偏移位置 DWORD Size; } IMAGE_DATA_DIRECTORY;
导出表 数据目录表索引是0
导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。
可选PE头的DataDirectory第0项即为我们的导出表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; //指向导出表文件名 RVA -->FOA+FileBuff=char *name; DWORD Base; // 导出函数的起始序号 DWORD NumberOfFunctions; // 导出函数个数 DWORD NumberOfNames; // 以名称导出函数的总数 DWORD AddressOfFunctions; // 导出函数地址表的RVA DWORD AddressOfNames; // 函数名称地址表的RVA DWORD AddressOfNameOrdinals; // 函数名序号表的RVA } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
但NumberOfFunctions!=导出函数个数,他等于导入函数的最大序号-最小序号+1
以下面这个def为例,NumberOfFunctions=7-2+1=6
NumberOfNames由于div是noname,它的数值是2
1 2 3 4 5 LIBRPARY "mydll" EXPORTS sum @2 div @3 NONAME fun1 @7
AddressOfFunctions/AddressOfNames/AddressOfNameOrdinals;
已知函数名字,通过AddressOfNames知道函数名字编号
接着通过AddressOfNameOrdinals,把名字编号对应到地址编号
最后通过AddressOfFunctions,用地址编号拿到RVA
三个表可以相互调用,名称->地址,地址->名称都是可以的
函数的 Ordinal(序号) = 地址编号 + Base。
这里解释一下
图中所标注的WORD/DWORD是指表中所存储元素的大小
结构体中的DWORD代表对应表的起始 RVA大小
RVA转FOA 一个 Section(区段) 同时有两个位置:
Section RVA:Section 在内存映像中的起始位置 Section FOA:Section 在 PE 文件中的起始位置
假设:
Section RVA = 0x1000 数据 RVA = 0x1500
那么数据距离 Section 开头:0x1500 - 0x1000 = 0x500
文件中:
Section FOA = 0x400 数据 FOA = 0x900
那么:0x900 - 0x400 = 0x500
所以:
1 数据RVA - SectionRVA=数据FOA - SectionFOA
原因:
虽然文件空间和内存映像空间的绝对位置不同,但是同一个数据相对于同一个 Section 开头的距离不变。
导入表 数据目录表索引是1
前置基础
1.调用dll文件函数原理 2.一个进程空间中的 exe dll文件如何被加载到内存的。 3.Exe文件调用的动态链接库在内存中与在硬盘中有什么不同。
设计c++代码调用dll文件中的sum函数
mydll.h
1 2 3 4 #pragma once #pragma comment (lib,"mydll.lib") extern "C" _declspec (dllimport) int sum (int a, int b); extern "C" _declspec (dllimport) void fun1 (char a, char c);
main.cpp
1 2 3 4 5 6 7 #include<Windows.h> #include"mydll.h" int main () { int b = sum (1, 2); return 0; }
call mydll
RVA=0x5191BC-0x40000(imagebase)=0x1191bc
接着看区段头看他落在了哪个位置,看到是在区段[4],且区段起始RVA=0x119000 , 区段FOA=0xc9000
计算偏移0x1191bc-119000=0x1bc
再次加上区段的FOA就得到了他在文件中的位置=0xc9000+0x1bc=0xc91bc
追踪过去可以看到他存储的是0x119414
而我们的RVA=0x5191BC在内存中存储的是0x6073F35F,dll函数内部的一个地址
实际这里存储的0x119414也是一个偏移,由于他们是在一个区段中
其文件地址为0x119414-0x119000+0xc9000=0xc9414
如下图所示,文件的函数地址部分存储的是函数名称
exec程序执行的时候可通过函数名来找函数地址,这个过程是由操作系统去实现的
程序在调用dll文件函数时,并不是把dll文件函数的代码编译到当前文件中,而是把d11文件对应的函数地址保存到了当前文件中
对于32位的程序来说都有一个4GB的内存空间,高2G是内核空间,低2G 是应用空间、
在文件当中对应的函数地址部分存储的是函数名称。
HMODULE =LoadLibrayA =IMAGEBASE
先加载EXE文件,之后看exe中调用了什么dll文件,之后通过LoadLibray把对应的dll文件也加载进应用空间中就拿到了模块基地址
GetProcessAddr (HMODULE, fun1) 之后就是拿到地址并填入call dword ptr ds: [0x51918C]中[0x51918C]处
Exe文件调用的动态链接库在内存中是一个地址,在文件中是一个函数名,在操作系统把dll文件加载到内存的时候就会调用几个API把它转化为地址
结构分析
一个导入表只有一份,但是一个导出表有多份,因为一个文件可能引入多个dll文件
引入几个文件就会有几个typedef struct _IMAGE_IMPORT_DESCRIPTOR
可以把IMAGE_IMPORT_DESCRIPTOR理解成 描述“当前 PE 文件需要哪个 DLL,以及这个 DLL 的导入信息在哪里”。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk;//INT导入名称表RVA->_IMAGE_THUNK_DATA32 } DUMMYUNIONNAME; DWORD TimeDateStamp;//时间戳/当可执行文件不与被输入的DLL进行绑定时,此字段为0 DWORD ForwarderChain; DWORD Name; //指向DLL的RVA DWORD FirstThunk; //IAT导入地址表rva,未运行时是函数名,运行后是函数地址 } IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
INT 不是一个单独的数据。
它是一组:IMAGE_THUNK_DATA32。对应一个导入函数
1 2 3 4 5 6 7 8 typedef struct _IMAGE_THUNK_DATA32 {//或IMAGE_THUNK_DATA64 union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32;
这个结构体大小是:4 字节。因为里面是一个 DWORD
该函数不只是存储的名称,也可以存储函数序号,如果最高位为1就是按照序号导入的,否则就是按照函数名RVA
第一位作为用来区分按序号/函数名导入,之后的31位就是存储的序号
之后通过 AddressOfData 指向 IMAGE_IMPORT_BY_NAME,其里面存贮真正的函数名字
1 2 3 4 typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint;//不同编译器放置的内容不一 CHAR Name[1];//真正的函数名 } IMAGE_IMPORT_BY_NAME;
INT调用流程
OriginalFirstThunk //imagebase+OriginalFirstThunk=文件中INT的位置 ↓ INT ↓dll IMAGE_THUNK_DATA //找到对应函数 ↓ IMAGE_IMPORT_BY_NAME//name字段存储函数名 ↓ “sum”
1 2 3 4 5 6 7 8 9 10 INT IAT │ │ ↓ ↓ THUNK THUNK │ │ ↓ ↓ IMAGE_IMPORT_BY_NAME 导入项 │ ↓ "sum"
之后就是去导出表中进行下一步操作
有一种情况是IAT在文件中也存储的是一个dll真实地址,但是要满足所有的dll文件的位置都是确定的,否则会冲突
当TimeDateStamp为0时,IAT指向导入名称表
若不为0,IAT中就会存储真实的地址
重定位表 数据目录表索引是5
重定位表主要用于解决:EXE/DLL 实际加载地址和编译时的首选加载地址不一致时,修正程序内部保存的绝对地址。
1 2 3 4 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; // PageRVA;当前页起始RVA(相对模块基址) DWORD SizeOfBlock; // 本块总字节数(包含本结构体 + 后面所有TypeOffset数组) } IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
重定位表由一串连续的重定位块串联组成,每一个重定位块对应一个 4KB(0x1000 )的内存页,每个块头部保存当前页面的起始 RVA 与本块总字节大小,蓝色块VirtualAddress=0x4000,代表这一页从 RVA=0x4000 开始;头部之后紧跟着多个 16bit 的 TypeOffset 单元,用来记录本页面内部需要重定位的偏移,页内偏移分别为 0x02、0x18、0x88,和页面起始 RVA 相加就能得到完整 RVA(0x4002、0x4018、0x4088);所有落在同一个 4KB 页面范围内的待重定位位置,都会被收纳到同一个重定位块中,不同页面的数据则放到下一个新的重定位块。
TypeOffset是16位WORD
高 4bit:重定位类型
低 12bit:页内偏移
如0011 011101111010:高4位:0011=3,低12位:000000000010页内偏移是2
此时目标 RVA = 块的 VirtualAddress (0x4000) + 页内偏移 (0x2) = 0x4002
但是拿到的地址不一定是需要修复的,看它的高4位作为标识,当他的十进制值等于3时才代表他需要被修复
TLS 数据目录表索引是0
TLS(Thread Local Storage,线程局部存储,PE 中的 TLS 目录)
一句话概括:TLS 是让每个线程拥有自己私有变量副本的机制;PE 文件里的 TLS 目录,用来告诉 Windows 加载器,在创建线程时自动分配、初始化这份线程私有数据
进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个线程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。优点:线程的数据交换变得非常快捷。缺点:多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的 BUG。 如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是 TLS
1 2 3 4 5 6 7 8 typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; // VA:TLS模板数据起始地址,无TLS变量填0 DWORD EndAddressOfRawData; // VA:TLS模板数据结束地址,无TLS变量填0 DWORD AddressOfIndex; // VA:TLS索引变量地址,不用填0 DWORD AddressOfCallBacks; // VA:TLS回调函数指针数组的VA,数组以NULL结尾 DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY32, *PIMAGE_TLS_DIRECTORY32;
如何创建TLS变量__declspec(thread) int g_number=100;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <Windows.h> #include <iostream> __declspec(thread) int g_number=100; HANDLE hEvent = NULL; DWORD WINAPI threadProc1(LPVOID lpParam) { g_number = 200; printf("thread 1: %d\n", g_number); SetEvent(hEvent); return 0; } DWORD WINAPI threadProc2(LPVOID lpParam) { HANDLE hEvent = NULL; WaitForSingleObject(hEvent,-1); printf("thread 2: %d\n", g_number); return 0; } int main() { hEvent=CreateEventA(NULL,FALSE,FALSE,NULL); //CreateThread`仅向操作系统申请创建线程,将线程置入就绪队列; // 不保证线程立刻执行,线程何时上 CPU 由 Windows 线程调度器决定。 HANDLE hThread1 = CreateThread(NULL, NULL, threadProc1, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, threadProc2, NULL, NULL, NULL); WaitForSingleObject(hThread2, -1); system("pause"); return 0; }
线程 1 只修改线自己的副本,修改为 200,不会改动线程 2 的副本。
线程 2 读取的是属于线程 2 自己的副本,没有被修改,所以读取结果始终是 100。
TLS回调函数 首先加上编译选项: 1、 #pragma comment(linker,"/INCLUDE:_tls_used") 2、 /*注册 TLS 函数.CRT$XLX 的作用 CRT 表示使用CRuntime 机制 X 表示表示名随机 L表示 TLS Callback section X也可以换成B~Y任意一个字符 */#pragma data_seg(".CRT$XLX") //存储回调函数地址 PIMAGE_TLS_CALLBACK pTLS CALLBACKs[] ={ TLS_CALLBACK1, TLS_CALLBACK2, 0 };#pragma data_seg()
TLS 回调不是普通函数,必须严格匹配 PIMAGE_TLS_CALLBACK 原型 ,并且要构造回调数组、填充 TLS 目录,让 PE 加载器识别。
约束:
函数名自定义,但调用约定、返回类型、参数类型不能改动 。
Reserved 是系统保留,只读,不要修改。
回调内部代码尽量简洁;不要随意调用复杂 API ,容易死锁。
且构造 TLS 回调指针数组末尾必须用 NULL 哨兵标记数组结束 ,否则系统越界访问内存。
1 2 3 4 PIMAGE_TLS_CALLBACK TlsCallbackArray[] = { TlsCallbackFunc, NULL // 终止哨兵,不可省略 };
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <Windows.h> #include <iostream> #pragma comment(linker,"/INCLUDE:__tls_used") __declspec(thread) int g_number=100; HANDLE hEvent = NULL; DWORD WINAPI threadProc1(LPVOID lpParam) { g_number = 200; printf("thread 1: %d\n", g_number); SetEvent(hEvent); return 0; } DWORD WINAPI threadProc2(LPVOID lpParam) { WaitForSingleObject(hEvent,-1); printf("thread 2: %d\n", g_number); return 0; } void NTAPI t_TlsCal1Back_A(PVOID D1lHandle, DWORD Reason, PVOID Reserved) //钩子函数的参数列表、参数类型、顺序、返回值、`WINAPI`调用约定,全部必须和原版 API 一模一样。 { printf("TLS Callback executed\n"); } /*注册 TLS 函数.CRT$XLX 的作用 CRT 表示使用CRuntime 机制 X 表示标识名随机 L表示 TLS Callback section X也可以换成B~Y任意一个字符 */ #pragma data_seg(".CRT$XLX") //存储回调函数地址 PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] ={ t_TlsCal1Back_A, 0 }; #pragma data_seg() int main() { hEvent=CreateEventA(NULL,FALSE,FALSE,NULL); //CreateThread`仅向操作系统申请创建线程,将线程置入就绪队列; // 不保证线程立刻执行,线程何时上 CPU 由 Windows 线程调度器决定。 HANDLE hThread1 = CreateThread(NULL, NULL, threadProc1, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, threadProc2, NULL, NULL, NULL); WaitForSingleObject(hThread2, -1); system("pause"); return 0; }
TLS 函数在以下四种情况都会被调用: #define DLL_PROCESS_ATTACH 1//进程创建时 #define DLL_THREAD_ATTACH 2 //线程创建时 #define DLL_THREAD_DETACH 3//线程销毁时 #define DLL_PROCESS_DETACH 0//进程销毁时
OD加载一个程序,程序最先执行的代码是OEP 但是TLS函数在OEP之前就执行了
所以很多人会把反调试加到TLS中,在真正执行函数之前就检测反调试并退出
内存管理 堆 每个进程都有自己独立的 4G 虚拟内存空间(32 位系统),高 2G 归操作系统内核,低 2G 给用户程序使用 。 进程里代码操作的全部是虚拟地址 ;虚拟地址→物理地址的转换,由操作系统内核 + CPU 的 MMU 内存管理单元一起完成。 所以正常情况下,不能直接用指针访问别的进程内存 (虚拟地址是进程私有的,别的进程相同虚拟地址指向完全不同的内容)。
Windows 会做内存共享(写时复制 COW) :多进程加载同一个 dll/exe,代码段在物理内存只保存一份 ,然后映射到每个进程各自的虚拟地址空间,节省物理内存。
当所有进程占用内存总和超过物理内存大小,操作系统会把物理内存里暂时不用的数据,换到硬盘上的页面文件(pagefile.sys) 。需要的时候再换回物理内存,这就是虚拟内存 / 页面置换 。
windows有三种管理内存的方式
堆(Heap)
适合分配大量小型对象。使用简单,由系统自动管理内存空间;缺点是效率偏低,内存控制不够灵活,容易产生内存碎片。常用 API:HeapAlloc,C 语言 malloc 底层基于堆实现。堆是操作系统在虚拟内存基础上封装实现的。
虚拟内存(Virtual Memory)
适合分配大型对象或大型数组。以内存页为单位操作,提供丰富接口,可以精确控制内存的状态与读写执行权限。常用 API:VirtualAlloc、VirtualFree、VirtualProtect。
文件映射(内存映射文件 File Mapping)
适合处理大型数据流,也用于本机不同进程间共享数据。可以把文件映射到进程虚拟地址空间,直接用指针访问内容,支持多进程共享内存。常用 API:CreateFileMapping、MapViewOfFile。
堆在 Windows 中以对象形式管理。我们可以手动创建堆对象,在该堆上分配、释放内存。C/C++ 的malloc、new底层都是依赖 Windows 堆对象完成内存分配。进程创建时,Windows 会自动给进程创建默认堆 。进程的默认堆不允许销毁 。一个进程可以有多个堆 (自己 HeapCreate 新建),但只有一个系统自带默认堆
API
功能
备注
HeapCreate
在进程内创建一个新的堆对象,返回堆句柄
创建后可被 HeapDestroy 销毁
GetProcessHeap
获取当前进程默认堆 的句柄
进程创建时系统自动生成
GetProcessHeaps
获取进程中全部堆 的句柄数组
含默认堆和所有新建堆
HeapAlloc
在指定堆上分配一块内存,返回指针
核心分配函数,对应HeapFree
HeapReAlloc
重新分配已分配的内存块(改大小)
可扩展或缩小内存块
HeapFree
释放堆上已分配的内存块
释放后指针失效
HeapSize
查询某内存块在堆中的大小
调试 / 校验用
HeapDestroy
销毁手动创建的堆对象
默认堆不可销毁
1 HeapCreate(建堆)→ HeapAlloc(分配)→ HeapReAlloc(调整)→ HeapFree(释放)→ HeapDestroy(销毁)
理解一下代码就好,这个用的不是很多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include<Windows.h> #include<iostream> int main() { //1. 创建一个堆对象 HANDLE hHeap = HeapCreate(0, 0, 0); //2. 申请堆内存 char* str = (char*)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, 10); strcpy_s(str, 10, "helloword"); printf("%s\n", str); HeapFree(hHeap, 0, str); HeapDestroy(hHeap); return 0; }
虚拟内存 操作系统采用分页机制管理内存,将虚拟地址空间划分为大小固定的内存页。32 位系统默认页面大小为 4KB(0x1000),4GB 虚拟地址空间一共包含 1M 个页面。
开启分页后,虚拟地址空间中的每一个页面,有三种状态:映射到物理内存、映射到硬盘上的页面交换文件,或者不映射到任何存储介质。应用程序 4GB 虚拟地址空间里,只有少量页面映射在物理内存中,大部分虚拟区域没有有效映射。
32 位下每个进程都拥有独立的 4GB 虚拟地址空间。不同进程可以使用相同的虚拟地址,但这些相同虚拟地址对应的物理内存可以完全不同。操作系统为每个进程维护一套独立的页目录与页表 ,虚拟地址到物理地址的映射关系就保存在这两级结构中。页表项记录虚拟页对应的位置:物理内存 / 硬盘交换文件 / 无映射。
CPU 地址转换流程:进程给出虚拟地址 → CPU 查询当前进程的页目录、页表 → 将虚拟地址翻译为物理地址,再访问物理内存条。
当物理内存资源紧张时,操作系统执行页面置换:把长时间较少访问的页面换出(swap out) ,写入硬盘的页面交换文件(pagefile.sys),释放物理页给正在频繁使用的页面。同时修改页表项标记,不再指向物理内存,而是标记页面数据存放在交换文件。当程序后续访问该页面时,触发缺页异常,操作系统将页面从交换文件**换入(swap in)**物理内存,更新页表映射,程序继续执行。
页交换文件逻辑:
程序访问虚拟内存地址,操作系统判断数据是否在内存中,如果在就从虚拟地址映射到的物理地址,如果不在就判断是否在页交换文件当中,如果在就查看物理内存是否有闲置空间,有的话,就将页交换文件载入到物理内存,如果没有闲置内存,就从物理内存中找到一个可以释放的页,然后将页保存到页交换文件中。
每个进程都拥有独立的页目录 ,页目录的物理地址会存入 CPU 的CR3寄存器。
页目录中的每一项,指向一张页表;页表里面存放虚拟页到物理页的映射条目(PTE,页表项) ,记录虚拟页面对应的物理页号、页面属性。
进程切换时,操作系统把当前进程页目录的物理地址写入 CR3,CPU 就拿到这个进程的整套映射表。
CPU 做地址翻译:虚拟地址 → 查询页目录 → 找到对应页表 → 查询页表项 → 得到物理地址。
页目录、页表本身存放在物理内存 中,不是放在虚拟地址空间。
Windows 虚拟内存页面有三种状态:空闲、保留、提交。
状态
说明
空闲(FREE)
内存页不可用,没有预定,也没有映射。
保留 (Reserve)
预定一段虚拟地址空间,没有映射到物理内存 ,内存仍然不可访问。只是占用虚拟地址,防止别的内存分配占用这片虚拟地址。
提交 (Commit)
虚拟内存页完成分配,建立了到物理内存(或页面文件)的映射,进程可以正常读写使用。
Windows 虚拟内存映射到物理内存分为三种方式:
类型
描述
补充考点
Private(私有内存)
进程私有内存,不会被其他进程共享;典型为堆、栈。
写时复制 COW,修改时会单独分配物理页,其他进程不受影响。
Mapped(文件映射 / 共享内存)
由其他内存或者文件映射生成,可以用于进程间共享内存。
调用CreateFileMapping、MapViewOfFile,多个进程可映射到同一块物理内存。
Image(映像内存)
从 PE 映像(exe、dll 文件)映射而来,程序加载时使用。
EXE/DLL 的代码段、只读数据段,多个进程加载同一个 DLL,共用同一份物理内存。
Private(私有内存):A和B和互不影响
Mapped(文件映射 / 共享内存):A和B和相互影响
Windows 虚拟内存管理最小单位是**内存页,默认 4KB(0x1000)**划分属性的最小单位
属性
含义
考点说明
ReadOnly
只读
只能读,尝试写入会触发异常;PE 文件的常量段常用。
READ_WRITE
读写
可读、可修改;进程堆、全局变量常用此属性。
EXECUTE
执行
内存里面是机器代码,可以运行;默认不可写 。
EXECUTE_READ_WRITE
可读可写可执行
RWX,最危险权限,恶意代码经常申请这种页面放 shellcode。
WRITE_COPY
写时拷贝(写时复制 COW)
页面只读共享,一旦进程尝试写入,系统复制一份独立页面给该进程,之后修改互不影响。
虚拟地址API
API
功能
考点说明
VirtualAlloc
在本进程 分配 / 预定一块虚拟内存
两种模式:1. 预定 (Reserve):只占虚拟地址,不分配物理内存2. 提交 (Commit):分配物理内存,建立映射
VirtualAllocEx
可在其他(远程)进程 分配 / 预定虚拟内存
注入常用 API,给目标进程开辟一块虚拟地址空间
VirtualFree
释放本进程 虚拟内存
可以只 Decommit(释放物理内存,保留虚拟地址预订),或者直接 Release 释放整个虚拟区域
VirtualFreeEx
释放其他进程 的虚拟内存
远程释放内存
VirtualLock
锁定内存,禁止页面被交换到硬盘 pagefile
锁定后的页面永久留在物理内存,不会被置换出去
VirtualUnlock
解锁内存
解除锁定,页面可以被操作系统换出到交换文件
VirtualProtect
修改本进程 内存页的读写执行权限属性
就是前面学的ReadOnly/READ_WRITE/RWX,以页 (4KB) 为单位修改
VirtualProtectEx
修改其他进程 内存页权限
注入必备:分配内存后改成可执行,存放 shellcode
ReadProcessMemory
读取远程进程内存数据
跨进程读内存
WriteProcessMemory
写入数据到远程进程内存
注入常用,把 shellcode 写入目标进程
VirtualQuery
查询本进程 内存区域状态
查询这块虚拟地址:预定 / 提交、页面属性、大小
VirtualQueryEx
查询其他进程 内存状态
远程查询内存信息
这里放一个代码可供调试观察
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include<Windows.h> #include<iostream> int main() { // VirtualAlloc:预定+提交1字节虚拟内存,页面属性读写 // 虽然申请大小写1,底层操作系统分配完整一整个4KB页面 char* str = (char*)VirtualAlloc(0, 1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); strcpy_s(str, 10, "helloword"); printf("%s\n", str); // MEM_RELEASE:释放整块虚拟地址区域(预定+提交全部回收) VirtualFree(str, 0, MEM_RELEASE); char name[] = "51hook"; // 栈上数组,字符串存放在栈内存,栈内存默认可写 char* cname = (char*)"51hook"; //字符串放在PE的只读代码段(.rdata),指针变量在栈上,指针指向的字符串内容不可写 name[1] = '2'; DWORD oldProtect = 0; // 修改cname指向页面的内存属性:改为PAGE_READWRITE读写,保存原来的页面权限到oldProtect // VirtualProtect修改粒度:整页4KB VirtualProtect(cname, 7, PAGE_READWRITE, &oldProtect); cname[1] = '2'; // 修改常量字符串,修改成功的前提是VirtualProtect修改页面权限成功 return 0; }
共享内存 文件映射(Mapping)是一种将文件内容映射到进程虚拟内存 的技术。
映射成功的文件可以用视图(View) ,来引用这段内存,从而达到操作位于此段内存中的文件的目的。
文件映射最大的一个特点:可以在不同的进程间共享数据(跨进程 IPC)
文件映射:建立虚拟内存地址和磁盘文件的映射绑定。修改这块虚拟内存,本质就是修改磁盘文件对应的区域(操作系统自动同步)
ReadFile 普通读写:把文件内容拷贝一份到虚拟内存缓冲区,改的只是这份副本;最后还要调用 WriteFile,再把副本拷贝回磁盘
Mapping 内核对象 = 内核里的一张 “登记卡片” ,记录一件事: 这块映射对应的后备存储(磁盘文件 / 页面文件)、大小、权限、名字(可选,用于进程间共享)。它不是文件本身,也不是内存视图 ,只是内核的一个记录。
CreateFileMapping / OpenFileMapping → 操作内核 Mapping 对象 (内核里的东西,还没绑定到你的进程虚拟地址) MapViewOfFile / UnmapViewOfFile → 操作当前进程虚拟地址空间 ,把映射挂到你的程序内存里 FlushViewOfFile → 强制把内存修改刷回磁盘文件
API
含义 + 大白话
什么时候用
CreateFileMapping
创建一个 Mapping 内核对象登记:我准备把某个文件做成内存映射,指定大小、页面权限
【进程 A,创建方】新建映射对象,可给对象起名字,供别的进程打开
OpenFileMapping
打开已经存在的 Mapping 内核对象
【进程 B,通信方】别的进程已经建好 Mapping,B 用名字打开这个现成对象(进程间共享内存核心 API)
MapViewOfFile
将 mapping 对象的文件映射到内存中👉生成视图 View :在当前进程虚拟地址空间分配一块虚拟地址,返回指针。拿到指针就能读写,就像普通内存
必须在 CreateFileMapping / OpenFileMapping 之后!没有调用这个,你拿不到内存指针,没法读写
UnmapViewOfFile
取消文件映射(销毁 View 视图)解绑虚拟地址和文件映射关系
用完内存指针之后调用。释放进程虚拟地址。⚠️ 注意:只是解除视图,Mapping 内核对象还在!
FlushViewOfFile
刷新缓存区,将映射在内存中的文件写回到硬盘中
可选!修改映射内存后,OS 默认延迟刷盘;这个 API 强制立刻把改动写入磁盘,防止程序崩溃丢数据
文件映射使用
在c++源码文件的同一目录下创建test.txt,随便给他个内容为
1 1111111111111111111111111111111111111111111111111111111111111111
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include<Windows.h> #include<iostream> int main() { //创建文件句柄 HANDLE hFile = CreateFileA("test.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); //创建Mapping对象 HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, GetFileSize(hFile, 0), NULL); //建立映射 char* fileBuff = (char*)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0); printf(" · "); return 0; }
直接在VS中修改,可以看到txt中的内容也会跟着改变
共享内存
即多个进程的修改可以相互影响
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include<Windows.h> #include<iostream> #include<cstdio> int main() { HANDLE hMapping = INVALID_HANDLE_VALUE; hMapping = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, "FILE_MAPPING_51HOOK"); if (hMapping == NULL) { //创建Mapping对象 hMapping = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4, "FILE_MAPPING_51HOOK"); } //建立映射 DWORD* number = (DWORD*)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 4); if (*number >= 3) { MessageBoxA(0, "The number of running programs exceeds 3", "Tip", MB_OK); return 0; } *number += 1; printf("Running program count: %d\n", *number); system("pause"); *number -= 1; return 0; }
IAT hook 如图看到了MessageBoxA函数的IAT表中的调用地址
可以考虑修改IAT表,当程序去调用如messageBOX的函数时,让程序去调用我们的函数
hook应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #pragma once #include <Windows.h> DWORD* g_iatAddr = NULL ; DWORD* g_unHookAddr = NULL ; int WINAPI HookMessageBoxW ( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType ) ;BOOL InstallHook () ; BOOL UninstallHook () ; DWORD* GetIatAddr (const char * dllName, const char * dllFunName) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <Windows.h> DWORD* g_iatAddr = NULL ; DWORD* g_unHookAddr = NULL ; typedef int (WINAPI* PFN_MessageBoxW) (HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) ;int WINAPI HookMessageBoxW ( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) { PFN_MessageBoxW OriginalMsgBox = (PFN_MessageBoxW)g_unHookAddr; int result = OriginalMsgBox (0 , L"Hooked" , L"Tip" , MB_OK); return result; } BOOL InstallHook () { DWORD dwOldProtect = 0 ; VirtualProtect (g_iatAddr, 4 , PAGE_EXECUTE_READWRITE, &dwOldProtect); *g_iatAddr = (DWORD)HookMessageBoxW; VirtualProtect (g_iatAddr, 4 , dwOldProtect, &dwOldProtect); return TRUE; } BOOL UninstallHook () { DWORD dwOldProtect = 0 ; VirtualProtect (g_iatAddr, 4 , PAGE_EXECUTE_READWRITE, &dwOldProtect); *g_iatAddr = (DWORD)g_unHookAddr; VirtualProtect (g_iatAddr, 4 , dwOldProtect, &dwOldProtect); return TRUE; } DWORD* GetIatAddr (const char * dllName, const char * dllFunName) { HMODULE hModule = GetModuleHandleA (0 ); DWORD dwhModule = (DWORD)hModule; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwhModule; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(dwhModule + pDosHeader->e_lfanew); PIMAGE_DATA_DIRECTORY dataDirectory = &pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR pImageImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(dataDirectory->VirtualAddress + dwhModule); while (pImageImportTable->Name) { char * iatDllName = (char *)(pImageImportTable->Name + dwhModule); if (_stricmp(iatDllName, dllName) == 0 ) { PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pImageImportTable->OriginalFirstThunk + dwhModule); PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pImageImportTable->FirstThunk + dwhModule); while (pINT->u1.F unction) { if ((pINT->u1. Ordinal & 0x80000000 ) == 0 ) { PIMAGE_IMPORT_BY_NAME pImorotName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.F unction + dwhModule); if (strcmp (pImorotName->Name, dllFunName) == 0 ) { return (DWORD*)pIAT; } } pINT++; pIAT++; } } pImageImportTable++; } return NULL ; } BOOL WINAPI DllMain (HINSTANCE hInstance, DWORD callReason, LPVOID lpReserved) { if (callReason == DLL_PROCESS_ATTACH) { g_iatAddr = GetIatAddr ("user32.dll" , "MessageBoxW" ); g_unHookAddr = (DWORD*)*g_iatAddr; InstallHook (); } else if (callReason == DLL_PROCESS_DETACH) { UninstallHook (); } return TRUE; }
假设不执行 UninstallHook,直接 FreeLibrary :
IAT 表里仍然保存着HookMessageBoxW的地址
HookMessageBoxW这个函数代码在DLL 中
FreeLibrary → Windows 把你的 DLL 从内存卸载,DLL 代码被清空
后面程序再调用
去 IAT 拿到地址,尝试跳转到HookMessageBoxW
但是这块内存已经被释放,里面是垃圾数据 → 访问非法内存,程序直接崩溃闪退!
这就是需要卸载函数的原因:在 DLL 被系统释放前,修好 IAT ,让 IAT 不再指向 DLL 内部的钩子函数。
注意:编译出的文件不要用中文
此代码为32位dll,注入的exe文件也只能是32位
这里放一下编译后的[文件](通过网盘分享的文件:iatHook.dll 链接: https://pan.baidu.com/s/1FudMMGdisflogsnAb8kPSw?pwd=mryp 提取码: mryp 复制这段内容后打开百度网盘手机App,操作更方便哦)
测试dll文件,新建项目,把该dll文件与C++源码置于同一目录下
1 2 3 4 5 6 7 8 9 10 11 12 13 #include<Windows.h> int main() { HMODULE hModule = LoadLibraryA("iatHook.dll"); if (hModule) { MessageBoxW(0, L"halou", L"1111", MB_OK); } return 0; }
或者自己去编译一个exe去尝试
1 2 3 4 5 6 7 8 #include <Windows.h> #include <stdio.h> int main() { MessageBoxW(NULL, L"base text", L"title", MB_OK); getchar(); // 卡住程序,不会自动退出,方便注入、测试弹窗 return 0; }
inline hook hook的本质就是
IAThook缺点:不在 IAT表的函数没办法实现 hook。 Inline hook(内联 hook):让正常程序执行的流程跳转到我们写好的代码中,执行完后,跳转回原始地址
jmp指令本身是5字节
目标地址-JMP指令所在地址-5=E9后跟着的数值
流程
1.找到要hook的函数地址
2.保存要hook函数的前5个字节
3.计算目标函数距离jmp指令下一条指令的距离
4.改变函数的前5个字节,改成jmp 0xxxxxxxx (0xE9 offset)
1 2 3 4 5 6 7 8 9 #include<Windows.h> BOOL InitHook(); int WINAPI MyMessageBoxW( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType); BOOL InstallHook(); BOOL UninstallHook();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include<Windows.h> // 全局变量,改成unsigned char存机器码,解决C4309/C4838警告 DWORD g_unhookfun = NULL; unsigned char g_oldcode[5] = { 0 }; unsigned char g_newcode[5] = { 0xE9 }; // 0xE9 jmp // 前置声明,解决C3861找不到标识符 BOOL InitHook(); int WINAPI MyMessageBoxW( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType ); BOOL InstallHook(); BOOL UninstallHook(); //自定义钩子函数 int WINAPI MyMessageBoxW( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType) { UninstallHook(); int result = MessageBoxW(hWnd, L"51HOOK", lpCaption, uType); InstallHook(); return result; } //安装钩子 BOOL InstallHook() { DWORD oldProtect = 0; if (!VirtualProtect((LPVOID)g_unhookfun, 5, PAGE_EXECUTE_READWRITE, &oldProtect)) { return FALSE; } memcpy((LPVOID)g_unhookfun, g_newcode, 5); VirtualProtect((LPVOID)g_unhookfun, 5, oldProtect, &oldProtect); return TRUE; } //卸载钩子 BOOL UninstallHook() { DWORD oldProtect = 0; if (!VirtualProtect((LPVOID)g_unhookfun, 5, PAGE_EXECUTE_READWRITE, &oldProtect)) { return FALSE; } memcpy((LPVOID)g_unhookfun, g_oldcode, 5); VirtualProtect((LPVOID)g_unhookfun, 5, oldProtect, &oldProtect); return TRUE; } //初始化hook BOOL InitHook() { HMODULE hModule = LoadLibraryA("user32.dll"); if (hModule == 0) { return FALSE; } g_unhookfun = (DWORD)GetProcAddress(hModule, "MessageBoxW");//找到要修改的函数地址 if (g_unhookfun == NULL) { FreeLibrary(hModule); return FALSE; } memcpy(g_oldcode, (unsigned char*)g_unhookfun, 5); DWORD offset = (DWORD)MyMessageBoxW - (g_unhookfun + 5);//计算偏移 memcpy(&g_newcode[1], &offset, 4);//存储用于钩子替换 FreeLibrary(hModule); return TRUE; } BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD callReason, LPVOID lpReserved) { if (callReason == DLL_PROCESS_ATTACH) { InitHook(); InstallHook(); } else if (callReason == DLL_PROCESS_DETACH) { UninstallHook(); } return TRUE; }
inline hook的实战程序可用[这个](通过网盘分享的文件:crack.exe 链接: https://pan.baidu.com/s/1td8I-hZVC7UK5_82oCDWiQ?pwd=ytxu 提取码: ytxu 复制这段内容后打开百度网盘手机App,操作更方便哦),这是之前在网上找的一个逆向题
这里也是同样放一下编译后的[dll文件](通过网盘分享的文件:test.dll 链接: https://pan.baidu.com/s/1n7gUMdiz0t4_epmkvmSgTQ?pwd=jgm7 提取码: jgm7 复制这段内容后打开百度网盘手机App,操作更方便哦)
以及我的dll注入工具[Extreme Injector v3](通过网盘分享的文件:Extreme Injector v3.exe 链接: https://pan.baidu.com/s/1AUNdOF5yi8CMRXoa27xeig?pwd=vuvp 提取码: vuvp 复制这段内容后打开百度网盘手机App,操作更方便哦)