子文档 3:Thunk 方法与 APC 队列(src/injlib/injlib.c 下半部 + shellcode)
这部分实现了 APC 的初始化、内存区段的创建以及 shellcode 的部署。
1. InjpQueueApc:通用 APC 入队函数
该函数负责分配 KAPC 结构(从非分页池 NonPagedPoolNx 分配,带 INJ_MEMORY_TAG 标签)。调用 KeInitializeApc 时指定 Environment = OriginalApcEnvironment,KernelRoutine = &InjpInjectApcKernelRoutine,NormalRoutine 则由调用者传入。KeInsertQueueApc 将 APC 插入目标线程队列(此处为 PsGetCurrentThread())。
注意:为什么 APCs 总是排入当前线程而非目标进程的主线程?因为在映像加载回调的上下文中,当前线程正是执行加载操作的线程(通常是进程的主线程或加载器线程)。注入 APC 到当前线程是安全的,且能保证在返回用户态时触发。
2. 内核 APC 解决死锁(InjpInjectApcKernelRoutine)
在 InjInject 函数中(由 InjpInjectApcNormalRoutine 调用),如果直接在映像加载回调里调用 ZwMapViewOfSection,很可能重入 NtMapViewOfSection 内部已持有的 AddressCreationLock。为规避此问题,InjInject 被设计为由内核 APC 间接调用。代码中并未显式调用 InjpQueueApc(KernelMode, ...)?实际上,InjInject 本身是由 InjpInjectApcNormalRoutine 调用的,而 InjpInjectApcNormalRoutine 是作为用户态 APC 的 NormalRoutine 传入的。但在 InjLoadImageNotifyRoutine 中,调用的是:
c
InjpQueueApc(KernelMode, &InjpInjectApcNormalRoutine, InjectionInfo, NULL, NULL);
这里将 NormalRoutine 设为 InjpInjectApcNormalRoutine,但 ApcMode 却是 KernelMode。这有些反直觉。实际上,对于内核 APC,NormalRoutine 并非由 KiDeliverApc 在用户态执行,而是由内核 APC 分发器在 IRQL 1 执行。InjpInjectApcNormalRoutine 内部调用了 InjInject,而 InjInject 执行区段映射时,此时已不在映像加载的原始调用栈上,因此 AddressCreationLock 已被释放。
3. 区段创建与双映射技巧(InjInject)
InjInject 首先创建 ZwCreateSection,指定 PAGE_EXECUTE_READWRITE 作为区段的最大权限。随后调用 ZwMapViewOfSection 映射到当前进程(即目标进程,因为 APC 执行时已附加到目标进程上下文)的 PAGE_READWRITE 视图。
接着,将 shellcode(InjThunk[Architecture].Buffer)复制到视图起始位置,紧随其后复制 DLL 路径(InjDllPath[Architecture].Buffer)。注意,shellcode 中硬编码了 LdrLoadDll 的参数布局,因此路径必须紧跟在 shellcode 后面。
写完后,调用 ZwUnmapViewOfSection 解除映射,再以 PAGE_EXECUTE_READ 重新映射同一区段。这时,用户态内存变为可执行但不可写。
4. 架构相关的 Thunk Shellcode 分析
以 x64 的 InjpThunkX64 为例(反汇编):
asm
sub rsp, 38h ; 分配影子空间
mov rax, rcx ; NormalContext (LdrLoadDll 地址)
mov [rsp+20h], r8w ; 保存路径长度
mov [rsp+22h], r8w ; 再次保存
lea r9, [rsp+40h] ; 指向栈上的 BaseAddress 输出位置
mov [rsp+28h], rdx ; 保存路径指针
lea r8, [rsp+20h] ; 指向 UNICODE_STRING 结构
xor edx, edx ; DllCharacteristics = 0
xor ecx, ecx ; SearchPath = NULL
call rax ; 调用 LdrLoadDll
add rsp, 38h
ret
该 shellcode 完全符合 x64 调用约定,将 4 个参数正确填入寄存器,并确保 RSP 对齐。ARM64 版本则使用 blr x9 等指令,逻辑一致。