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