1. 项目概述与核心挑战

在物联网设备呈指数级增长的今天,固件安全已经从一个技术问题演变为一个严峻的公共安全挑战。想象一下,从智能摄像头、路由器到工业控制器,数以百亿计的设备运行着闭源的、未经充分安全审计的固件。攻击者一旦发现某个通用组件(如某个网络协议栈或加密库)中的漏洞,就能像拿到一把“万能钥匙”,轻松入侵海量设备,发起大规模DDoS攻击或构建僵尸网络。我过去几年深度参与了不少企业级物联网安全评估项目,一个最直观的感受是:传统的漏洞检测方法在这里几乎“失灵”了。你面对的不是几个可调试的Windows或Linux程序,而是成千上万种不同处理器架构(ARM、MIPS、PowerPC等)、不同编译选项、甚至被厂商做过定制混淆的二进制固件镜像。直接进行动态分析或符号执行?且不说为每种设备搭建仿真环境的巨大成本,光是处理那些千奇百怪的外设驱动就足以让人崩溃。

因此, 代码相似性检测 技术成为了固件漏洞挖掘的“救命稻草”。其核心思想朴素而有力:如果已知一个漏洞的代码特征(比如某个存在缓冲区溢出漏洞的 libupnp 库函数),那么只要能在目标固件中找到结构或语义上高度相似的代码片段,就可以快速定位潜在的漏洞点,而无需理解每一行代码的具体逻辑。这就像在茫茫人海中通过DNA比对寻找亲属,而不是去逐一调查每个人的生平。然而,把这项技术应用到物联网固件上,却面临着三大“拦路虎”:

  1. 特征有效性 :固件代码经过编译、优化、甚至混淆后,语法层面(如指令序列、寄存器分配)差异巨大。单纯匹配字符串或简单的控制流图(CFG)特征,误报和漏报率会非常高。
  2. 分析效率 :物联网固件动辄几十甚至上百MB,包含数万个函数。如果对每个函数都进行精细化的图匹配(如计算图编辑距离),计算开销将是天文数字,无法实现大规模快速筛查。
  3. 架构泛化能力 :一个用ARM编译器 -O2 优化编译的函数,和一个用MIPS编译器 -Os 优化编译的同一份源代码函数,其二进制表现形式天差地别。检测方法必须能穿透这些表层差异,捕捉到代码深层的、不变的“语义指纹”。

本文探讨的 基于两阶段代码相似性的物联网固件漏洞检测方法 ,正是为了系统性地解决这些痛点。它不是一个纸上谈兵的理论,而是我们在实际攻防对抗和自动化审计平台开发中,经过多次迭代验证的工程实践。其核心价值在于,它巧妙地在“效率”和“精度”之间找到了一个平衡点:第一阶段用神经网络进行快速粗筛,在海量函数中迅速定位“嫌疑犯”;第二阶段则对重点目标进行更精确的“庭审”,通过分析函数调用关系来最终定罪。下面,我将结合大量实操细节,为你彻底拆解这套方法的每一个环节。

2. 核心思路与两阶段架构设计

面对固件漏洞检测的效率与精度矛盾,一个很自然的想法是分步处理:先快速过滤掉明显无关的部分,再对可疑目标进行深度分析。我们提出的两阶段方法正是这一思想的系统化实现。整个流程的顶层设计如下图所示,它清晰地划分了职责边界和数据流。

[输入:原始固件二进制 & 已知漏洞函数特征]
        |
        v
[阶段一:粗粒度快速筛查]
    |-- 特征提取与选择(遗传算法优化)
    |-- 函数嵌入生成(残差神经网络)
    |-- 向量相似度计算(余弦相似度)
    |-- 输出:高相似度嫌疑函数列表
        |
        v
[阶段二:细粒度精确验证]
    |-- 构建局部调用图(LCG)
    |-- 多层加权二分图匹配
    |-- 计算图结构相似度
    |-- 输出:确认的漏洞函数及置信度

这个架构的关键在于,两个阶段并非孤立,而是紧密协作。第一阶段建立的 函数嵌入向量 ,不仅是快速比对的依据,也作为第二阶段图匹配中节点相似度的计算基础。这种设计避免了重复的特征提取和计算,极大提升了整体效率。

2.1 第一阶段:基于神经网络嵌入的快速筛查

