本教程演示如何使用 Claude Agent SDK 构建一个漏洞发现代理,它借助 Claude Code 内置的 Read、Grep 和 Glob 工具读取源代码,分析哪些输入可能导致内存损坏,并生成可供审查者直接采用的研究结论。

安全团队希望在攻击者发现内存安全漏洞之前就能将其定位,但现有工具存在诸多困难:静态分析器误报率极高,导致审查者不再阅读其输出;模糊测试(fuzzer)则需要为每个入口点手工编写测试框架才能有所发现。本教程演示如何使用 Claude Agent SDK 构建一个漏洞发现代理,它借助 Claude Code 内置的 Read、Grep 和 Glob 工具读取源代码,分析哪些输入可能导致内存损坏,并生成可供审查者直接采用的研究结论。

学完本教程后,你将能够:

  • 通过一个多轮 ClaudeSDKClient 会话运行“先引导、后访谈”的威胁建模流程,并生成 THREAT_MODEL.md 文件
  • 使用内置的 Read/Grep/Glob 工具驱动自动化查找循环,无需自己编写文件访问代码
  • 将查找、分类和报告串联为独立的 query() 调用,输出符合预定义模式的 JSON

前置条件

所需知识:

  • Python 基础知识,包括 async/await
  • 足够的 C 语言能力,能读懂一个 45 行的文件并能识别 memcpy

所需工具:

  • Python 3.11+
  • Node.js 18+ 及 Claude Code CLI:npm install -g @anthropic-ai/claude-code
  • Anthropic API 密钥

针对任何真实目标的要求: 获得评估该代码的授权。本教程附带一个极小的自包含文件 canary.c,其中植入了多个漏洞,以便你在不接触生产代码的情况下完整运行整个流程。

步骤 1:设置环境与行动背景

我们定义一个 ENGAGEMENT_CONTEXT 块,并将其作为 system_prompt 传递给每个代理。它记录了本次评估的范围(代码所有者授权、隔离只读沙箱、发现结果将负责任地披露),使得流程中的每一步都遵循相同的、已记录的基本规则。请确保对任何真实目标都满足这三项声明。

 

关于网络安全防护的说明: Claude 在 API 层应用了实时网络安全防护。如果你的工作涉及真实代码库并触发了这些防护措施,请通过该页面申请网络安全验证计划(Cyber Verification Program,CVP):这是一个免费的、基于申请的计划,能让专业人士以最小干扰继续合法的双重用途安全工作。

 

模型:claude-opus-4-7
  • 1.

步骤 2:加载测试目标

vulnerability_detection_agent/canary/canary.c 是一个约 45 行的 C 程序,故意植入了三个内存安全漏洞(堆缓冲区溢出、栈缓冲区溢出和释放后使用),每个漏洞可通过输入文件开头的不同“魔法字节”触发。这些漏洞未做标记;步骤 4 中的查找代理必须像处理真实代码一样,通过阅读逻辑来定位它们。当你准备好测试自己的代码时,请将 TARGET_DIR 指向你的代码仓库。

// canary.c
// 入口:./canary 
#include 
#include 
#include 

staticvoid parse_alpha(const unsigned char *data, size_t len) {
    unsigned char *buf = malloc(32);
    memcpy(buf, data, len);
    printf("alpha: %02x\n", buf[0]);
    free(buf);
}

staticvoid parse_bravo(const unsigned char *data, size_t len) {
    char name[16];
    memcpy(name, data, len);
    name[15] = 0;
    printf("bravo: %s\n", name);
}

staticvoid parse_charlie(const unsigned char *data, size_t len) {
    char *p = malloc(64);
    if (len > 0 && data[0] == 0xff) {
        free(p);
    }
    memcpy(p, data, len < 64 ? len : 64);
    printf("charlie: %p\n", (void *)p);
}

