3、补丁文件格式解析:统一差异格式、上下文差异格式、补丁元数据解读
做漏洞狩猎,说白了就是跟补丁打交道。你拿到一个安全更新,第一件事就是看它改了啥。但补丁文件本身长什么样?不同格式之间有什么区别?元数据里又藏着哪些线索?
我刚开始做这行的时候,对着一个几千行的 diff 文件发懵,完全不知道从哪下手。后来踩的坑多了,才慢慢摸出门道。今天咱们就把补丁文件格式这事儿彻底聊透。
3.1 统一差异格式(Unified Diff)
这是目前最主流的补丁格式。Git 默认用的就是它。为什么叫「统一」?因为它把上下文和改动内容合并在一起展示,看起来更紧凑。
一个典型的 unified diff 长这样:
--- a/src/vuln.c
+++ b/src/vuln.c
@@ -10,7 +10,7 @@
char buf[64];
char *input = get_user_input();
- strcpy(buf, input);
+ strncpy(buf, input, sizeof(buf) - 1);
process_data(buf);
return 0;
我来拆解一下每个部分:
- 文件头:
---和+++分别表示旧文件和新文件。路径通常是相对于项目根目录的。 - 块头(Hunk Header):
@@ -10,7 +10,7 @@这个看着唬人,其实很简单。前半段-10,7表示旧文件从第10行开始,连续7行;后半段+10,7表示新文件同样位置也是7行。 - 上下文行:以空格开头的行,表示没有变化的部分。这些行用来定位改动的位置。
- 删除行:以
-开头的行,表示旧文件中有、新文件中删掉的内容。 - 新增行:以
+开头的行,表示新文件中新增的内容。
@@ 块头。如果改动集中在同一个函数内,那大概率是逻辑修复。如果改动分散在多个文件、多个位置,那可能是架构层面的安全加固。
为什么会用 -10,7 这种写法?其实是为了处理边界情况。比如文件开头或结尾的改动,行号会变成 -0,0 或 -end,0。我见过不少新手看到 -0,0 就懵了,其实它表示「在文件开头插入内容」。
3.2 上下文差异格式(Context Diff)
这种格式现在用得少了,但老项目里还能见到。它比 unified diff 更啰嗦,把上下文单独列出来。
举个例子:
*** src/vuln.c
--- src/vuln.c
***************
*** 10,16 ****
char buf[64];
char *input = get_user_input();
! strcpy(buf, input);
process_data(buf);
return 0;
--- 10,16 ----
char buf[64];
char *input = get_user_input();
! strncpy(buf, input, sizeof(buf) - 1);
process_data(buf);
return 0;
注意几个关键区别:
- 文件头用
***和---区分,而不是---和+++。 - 块分隔符是
***************,15个星号,看着挺壮观。 - 改动标记用
!而不是+/-。一个感叹号表示这一行被修改了。 - 上下文和改动内容分两段展示,先旧后新。
*** 10,16 **** 表示旧文件的第10到16行,--- 10,16 ---- 表示新文件的同样范围。我刚开始看的时候总搞混,后来干脆写了个小脚本自动转换。
说实话,我个人不太喜欢上下文格式。信息冗余太多,一个简单的改动要看好几屏。但有些老版本的 patch 工具只支持这种格式,你不得不学。
3.3 补丁元数据解读
补丁文件不只是代码改动,它还带着一堆元数据。这些信息往往比代码本身更有价值。
一个完整的 Git 补丁通常包含:
From: researcher@example.com
Date: Mon, 15 Jan 2024 10:30:00 +0800
Subject: [PATCH] Fix buffer overflow in input handling
A malicious user could trigger a stack buffer overflow by
providing a specially crafted input string. This patch replaces
the unsafe strcpy() with strncpy() to prevent the overflow.
Signed-off-by: Researcher Name <researcher@example.com>
---
我来逐个解读这些元数据:
| 字段 | 含义 | 狩猎价值 |
|---|---|---|
| From | 提交者邮箱 | 可以追踪到安全研究员的身份,有时能发现同一作者的其他修复 |
| Date | 提交时间 | 结合 CVE 发布时间,能判断是主动修复还是被动响应 |
| Subject | 提交标题 | 关键词如 "buffer overflow"、"use-after-free" 直接暴露漏洞类型 |
| 提交信息 | 详细描述 | 经常包含漏洞触发条件、影响范围、甚至 PoC 思路 |
| Signed-off-by | 签名者 | 表明谁批准了这个补丁,有时能追溯到安全团队 |
实战经验:我曾经通过一个补丁的提交信息,发现作者提到了 "similar issue in function X"。顺着这条线索,我在另一个函数里找到了同样的漏洞模式。说白了,补丁元数据就是一份「漏洞地图」,关键看你会不会读。
还有几个容易被忽略的元数据:
- CVE 编号:有些补丁会在提交信息里直接写 "CVE-2024-XXXXX",这是最直接的线索。
- Fixes 标签:Git 里常见的
Fixes: abc123def指向引入漏洞的原始提交。这个信息对理解漏洞的「前世今生」特别有用。 - Reported-by:谁报告的漏洞。如果看到熟悉的安全研究员名字,说明这个漏洞可能已经被深入分析过了。
free() 调用。仔细一看,那个 free() 的位置根本不对,反而引入了 use-after-free。所以记住:永远不要完全相信提交信息,代码才是最终的真相。
3.4 三种格式的对比与选择
我整理了一个对比表,方便你快速决策:
| 特性 | Unified Diff | Context Diff | 普通 Diff |
|---|---|---|---|
| 可读性 | 高 | 中 | 低 |
| 信息密度 | 高 | 低 | 中 |
| 工具支持 | 广泛(Git、diff、patch) | 有限(老版本 patch) | 基本(diff 默认格式) |
| 适用场景 | 日常代码审查、漏洞狩猎 | 兼容老旧系统 | 快速查看改动行号 |
我个人建议:能用 unified 就用 unified。它信息最全,工具支持最好,而且几乎所有现代版本控制系统都默认用它。只有在处理一些陈年旧项目时,才需要切换到 context 格式。
嗯,补丁文件格式这块儿就聊到这儿。你想想看,掌握了这些基础,再去分析安全补丁,是不是心里就有底了?下一章咱们聊聊怎么从补丁中逆向出漏洞的触发路径,那才是真正有意思的部分。