这一阶段的目标是“大海捞针”,核心是将复杂的函数代码转化为一个固定长度的、稠密的数值向量(即“嵌入”)。一旦函数被表示为向量,比较两个函数的相似度就变成了计算两个向量之间的余弦相似度,这是一个O(1)复杂度的操作,速度极快。

为什么是“嵌入”而不是直接比较代码? 这就好比比较两篇文章的相似性。逐字逐句对比(直接比对汇编指令)效率极低,且容易受同义词、变换语序(编译器优化)的影响。更好的方法是,将每篇文章映射到一个“语义空间”(例如通过Word2Vec),在这个空间里,语义相近的文章距离就近。函数嵌入也是同理,它将函数的控制流、统计特征等映射到一个高维空间,使得语义相似(即同源但编译后形式不同)的函数,其向量表示也接近。

注意 :这里“语义相似”在二进制分析中有特指。它指的是由同一份源代码,经过不同编译器、为不同架构、以不同优化选项编译后产生的多个二进制函数。它们在功能上完全等价,但二进制表现形式可能截然不同。

2.2 第二阶段:基于局部调用图(LCG)的精确验证

第一阶段找到了“长得像”的嫌疑函数,但“长得像”不一定就是“有相同的病”。一个函数是否构成漏洞,往往取决于它如何被调用、以及它如何调用其他函数。例如,一个危险的 strcpy 函数,如果其目标缓冲区大小始终被严格检查,那它可能就是安全的。

因此,第二阶段引入 局部调用图(Local Call Graph, LCG) 分析。LCG不仅包含目标函数本身,还包含了其直接调用者和被调用者(通常扩展2-3层)。通过比较嫌疑函数和已知漏洞函数的LCG结构相似性,我们可以更准确地判断漏洞触发的上下文环境是否也相似。这相当于不仅比对了嫌疑犯的指纹(函数本身),还比对了他的社会关系网(调用链),大大提高了认定的准确性。

两阶段联动的优势 :第一阶段用高效率覆盖全量,将需要昂贵图匹配计算的范围从数万个函数缩小到几十个高嫌疑函数。第二阶段则对这几十个函数进行“精准打击”,利用LCG分析弥补单纯函数相似性分析的不足。这种“粗筛+精查”的模式,在实践中被证明是应对海量固件分析的最优解之一。

3. 特征工程:如何为二进制函数“画像”

无论是神经网络的训练,还是后续的相似度计算,都依赖于如何有效地表示一个二进制函数。特征工程就是为函数制作“身份证”的过程,这张“身份证”必须满足两个要求:1) 判别力强 :能区分不同功能的函数;2) 鲁棒性高 :对编译架构、优化选项、编译器版本等变化不敏感。

我们最初从控制流图(CFG)和函数调用图中提炼了多达50个原始特征,主要分为三类:

  • 统计特征 :如各种指令类型(算术、逻辑、传输、跳转)的数量和比例、基本块数量、边数量、字符串常量、数值常量的分布等。
  • 结构特征 :描述CFG的拓扑性质,如图的深度、广度、节点的入度/出度分布、循环结构的深度等。
  • 调用特征 :描述函数在调用图中的角色,如调用其他函数的次数、被其他函数调用的次数、调用深度等。

3.1 使用遗传算法进行特征选择

直接使用50维特征进行嵌入,维度太高,会引入噪声并增加计算负担。并非所有特征都有用,有些特征判别力弱(如某些指令比例在不同函数间差异不大),有些特征之间存在强相关性(冗余)。我们的目标是自动筛选出一个最优的、加权的特征子集。

我们选择了 遗传算法 来完成这个任务。将其应用于特征选择,是一种模拟自然进化过程的启发式搜索方法,特别适合处理像特征组合优化这类组合爆炸问题。