int main(int argc, char **argv) {
    if (argc < 2) return1;
    FILE *f = fopen(argv[1], "rb");
    if (!f) return1;
    unsigned char buf[4096];
    size_t n = fread(buf, 1, sizeof buf, f);
    fclose(f);
    if (n < 1) return1;
    switch (buf[0]) {
        case'A': parse_alpha(buf + 1, n - 1); break;
        case'B': parse_bravo(buf + 1, n - 1); break;
        case'C': parse_charlie(buf + 1, n - 1); break;
        default: printf("unknown format\n");
    }
    return0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

步骤 3:对目标进行威胁建模(先引导,后访谈)

威胁建模独立于任何特定漏洞来回答“这个系统可能出什么问题、谁会这么做、哪些结果值得关注?”。一个威胁(“攻击者通过不受信任的文件解析实现内存损坏”)在补丁修复后依然存在;而一个漏洞(“第 31 行未对 len 进行边界检查”)则会被修复。查找循环负责搜索漏洞;威胁模型则告诉它去哪里搜索,并告诉分类阶段如何评分。

我们通过一个 ClaudeSDKClient 会话的两轮互动来构建威胁模型:

  • 引导: Claude 使用内置的 Read 工具读取代码,并草拟模型(上下文、资产、入口点和信任边界、威胁,以及代码无法回答的待解问题)。
  • 访谈: 应用程序所有者回答待解问题,Claude 调整可能性和影响,然后在目标文件旁写入 THREAT_MODEL.md。

将两轮放在同一个客户端会话中,意味着访谈轮可以查看引导轮的工具调用结果,而无需重新发送源代码。对于一个 45 行的测试程序,两轮内容都很精简;但关键在于输出结构:入口点表格(你将据此对真实代码仓库进行并行查找代理分区)和待解问题列表(引导到访谈的交接点)。

[tool] Read {'file_path': 'vulnerability_detection_agent/canary/cana...}
--- 引导草稿 ---
`★ 洞察 ─────────────────────────────────────`
- 该文件根据 `buf[0]` 分派到三个解析器,每个解析器包含不同类别的内存安全漏洞:堆溢出、栈溢出和释放后使用。这是典型的“每个解析器一个漏洞”测试程序。
- 传递给每个解析器的 `len` 值为 `n - 1`(最多 4095),但缓冲区大小分别为 32/16/64,因此每个路径都可被攻击者通过单个输入文件中的溢出长度触发。
- 仅从源代码进行威胁建模可以枚举漏洞类别和入口点,但无法判断谁在生产环境中提供 `argv[1]` —— 这决定了这些漏洞是本地小问题还是远程代码执行(RCE)的根基。
`─────────────────────────────────────────────────`

# 威胁模型:canary(文件格式解析器命令行工具)

## 1. 系统上下文
一个小型 C 命令行工具,以 `./canary ` 形式调用。它打开提供的路径,读取最多 4096 字节,并根据第一个字节(`A`/`B`/`C`)分派到三个解析器之一。源代码中不包含网络、进程间通信或权限管理代码。仅凭代码无法获知部署上下文(谁运行它、谁提供文件、是否被服务封装)。

## 2. 资产
| 资产 | 描述 | 敏感性 |
| --- | --- | --- |
| 进程内存 / 控制流 | `canary` 进程的堆和栈,包括返回地址和堆元数据 | 高 —— 损坏后可在进程的安全上下文中实现任意代码执行 |
| 输入文件内容 |`argv[1]` 读取的字节;格式选择器加上解析器载荷 | 低至中(内容本身);作为攻击向量时为高 |
| 宿主执行上下文 | 二进制文件运行时所属的 uid/角色/容器;该上下文的文件系统可达范围 | 未知 —— 取决于部署 |

## 3. 入口点与信任边界
| 入口点 | 描述 | 信任边界 | 可达资产 |
| --- | --- | --- | --- |
| `argv[1]` 路径 | 调用者提供的文件系统路径,通过 `fopen(..., "rb")` 打开 | 调用者 → 进程(路径遍历/符号链接暴露取决于调用者权限) | 进程可读取的任何文件 |
| 文件内容(前 4096 字节) | `fread` 读入 `buf`,由 `buf[0]` 分派 | 文件生产者 → 解析器 | 通过所有三个解析器访问进程内存 |
| `parse_alpha` 载荷(`A` 前缀) | 字节 1..n 通过 `memcpy` 复制到一个 32 字节的堆缓冲区 | 不受信任的文件 → 堆 | 32 字节分配相邻的堆 |
| `parse_bravo` 载荷(`B` 前缀) | 字节 1..n 复制到一个 16 字节的栈缓冲区 | 不受信任的文件 → 栈 | 返回地址、保存的帧指针、栈 canary(如果启用) |
| `parse_charlie` 载荷(`C` 前缀) | 字节 1..n 复制到一个 64 字节的堆缓冲区;第一个字节可能触发提前释放 | 不受信任的文件 → 堆 | 已释放块的元数据、tcache/fastbin 状态 |

## 4. 威胁
| 编号 | 威胁 | 攻击面 | 资产 | 影响 | 可能性 |
| --- | --- | --- | --- | --- | --- |
| T1 | 堆缓冲区溢出:`parse_alpha` 将最多 `n-1`(≤4095)个字节复制到一个 32 字节的 `malloc` 缓冲区,未做边界检查(canary.c:8-9|`A` 为前缀的输入 | 进程内存 | 堆损坏 → 潜在的 RCE | 高(存在恶意文件时) |
| T2 | 栈缓冲区溢出:`parse_bravo` 将最多 `n-1` 个字节复制到一个 16 字节的栈数组(canary.c:15-16);末尾的 `name[15]=0` 并不能阻止溢出,仅截断打印的字符串 |`B` 为前缀的输入 | 保存的返回地址/| 经典栈溢出 → 若无/绕过栈保护则 RCE ||
| T3 | 释放后使用/双重释放向量:`parse_charlie``data[0]==0xff` 时释放 `p`,然后立即向已释放的内存块 `memcpy`(canary.c:23-26);随后的 `printf` 还泄露了已释放的指针 |`C` 为前缀且第一个载荷字节为 `0xff` 的输入 | 堆分配器元数据 | UAF 写入 → 堆布局操纵、释放地址信息泄露 |-|
| T4 | `parse_alpha``parse_charlie` 中未检查 `malloc` 的返回值(canary.c:8, 22| 内存压力下的任何 `A``C` 输入 | 进程稳定性 | NULL 解引用导致拒绝服务(DoS) | 正常操作下低 |
| T5 | `parse_charlie` 中通过 `printf("%p", p)` 泄露指针(canary.c:27| 任何 `C` 输入 | ASLR 秘密 | 泄露堆地址,有助于利用 T1/T3 |T1/T3 结合时高 |
| T6 | `argv[1]` 的路径处理:未进行规范化或白名单检查;进程将打开任何可读取的路径,包括符号链接和设备节点 | 调用者控制的 argv | 宿主文件 | 信息泄露或挂起(例如 `/dev/zero`),取决于谁运行它 | 未知 —— 取决于调用者权限 |
| T7 | 静默截断:只读取前 4096 个字节;解析器处理截断后的数据,可能掩盖上游的畸形文件检测 | 任何输入 | 下游决策的完整性 | 逻辑错误,非内存安全漏洞 ||

## 5. 待解问题
- **谁提供 `argv[1]`** 仅本地用户、setuid 包装器、Web 上传处理器、MTA、沙箱运行器?这决定了 T1T3 是仅限本地的低级错误还是远程代码执行的基础。
- **二进制文件以什么 uid/能力/容器运行?** 成功的 RCE 的爆破半径取决于此(root vs. nobody vs. seccomp 限制)。
- **生产构建中的编译器和链接标志:** 是否启用了 `-fstack-protector-strong``-D_FORTIFY_SOURCE=2`PIERELROASLRCFI?这些会实质性地改变 T2 的可利用性和 T5 泄露的有用性。
- **使用的分配器**(glibc ptmalloc、musl、jemalloc、hardened_malloc)。T3UAF 利用原语因分配器而异。
- **`canary` 是直接调用还是通过先验证文件的包装器调用?** 上游的前端解析器/大小上限/魔法字节白名单可以消除 T1T3- **是否监控崩溃?** 未检查的 `malloc`T4)和 T6 的 DoS 仅在需要正常运行时间时才重要;仅凭源代码无法判断。
- **格式本身的预期威胁模型:** 是否有文档(规范、RFC、同级文件)定义了 `A`/`B`/`C` 格式,以便我们区分“畸形”和“对抗性”?
- **二进制文件的预期寿命和分发方式:** 是演示/测试工具(名称“canary”暗示如此)还是交付给客户?影响修复优先级和披露路径。

  [tool] Write {'file_path': 'vulnerability_detection_agent/canary/THRE...'}
--- 精炼后的 THREAT_MODEL.md ---
# 威胁模型:canary(本地文件格式解析器命令行工具)

## 1. 系统上下文
`canary` 是一个本地命令行工具,以 `./canary ` 形式调用。它打开提供的路径,读取最多 4096 字节,并根据第一个字节(`A`/`B`/`C`)分派到三个解析器之一。它**不面对网络**。在预期的威胁模型中,输入文件**完全由攻击者控制** —— 典型的交付方式是通过电子邮件或网页下载文件,然后由本地用户打开。进程以**调用用户的身份运行,无沙箱保护**,因此任何导致控制流接管的内存安全漏洞都会在该用户的安全上下文(主目录、SSH 密钥、浏览器配置、云凭据、用户具有写入权限的任何路径)中实现代码执行。

## 2. 资产
| 资产 | 描述 | 敏感性 |
| --- | --- | --- |
| 调用用户的帐户 | uid、主目录、Shell 历史、SSH 密钥、浏览器配置、云令牌、点文件、可写挂载点 | 高 —— 一旦攻破等于完全接管用户帐户,并获得横向移动的立足点 |
| 进程内存 / 控制流 | `canary` 进程的堆和栈、返回地址、堆元数据 | 高 —— 损坏是利用用户帐户资产的利用原语 |
| 输入文件内容 |`argv[1]` 读取的字节;格式选择器加上解析器载荷 | 作为数据:低;作为攻击向量:主要对象 |
| 主机完整性 | 用户可写入的持久化位置(crontab、`~/.bashrc``~/.config/systemd/user`、登录项) | 高 —— 在利用后极易触及;没有沙箱来限制 |

## 3. 入口点与信任边界
| 入口点 | 描述 | 信任边界 | 可达资产 |
| --- | --- | --- | --- |
| `argv[1]` 路径 | 调用者提供的文件系统路径,通过 `fopen(..., "rb")` 打开 | 本地用户 → 进程(相同 uid;边界在于不受信任的*文件内容*与解析器之间,而非调用者与进程之间) | 用户可读取的任何文件 |
| 文件内容(前 4096 字节) | `fread` 读入 `buf`;由 `buf[0]` 分派 | 不受信任的攻击者(文件作者) → 解析器 | 通过所有三个解析器访问进程内存 |
| `parse_alpha` 载荷(`A` 前缀) | 字节 1..n 通过 `memcpy` 复制到一个 32 字节的堆缓冲区 | 不受信任的文件 → 堆 | 32 字节分配相邻的堆 |
| `parse_bravo` 载荷(`B` 前缀) | 字节 1..n 复制到一个 16 字节的栈缓冲区 | 不受信任的文件 → 栈 | 返回地址、保存的帧指针、栈 canary(如果启用) |
| `parse_charlie` 载荷(`C` 前缀) | 字节 1..n 复制到一个 64 字节的堆缓冲区;第一个字节可能触发提前释放 | 不受信任的文件 → 堆 | 已释放块的元数据、tcache/fastbin 状态 |

## 4. 威胁
| 编号 | 威胁 | 攻击面 | 资产 | 影响 | 可能性 |
| --- | --- | --- | --- | --- | --- |
| T1 | 堆缓冲区溢出:`parse_alpha` 将最多 `n-1`(≤4095)个字节复制到一个 32 字节的 `malloc` 缓冲区,未做边界检查(canary.c:8-9|`A` 为前缀的攻击者文件 | 进程内存 → 用户帐户 | 严重 —— 以调用用户身份实现 RCE;完全接管帐户,无沙箱限制 | 高 —— 单个精心构造的文件即可轻松触发 |
| T2 | 栈缓冲区溢出:`parse_bravo` 将最多 `n-1` 个字节复制到一个 16 字节的栈数组(canary.c:15-16);末尾的 `name[15]=0` 仅截断打印输出,不能阻止溢出 |`B` 为前缀的攻击者文件 | 保存的返回地址 → 用户帐户 | 严重 —— 经典栈溢出转为用户级 RCE;可利用性取决于交付版本中的栈保护 / PIE / ASLR ||
| T3 | 释放后使用/双重释放:`parse_charlie``data[0]==0xff` 时释放 `p`,然后立即向已释放的内存块 `memcpy`(canary.c:23-26|`C` 为前缀且第一个载荷字节为 `0xff` 的攻击者文件 | 堆分配器元数据 → 用户帐户 | 严重 —— 通过堆布局操纵实现用户级 RCE 的原语 |-高 —— 可靠性因分配器而异,但原语干净 |
| T4 | `parse_alpha``parse_charlie` 中未检查 `malloc` 的返回值(canary.c:8, 22| 内存压力下的任何 `A``C` 输入 | 进程稳定性 | 低 —— 用户调用的命令行工具发生 NULL 解引用崩溃;烦人,非入侵 ||
| T5 | `parse_charlie` 中通过 `printf("%p", p)` 泄露指针(canary.c:27| 任何 `C` 输入 | ASLR 秘密 | 高 —— 直接向攻击者提供堆地址,使 T1/T3 即使在 ASLR 下也能可靠利用 | 高 —— 在 `C` 路径上无条件发生 |
| T6 | `argv[1]` 的路径处理:未进行规范化或白名单检查;`canary` 打开用户可读取的任何路径,包括符号链接和设备节点(例如 `/dev/zero` 导致挂起) | 调用者提供的 argv | 进程稳定性 / 调用者预期 | 低 —— 调用者即用户,因此无法通过路径技巧跨越任何权限边界 ||
| T7 | 静默截断:只读取前 4096 个字节;可通过将漏洞利用代码填充到前 4096 字节内绕过上游的畸形文件检测 | 任何输入 | 任何上游“扫描后打开”管道的完整性 | 单独看低;若前端有扫描器则相关 ||
| T8 | **(新增)** 社会工程交付:攻击面是“用户打开一个文件”。典型向量是电子邮件附件、即时通讯传输和浏览器下载。T1/T2/T3 中的任何一个,配合有说服力的诱饵,都可实现单击 RCE | 攻击者编写并交付给用户的文件 | 用户帐户 | 严重 —— 与 T1T3 相同;此威胁命名了交付机制 | 高 —— 触发 T1T3 的主要现实路径 |
| T9 | **(新增)** 文件关联 / 处理器注册:如果 `canary` 是(或成为)某个文件扩展名或 MIME 类型的注册处理器,则双击下载的文件会自动调用它,无需引导用户执行 shell 命令 | 操作系统文件关联层 | 用户帐户 | 严重 —— 将 T8 从“在此文件上运行此二进制文件”转变为“打开附件” | 未知,无部署配置;已标记给所有者 |
| T10 | **(新增)** 利用后的爆破半径:无沙箱,因此 `canary` 中的 RCE 立即拥有用户的全部环境权限 —— SSH 密钥、浏览器 cookie/会话令牌、云 CLI 凭据(`~/.aws``~/.config/gcloud`)、通过 `~/.bashrc` 持久化、用户级 cron、用户 systemd 单元、登录项 | T1/T2/T3 中任何一个成功 | 用户帐户、联网系统 | 严重 —— 从此处轻松横向移动到电子邮件、云、源代码管理 |T1/T2/T3 成立时高 |
| T11 | **(新增)** 语料库 / 模糊测试暴露:由于存在三个明显的内存错误和一个单字节分派,针对 `canary` 运行 `afl-fuzz``libFuzzer` 的第一个小时内就会产生崩溃输入。如果二进制文件被分发,研究人员和攻击者会很快发现这些漏洞 | 任何拥有二进制文件的攻击者 | 披露时间表 | 高 —— 迫使修复时间窗口很短 ||

## 5. 待解问题
所有先前的待解问题已由所有者回答解决,但以下残留项仍然存在,它们范围更窄,且仍依赖于代码或构建:

- **交付版本中的编译器/链接器加固:** 分发的构建是否使用 `-fstack-protector-strong``-D_FORTIFY_SOURCE=2``-fPIE`/`-pie`、完整 RELRO 编译,并且目标操作系统上启用了 ASLR?这会改变 T2 的利用可靠性和 T5 泄露的价值,但不会改变严重性等级。
- **交付版本中的分配器**(glibc ptmalloc vs. musl vs. 加固分配器):决定 T3 的哪些利用原语是可行的。
- **文件关联注册(T9):** 是否有任何安装程序或桌面条目将 `canary` 注册为某个扩展名或 MIME 类型的处理器?如果是,T8 将变成纯粹的单击 RCE- **签名/公证:** 二进制文件是否以操作系统守门人(macOS Gatekeeper、Windows SmartScreen、Linux 桌面“可执行位”提示)会在首次运行前警告用户的方式分发?这会影响 T8 的摩擦,但不会改变最终影响。
- **遥测/崩溃报告:** T1T4 的崩溃是否报告给了防御者可见的任何地方,或者失败的利用尝试是否悄无声息?
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.

步骤 4:运行自动化查找循环

如果使用原始消息 API,这一步将是一个手工编写的 while stop_reason == “tool_use” 循环,并配备自定义文件工具。Agent SDK 处理了所有这些:我们只需调用一次 query(),并设置 allowed_tools=[“Read”, “Grep”, “Glob”] 和 disallowed_tools=[“Bash”],Claude Code 便会自主运行探索-读取-推理循环。cwd=str(TARGET_DIR) 将代理指向测试程序,system_prompt={“type”: “preset”, “preset”: “claude_code”, “append”: …} 保留了 Claude Code 的默认系统提示(已告知代理其工作目录),同时附加了我们的行动背景,因此代理永远无需猜测自己的位置。通过禁止 Bash/Write/Edit,代理保持只读状态。

提示中最关键的部分仍然是质量等级评估标准。 若没有它,LLM 漏洞狩猎者会报告每一个能发现的空指针解引用和失败的断言,这些虽然是真正的崩溃,但几乎无法利用。该标准告诉代理哪些崩溃类别值得提交(堆/栈溢出、释放后使用、受控地址写入),哪些只是需要继续阅读的路标。这一个区块在很大程度上决定了安全工程师会采纳的报告与会被忽略的报告之间的差别。

生产版本会将 “Bash” 加入 allowed_tools,以便代理可以用 -fsanitize=address 编译并确认每个崩溃;但这应放在锁定的容器内进行,因此本教程保持只读状态。

[tool] Glob {'pattern': '**/*'}
  [tool] Read {'file_path': 'vulnerability_detection_agent/canary/cana...'}

`★ 洞察 ─────────────────────────────────────`
-38 行的单字节分派使得每个解析器可通过一个简单的文件前缀(`A`/`B`/`C`)独立触发,因此每个漏洞都是一个独立的攻击面。
- `parse_bravo``name[15]=0` 是一种常见的虚假安全模式:它为 `printf` 进行了空终止,但第 16 行的 `memcpy` 在该行运行之前已经写入了 16 字节帧之外。
- `parse_charlie` 将两个原语(条件释放后无条件写入)组合成一个干净的 UAF,其中的触发字节由攻击者选择,这比偶然的 UAF 更罕见,也更难修复。
`─────────────────────────────────────────────────`


F-01
canary.c:8-9
堆缓冲区溢出
`parse_alpha` 分配了一个固定 32 字节的堆缓冲区,然后通过 `memcpy``len` 个攻击者控制的字节复制进去,未进行边界检查。`len``n-1`,其中 `n` 最大为 4096,因此一个以 `A` 为前缀的文件最多可向 32 字节的内存块中写入 4095 字节,覆盖相邻的堆元数据和邻近分配。触发条件:任何以字节 `A` 开头、总大小超过 33 字节的文件。



F-02
canary.c:15-16
栈缓冲区溢出
`parse_bravo` 将最多 `n-1` 个攻击者控制的字节复制到一个 16 字节的栈数组 `name[16]` 中,未对 `len` 进行边界检查。随后的 `name[15]=0` 仅截断了 `printf` 的输出;`memcpy` 中的越界写入已经覆盖了保存的帧指针、返回地址以及任何栈 canary。触发条件:任何以字节 `B` 开头、载荷超过 16 字节的文件。



F-03
canary.c:22-26
释放后使用
`parse_charlie` 分配了 64 字节,当第一个载荷字节等于 `0xff` 时条件释放 `p`,然后无条件地 `memcpy``p` 写入。当触发字节存在时,`memcpy` 写入到已释放的内存块,破坏了分配器空闲列表元数据(glibc 上的 tcache/fastbin),并产生一个标准的堆布局操纵原语,最终实现任意写入/控制流劫持。随后的 `printf("%p", p)` 还泄露了堆指针,使得在 ASLR 下也能可靠利用。触发条件:以 `C` 开头、后面跟着字节 `0xff` 的文件。


三个高价值发现(F-01 堆溢出、F-02 栈溢出、F-03 UAF 带指针泄露)与威胁模型中的 T1/T2/T3+T5 匹配。在 45 行的源代码中,除此之外没有其他高价值的内存安全漏洞。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

步骤 5:筛选原始发现结果

查找代理已调优为高召回率,因此其输出通常包含重复项(一个根因从两条路径触发),偶尔也会包含一个站不住脚的发现。分类阶段就是过滤器:一个新的 query() 会话重新读取代码,验证每个发现是否与实际代码行对应,合并重复项(按根因),并重新推导严重性(基于威胁模型中信任边界内的可达性)。我们有意识不让分类阶段继承查找代理的严重性评分;独立重新推导是一种廉价的方式来发现过度自信。

[tool] Read {'file_path': 'vulnerability_detection_agent/canary/cana...'}

`★ 洞察 ─────────────────────────────────────`
- 所有三个漏洞都位于同一信任边界之后(攻击者编写的文件 → 解析器),由 `main` 中的单字节分派触发。无身份验证、无沙箱、相同 uid 的爆破半径,因此三者可达性完全相同,严重性由内存损坏原语本身决定。
- F-03 实际上是在一个调用点融合的两个漏洞:UAFT3)和 `printf("%p")` 堆指针泄露(T5)。按根因(两者都在 `parse_charlie`6 行代码中)将它们合并到 F-03 下是正确的,但正是这个泄露将 UAF 从“在 ASLR 下不可靠”升级为“一次成功”。
`─────────────────────────────────────────────────`

## 分类结果

| 编号 | 结论 | 代码行 | 威胁 | 严重性 | 备注 |
| --- | --- | --- | --- | --- | --- |
| F-01 | 真实 | canary.c:8-9 | T1 | **严重** | `malloc(32)` 然后 `memcpy(buf, data, len)``len` 最大为 4095(来自 `main``n-1`,第 39 行)。攻击者文件 `A` + 至少 33 字节溢出堆;无沙箱 → 以调用用户身份实现 RCE|
| F-02 | 真实 | canary.c:15-16 | T2 | **严重** | `char name[16]` 然后 `memcpy(name, data, len)`;第 17 行的 `name[15]=0`OOB 写入*之后*执行,仅截断 `printf`。经典栈溢出;利用可靠性取决于交付版本中的加固(威胁模型中的待解问题),但严重性等级不变。 |
| F-03 | 真实 | canary.c:22-27 | T3 + T5 | **严重** |22`malloc(64)`,第 24 行条件 `free(p)`(当 `data[0]==0xff` 时),第 26 行无条件 `memcpy`UAF 写入空闲列表元数据。第 27`printf("charlie: %p", p)` 无条件泄露堆指针,使 T1T3ASLR 失效。保留为一项发现(根因 = `parse_charlie`),但明确提及泄露。 |

**重复项:** 无需要合并的重复项。三个不同解析器中的三个不同根因。

**针对威胁模型的覆盖检查:** F-01/F-02/F-03 覆盖了 T1/T2/T3/T5T4NULL 返回的 `malloc`)和 T6T7(路径处理、4096 字节截断)有意排除在外,因为它们不是内存安全发现,且威胁模型已将其评为低风险。T8T11 是交付/部署/项目关注点,非源代码漏洞 —— 不预期在此轮覆盖。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

步骤 6:输出结构化报告

下游系统(问题跟踪器、仪表板、SIEM)需要结构化数据。最终一次无工具的 query() 将分类后的发现转换为符合明确模式(schema)的 JSON。我们将每个字段标记为必填,并使用 null 表示“不适用”,以便模型无需猜测可选性;在生产环境中,你将使用 jsonschema 进行验证,并在失败时重试。

{
  "findings": [
    {
      "id": "F-01",
      "category": "堆缓冲区溢出",
      "severity": "严重",
      "file": "canary.c",
      "description": "parse_alpha 分配了 malloc(32),然后通过 memcpy 将攻击者控制的数据(长度最多 4095 字节,来自 main 中第 39 行的 n-1)复制进去。攻击者编写的以 'A' 开头的文件,若后续内容超过 32 字节,则溢出堆分配。无沙箱保护,导致以调用用户身份实现 RCE。",
      "recommendation": "在 memcpy 之前根据分配大小验证 len(例如要求 len <= 32),或根据 len 分配缓冲区大小。在解析器边界拒绝过大的输入,并添加边界检查的复制辅助函数。"
    },
    {
      "id": "F-02",
      "category": "栈缓冲区溢出",
      "severity": "严重",
      "file": "canary.c",
      "description": "parse_bravo 声明了 char name[16],然后通过 memcpy 将攻击者控制的数据(长度最多 4095 字节)复制进去。第 17 行的 name[15]=0 空终止在越界写入之后执行,仅截断随后的 printf;它不能阻止栈溢出。利用可靠性取决于编译时加固,但漏洞类别不变。",
      "recommendation": "在 memcpy 之前将 len 限制为 sizeof(name)-1,或使用 strncpy/snprintf 并明确指定大小。确保构建中启用了栈保护(-fstack-protector-strong)、FORTIFY_SOURCE 和 PIE。"
    },
    {
      "id": "F-03",
      "category": "释放后使用并包含堆地址泄露",
      "severity": "严重",
      "file": "canary.c",
      "description": "parse_charlie 在第 22 行 malloc 了 64 字节,在第 24 行当 data[0]==0xff 时条件释放 p,然后无条件地在第 26 行向 p memcpy,损坏了空闲列表元数据。第 27 行的 printf(\"charlie: %p\", p) 无条件泄露堆指针,击败了 ASLR,将 UAF(以及 F-01 的堆溢出)从不可靠升级为一次成功可触发。",
      "recommendation": "在 free 后将 p 设为 NULL 并保护后续使用;重新设计结构,使释放和写入不能同时作用于同一指针。移除 %p 泄露(或将其置于调试构建中)以保护 ASLR。"
    }
  ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

总结与下一步

你已经使用 Agent SDK 构建了完整的威胁建模、查找、分类、报告流水线:一个多轮 ClaudeSDKClient 会话用于威胁建模,以及三个一次性的 query() 调用用于其余步骤,借助 Claude Code 内置的文件工具进行探索。

需要牢记的关键模式:

  • 使用 ClaudeSDKClient 进行对话,使用 query() 进行一次性任务。 威胁建模访谈需要引导轮作为上下文;查找、分类和报告不依赖彼此的工具调用记录,因此无状态调用更简单。
  • cwd + allowed_tools 替代了手工编写的工具。 作用域限定在目标目录的 Read/Grep/Glob 就是整个查找代理的脚手架。
  • 分类是单独的一轮。 独立于查找代理重新验证和评分,能够廉价地捕捉过度自信。

进一步探索:

  • 使用托管版本。Claude Code Security 以托管产品的形式提供相同的查找和分类能力,你只需指向一个代码仓库,Anthropic 负责处理沙箱化和扩展。
  • 扩展到真实代码仓库。 将 cwd 指向真实的代码仓库,在沙箱容器内将 “Bash” 加入 allowed_tools,以便代理能用 -fsanitize=address 编译并确认崩溃,然后使用 asyncio.gather 为威胁模型中的每个入口点生成一个 query()。
  • 将报告接入你的跟踪系统。 使用 jsonschema 验证步骤 6 的 JSON,将其映射到 SARIF 或你的工单模式,然后 POST 提交。

原文:https://platform.claude.com/cookbook/claude-agent-sdk-06-the-vulnerability-detection-agent

文章来自:51CTO

Loading

作者 yinhua

发表回复