跳到主内容
快讯直播
AI智模界
教程

用编码智能体做内核漏洞挖掘:从审计到补丁回归

这篇能做出什么

跟着走完一遍,你会得到一条能重复跑的流水线,以及六个可交付的产物:

1. scope.md——这次审计的边界(哪个子系统、哪些入口、哪些配置)。

2. suspects.json——一份带行号、带机制描述、带置信度的可疑点清单,由编码智能体批量产出、经人工初筛。

3. reachability.md——每条候选从用户态系统调用走到漏洞点的完整路径,以及三个闸门(编译配置、权限、命名空间)的结论。

4. poc.c——能在 QEMU 里稳定把内核打进 KASAN 报告的触发程序。

5. crash.log——符号化之后的崩溃现场,包含调用栈与复现率记录。

6. patch.mbox + regression.md——修复补丁与回归结论(原 PoC 不再触发、同类点已排查)。

关于"以公开的内核漏洞研究为模板",可以类比的理解是:那类工作把"读代码找问题"拆成了两段——可大规模并行的候选生成,和可执行的验证。编码智能体擅长的正是第一段:它能以很低的边际成本把几万行代码读完、把调用链拼出来、把猜测写成结构化文字。第二段(能不能真的打崩、修得对不对)仍然要靠编译、靠 QEMU、靠人来签字。别指望一句"帮我找内核漏洞"就有结果,指望的是"把审查吞吐量放大十倍,再由人收敛"。

---

前置条件清单

机器

  • 一台开发机:建议 8 核以上、32 GB 内存、100 GB 空闲磁盘(内核全量编译 + 调试符号 + 虚拟机镜像会吃掉不少空间)。
  • 虚拟机能力:KVM 可用(ls /dev/kvm 有输出)。没有 KVM 也能跑 QEMU,只是慢。

工具链

```bash

Debian/Ubuntu 系参考命令,包名以你所用发行版的官方仓库为准

sudo apt-get update

sudo apt-get install -y \

git build-essential flex bison bc libelf-dev libssl-dev \

qemu-system-x86 cpio kmod \

ripgrep cscope ctags sparse \

dwarves gdb \

python3 python3-pip

可选但强烈建议:语法模式匹配与静态检查

sudo apt-get install -y coccinelle smatch

```

编码智能体

需要一个能读整个仓库、能执行 shell 命令、能保持长上下文的编码智能体(各家产品能力差异较大,选型与用量以官方文档当前版本为准)。关键能力有两条:能按文件/按函数精确取片段,能把结果落成文件。只支持粘贴对话的聊天窗口,做不了后面的分片审计。

知识准备

  • 知道什么是系统调用入口、ioctl、netlink family、procfs/sysfs 属性文件。
  • 知道 KASAN 是什么、能读懂一份 KASAN 报告。
  • 知道内核的 capability 模型(CAP_NET_ADMIN、CAP_SYS_ADMIN 之类)和 user namespace 的作用。
  • 知道最基本的合规边界:只在自己搭的虚拟机里跑 PoC,别碰生产机,别碰别人的系统。

---

第 0 步:先把范围钉死

新手常犯的错是"我要审计 Linux 内核"。这不是范围,这是愿望。范围长这样:

```markdown

<!-- scope.md -->

目标

子系统:网络子系统中某个 netlink 协议族的属性解析路径

问题类型:长度校验缺失导致的越界读写

入口:socket(AF_NETLINK) -> bind -> sendmsg

配置前提:该协议族对应的 CONFIG_ 选项已开启

权限前提:是否需要 CAP_NET_ADMIN,是否可在 user namespace 内满足

不做什么

  • 不做竞态类问题(本轮不引入 KCSAN)
  • 不做需要特定硬件的问题
  • 不做已有 CVE 的复现(先去公开漏洞库排除)

```

范围越窄,智能体的输出越可用。写完之后,把这份文件放在工作目录根下,后面每一轮对话都让它先读这个文件。

---

第 1 步:准备源码与检索底座

内核源码从官方仓库获取(地址以 kernel.org 官方页面为准)。用浅克隆省时间,需要历史时再补。

```bash

变量按官方文档填写,避免记错地址

export KERNEL_GIT="<官方仓库地址>"

git clone --depth=1 "$KERNEL_GIT" linux

cd linux

git log -1 --oneline # 记录基线 commit,写进报告

建立符号索引,方便智能体和人都能快速定位

ctags -R --languages=C .

cscope -R -b -q

```

再建一张入口点清单,这是后面可达性分析的起点:

```bash

1) 系统调用表

rg -n "SYSCALL_DEFINE" --type c | wc -l

2) ioctl 命令定义

rg -n "_IO[CWR]*\(" include/uapi/ | head -50

3) netlink 操作表

rg -n "\.doit\s*=|\.dumpit\s*=" net/ | head -50

4) sysfs/procfs 属性

rg -n "DEVICE_ATTR|PROC_ENTRY|proc_create" | head -50

```

把结果整理成一张表格(入口名 / 文件:行 / 需要的权限 / 相关 CONFIG),存为 entries.md。这张表的价值在于:任何一条候选,如果不能挂到某个入口上,就先降级。

---

第 2 步:让智能体做分片审计

分片的原则:按函数边界或按文件给,一次只给几十到几百行相关代码,不要给一整个子系统。给太多,智能体会开始"总结"而不是"分析"。

提示词模板(直接照抄改路径):

```text

你是内核代码审计助手。先读工作目录下的 scope.md,严格遵守其中范围。

本轮审计目标文件:<相对路径>

相关调用者(已为你整理,不要自己去猜):

  • <函数A>(来自 <文件:行>)
  • <函数B>(来自 <文件:行>)

任务:找出"长度/边界/类型校验缺失,可能导致越界读写"的位置。

硬性要求:

1. 每条结论必须给出 文件:行号,并原样粘贴该处 3~8 行代码。

2. 必须用一句话说明"为什么这里可能越界"(哪个输入、经过什么变换、和哪个缓冲区长度比较缺失)。

3. 必须给出置信度 high/medium/low,并说明 low 的原因。

4. 不要提"建议加注释""建议重构"这类非安全问题。

5. 不确定就写 low,不要编造行号。找不到就返回空数组。

输出严格 JSON,不要额外文字:

```

输出格式用一个固定 schema,方便脚本汇总:

```json

{

"file": "net/xxx/yyy.c",

"line": 1234,

"snippet": "原样粘贴的代码",

"function": "foo_parse",

"sink": "memcpy 到栈缓冲区",

"mechanism": "属性长度取自报文且未与 sizeof(dst) 比较",

"confidence": "medium",

"needs": ["CAP_NET_ADMIN"]

}

```

把所有分片的 JSON 追加到 suspects.jsonl,然后去重、按 file 分组。经验上,一轮下来会有几十到上百条,其中大部分是误报——这很正常,第 3、4 步就是用来收敛的。

这一轮的产出必须能被人复核。找个时间把 confidence: high 的条目逐条打开看,如果连续几条都是"行号对不上",说明提示词里的约束还不够,回去加"找不到就返回空数组"。

---

第 3 步:机器侧交叉验证,先把明显误报干掉

人工逐条读太慢,先用工具做减法。

```bash

1) 编译告警(很多越界在 -Wstringop-overflow 一类告警里有影子)

make defconfig

make -j"$(nproc)" 2>&1 | tee build.log

rg -n "warning:" build.log | rg -i "overflow|bounds|truncat|size"

2) sparse / smatch 对可疑文件的定点检查

make C=2 CHECK=sparse net/xxx/ 2>&1 | tee sparse.log

smatch --project=kernel net/xxx/yyy.c 2>&1 | head -50

```

再写几条 Coccinelle 规则,把"复制了用户数据但没有长度校验"这类固定模式批量捞出来:

```c

// check_len.cocci

// 用法:spatch --sp-file check_len.cocci --dir net/xxx/

@rul@

expression src, dst, len;

@@

copy_from_user(dst, src, len);

...

  • if (...)

```

... 前后的模式要按你的实际场景写细,写得太宽会淹掉结果。这条规则的作用不是直接定漏洞,而是把"同一类写法"全找出来——补丁阶段还要用一次(见第 7 步)。

---

第 4 步:可达性分析,这一步决定成败

一条"理论上有越界"的代码,如果那行代码所在的 #ifdef 在你编译的配置里是关的,或者进这个函数需要 CAP_SYS_ADMIN 而你只能拿到普通用户,那它就不是可交付的发现。

对每条候选,让智能体做一次"路径证明",输出必须逐跳:

```text

从 <syscall 名> 出发,写出到达 <file:line> 的完整调用链。

每一跳给出:函数名、所在文件:行、进入该分支的条件(if 语句或 switch case)。

并逐项回答:

1. 这条路径依赖哪些 CONFIG_ 选项?在当前 .config 里是 y/m/n?

2. 路径上是否有 capable()/ns_capable() 检查?需要哪个 capability?

3. 能否在 user namespace 内满足上述 capability?

4. 是否有更短的替代路径?如果有,写出来。

不确定的项标注 UNKNOWN,不要猜。

```