实操中的算法设计要点:

  1. 编码 :每个“个体”用一个50位的二进制染色体表示,每一位对应一个原始特征,“1”表示选择该特征,“0”表示不选。
  2. 适应度函数 :这是遗传算法的“指挥棒”。我们使用第一阶段神经网络生成的函数嵌入的评估结果来定义适应度。具体来说,我们准备一个由同源函数对(正样本)和不同源函数对(负样本)组成的数据集。对于一个给定的特征子集,我们用它训练嵌入模型,然后计算模型区分正负样本的能力(例如,使用嵌入向量间的欧氏距离,同源函数距离应小,不同源函数距离应大)。这个能力的量化指标(如准确率的倒数或距离差异的某种度量)即为适应度值,值越大表示该特征子集越好。
  3. 选择、交叉、变异
    • 选择 :根据适应度高低,采用轮盘赌策略选择特征子集进入下一代。适应度高的个体被选中的概率大。
    • 交叉 :随机配对选中的个体,交换它们染色体的一部分,产生新的子代特征组合。
    • 变异 :以很小的概率随机翻转染色体上的某些位(即随机添加或删除某些特征),保持种群的多样性,避免陷入局部最优。
  4. 迭代 :重复选择、交叉、变异过程数十代,最终种群中适应度最高的个体所代表的特征子集,就是我们的最优选择。

经过遗传算法优化后,特征数量从50维降到了10维,并且每个特征都被赋予了一个权重,表示其重要性。下表展示了我们实验中筛选出的部分核心特征及其权重示意:

特征类别 特征描述 权重示意 说明
结构特征 CFG的图直径 0.15 反映函数逻辑的复杂度和最长执行路径。
结构特征 基本块的平均入度 0.12 表征代码的汇聚程度,与循环、异常处理结构相关。
统计特征 转移指令(跳转、调用)占比 0.10 直接关联控制流的复杂度。
调用特征 函数的入度(被调用次数) 0.18 反映函数在程序中的重要性或通用性。
调用特征 函数的出度(调用其他函数次数) 0.14 反映函数的依赖性和复杂性。
统计特征 唯一数值常量的数量 0.08 与算法、魔术字、配置参数相关,是强语义特征。
结构特征 深度大于2的循环数量 0.13 深层循环往往是性能关键点或潜在漏洞点。
统计特征 字符串引用数量 0.10 与日志输出、调试信息、特定协议关键字相关。

实操心得:特征选择的陷阱 :最初我们尝试使用过滤式方法(如基于相关系数)或封装式方法(如递归特征消除)进行特征选择,但效果都不如遗传算法。原因在于,特征之间的交互效应非常复杂。例如,“基本块数量”和“转移指令数量”单独看可能权重都不高,但它们的组合却能极好地标识一类函数。遗传算法通过种群搜索,能更好地发现这种组合效应。一个关键技巧是,用于遗传算法训练的数据集必须足够多样,要涵盖不同架构(ARM, MIPS, x86)、不同编译器(GCC, Clang)、不同优化等级(-O0, -O1, -O2, -Os)编译的同源函数,这样才能确保选出的特征真正具有跨平台鲁棒性。

4. 第一阶段实现:从函数到向量——残差网络嵌入模型

有了精选的特征,下一步就是如何将这些特征转化为一个具有强大表示能力的向量。我们设计了一个基于 残差网络(ResNet) 的图神经网络模型来完成这个“嵌入”过程。这里需要理解一个关键概念:我们不是把整个函数作为一个扁平的特征向量输入,而是将其 控制流图(CFG) 作为图结构数据输入模型。

4.1 模型输入:图的构建

每个函数被表示为一个图 G = (V, E, ψ)

  • V :节点集合,每个节点代表一个 基本块
  • E :边集合,代表基本块之间的跳转关系。
  • ψ :节点特征,每个基本块的特征向量。这个向量由该基本块内的指令统计特征(如指令类型分布、常量值)构成,并拼接上一步得到的全局函数特征(如调用特征)。这样,每个节点既包含局部信息,也感知全局信息。

4.2 残差网络:学习深层图表示

为什么用图神经网络(GNN)和残差结构? 传统的神经网络处理网格数据(如图像)很拿手,但处理图这样的非欧几里得数据则很吃力。GNN的核心思想是 消息传递 :每个节点通过聚合其邻居节点的信息来更新自己的表示。经过多轮迭代,一个节点的表示就能捕获其所在图结构的局部拓扑信息。

我们采用了一种类似 图卷积网络(GCN) 的传播规则,但为了学习更深的函数语义,引入了 残差连接 。普通的深层网络在训练时会出现梯度消失或爆炸问题,导致难以优化。残差连接通过“短路”机制,让信息可以直接跨层传输,使得训练非常深的网络成为可能。

我们设计的核心更新公式如下:

