这期解读的安全论文是来自安全顶级会议之一的NDSS 2026,论文题目是Accurate Identification of the Vulnerability-Introducing Commit based on Differential Analysis of Patching Patterns,
官网链接为 https://www.ndss-symposium.org/ndss-paper/accurate-identification-of-the-vulnerability-introducing-commit-based-on-differential-analysis-of-patching-patterns/
一、论文背景
这篇论文关注的是漏洞引入提交识别问题,也就是当某个软件版本被发现存在漏洞之后,如何沿着代码提交历史向前追溯,准确找出漏洞最早被引入的那个 commit。论文将这个 commit 称为 Vulnerability-Introducing Commit,简称 VIC。这个问题在漏洞管理中非常关键,因为安全团队在处理 CVE 时,不能只关心“当前版本有没有修复”,还需要判断“哪些历史版本实际上已经受到影响”。如果无法准确定位 VIC,就很难确定漏洞影响范围,也会影响补丁回溯、版本修复、供应链安全检测和漏洞数据库维护等工作。
从实际背景来看,NVD、OSVDB、Bugtraq 等漏洞数据库通常会记录 CVE 描述、受影响版本和补丁信息,Snyk、Dependency-Check、OSSIndex 等安全工具也高度依赖这些数据库。但论文指出,公开漏洞数据库中的 affected versions 信息并不总是可靠。例如,已有研究发现约 25% 的版本被错误标记为受影响版本,还有研究显示,只有 59.82% 的漏洞报告能够与 NVD 完全匹配。这意味着,如果企业只依赖漏洞数据库给出的影响版本范围,就可能出现两类问题:一类是把并不受影响的版本误判为存在漏洞,造成误报和不必要的修复成本;另一类是把真实受影响的历史版本漏掉,导致漏洞长期残留。因此,论文认为更可靠的方式是回到代码演化历史中,识别漏洞最早是从哪个 commit 被引入的。
现有方法虽然已经尝试通过补丁回溯 VIC,但仍存在明显局限。典型方法如 B-SZZ、V-SZZ 等,主要关注安全补丁中的删除语句,认为被删除的代码往往就是漏洞相关代码。这种思路有一定合理性,因为很多补丁确实会删除错误逻辑,但论文认为这种假设过于粗糙,原因主要有三点:
第一,补丁中的代码不一定都与漏洞修复直接相关。安全补丁中常常混入函数重命名、变量声明、注释、格式调整、代码重构等内容。这些语句虽然出现在 patch 中,但并不决定漏洞是否存在。如果将它们也纳入 VIC 匹配,一旦历史版本中发生过重构或格式变化,就可能导致匹配失败,从而误判漏洞引入位置。
第二,真正触发漏洞的关键语句不一定出现在补丁中。例如修复缓冲区溢出时,补丁可能只是新增边界检查语句,但真正访问缓冲区的语句可能没有被修改;修复 use-after-free 时,补丁可能新增释放前的状态检查,但真正触发重复释放的控制流语句可能仍然是原代码中的未修改语句。如果只分析 patch 中的新增行和删除行,就会漏掉这些漏洞触发路径上的核心语句。
第三,补丁和早期版本之间可能存在大量代码演化差异。漏洞通常是在较新版本中被发现并修复的,但它可能早在很久以前就被引入。早期版本中的代码结构、函数名称、变量名称、控制流组织方式可能都发生过变化,因此不能简单地用“补丁文本是否完全匹配”来判断历史版本是否存在漏洞。
论文以CVE-2023-6111作为运行示例说明这一问题。该漏洞是 Linux kernel 中的 use-after-free 漏洞,补丁中有些代码只是抽取函数、减少重复逻辑,并不是漏洞本身;但某些没有被修改的控制流语句却是触发漏洞的关键条件。由此可以看出,VIC 识别的难点不在于简单匹配补丁文本,而在于理解补丁背后的漏洞修复语义:补丁到底修复了什么错误逻辑,哪些语句真正决定漏洞是否存在,以及这些关键语句最早出现在历史代码中的哪个位置。
二、工作概述
针对上述问题,论文提出了一个名为VicDiff的自动化 VIC 识别方法。它的核心思想是:不要把补丁看作一组孤立的新增行和删除行,而要分析新增语句与删除语句之间的关系,从补丁修复模式中还原漏洞相关逻辑,再用这组真正关键的语句序列去历史版本中回溯匹配。换句话说,VicDiff 并不是简单判断“某个补丁是否存在”,也不是简单追踪“哪一行删除语句最早出现”,而是先理解补丁的修复行为,再提取能够代表漏洞存在性的关键代码序列。
具体来说,VicDiff 主要完成三件事:一是从补丁中去掉明显无关的噪声语句,只保留漏洞相关语句;二是对补丁中的新增语句和删除语句进行差分分析,判断它们属于哪一种修复模式,例如新增缺失检查、删除错误语句、修改错误调用、调整参数、移动语句位置等;三是基于这些修复模式,从漏洞文件中提取一条更精炼的 vulnerability-critical statement sequence,简称 VCSeq,即漏洞关键语句序列,并用它去历史提交中寻找漏洞最早出现的位置。
论文第 4 页图 3 展示了 VicDiff 的整体流程。整个过程可以概括为:
- 输入安全补丁:以某个 CVE 的 security patch 作为分析起点;
- 过滤噪声语句:删除函数重命名、注释、空行、变量声明、方法抽取等与漏洞逻辑关系不大的语句;
- 差分分析补丁模式:结合漏洞修复前文件 Fv 和修复后文件 Fp,分析控制流、数据流、语句位置、操作符和参数差异;
- 提取漏洞关键语句序列 VCSeq:不是直接使用完整 patch,而是提取真正决定漏洞是否存在的关键语句;
- 筛选相关历史提交和文件版本:只关注涉及漏洞函数的 commits,减少无关代码演化带来的干扰;
- 历史版本匹配:按照逆时间顺序在历史文件中匹配 VCSeq,找到从“漏洞逻辑存在”到“漏洞逻辑不存在”的边界,从而确定 VIC。
这个流程的创新点在于,它把补丁分析从传统的文本级匹配提升到了模式级和语义级分析。传统方法更像是在问:“这几行代码有没有出现过?”而 VicDiff 更进一步问:“这些代码变化代表了哪种漏洞修复行为?真正导致漏洞存在的代码逻辑是什么?这段逻辑最早从哪个 commit 开始出现?”这种方式既能减少无关补丁语句导致的误判,也能补充那些没有直接出现在补丁中、但对漏洞触发至关重要的语句。
从实验结果看,论文使用 Linux kernel、OpenSSL、Wget、MySQL、FFmpeg 等开源项目构建数据集。其中,Linux kernel 数据集包含6,920 个 CVE和 5,859,238 个代码提交,总体数据集共涉及 6,943 个 CVE。实验结果显示,在 Linux kernel 上,VicDiff 的 **precision 达到 94.94%,recall 达到 86.92%**,明显优于 B-SZZ、V-SZZ 和 Redebug 等基线方法。论文还通过消融实验说明,噪声过滤和漏洞关键语句序列提取是性能提升的关键:如果不进行噪声过滤,容易因为重构、变量声明等无关语句造成漏报;如果只依赖删除语句,又会漏掉大量没有直接出现在 patch 中的关键漏洞触发语句。因此,VicDiff 的优势主要来自它对补丁修复语义的更细粒度理解。
三、工作具体说明
VicDiff 的第一步是过滤补丁中的噪声语句。论文认为,安全补丁虽然包含漏洞修复信息,但补丁并不等于漏洞逻辑本身。一个 patch 中除了真正修复漏洞的代码外,还可能包含大量非语义修改。例如,函数重命名可能只是为了提升可读性;方法抽取可能只是为了减少重复代码;变量声明通常只是为了支持后续新增逻辑;注释和空行更不会影响程序实际执行。如果这些语句被纳入 VIC 匹配,那么历史版本中只要出现一次函数重构、命名调整或格式变化,就可能导致匹配失败,从而把仍然存在漏洞的版本误判为不受影响。为此,VicDiff 通过语法分析和正则匹配过滤五类常见噪声语句:函数重命名、方法抽取、变量声明、注释行和空行。以 CVE-2023-6111 为例,补丁中新增的辅助函数和部分变量声明会被视为噪声过滤掉,系统只保留真正可能影响漏洞逻辑的新增语句和删除语句,形成 vulnerability-related statements,简称 VRStmt,即漏洞相关语句集合。这个步骤的意义在于,先把补丁“净化”为更接近漏洞本质的代码变化集合,减少后续分析被重构类修改干扰的可能性。
第二步是论文的核心,即基于差分分析识别补丁修复模式。论文指出,补丁中的新增语句和删除语句并不是互相独立的。一个新增语句可能是对某个删除语句的替换,也可能是把原来的逻辑移动到了其他位置,还可能是在原有控制流附近补充缺失的安全检查。因此,VicDiff 会比较漏洞修复前文件 Fv 和修复后文件 Fp 的控制流图,分析补丁语句在位置、操作符和参数上的差异,并把语句划分为不同的 patching patterns。论文第 5 页图 4 总结了这些模式:最基础的是 A、R、M 三类,其中 A 表示新增语句,常见于补充边界检查、权限检查或状态校验;R 表示删除错误语句,常见于移除会导致漏洞的冗余逻辑、危险调用或错误释放;M 表示修改语句,通常意味着原有语句存在缺陷,需要删除错误逻辑并插入正确逻辑。
对于更复杂的“既有新增也有删除”的补丁,论文进一步把 M 类型细分为 M.1 到 M.5。可以这样理解:
- M.1:原位置直接修改。控制流位置基本不变,只是某条语句本身被修正,通常对应明显的编码错误修复。
- M.2:前后定位节点一致。修改发生在同一个局部代码块中,前后逻辑结构基本一致,说明修复集中在某个局部语句上。
- M.3:部分定位节点一致,且控制语句类型相同。说明修复前后逻辑意图相近,但实现条件发生了调整。
- M.4:操作符相同但参数不同。说明函数调用或操作本身没有变,但传入的数据发生变化,通常暗示数据流存在问题。
- M.5:语句本身相同但位置变化。说明语句内容可能没错,但原本放置的位置或控制依赖不合适,可能导致执行时机错误。
通过这种分类,VicDiff 不只是判断“代码发生了变化”,而是进一步理解“这次变化是在修复哪一种漏洞逻辑”。这也是论文区别于传统 SZZ 类方法的重要地方:传统方法主要看删除了什么,而 VicDiff 会同时分析新增、删除、修改、移动、参数变化和数据流关系。
第三步是根据修复模式提取漏洞关键语句序列 VCSeq。这一步非常关键,因为 VIC 回溯最终依赖的不是完整补丁,而是一组真正决定漏洞是否存在的关键语句。论文认为,不同修复模式对应不同的关键语句提取策略。例如,对于 Pattern R,删除语句本身通常就是错误逻辑,因此会被直接加入 VCSeq;对于 Pattern M.1、M.2、M.3,原始被修改的语句往往就是漏洞关键点,也会直接加入;对于 Pattern A,由于新增语句在历史版本中本来不存在,不能直接拿新增语句去匹配,所以 VicDiff 会沿着数据流寻找与新增检查相关的上游赋值语句和下游使用语句,把这些仍然存在于漏洞版本中的语句加入 VCSeq;对于 Pattern M.4,系统不仅会加入原删除语句,还会结合新增语句的数据流寻找相关依赖;对于 Pattern M.5,由于问题往往不在语句内容本身,而在执行位置或控制依赖,因此系统会把其控制流相邻语句加入关键序列。
这个设计解决了一个非常实际的问题:很多漏洞的关键触发逻辑并不完整地出现在 patch 中。如果直接匹配 patch,可能过于依赖表面文本;如果只匹配删除语句,又可能漏掉真正的触发条件。VCSeq 则试图在两者之间取得平衡:它不是完整补丁,也不是单一删除行,而是一组更能代表漏洞存在性的关键语句。以 CVE-2023-6111 为例,最终提取出的关键序列包括第 21、26、31 行,这些语句共同反映了漏洞触发路径,而不是简单把补丁中所有新增删除行都拿来匹配。这样一来,VicDiff 对漏洞逻辑的刻画更加准确,也更适合在历史版本中回溯。
最后一步是在历史提交中匹配 VCSeq 并确定 VIC。VicDiff 会先筛选与漏洞函数相关的提交和文件版本,因为大型项目中绝大多数 commits 与某个特定漏洞无关,如果不筛选会造成巨大的计算开销和噪声干扰。对于函数名随版本演进而变化的问题,论文设计了函数声明栈,用来记录函数旧名称和新名称,避免因为函数重命名导致回溯中断。随后,系统按照逆时间顺序扫描相关文件版本,在每个版本的漏洞函数中按顺序匹配 VCSeq。如果某个历史版本仍然能够完整匹配这组关键语句,就认为该版本仍然存在漏洞;当继续向更早版本回溯时,第一次出现无法匹配的位置,就说明漏洞逻辑在此之前不存在,因此最后一个仍然匹配的提交就是漏洞引入提交。COM1 是安全补丁,COM2 是真正的 VIC,COM3 是漏洞引入前的最后一个无漏洞提交。B-SZZ 会因为追踪所有删除语句而误判,V-SZZ 会因为过度依赖删除语句的首次出现位置而定位不准,Redebug 则会因为匹配补丁中的定位行和上下文行而受到历史代码变化影响;相比之下,VicDiff 通过提取 VCSeq,更准确地抓住了漏洞存在所依赖的关键逻辑,因此能够正确识别 COM2。总体来看,这篇工作的价值在于,它把补丁中的“修复行为”转化为可回溯的“漏洞关键逻辑”,从而在大规模开源项目中更准确、更高效地识别漏洞最早被引入的位置。