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个星号,看着挺壮观。
  • 改动标记用 ! 而不是 +/-。一个感叹号表示这一行被修改了。
  • 上下文和改动内容分两段展示,先旧后新。
注意:上下文格式的块头写法跟 unified 不一样。*** 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:谁报告的漏洞。如果看到熟悉的安全研究员名字,说明这个漏洞可能已经被深入分析过了。
避坑指南:我曾经遇到过一个补丁,提交信息写的是 "Fix memory leak",但实际改动里却包含了一个 free() 调用。仔细一看,那个 free() 的位置根本不对,反而引入了 use-after-free。所以记住:永远不要完全相信提交信息,代码才是最终的真相。

3.4 三种格式的对比与选择

我整理了一个对比表,方便你快速决策:

特性 Unified Diff Context Diff 普通 Diff
可读性 高 中 低
信息密度 高 低 中
工具支持 广泛(Git、diff、patch) 有限(老版本 patch) 基本(diff 默认格式)
适用场景 日常代码审查、漏洞狩猎 兼容老旧系统 快速查看改动行号

我个人建议:能用 unified 就用 unified。它信息最全,工具支持最好,而且几乎所有现代版本控制系统都默认用它。只有在处理一些陈年旧项目时,才需要切换到 context 格式。

嗯,补丁文件格式这块儿就聊到这儿。你想想看,掌握了这些基础,再去分析安全补丁,是不是心里就有底了?下一章咱们聊聊怎么从补丁中逆向出漏洞的触发路径,那才是真正有意思的部分。