我最早接触到二进制代码相似性分析是在一次供应链安全排查任务里。面对几万个编译产物人工审计几乎不可能而传统漏洞特征库又因为编译器版本和架构差异频频误报。那时候就在想有没有一种方法能像给函数“拍一张语义照片”一样既能跨平台、跨编译器地识别相似逻辑又能撑住百万级别函数的检索压力。这篇关于KEENHash的论文正是在这个方向上给出了一套完整方案。论文的全称是《KEENHash - 基于函数语义的程序嵌入哈希以实现大规模二进制代码相似性分析》。简单来说它解决的是“在不知道源码的情况下如何判断两个二进制函数是否逻辑相似”这个老问题但它的切入方式比较特别不直接比对控制流图不依赖人工特征而是把函数编译成一种“语义向量”再把这个向量压成紧凑的哈希用哈希来做大规模相似检索。这个思路很适合两类读者参考一类是做恶意代码同源性分析、固件漏洞批量排查的安全研究人员另一类是对二进制分析、程序嵌入方向感兴趣想知道深度学习怎么落地到代码比对场景的工程师。1. 论文到底在解决什么问题1.1 二进制相似性分析为什么这么难二进制相似性分析的核心任务说来简单给定两个或多个二进制函数判断它们在语义上是否相同或相似。这里的“相似”不是字面字节重复而是逻辑层面的等价。举个场景一个开源库被编译成不同版本分别用GCC和Clang、在ARM和x86架构下生成产物然后被分别打包到不同固件里。如果这个库存在已知漏洞安全人员就需要在二进制包里找到所有对应函数。这时候函数名和符号通常已被剥离指令集又不一致甚至编译优化选项也完全不同比对难度直线上升。传统方法里最常见的是指令序列特征比对、控制流图同构检测、基于AST的相似度计算。这些方法在小规模样本、单一编译器场景下效果尚可但一旦跨架构、跨优化级别它们的表现就很不稳定。原因在于这些方法捕捉的是“语法”层面的相似而不是“语义”层面的相似。比如同样的逻辑x86上可能用三条指令完成ARM上因为条件执行机制可能是一个分支块优化级别O0和O3的指令排布差别更像是两个人在写同一道题的完全不同的解法。语法层面变了语义层面没变传统特征自然就失效了。更麻烦的是规模问题。现代固件动辄几十万函数通过控制流图一对一做复杂图匹配计算量基本不可接受。安全分析需要的是能先快速排除绝大多数不相关项再把少数候选函数交给精细分析的手段。这本质上要求相似性分析系统具备两个能力一是语义表征能力二是可索引检索能力。前者决定“认不认得准”后者决定“跑不跑得动”。1.2 现有方案的三大痛点我梳理了一下目前学术和工业界常见的几类方案它们各自有绕不开的痛点。第一类是传统手工特征方案。取n-gram操作码序列、图哈希、各种统计量比如基本块数量、指令类型分布。优点是速度快缺点是鲁棒性差。换个编译器、改个优化级别特征分布就剧烈漂移。这类方案在真实漏洞检测场景里经常出现“表面相似率不低、实际匹配率很低”的尴尬。第二类是基于图匹配的精确方案。用控制流图CFG或带属性控制流图ACFG做子图同构或图编辑距离计算。优点是精确度高缺点是复杂度爆炸。论文里有测试提到对中大函数做一次完整图匹配可能需要毫秒到秒级但测一个拥有百万函数的固件库就是天文数字。这类方案适合做种子匹配和后验校验不适合做第一遍全库扫描。第三类是深度学习嵌入方案。用GNN或Transformer把函数映射成向量再用向量距离算相似度。这解决了“语义特征提取”的问题但在“大规模检索”和“鲁棒性”上仍有瓶颈。直接把高维浮点向量存库然后线性扫描内存和计算开销都很大而把嵌入向量量化为二进制哈希再检索又会面临“量化损失吃掉语义精度”的问题。KEENHash在这个环节做了一个关键选择把哈希学习和语义嵌入放到同一个框架里联合优化而不是先训好嵌入、再事后量化。这个差异对精度的影响是非常直观的。2. KEENHash的核心思路与方法选型2.1 为什么要用函数语义而不是结构特征KEENHash选“函数语义”作为切入点是有明确理由的。函数在二进制分析里是最小且最完整的语义单元一个函数通常对应一个独立逻辑实体边界清晰内部包含多条基本块和边关系。相比任意指令窗口或基本块函数级别的表征具备更好的可解释性和稳定性。哪怕两个函数由不同编译器生成、指令排布差异巨大只要源逻辑一致其函数级行为结构仍然保持着可迁移的共性。这份论文在语义建模上借鉴了自然语言处理和程序分析的交叉思路。它并不直接对汇编指令序列建模而是先把指令进行规范化表达再把控制流信息作为结构化上下文喂给神经网络。也就是让模型看到的不只是“一条指令接着一条指令”还包括“这几个语句块之间存在分支、汇合、回边关系”这样嵌入更容易学到接近真实语义的表象。这一点很关键因为跨架构比对时指令文本完全不同真正稳定的是它们组合出来的结构逻辑。这里有一个容易被忽略的细节论文对输入做了跨架构规范化。它把特定架构的指令映射成中间表示或者统一描述符把寄存器操作、内存访问、跳转目标都归一化成通用形式。这一步直接降低了模型对指令文字的过拟合也使得同一套模型可以同时处理x86、ARM、MIPS等不同平台的输入。很多嵌入类方法在单一架构上效果不错一跨架构就明显衰退问题往往就出在没做这一步归一化。2.2 为什么要用程序嵌入哈希而不是直接向量检索如果已经有了函数语义向量一个最朴素的检索方式就是把所有函数向量存进向量数据库用最近邻搜索找相似项。这个方案在中小规模下可行但放到“大规模二进制相似性分析”场景里存在三个现实障碍一是向量维度高常见为64到256维浮点百万函数需要占用几百MB到GB级内存二是高维空间最近邻搜索存在维度灾难索引结构复杂度高三是向量检索的结果通常是一堆浮点相似度很难直接做阈值判定和进一步索引。KEENHash选择走哈希路线本质上是把“连续表示”压缩成“离散指纹”让相似函数在汉明空间内有较短的二进制距离。好处很明显哈希占用的存储极低且可以直接用位运算计算距离支持极快的大规模扫描。更关键的是哈希离散化之后可以做聚类和分桶从而把“全库比对”转化为“只比同桶内函数”检索复杂度可以下降几个数量级。但这个思路有一个经典的坑如果直接拿训练好的浮点向量做二值化比如取符号位或者做阈值截断通常会损失大量信息导致哈希空间里的相似关系与向量空间严重脱节。KEENHash的解决办法是在训练阶段就引入哈希层让神经网络在生成嵌入的同时就将向量推向二值化目标通过损失函数同时约束“语义相近的函数哈希距离小语义无关的哈希距离大”。这样学出来的哈希不是对嵌入的后处理而是嵌入学习的一部分。这段设计是整篇论文里我认为最具实操价值的地方。3. 技术细节逐层拆解3.1 函数语义嵌入怎么生成论文的嵌入生成流程可以分成四步函数抽取、指令规范化、控制流编码、语义嵌入映射。第一步是函数抽取。用IDA或Ghidra的脚本批量分析输入二进制识别函数边界把每个函数作为一个独立样本导出内容包括基本块列表和块间跳转关系。这一步看起来简单但非常影响后续质量如果中间有大量函数边界识别错误后面所有嵌入和哈希都会受牵连。论文在预处理时加入了对“库函数识别”和“混淆代码”的处理逻辑不过这更多是工程层面的保障。第二步是指令规范化。每条指令被解析成操作码和操作数操作数中的立即数会被归一化到相对位置或者不连续值区间寄存器名会被统一成按用途分类的类别。这样做的目的是减少因架构差异带来的字面差异比如x86的mov eax, [rbp-0x10]和ARM的ldr r0, [sp, #0x8]在规范化后都描述成“把栈上局部变量载入通用寄存器”的语义模式模型的输入层不再被无意义的架构符号干扰。第三步是控制流编码。论文不是简单地把所有基本块拼成长序列而是保留控制流图的结构利用类似图神经网络的机制让每个基本块的表示聚合其相邻基本块的信息。这一层能让模型捕捉到“循环”“提前返回”“分支合流”等高阶语义结构。对于关键控制流结构模型最终会形成一种“这个函数有循环且循环内有函数调用”之类的隐含表征这种表征在语义比对时比单纯的指令统计要稳定得多。第四步是语义嵌入映射。经过上述编码后的结构表示被投影到一个低维稠密向量空间中。训练阶段使用大量同类函数和异类函数作为监督信号通过对比学习使得“兄弟函数”在向量空间内距离近“无关函数”距离远。这个向量就是后续哈希编码的基础。论文在输出层之前加了若干全连接层做非线性映射再接入哈希编码层确保语义信息能够被哈希层有效利用。3.2 哈希编码与相似性量化哈希编码层采用典型的多比特哈希设计。假设指定哈希码长度为L比特模型输出L维的实数值向量每一维通过符号函数量化成0/1。训练过程中符号函数不可导论文采用直通估计器STE或者软化近似来处理梯度回传这是端到端训练得以成立的技术关键。损失函数设计上KEENHash综合权衡了三类信号。第一类是语义相似度损失对于同源函数对希望它们的哈希汉明距离尽量小对于无关函数对希望尽量大。这个目标通常用对比损失或三元组损失实现。第二类是量化损失希望网络输出的实数值尽量接近离散值这样训练好的模型在实际使用时不会有量化断层。第三类是平衡性正则希望哈希位在整个数据集上的取值分布尽量均匀避免某些位恒为0或恒为1以保证哈希桶的均匀性避免大量函数集中到少数几个桶中。相似性量化方面论文使用汉明距离作为两个函数哈希之间的相似性度量。汉明距离计算本质上是先用异或操作找出差异位再统计1的数量在CPU上可以用POPCNT指令快速完成百万次两两比对也就是毫秒级。更进一步的优化是把哈希表拆成多个子表通过多次子串匹配来加速这也依赖哈希码的良好比特均匀性。所以这张损失函数里正则项不是凑数而是检索性能的隐性前提。3.3 大规模检索流程在推理阶段KEENHash的检索流程分解为三步第一步对目标二进制提取函数并计算哈希。每次库扫描前系统先用训练好的模型把候选二进制里的所有函数batch式推理成哈希并进行索引持久化。如果库已经建好索引新增函数时只需对增量做同样处理。第二步建立倒排/分桶索引。按照哈希码的前k个比特划分桶查询时根据待查函数的哈希前缀取出对应候选集。这里的关键是“查询函数的哈希与库函数哈希即使在部分比特位上不同也可能落在同一个前缀近邻集合中”所以论文会生成多个查询副本对哈希码进行少量随机扰动或采用子段组合以提高召回率。第三步在两两候选集内计算完整汉明距离并按阈值返回Top-K或距离小于阈值的函数列表。这一步可以通过位运算SIMD优化也可以在GPU上批量执行。实际工程应用中还可以在后面接一个“重校验”层对哈希命中的函数用Ghidra反向生成伪代码或对CFG做子图匹配剔除误报。这套流程里最值得关注的工程点是“粗筛精排”的结构。哈希检索只保证召回但允许一定误报最终精度由精排保证。很多人一上来就追求哈希直接达到高精度其实会大大牺牲召回率反而不利于实际使用。4. 实验验证与效果对比4.1 测评数据集与评估维度论文的实验评估不是只在单一数据集上跑一个准确率就收工而是设计了多个不同层面的评估来验证方法在各环节的可靠性。数据集方面我理解它覆盖了三个级别跨编译器GCC与Clang、跨优化级别O0至O3、跨架构x86、ARM、MIPS等。这些因素正是二进制相似性分析在实际场景中误差的主要来源。除此之外还引入了不同来源的恶意样本与开源项目样本用来测试“未知二进制文件之间能否准确找到同源关系”。评估维度上也做了细分。第一维是函数级检索的召回率与精确率考察给定一个目标函数能否准确检索到同源函数。第二维是二进制级同源判定即通过多个函数的相似映射关系判断两个完整二进制文件是否派生自相同源码。第三维是搜索性能包括单次检索耗时、内存占用、库索引构建时间。这个多维度设计我认为很重要因为很多论文只在函数级别刷指标却不谈实际建库和查询的成本。KEENHash这篇的思路是顺着真实应用链来评估的这也是它值得工程人员参考的原因之一。4.2 关键实验结果与结论从论文报告的结果来看KEENHash在跨架构、跨编译器场景下的性能显著优于传统特征基线整体趋势是召回率在较高精确率区间内不会剧烈下降表现比较稳定。在100万级函数规模的数据库上进行查询单函数哈希的检索耗时降到了毫秒级索引内存开销也远低于存储完整浮点向量的方案。对比同类深度学习嵌入方法当嵌入向量被直接保留不做量化时精度往往略高于哈希化之后的效果但存储与检索开销大涨。KEENHash通过联合优化哈希层尽量缩小了这两者之间的精度差距。论文给出了一组典型的权衡数据趋势在保持与浮点检索相当的精度的前提下哈希化的存储开销能压到原来的八分之一甚至更低检索速度则提升一到两个数量级。另外消融实验显示指令规范化和控制流建模各自对最终精度贡献不小。去掉控制流结构建模后嵌入对长函数和复杂分支结构的区分能力明显下降去掉指令规范化后跨架构场景的精度下滑也很明显。这说明本文所谓“函数语义”不是靠单一技巧获得的而是由这条完整处理链路共同支撑的。4.3 消融实验与参数敏感性我对论文里两类细节比较留意一是哈希长度的影响二是训练数据规模与多样性的影响。哈希长度是系统设计中最直接的旋钮。短哈希比如32位内存占用少、检索快但容易发生碰撞导致候选函数过多或漏检。长哈希比如256位表达能力更强搜索精度更高但存储和比对开销也相应增加。论文做了不同长度下的对比实验结论大致是在64到128位区间存在一个性价比拐点超过这个长度后精度提升边际递减而检索成本仍然线性上涨。如果你要落地这套方案哈希长度建议设定在类似区间内再根据业务数据调整。训练数据方面论文强调了需要包含大量跨架构、跨编译器的同源函数对否则模型很难泛化。如果训练集只含同类编译器的样本嵌入会对编译器的特定风格过拟合在实际海量异构固件场景下表现会打折扣。这也是一个典型的“数据决定天花板”问题。KEENHash的对比学习框架对数据增强的敏感度很高因此在复现时最需要花心思的地方反而是数据准备而不在模型结构本身。5. 我认为KEENHash值得关注的地方与实战思考5.1 论文的贡献与适用范围KEENHash这篇文章的价值在我看来不在于提出一个全新的网络结构而在于把“函数语义嵌入”和“哈希检索”这两个原本独立的技术栈拧成了一套可落地的系统。它明确论证了一件事在二进制相似性分析里哈希索引不是只能牺牲精度换性能而是可以通过端到端训练同时获得高精度与高性能。这一点对工程实践影响很大因为很多安全团队不是没有分析能力而是没有在海量样本里快速找到重点关注目标的能力。适用场景方面这套方案最适合用于大规模批量比对比如固件漏洞影响范围排查、恶意样本同源分析、代码复用追踪、开源组件版本鉴定。在这些场景里第一遍需要的是快速过滤掉99%的不相关函数把候选集从几百万压到几十个用户再对候选做人工或精确比对效率可以得到数量级提升。它对“在线实时的细粒度反混淆追踪”这类极端任务并不合适因为哈希检索本质上是近似检索反混淆场景更吃的是精细的指令级推理。5.2 复现和落地时容易踩的坑如果打算复现论文或者做类似方向的工作我有几个亲测踩过或见过别人踩的坑值得提一下。第一个坑是把训练样本直接简化成“同一函数不同编译产物”的正样本对。真实固件里存在大量裁剪、内联、版本微调简单同源对训练的模型在部分修改场景下会失效。建议加入多种不同程度的非同一源码函数对作为难负样本让模型学会“区分相似但不相同”的边界。第二个坑是强制二值化阶段的输出分布不受控。如果不加平衡正则训练后期经常出现部分哈希位被闲置的现象导致哈希空间有效容量下降。我见到很多人调模型时发现单个函数检索精度很高但全库检索时桶分布极不均匀候选集冲突严重最终不得不回退成线性扫描原因就在这里。第三个坑是建库时还没做函数去重。同一个二进制里反汇编器可能会把某些内联函数识别成多个独立函数或者同一个库函数的多个实例重复出现。直接把原始导出结果全部送入库会增加存储与检索压力还会在评估时产生“假阳性”干扰。稳妥的做法是先生成哈希再做一次近似重复检测顶部去重后再建立最终索引。第四个坑是评测指标单一。建议同时关注P10、召回率、查询延迟、索引构建时间、内存占用这五个指标而不是只看一个准确率。实用系统的瓶颈往往在索引构建和内存上很多论文看起来很好一跑百万级函数估值就崩了。5.3 这类方向后续可能的演进方向KEENHash这套“语义嵌入哈希”的组合后续能扩展的方向很多。比如结合大语言模型提升语义理解能力在指令规范化与中间表示上引入更丰富的预训练知识让函数嵌入泛化能力更强。又比如从函数级延伸到跨函数级调用图级用函数哈希作为节点特征再对调用图做嵌入和哈希这样可以支撑对恶意软件家族级别的同源聚类。另一个值得关注的方向是“可解释哈希”。当前哈希是不可解释的命中之后只能靠精排说明“为什么像”。如果能给哈希位赋予语义标签比如某一位代表是否有循环、某一位代表是否调用特定API安全分析人员就能更快理解候选结果也能辅助误报排查。这个方向在可预见的工程价值上非常可观。回到我开头提到的那个供应链排查场景如果当时能有一个类似KEENHash的系统把几万函数先压成哈希、快速锁定几十个值得细看的函数再导到Ghidra里逐个人工确认整体工作量至少能降低一个数量级。工具是死的思路是活的。“把语义嵌入与哈希检索联合优化”这个设计理念放在今天的恶意代码分析、固件安全、代码合规审计里都是站得住脚的方向。这篇论文里每个模块单独拎出来都不算新鲜但它在系统层面的取舍和整合方式很值得做工具链的人仔细读几遍。
阅读完成 · 觉得有帮助?