内核扫描

流程

整个流程分为 6 个关键阶段

第一阶段:用户层筹备 (Batching & Packing)

代码位置DriverController::KernelScan

用户层采用了分批处理策略。这是为了防止单次 IOCTL 执行时间过长触发 Windows 的 DPC Watchdog Violation(蓝屏)。

  1. 分块策略
    • 定义 MAX_REGIONS_PER_BATCH = 512。如果游戏有 10,000 个内存块,循环会执行约 20 次。
    • 定义 OUTPUT_BUF_SIZE = 1MB。这是给驱动预留的“结果存放区”。
  2. 构造 Payload
    • 每次循环,计算当前 Batch 需要的输入大小:Header大小 + (512 * Region结构体大小)
    • inputBuffer 被填充:包含目标 PID、扫描数值(例如搜索 100)、扫描类型(Exact Value),以及这 512 个具体的内存区域(Start, Size)。
  3. 发送 Wrapper
    • 调用 IoControl(内部调用 DeviceIoControl)。
    • 关键点:发送的不是原始数据,而是 Wrapper
    • ControlCode = 0 (暗号)。
    • Wrapper.ControlCode = IOCTL_CE_SCAN_VALUE (真实意图)。
    • OutputBuffer = 1MB 的空白内存。

第二阶段:内核入口与解包 (Sanitization & Allocation)

代码位置MyDeviceIoControl (Hook 函数)

驱动收到 IOCTL 0,进入劫持函数。

  1. 解包意图
    • InputBuffer 强转 Wrapper,读出真实的 IOCTL_CE_SCAN_VALUE
    • 读出用户层数据的指针 userPtr 和大小 inputDataSize
  2. **分配双向缓冲区 (The Dual-Use Buffer)**:
    • 计算 allocSize = max(输入大小, 输出大小 1MB)
    • 使用 ExAllocatePoolWithTag 申请 **NonPagedPool (非分页内存)**,命名为 kernelBuffer
    • 这一步至关重要:这块内存既用于存放进来的“扫描任务”,也用于存放稍后产生的“扫描结果”。
  3. **安全拷贝 (Deep Copy)**:
    • RtlCopyMemory 将用户层的 inputBuffer 完整复制到 kernelBuffer
    • 此时,驱动不再依赖用户层指针,避免了 TOCTOU 攻击或用户进程崩溃导致的内核蓝屏。
  4. 伪造上下文
    • 构造 FakeIRP
    • SystemBuffer 指向 kernelBuffer
    • 侧信道传参FakeIRP.IoStatus.Information = allocSize。告诉逻辑层:“我申请了 1MB 空间给你用”。

第三阶段:逻辑分发 (Dispatch)

代码位置IOCTL_CE_SCAN_VALUE 处理分支

  1. 获取缓冲区大小
    • 由于是手动映射(无 irpStack),驱动读取 IoStatus.Information,得知缓冲区总共有 1MB。
    • inputoutput 指针指向同一个地址 (SystemBuffer)。
  2. 安全检查
    • 检查 RegionCount 是否异常(防止整数溢出)。
    • 根据 RegionCount 动态计算真实的输入数据长度。
  3. 调用引擎
    • 调用 KernelValueScan(input, inputLen, output, 1MB, ...)

第四阶段:内核扫描引擎 (The Engine)

代码位置KernelValueScan

这是最核心的算法部分,解决了一个致命的内存重叠问题

  1. **自我备份 (Self-Backup)**:

    • 危机:因为 inputoutput 指向同一块内存(SystemBuffer)。如果直接开始扫描并把结果写入 output,第一个结果可能就会覆盖掉 input->Regions 里的数据,导致后续扫描出错。

    • 解决

      1
      2
      inputCopy = ExAllocatePoolWithTag(..., inputSize, ...);
      RtlCopyMemory(inputCopy, input, inputSize);

      驱动把扫描任务备份到了 inputCopy。现在,它从 inputCopy 读取任务,往 output (SystemBuffer) 写结果。

  2. **挂靠进程 (Attach)**:

    • PsLookupProcessByProcessId 获取 EPROCESS。
    • KeStackAttachProcess 切换 CR3。此刻,驱动的虚拟地址空间变成了目标进程的地址空间。
  3. **零拷贝扫描 (Zero-Copy Scan)**:

    • 不需要 ReadProcessMemory。驱动直接用 for 循环遍历 Region.StartRegion.End
    • 直接解引用指针 *(int*)currentAddress 读取数值。
    • 异常保护:包裹在 __try / __except 中。如果读到了未提交的内存(无效页),SEH 会捕获异常,防止蓝屏。
  4. 填充结果

    • 发现匹配值 -> 写入 output->Addresses[count]
    • 如果 count 超过了 1MB 能容纳的最大数量 -> 设置 output->Full = 1 -> 停止扫描。
  5. 资源释放

    • KeUnstackDetachProcess 切回驱动原本的内存空间。
    • 释放 inputCopy

第五阶段:数据回传 (Return)

代码位置MyDeviceIoControl 尾部

  1. 扫描函数返回后,kernelBuffer 的前部已经被扫描结果(KERNEL_SCAN_OUTPUT 结构 + 地址数组)覆盖了。
  2. IoStatus.Information 记录了结果的真实字节数。
  3. RtlCopyMemorykernelBuffer 中的数据拷贝回用户层提供的 OutputBuffer
  4. 释放 kernelBuffer

第六阶段:用户层接收 (Result Handling)

代码位置DriverController::KernelScan

  1. IoControl 返回 true
  2. 用户层将 outputBuffer 强转为 KScanOutput*
  3. 遍历 Addresses 数组,将结果存入 outResults
  4. 检查 pOutput->Full。如果是 true,打印警告(说明这 1MB 不够装,可能漏掉了结果)。
  5. 循环继续currentRegionIdx += 512,开始下一批次的扫描。

总结

  1. 零拷贝读取:在扫描核心循环中,没有 memcpy,直接通过指针读取物理内存(映射后),速度达到内存带宽极限。
  2. 大吞吐量:虽然分块了,但每次交互处理 512 个内存段,相比 RPM 每次系统调用只读一块,上下文切换开销几乎可以忽略不计。
  3. 内存安全
    • **Double Buffer (InputCopy)**:解决了输入输出重叠的逻辑炸弹。
    • NonPagedPool:解决了 VMCALL 需要物理内存常驻的问题。
    • SEH:解决了无效指针导致的 BSOD。