三个闸门逐条落实成可执行的检查:

```bash

闸门一:配置

rg -n "CONFIG_XXX" .config

闸门二:权限检查

rg -n "capable|ns_capable|CAP_" net/xxx/yyy.c

闸门三:命名空间与设备可见性

rg -n "netns|user_ns|current_user_ns" net/xxx/yyy.c

```

把结论写进 reachability.md,一条候选一段。只有三个闸门都过了的候选,才进入 PoC 阶段。把过不了的也留着,标上"配置依赖",将来换配置时可以捡回来。

---

第 5 步:PoC 生成与最小化

先让智能体产出"触发骨架",不要一上来就要完整程序:

```text

基于 reachability.md 中第 N 条路径,写一个最小 C 程序,要求:

1. 只使用 libc 和 raw syscall,不引入第三方库。

2. 按顺序列出每一步:socket/bind/sendmsg 或 open/ioctl。

3. 明确给出报文的字段布局(用 struct + 注释标出偏移和长度)。

4. 在关键位置打印返回值并 errno。

5. 说明需要什么权限运行。

6. 只负责触发,不负责利用。

```

PoC 骨架大致长这样(结构体字段按你的实际协议填):

```c

// poc.c —— 骨架示例,字段需按目标协议替换

#define _GNU_SOURCE

#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <sys/socket.h>

struct msg_hdr {

unsigned short len; /* 报文声称的长度 */

unsigned short type;

/* ... 后续字段 */

};

int main(void)

{

int fd = socket(/* family */ 0, SOCK_RAW, 0);

if (fd < 0) { perror("socket"); return 1; }

unsigned char buf[256];

memset(buf, 0, sizeof(buf));

struct msg_hdr *h = (struct msg_hdr *)buf;

h->len = 0xffff; /* 与缓冲区实际长度不一致 */

h->type = 1;

ssize_t n = send(fd, buf, sizeof(buf), 0);

printf("send -> %zd, errno=%d (%s)\n", n, errno, strerror(errno));

close(fd);

return 0;

}

```

编译并投递到虚拟机:

```bash

gcc -O1 -g -static -o poc poc.c

通过 9p 共享目录投递,避免来回 scp

mkdir -p share && cp poc share/

```

启动一个带 KASAN 的目标内核(配置片段):

```bash

cd linux

make defconfig

make kvm_guest.config # x86 上的常用目标

./scripts/config --enable KASAN

./scripts/config --enable KASAN_INLINE

./scripts/config --enable DEBUG_INFO

./scripts/config --enable DEBUG_KERNEL

./scripts/config --disable RANDOMIZE_BASE # 便于读栈,正式测试可再打开

./scripts/config --enable <你的子系统对应 CONFIG>

make olddefconfig

make -j"$(nproc)"

```

```bash

启动(rootfs 用你自己构建的 initramfs 或发行版镜像,构建方式以官方文档为准)

qemu-system-x86_64 \

-kernel arch/x86/boot/bzImage \

-append "console=ttyS0 root=/dev/ram rdinit=/bin/sh nokaslr" \

-initrd "$ROOTFS" \

-m 2G -smp 2 -nographic \

-virtfs local,path="$PWD/share",mount_tag=host0,security_model=none \

-enable-kvm

```

虚拟机内:

```bash

mount -t 9p -o trans=virtio host0 /mnt

/mnt/poc

dmesg | tail -60

```

最小化:确认能打崩之后,逐条删掉无关的 printf、无关的字段赋值,每次删完重跑一遍。目标是一页以内、能稳定复现。这一步别省——后面补丁回归要靠它当判据。

---

第 6 步:崩溃判定与符号化

KASAN 报告要看三样东西:报告类型(slab-out-of-bounds / use-after-free / stack-out-of-bounds)、访问的函数与偏移、分配与释放的调用栈。

```bash

把原始 dmesg 变成带行号的栈

./scripts/decode_stacktrace.sh vmlinux < crash.log | tee crash.decoded.log

单点定位

addr2line -e vmlinux -f -i 0xffffffffXXXXXXXX

```

判定要点:

  • BUG: KASAN: ... 是你的目标;WARNING:、BUG: sleeping function called from invalid context 属于另一类问题,别混进报告。
  • 复现率要记录:连跑 20 次,记成功次数。只在 1/20 次复现的,标"不稳定",先查是不是时序或 CPU 数相关(把 -smp 从 2 改到 1 再试)。
  • 崩溃点和你审计时定位的行不一致时,以崩溃点为准,回头修 reachability.md——这通常说明审计阶段漏了一条路径。

