3、Git基础与补丁提取:Git常用命令、生成补丁文件、提取特定版本的补丁

做漏洞分析,说白了就是跟代码的「前世今生」打交道。你拿到一个CVE公告,光看diff可能一头雾水。但如果你能亲手把补丁从Git仓库里「拎」出来,对比一下修复前后的变化,很多隐藏的漏洞就藏不住了。这一章,我就带你过一遍Git里最核心的几个操作,专门为补丁分析服务。

3.1 我常用的几个Git命令

Git命令很多,但做补丁分析,你不需要全记住。我个人习惯,日常就靠下面这几个命令吃饭。

命令 作用 我为什么常用它
git log 查看提交历史 找漏洞修复的commit,看作者、时间、描述
git show 显示某个commit的详细改动 直接看diff,比去网页上翻方便多了
git diff 比较两个版本之间的差异 对比修复前后的代码,找逻辑漏洞
git format-patch 生成补丁文件 把commit导出成.patch文件,方便分享或存档
git apply 应用补丁文件 在本地复现漏洞环境,或者测试补丁效果

嗯,这里要注意一点:git log 默认只显示当前分支的历史。如果你发现某个漏洞修复的commit不在当前分支上,记得加个 --all 参数。我曾经因为没加这个参数,找了半天没找到那个关键的commit,后来才发现它躺在另一个分支里。

3.2 生成补丁文件

生成补丁文件,是漏洞分析里很关键的一步。你想想看,你从开源项目里找到了一个修复commit,怎么把它提取出来?总不能截图吧?

Git提供了两种方式:

3.2.1 用 git format-patch 生成标准补丁

这个命令会生成一个 .patch 文件,里面包含了commit的作者、时间、描述和具体的代码改动。格式是标准的,可以直接发给别人,或者用 git am 导入到另一个仓库。

# 生成最近1个commit的补丁
git format-patch -1

# 生成从 commitA 到 commitB 之间的所有补丁
git format-patch commitA..commitB

# 生成某个commit的补丁(比如漏洞修复的那个)
git format-patch -1 <commit-hash>

举个例子,假设你找到了一个修复了缓冲区溢出的commit,哈希是 abc123。你只需要跑:

git format-patch -1 abc123

然后当前目录下就会多出一个 0001-xxx.patch 文件。打开看看,里面就是完整的补丁内容。

我的小技巧: 我习惯在生成补丁时加上 --stdout 参数,把内容直接输出到终端,方便快速浏览。比如 git format-patch -1 abc123 --stdout。这样就不用生成文件,看完就完事了。

3.2.2 用 git diff 生成简易补丁

有时候你不需要完整的commit信息,只想看代码改了什么。这时候用 git diff 更直接。

# 比较两个commit之间的差异
git diff commitA commitB

# 比较工作区和暂存区的差异
git diff

# 比较暂存区和最新commit的差异
git diff --cached

举个例子,你想看漏洞修复前(commit old)和修复后(commit new)到底改了哪些行:

git diff old new

输出结果里,- 开头的行是删除的,+ 开头的行是新增的。漏洞分析时,我特别关注那些被删除的代码——往往就是漏洞所在。

注意: git diff 生成的补丁不包含commit的元信息(作者、时间、描述)。如果你需要保留这些信息,请用 git format-patch。我曾经在提交漏洞报告时,直接用 git diff 生成的补丁发给厂商,结果对方问我「这是谁改的?什么时候改的?」——尴尬。

3.3 提取特定版本的补丁

在实际分析中,你经常需要提取某个特定版本的补丁。比如,Linux内核的某个CVE修复,可能是在5.10.100版本里。你怎么把这个版本的补丁提取出来?

我的做法分三步:

  1. 找到修复commit:用 git log --oneline 搜索,或者直接看CVE公告里给出的commit哈希。
  2. 确认父commit:修复commit的父commit,就是漏洞存在时的代码。用 git show <commit-hash>^ 可以查看父commit的内容。
  3. 生成补丁:用 git format-patch 或 git diff 提取差异。

举个例子,假设CVE-2024-12345的修复commit是 def456,它的父commit是 abc123。你想提取这个补丁:

# 方法一:用 format-patch
git format-patch -1 def456

# 方法二:用 diff
git diff abc123 def456

两种方法都行。我个人更推荐 format-patch,因为它保留了commit信息,方便后续追溯。

避坑指南: 我曾经遇到过一个情况,某个漏洞修复commit并不是直接基于漏洞版本提交的,而是基于一个更新的版本。这时候直接 git diff 出来的补丁可能包含大量无关的改动。怎么办?我的经验是:先找到漏洞版本对应的tag或分支,然后在这个基础上 cherry-pick 修复commit,再生成补丁。这样得到的补丁才是「纯净」的。

3.4 实战:从Linux内核提取一个补丁

光说不练假把式。我们模拟一个场景:假设你发现Linux内核的某个驱动存在一个整数溢出漏洞,修复commit是 a1b2c3d4。你想提取这个补丁,并分析漏洞细节。

步骤:

  1. 克隆仓库:git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
  2. 切换到修复commit:git checkout a1b2c3d4
  3. 生成补丁:git format-patch -1 a1b2c3d4
  4. 查看补丁:用文本编辑器打开生成的 .patch 文件,或者直接用 cat 查看。

补丁文件里,你会看到类似这样的内容:

From a1b2c3d4... Mon Sep 17 00:00:00 2001
From: Author Name <author@example.com>
Date: Mon, 1 Jan 2024 12:00:00 +0000
Subject: [PATCH] Fix integer overflow in xxx driver

---
 drivers/xxx/xxx.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/xxx/xxx.c b/drivers/xxx/xxx.c
index abc123..def456 100644
--- a/drivers/xxx/xxx.c
+++ b/drivers/xxx/xxx.c
@@ -100,7 +100,7 @@ static int xxx_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
 {
     int ret;
     unsigned long size;
-    size = arg + sizeof(struct xxx_data);
+    if (arg > ULONG_MAX - sizeof(struct xxx_data))
+        return -EINVAL;
+    size = arg + sizeof(struct xxx_data);
     ...
 }

看到了吗?修复前,arg + sizeof(struct xxx_data) 可能溢出,导致 size 变成一个很小的值,进而引发缓冲区溢出。修复后,加了一个溢出检查。这就是漏洞的根因。

我的经验: 分析补丁时,别只看改动的代码。我习惯同时看看上下文,特别是函数入口和出口。有时候漏洞的触发条件就藏在那些没改动的代码里。比如这个例子,xxx_ioctl 函数里可能还有其他路径没有做检查,那可能就是另一个漏洞。

好了,这一章的内容就到这里。Git基础操作是补丁分析的「基本功」,练熟了,后面分析复杂漏洞时才能游刃有余。下一章,我们会深入补丁的diff分析,看看怎么从一行代码的改动里挖出隐藏的漏洞。