CE-refactoring
内核扫描
流程
整个流程分为 6 个关键阶段:
第一阶段:用户层筹备 (Batching & Packing)
代码位置:DriverController::KernelScan
用户层采用了分批处理策略。这是为了防止单次 IOCTL 执行时间过长触发 Windows 的 DPC Watchdog Violation(蓝屏)。
- 分块策略:
- 定义
MAX_REGIONS_PER_BATCH = 512。如果游戏有 10,000 个内存块,循环会执行约 20 次。 - 定义
OUTPUT_BUF_SIZE = 1MB。这是给驱动预留的“结果存放区”。
- 定义
- 构造 Payload:
- 每次循环,计算当前 Batch 需要的输入大小:
Header大小 + (512 * Region结构体大小)。 inputBuffer被填充:包含目标 PID、扫描数值(例如搜索 100)、扫描类型(Exact Value),以及这 512 个具体的内存区域(Start, Size)。
- 每次循环,计算当前 Batch 需要的输入大小:
- 发送 Wrapper:
- 调用
IoControl(内部调用DeviceIoControl)。 - 关键点:发送的不是原始数据,而是 Wrapper。
ControlCode= 0 (暗号)。Wrapper.ControlCode=IOCTL_CE_SCAN_VALUE(真实意图)。OutputBuffer= 1MB 的空白内存。
- 调用
第二阶段:内核入口与解包 (Sanitization & Allocation)
代码位置:MyDeviceIoControl (Hook 函数)
驱动收到 IOCTL 0,进入劫持函数。
- 解包意图:
- 从
InputBuffer强转Wrapper,读出真实的IOCTL_CE_SCAN_VALUE。 - 读出用户层数据的指针
userPtr和大小inputDataSize。
- 从
- **分配双向缓冲区 (The Dual-Use Buffer)**:
- 计算
allocSize=max(输入大小, 输出大小 1MB)。 - 使用
ExAllocatePoolWithTag申请 **NonPagedPool (非分页内存)**,命名为kernelBuffer。 - 这一步至关重要:这块内存既用于存放进来的“扫描任务”,也用于存放稍后产生的“扫描结果”。
- 计算
- **安全拷贝 (Deep Copy)**:
RtlCopyMemory将用户层的inputBuffer完整复制到kernelBuffer。- 此时,驱动不再依赖用户层指针,避免了 TOCTOU 攻击或用户进程崩溃导致的内核蓝屏。
- 伪造上下文:
- 构造
FakeIRP。 SystemBuffer指向kernelBuffer。- 侧信道传参:
FakeIRP.IoStatus.Information = allocSize。告诉逻辑层:“我申请了 1MB 空间给你用”。
- 构造
第三阶段:逻辑分发 (Dispatch)
代码位置:IOCTL_CE_SCAN_VALUE 处理分支
- 获取缓冲区大小:
- 由于是手动映射(无
irpStack),驱动读取IoStatus.Information,得知缓冲区总共有 1MB。 input和output指针指向同一个地址 (SystemBuffer)。
- 由于是手动映射(无
- 安全检查:
- 检查
RegionCount是否异常(防止整数溢出)。 - 根据
RegionCount动态计算真实的输入数据长度。
- 检查
- 调用引擎:
- 调用
KernelValueScan(input, inputLen, output, 1MB, ...)。
- 调用
第四阶段:内核扫描引擎 (The Engine)
代码位置:KernelValueScan
这是最核心的算法部分,解决了一个致命的内存重叠问题。
**自我备份 (Self-Backup)**:
危机:因为
input和output指向同一块内存(SystemBuffer)。如果直接开始扫描并把结果写入output,第一个结果可能就会覆盖掉input->Regions里的数据,导致后续扫描出错。解决:
1
2inputCopy = ExAllocatePoolWithTag(..., inputSize, ...);
RtlCopyMemory(inputCopy, input, inputSize);驱动把扫描任务备份到了
inputCopy。现在,它从inputCopy读取任务,往output(SystemBuffer) 写结果。
**挂靠进程 (Attach)**:
PsLookupProcessByProcessId获取 EPROCESS。KeStackAttachProcess切换 CR3。此刻,驱动的虚拟地址空间变成了目标进程的地址空间。
**零拷贝扫描 (Zero-Copy Scan)**:
- 不需要
ReadProcessMemory。驱动直接用for循环遍历Region.Start到Region.End。 - 直接解引用指针
*(int*)currentAddress读取数值。 - 异常保护:包裹在
__try / __except中。如果读到了未提交的内存(无效页),SEH 会捕获异常,防止蓝屏。
- 不需要
填充结果:
- 发现匹配值 -> 写入
output->Addresses[count]。 - 如果
count超过了 1MB 能容纳的最大数量 -> 设置output->Full = 1-> 停止扫描。
- 发现匹配值 -> 写入
资源释放:
KeUnstackDetachProcess切回驱动原本的内存空间。- 释放
inputCopy。
第五阶段:数据回传 (Return)
代码位置:MyDeviceIoControl 尾部
- 扫描函数返回后,
kernelBuffer的前部已经被扫描结果(KERNEL_SCAN_OUTPUT结构 + 地址数组)覆盖了。 IoStatus.Information记录了结果的真实字节数。RtlCopyMemory将kernelBuffer中的数据拷贝回用户层提供的OutputBuffer。- 释放
kernelBuffer。
第六阶段:用户层接收 (Result Handling)
代码位置:DriverController::KernelScan
IoControl返回true。- 用户层将
outputBuffer强转为KScanOutput*。 - 遍历
Addresses数组,将结果存入outResults。 - 检查
pOutput->Full。如果是 true,打印警告(说明这 1MB 不够装,可能漏掉了结果)。 - 循环继续:
currentRegionIdx += 512,开始下一批次的扫描。
总结
- 零拷贝读取:在扫描核心循环中,没有
memcpy,直接通过指针读取物理内存(映射后),速度达到内存带宽极限。 - 大吞吐量:虽然分块了,但每次交互处理 512 个内存段,相比 RPM 每次系统调用只读一块,上下文切换开销几乎可以忽略不计。
- 内存安全:
- **Double Buffer (InputCopy)**:解决了输入输出重叠的逻辑炸弹。
- NonPagedPool:解决了 VMCALL 需要物理内存常驻的问题。
- SEH:解决了无效指针导致的 BSOD。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Meng's blog!

