这篇能做出什么
跟着走完一遍,你会得到一条能重复跑的流水线,以及六个可交付的产物:
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. 把结论沉淀成报告模板。 触发路径、三闸门结论、复现率、修复点、回归结论,五段式固定下来。写一次模板,之后每次只填空。