µ_i^(t) = ReLU( W1 * v_i + σ( W2 * Σ_(j∈N(i)) µ_j^(t-1) + W3 * Σ_(j∈N(i)) v_j ) )
  • µ_i^(t) :第 t 轮迭代后,节点 i 的嵌入向量。
  • v_i :节点 i 的初始特征向量。
  • N(i) :节点 i 的邻居节点集合。
  • µ_j^(t-1) :上一轮迭代中邻居节点 j 的嵌入。
  • W1, W2, W3 :可训练的权重矩阵。
  • σ :一个多层感知机(MLP),用于学习复杂的非线性聚合函数。
  • ReLU :激活函数。

这个公式的含义是:当前节点的嵌入,由它自身的特征( W1*v_i )、邻居节点上一轮的嵌入信息的聚合( W2*聚合项 )、以及邻居节点初始特征的聚合( W3*聚合项 )共同决定。残差连接体现在,节点自身特征 v_i 直接参与了最终输出。

4.3 从节点嵌入到函数嵌入

经过 T 轮迭代后,每个基本块都获得了一个稳定的嵌入向量。整个函数的嵌入向量 µ(f) ,通过对所有基本块的嵌入向量进行 聚合 (例如求和或求平均)得到。这个向量就是该函数在高维语义空间中的“坐标”。

4.4 模型训练与相似度计算

训练目标 :我们的训练数据是成对的函数 (f_i, f_j) ,并带有标签 label label=1 表示 f_i f_j 是同源函数(编译差异), label=-1 表示不同源。训练的目标是,让模型学习到的嵌入向量之间的 余弦相似度 ,尽可能接近真实的标签。

  • 损失函数采用均方误差: Loss = Σ (cosine_sim(µ(f_i), µ(f_j)) - label)^2
  • 优化器使用 随机梯度下降(SGD) 或其变种(如Adam)。

训练完成后,我们就得到了一个可以将任意二进制函数映射为固定长度向量的模型。在实际检测中:

  1. 用IDA Pro等工具分析目标固件,提取每个函数的特征并构建CFG。
  2. 使用训练好的模型,为每个函数生成嵌入向量,存入 FirmwareDB
  3. 同样,为所有已知的漏洞函数生成嵌入向量,存入 VulnerabilityDB
  4. 对于 FirmwareDB 中的每个向量,使用 局部敏感哈希(LSH) 技术在 VulnerabilityDB 中进行快速近邻搜索,找出相似度超过阈值(如0.8)的候选漏洞函数。

注意事项:训练数据集的构建 :这是整个项目成败的关键。我们使用了 OpenSSL BusyBox 这两个广泛存在于嵌入式设备中的开源项目。用不同的编译器(gcc, clang)、为不同的架构(ARM, MIPS, x86)、以不同的优化级别(-O0, -O1, -O2, -Os)进行交叉编译,得到大量二进制文件。从中提取函数,并利用调试信息或符号表,精确地标记出哪些函数来自同一份源代码。这个过程需要大量的自动化脚本和存储资源,但一劳永逸。一个高质量的、覆盖广泛的训练集,是模型具备强大泛化能力的基础。

5. 第二阶段实现:调用关系洞察——局部调用图相似性分析

第一阶段找到了“疑似”漏洞函数,但误报依然存在。例如,两个不同的加密函数可能具有非常复杂的、相似的控制流结构(很多循环和分支),导致嵌入向量接近。但它们一个是AES,一个是SHA256,显然不是同一个漏洞。第二阶段的目的就是引入新的判断维度—— 函数调用上下文 ,来滤除这类误报。

5.1 局部调用图(LCG)的构建与意义

对于一个目标函数 f ,其k层LCG定义为:以 f 为中心,向外辐射k层的所有函数调用节点及其调用边构成的子图。通常k取2或3。例如,一个2层LCG包含:

  • 第0层:函数 f 本身。
  • 第1层:所有被 f 直接调用的函数(callees),以及所有直接调用 f 的函数(callers)。
  • 第2层:上述第1层函数的调用者和被调用者(排除已包含的节点)。

为什么LCG比单个函数更可靠? 漏洞的触发往往依赖于特定的代码路径。一个存在栈溢出的函数 strcpy ,只有当其调用者传递给它的目标缓冲区大小小于源缓冲区大小时,漏洞才会被利用。如果调用者总是先进行严格的长度检查,那么这个 strcpy 在上下文中就是安全的。LCG捕捉的正是这种“上下文”信息。此外,调用图结构对编译器优化和架构差异的敏感性远低于函数内部的指令序列,因此更具鲁棒性。