---

第 7 步:打补丁与回归验证

补丁要针对根因,不是针对崩溃的那一行。比如根因是"长度字段没有和实际缓冲区比较",那修复应该在校验处,而不是在 memcpy 前加个 if。

```diff

--- a/net/xxx/yyy.c

+++ b/net/xxx/yyy.c

@@ -1230,6 +1230,10 @@ static int foo_parse(struct sk_buff *skb)

if (nla_len(attr) < sizeof(struct foo_hdr))

return -EINVAL;

+ if (nla_len(attr) > sizeof(foo->buf))

+ return -EINVAL;

+

memcpy(foo->buf, nla_data(attr), nla_len(attr));

return 0;

}

```

回归做三件事:

```bash

1) 原 PoC 在新内核上不再触发

重新编译、重新启动、重跑,dmesg 里不应出现 KASAN 报告

gcc -O1 -g -static -o poc poc.c && cp poc share/

2) 同类点排查:用第 3 步的 coccinelle 规则再扫一遍,

确认"同一类写法"的其他位置也已被覆盖或确认安全

spatch --sp-file check_len.cocci --dir net/xxx/ 2>&1 | tee rerun.log

3) 子系统自测 + 模糊测试短跑(工具可用 syzkaller,构建方式以官方文档为准)

跑一遍相关内核自测,再用 syzkaller 对该子系统跑一轮限定时间的语料

```

最后让智能体做一次补丁审查,问法要具体:

```text

这是补丁 diff 与原始触发路径。请回答三个问题,每个问题都要引用代码行:

1. 修复点是否覆盖了触发路径上的所有分支?还有哪条路径能绕过这个校验?

2. 这个校验会不会误伤合法的短报文(回归风险)?

3. 同一文件中是否存在结构相同的调用点未被修改?

```

把回答整理进 regression.md。需要对外提交时,用 git format-patch 生成 mbox,走对应项目的提交流程——具体流程以该项目官方文档为准。

---

常见坑与排错

行号对不上。 智能体在长上下文里会"记错"行号。对策:强制它原样粘贴代码片段,你用 rg 反查片段是否真的存在于该文件。片段对不上就整条丢弃,不要试图"人工纠正"。

候选一大堆,全是误报。 通常是提示词太宽("找安全问题")或者分片太大。把问题类型收窄到"长度校验缺失",把分片收到单个函数,误报率会明显下降。

代码看着有问题,但虚拟机里打不崩。 先查三个闸门:CONFIG 是不是没开(rg CONFIG_XXX .config)、权限够不够、是不是被前置校验拦住了。再查是不是走了另一条分支(加 printk 或直接在 gdb 里下断点)。

QEMU 启动就 panic。 常见原因是 rootfs 与内核不匹配、-append 里的 root= / rdinit= 写错、initramfs 里缺 /init。先用最小 initramfs 把 shell 起起来,再谈别的。开了 -enable-kvm 时若嵌套虚拟化不可用,去掉这个参数。

KASAN 报告被刷屏盖掉。 加大环形缓冲区:-append 里加 log_buf_len=4M,或者崩溃后立刻 dmesg | head -200,别等。

只有本机能复现。 记录 -smp、内存大小、是否 nokaslr、是否启用 KVM。报告里把这四项写清楚,否则别人复现不了。

补丁把合法用例打挂了。 说明校验过严。先确认协议规范里这个字段的合法取值范围,再决定是加下界还是上界检查。

别在生产机跑 PoC。 用虚拟机、用快照、用完就删。这条没有例外。

---

下一步建议

1. 把流程脚本化。 把第 2、3、4 步串成一个脚本:输入文件列表 → 输出 suspects.jsonl → 跑静态检查 → 生成待人工复核清单。跑顺之后,每次内核更新只需重跑一遍。

2. 接入模糊测试。 人工构造的 PoC 只覆盖你想到的路径。用 syzkaller 对同一个子系统做持续跑,把崩溃去重后回灌到你的候选池,两者互补。

3. 建自己的模式库。 每确认一个真实问题,就写一条 coccinelle 规则、一条提示词片段。半年后这个库就是你团队的审计加速器。

4. 做差分审计。 老版本修过的问题,在新分支、新驱动里常常换个写法重来。拿历史修复 commit 当种子,让智能体在新代码里找"结构相同但没修"的点,命中率通常不低。

5. 把结论沉淀成报告模板。 触发路径、三闸门结论、复现率、修复点、回归结论,五段式固定下来。写一次模板,之后每次只填空。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。