Files
reverse-skill/skills/patch-diff-exploit/references/root-cause-and-poc.md
T
2026-08-08 18:32:33 +08:00

5.1 KiB
Raw Blame History

根因反推与 PoC 编写

拿到 diff 报告只是第一步。真正的工作是看着 before/after 反推出 bug class,然后写出能稳定触发的 PoC。本文档给出反推模式表、LLM 辅助 prompt、PoC 编写模板、验证方法。


1. 补丁修复模式 → 漏洞类型反查

1.1 整数溢出 / 下溢

新增模式:

// 加法溢出检查
if (a > UINT_MAX - b) goto error;
if (a + b < a) goto error;
__builtin_add_overflow(a, b, &sum);

// 乘法溢出检查
if (a != 0 && b > UINT_MAX / a) goto error;
__builtin_mul_overflow(a, b, &prod);

// 减法下溢检查
if (a < b) goto error;

反推:

  • 看 a 和 b 是否来自用户态可控输入
  • 通常前后会有 kmalloc(a+b) / RtlAllocateHeap(size) 等分配,溢出后分配过小 → 后续 memcpy → 堆 OOB 写

PoC 思路:

  • 让 a + b 实际溢出回小值,但拷贝长度还按未溢出的算
  • 边界值: a = 0xFFFFFFF0, b = 0x100 (32 位) / a = 0xFFFFFFFFFFFFFFF0, b = 0x100 (64 位)

1.2 越界读 / 越界写

新增模式:

if (idx >= ARRAY_SIZE(arr)) return -EINVAL;
if (offset + len > buf_size) return STATUS_INVALID_PARAMETER;
if (len > sizeof(local_buf)) return -E2BIG;

// Windows 内核典型
if (InputBufferLength < sizeof(MY_STRUCT)) return STATUS_BUFFER_TOO_SMALL;
if (req->Length > KERNEL_MAX) return STATUS_INVALID_PARAMETER;

反推:

  • 看新增 if 守护了哪个后续操作(memcpy / memmove / 数组下标)
  • 没守护前的版本可以传超大 length / 超大 index

PoC 思路:

  • 把 length 字段设成超过 buf 的值
  • 把 index 字段设成 array_size + N
  • 用 IOCTL InputBufferLength 调到刚好绕过旧检查但触发新检查

1.3 竞争条件 (Race / TOCTOU)

新增模式:

// 加锁
KeAcquireSpinLockAtDpcLevel(&obj->Lock);
mutex_lock(&inode->i_mutex);
spin_lock(&list->lock);

// 引用计数原子化
InterlockedIncrement(&obj->RefCount);
ObReferenceObject(obj);
get_file(file);

// 检查 - 操作之间不再释放锁

反推:

  • 加锁说明此前同一对象在并发场景下被改了
  • 引用计数加 ref 说明此前对象可能在用的时候被释放
  • 找两个 syscall 路径,其中一个会修改 / 释放对象,另一个会用对象

PoC 思路:

// 经典 hammer
DWORD WINAPI thread_close(LPVOID arg) {
    while (running) CloseHandle((HANDLE)arg);
    return 0;
}
DWORD WINAPI thread_use(LPVOID arg) {
    while (running) DeviceIoControl((HANDLE)arg, IOCTL_X, ...);
    return 0;
}
// CreateThread 各开几个,sleep 30s 等崩
  • 多线程绑核 (SetThreadAffinityMask) 提高竞争概率
  • 用 _mm_pause() / sched_yield() 控制窗口
  • 配合 syscall pinning(同一对象的两个不同入口)

1.4 未初始化内存信息泄漏

新增模式:

// 字段清零
RtlZeroMemory(&output, sizeof(output));
memset(buf, 0, sizeof(buf));
output.reserved = 0;
output.padding = 0;
output._pad1 = 0;

// 整个结构归零再赋值
KeStackAttachProcess; output = (MY_STRUCT){0};

反推:

  • 旧版从内核栈 / 内核堆拷数据到用户态时没清 padding / reserved 字段
  • padding 内残留前一次函数调用的栈数据 → 内核地址泄漏 (绕 KASLR)

PoC 思路:

  • 反复调 IOCTL 取 output buffer
  • 把每个字节按 8 字节解析,找像内核地址的值(Windows: 0xFFFFxxxx / Linux: 0xFFFFFFFFxxxxxxxx)
  • 找规律 → 推 kbase

1.5 UAF / 引用计数错误

新增模式:

// Windows
if (InterlockedDecrement(&obj->RefCount) == 0) {
    Free(obj);
    return;
}

// Linux
if (refcount_dec_and_test(&obj->ref)) {
    kfree(obj);
}

// 或新增 ObReferenceObject 在某条路径上(之前漏加 ref)

反推:

  • 旧版某条出错路径 free 了对象但调用者还会用
  • 或者多路径同时持有指针,其中一条 free 后其他没感知

PoC 思路:

// spray → free → reuse
HANDLE objs[1000];
for (int i = 0; i < 1000; i++) objs[i] = CreateObject(...);
trigger_free(objs[500]);                          // 触发漏洞 free
spray_kernel_pool(0xDEADBEEFDEADBEEF, target_sz); // 占住被 free 的洞
use_after_free(objs[500]);                        // 触发用 -> 控数据
  • Windows 内核常用 NtAllocateReserveObject / Pipe 属性 / Window Class 名称喷
  • Linux 常用 msgsnd / setxattr / userfaultfd-stalled vma 喷

1.6 用户态指针未校验

新增模式:

// Windows
ProbeForRead(UserBuffer, Length, sizeof(ULONG));
ProbeForWrite(UserBuffer, Length, sizeof(ULONG));

// __try/__except 包裹访问
__try {
    Probe...
    RtlCopyMemory(KernelBuf, UserBuffer, Length);
} __except(EXCEPTION_EXECUTE_HANDLER) { ... }

// Linux
if (!access_ok(VERIFY_WRITE, ptr, size)) return -EFAULT;
copy_from_user(kbuf, uptr, len);

反推:

  • 旧版直接 deref 用户态指针没 ProbeForRead/access_ok
  • 用户态可以传内核地址 → 任意地址读 / 任意地址写

PoC 思路:

  • 传一个内核空间指针(例如 ntoskrnl 某个全局变量地址)作为输入指针
  • 内核执行 *UserBuffer = X 时实际写到内核地址 → 任意写

1.7 权限校验缺失

新增模式:

if (!SeAccessCheck(...)) return STATUS_