5.2 多层加权二分图匹配算法

比较两个LCG的相似度,本质上是比较两个图的相似度。我们将固件函数的LCG(记为 G_f )和漏洞函数的LCG(记为 G_vul )视为一个 多层加权二分图

算法步骤详解:

  1. 图对齐与分层 :以两个中心函数(即待比较的嫌疑函数和漏洞函数)为对齐点,将各自的LCG按调用距离分层。距离中心函数为 i 跳的所有函数属于第 i 层。
  2. 构建二分图 :对于每一层 i ,将 G_f i 层的函数集合和 G_vul i 层的函数集合分别作为二分图的两部分。
  3. 计算节点相似度 :对于二分图中任意一对来自两边的函数节点 (u, v) ,其相似度 Sim(u, v) 直接使用第一阶段计算出的 函数嵌入向量的余弦相似度 。这充分利用了一阶段的成果。
  4. 层内最优匹配与权重分配 :对于每一层,我们的目标是找到一种匹配方式,使得左右两边节点配对的总相似度最大。这是一个经典的 二分图最大权匹配 问题,可以使用匈牙利算法或KM算法求解。求解后,我们得到该层的匹配总得分 S_i
    • 权重 :并非所有层都同等重要。直观上,距离中心函数越近的层(如第0、1层),对漏洞语义的影响越大。我们通过实验确定一个衰减权重 W_i ,通常 W_0 > W_1 > W_2
  5. 计算整体LCG相似度 :将各层的加权得分相加,得到两个LCG的最终相似度: Sim(G_f, G_vul) = Σ (W_i * S_i)

这个算法巧妙地将复杂的图相似度比较,分解为多个层的、基于一阶段向量相似度的匹配问题,大大降低了计算复杂度。同时,通过加权机制,强调了调用关系中更近的部分。

5.3 阈值设定与结果判定

经过两阶段分析,每个嫌疑函数会得到两个相似度分数:

  1. S1 : 一阶段函数嵌入相似度(0~1之间)。
  2. S2 : 二阶段LCG相似度(0~1之间)。

最终的决策需要结合这两个分数。我们实践中采用一种 分级告警 策略:

  • 高危 S1 > θ1 (如0.85) S2 > θ2 (如0.75)。这类结果置信度最高,极大概率是真实漏洞,需要立即人工复核并上报。
  • 中危 S1 > θ1 S2 处于中等范围(如0.5~0.75)。可能是漏洞,但调用上下文有差异,或者是库函数的不同用法。需要中级优先级的审计。
  • 低危/信息 :仅 S1 较高, S2 很低。可能是功能相似但无关的函数,或是一阶段模型的误报。可以批量记录,供后续模型优化使用。

实操心得:LCG层数(k值)的选择 :k值不是越大越好。k越大,考虑的上下文越广,理论上精度越高。但随之而来的是计算量指数级增长和噪声引入。更多的层意味着包含更多不相关或高度可变的函数(如通用的内存操作函数 malloc/free ),这些噪声会稀释核心调用关系的信号。经过大量测试,对于大多数用户态漏洞, k=2 是一个最佳平衡点,既能捕获核心的调用关系(调用者-目标函数-被调用者),又不会引入过多噪声。对于某些涉及深度回调或复杂状态机的漏洞(如一些内核驱动漏洞),可以酌情考虑k=3。

6. 系统实现、评估与实战经验

理论再完美,也需要实战检验。我们基于上述方法,用Python和C++混合编程实现了一个原型系统,并进行了全面的评估。

6.1 系统实现要点

  1. 前端与反汇编 :集成 IDA Pro 的Python SDK或开源的 angr / Ghidra 框架,实现固件的自动加载、反汇编和函数特征提取。这部分是性能瓶颈之一,需要良好的并行化处理。
  2. 特征提取管道 :将遗传算法筛选出的10维特征提取过程模块化、管道化。每个特征一个独立模块,便于调试和扩展。
  3. 神经网络模型 :使用 PyTorch TensorFlow 实现残差图神经网络。模型训练在GPU服务器上进行,推理阶段可以部署在CPU环境。
  4. 向量数据库 :使用高效的向量检索库,如 FAISS (Facebook AI Similarity Search)或 Annoy ,来存储和快速查询 VulnerabilityDB 。LSH索引的构建是关键,需要在召回率和精度之间权衡。
  5. LCG分析引擎 :实现二分图匹配算法,并缓存中间结果。因为同一个固件中,不同函数可能共享大量相同的调用者/被调用者,缓存可以避免重复计算。

