
做了一段代码模型鲁棒性测试之后我越来越觉得对抗攻击这行水比想象中深。特别是从图像攻击转到代码领域的时候会明显感受到一种断层你以为把像素上的那套思路搬过来就能用结果发现代码是离散的、有语法约束的、甚至可以编译执行的稍微改错一个符号模型没被骗到程序先跑不起来了。这也是我决定认真整理Code-Level Adversarial Attacks相关工作、并复现梳理一遍的原因。这篇文章我会把核心问题定义、主流方法、实际实验中的操作细节和踩坑经验都摊开讲给正在接触代码模型安全、对抗样本、鲁棒性测试的读者一个可以直接参考的思路框架。先说清楚一个容易混淆的点代码级对抗攻击不是指“写代码去实施攻击”而是指针对代码模型比如代码补全、缺陷检测、代码检索、程序翻译这类以源代码为输入或输出的模型生成对抗样本的过程。攻击的最终产物是一段被精心扰动过的代码这段代码在人类的视角里看起来正常、能够通过语法检查、甚至保持原有语义但会让模型做出完全错误的预测。这块工作的价值在于暴露模型在实际开发场景中的脆弱性为后续代码安全评估和鲁棒性提升提供依据。我从项目背景、方法分类、实操流程、踩坑记录和后续方向五个部分来梳理。1. 为什么要在代码层面较劲问题背景与核心价值1.1 从像素扰动到结构化输入完全不同的攻击场景图像对抗攻击之所以直观是因为图像空间是连续的你可以给每个像素加一个极小的噪声扰动人眼看不出差别模型预测结果却可能从“猫”变成“狗”。代码和图像在攻击视角下存在几个核心差异。代码输入是离散的符号序列。你没法像像素那样做“微小连续偏移”。要么替换一个标识符要么插入一条语句要么调整一下空格和换行每一个离散改动都可能在模型特征空间里引起非常剧烈的变化。这个特性决定了梯度类攻击方法在代码场景下不能直接使用因为梯度是针对连续嵌入的而代码令牌是离散的整数索引。代码有强结构约束。一段代码必须满足语法规则否则根本无法被解析器处理更别说送给模型当输入了。稍微多想想还会发现就算语法正确也不代表语义正确。比如把a x y改成a y x语法对语义多数情况下也对但如果x和y是浮点数交换顺序可能带来微小精度差异更复杂的情况是把语句重新排序可能导致某个变量尚未定义就被引用。代码可以实际执行。对于有运行环境的场景攻击者构造的样本还要保证“改了之后依然能做同样的事情”否则在基于执行结果的任务比如程序修复验证里扰动样本根本骗不过下游校验器。这也是代码级对抗攻击比图像级难出几截的核心原因。1.2 谁在关心这类攻击应用场景与破坏面代码级对抗攻击并不是纯粹学术搬砖。它在几个现实场景里都有直接影响。第一个核心场景是AI辅助编程工具的安全性评估。现在的开发者几乎都会用代码补全、代码生成类工具。如果攻击者可以通过精心构造的注释或上下文提示诱导模型生成带漏洞的代码——比如生成带有命令注入风险的SQL查询拼接——那影响面是直接落在生产代码里的。这种攻击并不需要模型权重只需要构造一个恶意的输入上下文。第二个场景是自动化缺陷检测模型。安全团队越来越多用机器学习模型做漏洞扫描和危险代码定位。如果攻击者对这部分代码做语义等价的改写把某个危险函数调用隐藏在不透明的控制流和别名关系背后检测模型可能直接判为“干净代码”攻击者就可以把恶意代码绕过审查提交进主干。第三个场景是代码搜索引擎和相似代码匹配。攻击者可以通过少量标识符重命名或调整代码风格让恶意片段在检索系统里“隐形”既不会被相似性匹配命中也不会被开源协议或许可证检测发现。第四个场景是大模型的代码生成与指令遵循能力。往系统提示里塞一段“看起来无害但实际误导性强”的样例模型可能就学会了错误模式。这本质上属于代码层面的提示注入也经常被归类到代码攻击的讨论范畴。一句话总结只要代码模型被部署到实际研发链路里就有被攻击的入口。做这块研究的核心目的不是为了“攻击”本身而是更早发现这些入口并提前建立评估和防御手段。2. 代码级对抗攻击的问题定义与方法分类2.1 攻击的核心约束语法正确、语义保持、改动最小给代码模型构造对抗样本本质上是一个约束优化问题。我们可以把目标定义为在保证扰动代码仍然合法、且与原始代码语义等价的条件下最大化模型错误概率。写出来大致是这样一个思路原始代码为x模型为f目标标签为y目标代码为x需要满足x的AST抽象语法树解析成功x与x保持语义等价通常用执行结果或测试用例集合来判断x与x的差异尽量小比如编辑距离或AST节点修改率可控目标函数是最大化loss变化使f(x) ≠ y或f(x)完全错误语义保持是最难量化的约束。图像攻击里可以用L2距离衡量扰动大小代码里没有自然度量的“距离”。常用的替代方案有三种基于等价编译的检查、基于测试套件的执行结果对比、基于人工规则保证语义不变性。所谓人工规则就是只允许做已知安全的等价变换比如变量统一重命名、去掉冗余括号、调整注释等。这种方式最可控但攻击空间也窄实际效果有限。注意很多论文宣称的“攻击成功”其实只在“语法通过”和“模型输出改变”两个条件上衡量并没有真正验证语义保持。如果你在复现过程中发现某个方法的样本模型倒是骗过去了程序却跑不出原结果那就是典型的语义漂移问题这种样本在实际应用里价值不大。2.2 四种主流攻击范式对比代码级对抗攻击的方法大致分成四类每个都有不同的适用条件和优缺点。我把它们放在一张表里对比攻击范式核心思路典型攻击面优点主要局限基于梯度与替代模型训练一个本地白盒替代模型用梯度信息挑选敏感令牌并替换令牌替换、重命名效率高可解释性强对目标模型依赖强跨模型迁移率可能很低基于搜索与进化把代码变换动作当作搜索步用模型反馈做打分逐步逼近最优扰动语句删除、插入、重排黑盒可用不依赖梯度查询开销大容易陷入局部最优基于生成模型改写借助大模型或翻译模型产生“自然变体”再筛选有效扰动整行改写、注释注入、风格变换攻击丰富度高贴近真实语言习惯可控性差需要额外解码开销和过滤机制后门与数据投毒在训练数据中植入带marker的代码对使模型学到隐藏关联训练集污染几乎不依赖推理期查询需要影响训练流程攻击周期长这四种范式并不是互斥的。实际项目里最常用的是“搜索 生成模型过滤”的组合套路先用大模型生成一批语义等价的候选改写再用本地模型做筛选器挑出最容易让目标模型出错的候选继续迭代。这样的流程既能顾及代码的自然性又能控制查询次数。2.3 攻击面整理代码的哪个位置最容易下手从微观视角看一段代码可以被攻击的位置其实挺多。我习惯按以下维度检查标识符层。函数名、变量名、类名的替换。比如把download_file改成fetch_resource模型对意图的理解可能产生偏移。注释与文档字符串层。注释内容通常是预训练模型重点关注的信息来源一句看似无关的注释就能引导生成方向改变。语法结构层。包括括号冗余、三元表达式与if-else转换、for循环与while循环互换。语句级控制流层。插入不透明谓词、调整语句顺序、拆分复合语句。这个层面离“语义等价”最危险也最容易出问题。格式与风格层。调整换行、空格、缩进的大小。对某些以字符级tokenizer处理的模型来说光改格式也可能造成输出差异虽然这个攻击面比较弱。我自己的经验是对代码补全类模型注释层和标识符层的攻击成功率显著高于格式化层。对缺陷检测模型控制流混淆层的攻击更有效因为检测模型往往依赖局部模式匹配一旦控制流被打散模式就消失了。3. 核心方法拆解从标识符重命名到控制流混淆3.1 标识符与注释层扰动最温和但很有效的入口标识符重命名是最基础的代码级攻击手段。人类阅读代码时名字承担了大量语义信息模型同样如此。预训练模型在训练阶段学习了“变量名暗示其用途”的统计规律比如一个叫timeout_sec的参数大概率和时间相关。攻击者把timeout_sec改成t模型可能就丢失了时间约束的上下文在补全时产生错误逻辑。但标识符重命名有个要注意的地方部分任务的语义保持依赖名字本身。比如某个程序里使用了反射机制按名字查找方法或者通过配置文件绑定特定字段名这种情况下改名就破坏了执行语义。所以在生成候选时我会先用符号表分析确认哪些标识符可以安全替换避免全盘替换导致程序崩溃。注释层扰动更轻量。常见的操作是修改注释内容从“返回两个日期之间的天数”改成“返回两个日期之间的毫秒数”然后观察模型补全的行为变化。注释攻击不需要动代码逻辑语法安全性天然满足但它的有效性高度依赖模型是否关注注释上下文。实测下来对以源代码和注释联合预训练的模型注释扰动效果明显对纯代码训练的模型效果就弱一些。3.2 控制流等价变换与不透明谓词更硬核的攻击面控制流层面的攻击本质上在做程序混淆。我们要保持程序的输入输出行为不变但让抽象语法树结构和执行路径顺序变得面目全非。一个比较典型的操作是插入不透明谓词。所谓不透明谓词就是“条件表达式的值在运行期恒为真或恒为假但静态分析很难直接判断”的表达式。比如int x 3; if (x * x 9) { download_sensitive_data(); } else { download_sensitive_data(); }这个例子中两个分支做的事情一样所以无论条件真假程序行为一致但检测模型需要在控制流图里多走几个节点才能发现两个分支汇合后行为相同。再进阶一点的版本会用等价但更复杂的表达式比如x * (x - 1) % 2 0对整数x恒为真。这类攻击对缺陷检测模型的干扰效果非常突出因为很多缺陷模式比如危险函数调用是基于语句局部上下文匹配的一旦调用点被冗余分支包裹特征向量的局部结构就被稀释了。做控制流变换时我强烈建议先用现有编译器的优化信息辅助验证语义等价性。比如用-O0和-O2分别编译原始代码和扰动代码并对比执行结果或者直接用差分测试的方式在随机输入上做等价性校验。只靠肉眼判断“这两个分支结果一样”是不够的遇到数组越界、整数溢出、浮点精度差异时容易翻车。3.3 针对代码大模型的提示级攻击不碰代码体只碰上下文最近越来越多的工作把注意力放在提示级攻击上。严格说它不一定修改目标代码本身而是修改输入给模型的上下文提示让模型对同一段代码产生不同的理解。比如在一个漏洞检测场景中给模型输入的代码片段前面加一句“这是一段经过安全审查的测试代码”或者把任务描述从“找出危险调用”改成“分析代码可读性”模型的注意力分配会完全不同漏洞可能就检测不出来了。这种方法的好处是无需修改代码语法攻击成本最低坏处是它对目标模型的对齐方式和提示模板非常敏感换个模型可能就失效了。我在复现这类攻击时发现一个有意思的现象把同类提示写成不同语言中英文夹杂也会产生影响。某些代码模型的训练语料主要来自英文GitHub提交信息中文提示可能激活的语义空间不太一样。这块可以作为提示攻击的一个扩展方向但实验稳定性还需要更多样本支撑。4. 实操过程与工具链参考4.1 搭建最基础的评估环境在开始生成对抗样本之前先把评估环境搭稳。我推荐的基线配置是数据集选取某个代码任务基准的测试集子集比如代码缺陷检测或代码检索任务从里面随机抽500到1000条样本。样本规模太小统计意义不够太大查询开销扛不住。目标模型优先选择一个开源可本地部署的代码模型。本地部署的最大优势是能自由查看中间层输出和tokenize结果便于调试。只有到了后期验证迁移性时再考虑换成更大规模的商用API模型。评估指标主指标是攻击成功率辅助指标包括平均查询次数、输入相似度编辑距离或AST差异率、语义保持率。工具链代码解析用通用解析库差分测试用轻量脚本跑随机输入对比攻击循环用Python脚本控制。很多人一上来就对着黑盒API狂发请求既没调试环境又踩限流最后拿到的攻击成功率根本没有可解释性。正确做法是先本地白盒跑通再转向黑盒验证。4.2 一个完整的攻击流程示例我这里用一个简化的场景做演示假设我们已经有一个代码缺陷检测模型希望构造一段包含危险函数调用的代码让模型输出“安全”。整个流程分六步。第一步选定种子样本。从测试集中取一段代码确认它本身带有一个明确危险模式比如直接拼接用户输入构造系统命令。用目标模型预测原始结果记录为基准输出。第二步生成候选扰动。我习惯混合两种方式一是基于规则模板对变量名做同义词替换、对语句做结构变换二是调用本地生成模型给定提示“请对下面代码做语义等价的风格改写”批量产出候选。这一步一次性生成50到100个候选。第三步语法与语义过滤。用解析器逐个解析候选失败的直接丢弃。再对通过语法的候选做差分测试在若干组随机输入上运行原始代码和候选代码对比输出结果。所有输入输出不一致的候选都丢弃。这一步会把候选数量从100个压到20个左右。第四步模型打分。把剩余候选送入目标模型记录模型输出的概率向量或分类结果计算每个候选对模型预测的扰动程度。排序取出前5个“最有可能让模型犯错”的候选。第五步迭代优化。对选出的前3个候选再做一轮“局部扰动深化”比如对它们继续插入不透明谓词然后回到第三步重新过滤。循环3到5轮。第六步汇总验证。把所有最终样本整理出来用独立测试集重新验证攻击成功率、语义保持率和可迁移性。特别要检查的目标是不能只在一两个种子上成功要统计整体成功率分布。下面是一段简化的攻击筛选伪代码可以直观说明流程def attack_pipeline(seed_code, target_model, generator, num_iters4): candidates [seed_code] for i in range(num_iters): new_candidates [] for code in candidates: variants generator.generate_variants(code, k20) for v in variants: if not parse_ok(v): continue if not semantic_equiv(seed_code, v): continue new_candidates.append(v) scored [] for v in new_candidates: loss target_model.get_loss(v, target_labelbenign) scored.append((loss, v)) scored.sort(reverseTrue) candidates [v for _, v in scored[:5]] return candidates这个流程看着不复杂但每一步的工程细节都挺多。比如semantic_equiv怎么实现最稳、generator怎么控制输出不过于发散、target_model.get_loss拿的是logits还是最终分类概率都会直接影响实验结果。4.3 攻击模型的能力边界与查询预算管理做黑盒攻击时要注意目标模型的查询预算。以代码缺陷检测API为例如果每次请求都需要上传整段代码一个候选就要消耗几十上千token循环跑五轮动辄几千次请求。我踩过API限流导致实验中断的坑后来把预算管理前置了。我的做法是这样提前设定总查询次数上限然后分配到每次迭代。比如总共一万次查询第一轮用一半第二轮用三分之一后面几轮平分剩下额度。这样能保证即使前面生成质量不高后面仍有空间调整策略。另一个经验是采用“先筛选后查询”的思路先用本地替代模型对候选排序只把排名靠前的候选发给远程目标模型能有效减少远程查询量。4.4 影响攻击成功率的参数细节同样一套方法参数设置不同攻击成功率能差出几十个百分点。几个关键点候选生成温度。生成模型的温度过高会产生大量语法错误候选过滤后所剩无几温度过低又缺乏多样性。我常用的区间是0.7到0.9根据任务自动调整。相似度容忍度。编辑距离限制太严格候选空间太小太宽松又容易破坏语义。我一般把AST节点修改率控制在20%到40%之间。差分测试的输入分布。随机输入必须覆盖典型边界情况比如零值、负数、超长字符串、空数组。如果只测几组普通输入语义等价验证会漏掉隐蔽行为差异。5. 常见问题与避坑记录5.1 语义等价验证的假阳性这是代码级对抗攻击实验中最容易出问题、也最容易毁掉结果可信度的一环。我们以为候选代码和原代码“等价”但实际上在某些边缘输入下行为不同。最常见的原因浮点运算顺序导致精度差异。a b c和(a b) c在数学上等价浮点上不一定。无符号整数溢出的未定义行为。依赖求值顺序的表达式比如f(i, i)这类代码。隐式类型转换差异。避免这个坑的唯一办法是差异化测试样本一定要包含边界值并且至少跑三组不同的随机种子。如果被测程序本身有随机性或依赖外部状态还需要控制外部依赖。遇到输出不一致的候选宁可丢弃也不要留下来凑数量。5.2 模型对“微小改动”的响应阈值有些攻击工作只报告“最终攻击成功”却不报告“模型输出发生实质改变所需的最小改动量”。我在实测中发现部分代码模型对单个标识符替换特别敏感但对两个以上符号的联合修改反而不敏感。原因可能是预训练模型对token级扰动有隐式的鲁棒性多个方向的改动互相抵消了。如果你发现单步扰动攻击成功率很低可以先做一个敏感性扫描对种子代码每次只改一个token记录模型预测变化幅度最大的前10个位置再针对这些位置做聚焦攻击。这个策略能明显提升后续多步攻击的效率。5.3 白盒梯度信息在代码场景下的失效我很早做过一个尝试直接把图像攻击里的PGD思路搬到代码嵌入上在嵌入空间做梯度上升然后找最近的token替换。结果不太理想。原因很好理解代码模型的嵌入空间里最近邻token往往和原token语义相近替换后模型输出并不改变而能够真正改变模型行为的token在嵌入空间里距离可能很远梯度指向的“最近可行方向”根本够不到它。更好的用法是把梯度当作“token敏感度排序器”而不是替代方向。先计算每个token位置的梯度幅度挑出影响最大的若干位置再用组合搜索或生成模型在这些位置做离散替换。5.4 黑盒攻击的限流与并发控制黑盒场景下请求频率控制是个容易被忽略的工程问题。很多API是按分钟或按小时限流的一次性并发太多请求会触发熔断导致整个实验中止。我后来用的是一个简单的“令牌桶”调度器把查询请求均匀撒在时间轴上同时在本地做了断点续跑。每次记录已完成样本的索引中断后从最近断点继续不浪费之前消耗的配额。5.5 指标呈现上的“虚高”实验指标最容易虚高的地方是攻击成功率只按模型硬标签计算而不看logits变化。有些样本模型虽然标签翻转了但logits差异只有0.001这种攻击本质上很不稳定换个随机种子可能就失败了。更可靠的做法是同时记录置信度偏移量并设置一个最小置信度变化阈值。如果某些样本的置信度变化低于这个阈值即使标签翻转也要标记为“弱攻击”。我自己习惯在报告中把“强成功”和“弱成功”分开统计强成功指目标类概率下降超过某个比例弱成功指只发生了标签翻转。这样写出来的实验结论经得起推敲也更容易定位方法真正有效的作用区间。6. 防御视角与几个值得发力的方向做攻击研究的人如果完全忽视防御容易被视为只破不立。我自己的习惯是在每次攻击实验完成后反推出至少一条可落地的防御建议让工作更有闭环价值。从目前实测过的手段看代码模型的对抗防御大致有三个思路。第一个是对抗训练。把攻击过程中生成的样本混入训练集做增强简单有效但代码样本的多样性太强固定攻击方法生产的数据很难覆盖全局威胁。效果有限但能提高特定攻击面的鲁棒性。第二个是输入净化与异常检测。在推理阶段增加一道检查使用AST结构对比、语义哈希等方式判断输入是否带有可疑的混淆特征。标签适合防御控制流混淆类攻击对语义保持型的自然改写类攻击效果较弱。第三个是更有前景的方向基于形式化语义的程序嵌入。用执行轨迹或依赖图替换或增强token序列表征让模型不再只依赖表面的token模式。如果模型真正吃进去的是程序行为的信息那么单纯改名、加混淆分支就很难骗过它。当然这块训练成本高目前还比较前沿。如果将来继续推进这块工作我个人最想看的方向有三个一是代码多模态模型文本、AST、CFG联合建模在对抗鲁棒性上的表现二是从漏洞检测攻击迁移到代码生成攻击的通用方法三是对抗攻击与后门攻击在代码场景下的技术融合。这里面每一个都很能打值得持续投入时间。最后再分享一个小技巧。做代码级对抗攻击时不要一开始就瞄准复杂的大模型。先用一个轻量的小模型把攻击流程跑通确认语义等价校验和攻击成功率计算两个环节的逻辑没问题再慢慢换成大规模模型做迁移实验。这个习惯帮我省下了大量调试时间也让每一步实验结论都更干净。