6.2 性能与准确性评估

我们在两个数据集上进行了评估:

  1. 标准数据集 :从多个开源项目(OpenSSL, BusyBox, Coreutils)编译的跨平台二进制函数对。
  2. 真实世界数据集 :从公开漏洞库(如CVE)中提取的漏洞函数,并在真实的厂商固件(如D-Link, TP-Link, Netgear的旧版本固件)中寻找它们。

主要结果:

  • 准确性(AUC) :我们的方法在标准数据集上达到了 0.981 的AUC(Area Under Curve),高于同期最先进方法Gemini的0.971。AUC越接近1,说明模型整体区分同源/不同源函数的能力越强。
  • 效率
    • 模型重训练时间 :当有新的漏洞样本加入,需要更新模型时,平均重训练时间约为 1小时 。这得益于我们相对轻量的网络结构和特征维度,使得模型能够快速适应新的数据分布。相比之下,一些基于复杂图匹配的方法重训练可能需要数天。
    • 检测速度 :对于一个50MB大小的固件(约含3万个函数),第一阶段全量扫描可在10分钟内完成(在单台标准服务器上)。第二阶段仅对第一阶段筛选出的Top 100嫌疑函数进行分析,耗时在2分钟以内。
  • 真实漏洞检测 :在针对已知漏洞的真实固件测试中,我们的方法在返回的相似度最高的前30个嫌疑函数里,成功识别出了 26个 真正的漏洞函数,真阳性率(TPR)高达 86.7% 。这证明了其在实战中的高可用性。

6.3 常见问题与排查技巧实录

在实际部署和运行过程中,我们踩过不少坑,也总结了一些排查技巧:

问题1:第一阶段召回率很高,但精度不足,产生大量误报。

  • 可能原因 :训练数据集中同源函数对的“多样性”不足。例如,只包含了 -O2 优化下的同源对,缺少 -Os (尺寸优化)下的样本。导致模型对某些优化模式下的函数泛化能力差。
  • 排查与解决
    1. 检查训练集 :确保训练集覆盖了所有常见的架构(ARMv7, ARM Thumb, MIPS32, MIPS64, x86, x64)、主流编译器(GCC, Clang, IAR)和优化级别(-O0, -O1, -O2, -Os, -Oz)。
    2. 分析误报样本 :手动分析一批高相似度的误报函数对。看看它们是否属于某一类特定的函数(如数学计算函数、字符串处理函数)。如果是,考虑在特征集中增加或强化能区分这类函数的特征(例如特定指令序列模式)。
    3. 调整相似度阈值 :适当提高第一阶段的相似度阈值 θ1 ,虽然会降低一些召回率,但能显著提升进入第二阶段的样本质量。

问题2:第二阶段LCG相似度计算非常慢。

  • 可能原因 :目标函数位于调用图非常密集的区域(例如一个通用的工具函数被大量其他函数调用),导致其LCG规模爆炸(节点数过多)。
  • 排查与解决
    1. 限制LCG规模 :为LCG的每一层设置一个最大节点数上限(例如每层最多50个节点)。当节点数超过上限时,优先保留那些与中心函数嵌入相似度更高的邻居节点。这是一种以微小精度损失换取巨大性能提升的策略。
    2. 并行化计算 :第二阶段各个嫌疑函数的LCG相似度计算是相互独立的,可以很容易地分配到多个CPU核心上并行执行。
    3. 缓存节点相似度 :如前所述,不同LCG之间共享大量节点。建立一个全局缓存字典,键为函数对 (func_id_f, func_id_vul) ,值为其第一阶段相似度。每次计算前先查询缓存,避免重复计算。

问题3:对于某些高度混淆或加壳的固件,函数识别和特征提取失败。

  • 可能原因 :反汇编工具无法正确识别函数边界,或者混淆技术(如花指令、控制流平坦化)破坏了正常的CFG结构。
  • 排查与解决
    1. 预处理与脱壳 :对于已知的加壳方式,先进行脱壳处理。对于混淆,可以尝试使用一些反混淆脚本或工具(如基于angr的简单反平坦化)进行预处理,但这通常是个案处理,难以通用化。
    2. 降级使用 :如果无法恢复清晰的结构,本方法的效力会大打折扣。此时可以退而求其次,主要依赖 统计特征 (如指令熵、字节分布)和 调用特征 (虽然调用关系也可能被混淆,但相对稳定)进行非常粗粒度的筛选,结果仅供参考,必须结合动态分析或人工审计。
    3. 标记为困难样本 :将此类固件标记,用于后续研究如何增强模型对混淆代码的鲁棒性。

问题4: VulnerabilityDB (漏洞特征库)的维护问题。

  • 挑战 :漏洞库需要持续更新,新漏洞的样本(二进制代码)如何获取?如何保证入库特征的质量?
  • 我们的实践
    1. 来源 :主要从三个渠道获取:a) 开源软件的安全补丁,通过对比补丁前后版本二进制文件,提取漏洞函数。b) 公开的PoC(概念验证)代码,自行编译生成带漏洞的二进制片段。c) 商业漏洞情报平台。
    2. 质量控制 :每个入库的漏洞函数,必须附带清晰的元数据:CVE编号、受影响软件版本、漏洞类型、函数名(如果有)、源代码片段(如果有)。并且,要用多个编译设置生成该漏洞函数的变体,一并入库,以增强模型的泛化能力。
    3. 版本化 :对 VulnerabilityDB 进行版本管理。当模型更新或特征定义变化时,需要重新生成整个数据库的嵌入向量。

7. 方法局限性与未来展望

没有任何一种方法是银弹,我们的两阶段方法同样有其适用范围和局限性。

主要局限性:

  1. 对编译器内置函数的处理 :像 memcpy , strlen 这类编译器内置/intrinsic函数或高度优化的库函数,其实现可能因编译器和架构而异,甚至被内联展开。我们的特征和模型可能难以准确判断两个不同版本的内置函数是否“同源”。实践中,我们通常将这些函数列入白名单,不进行相似性检测,而是直接通过版本匹配或代码审计处理。
  2. 漏洞变体检测 :如果漏洞代码被进行了非等价的重写(例如,一个缓冲区溢出漏洞,从数组下标错误被改为了指针算术错误),但语义和漏洞模式相同,我们的方法可能无法检测,因为它严重依赖于代码结构的相似性。这需要结合更高级的符号执行或污点分析。
  3. 资源消耗 :虽然比纯图匹配快很多,但特征提取和神经网络推理仍需可观的计算资源。对于超大规模(如数TB)的固件仓库进行全量扫描,仍需分布式计算集群的支持。

未来可能的改进方向:

  1. 引入动态特征 :在可能的情况下,结合轻量级的动态分析(如模拟执行片段代码,收集部分执行轨迹、内存访问模式),将动态特征与静态特征融合,可以进一步提升对混淆代码和漏洞变体的检测能力。
  2. 图神经网络架构升级 :尝试更先进的图神经网络模型,如Graph Attention Networks (GAT),让模型能更智能地关注CFG和LCG中与漏洞更相关的关键节点和边,而不是平等对待所有信息。
  3. 无监督/自监督学习 :目前严重依赖有标签的同源函数对进行训练。未来可以探索无监督或自监督的方法,利用海量未标注的二进制代码进行预训练,学习通用的二进制代码表示,再针对漏洞检测任务进行微调,以减轻对标注数据的依赖。
  4. 与符号执行结合 :将本方法作为“指路人”,快速定位到高风险的嫌疑函数和可疑调用路径,然后针对这些狭窄的路径启动资源消耗大的符号执行或concolic执行,进行深度的漏洞验证和利用链生成,形成“静态筛查 -> 动态验证”的闭环。

在我个人看来,物联网固件安全分析正在从“手工作坊”走向“自动化工厂”。基于代码相似性的检测方法是这个自动化流水线上的核心“筛选机”。它无法替代安全研究员深厚的漏洞挖掘功力,但能极大解放他们的生产力,让他们从海量的、重复的代码浏览中脱身,专注于最有可能产生成果的复杂逻辑分析和漏洞利用链构造。这项技术的价值不在于追求百分之百的自动化,而在于成为一个强大的“力量倍增器”。随着算法和算力的持续进步,我相信这类技术会成为物联网设备出厂前安全审计、供应链风险管控以及网络空间资产漏洞排查的标配工具。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