4、特征工程:AST抽象语法树特征、CFG控制流图特征、操作码序列特征、语义嵌入(CodeBERT)

好,咱们进入第四章。特征工程。

说实话,这一章是整个模型训练里最吃经验的部分。你模型结构再花哨,喂进去的特征是垃圾,出来的一定是垃圾。我在做第一个智能合约审计模型时就吃过这个亏——特征选得太粗糙,准确率死活上不去。后来老老实实把AST、CFG、操作码、语义嵌入全撸了一遍,效果才起来。

今天咱们就把这四类特征掰开揉碎讲清楚。

4.1 AST抽象语法树特征

AST是什么?说白了,就是编译器把源代码解析成一棵树。每个节点是一个语法结构,比如函数定义、变量声明、表达式。

我习惯把AST特征分成两类:结构特征统计特征

4.1.1 结构特征

结构特征关注的是树本身的形状。比如:

  • 树深度:嵌套越深,逻辑越复杂,风险越高
  • 节点类型分布:比如函数调用节点、赋值节点、条件节点的数量
  • 子树模式:某些危险模式在AST上是有固定子树的,比如“tx.origin 检查”

举个例子,检测重入漏洞时,我关注的是“外部调用”节点后面跟着“状态更新”节点。这种模式在AST上表现为一个特定的子树结构。

关键点:AST特征对语法层面的漏洞非常敏感,比如未检查的返回值、错误的修饰符顺序。但对语义层面的漏洞(比如逻辑错误)就力不从心了。

4.1.2 统计特征

统计特征更粗暴一些——直接数数。比如:

  • 函数数量
  • 变量声明数量
  • require/assert语句数量
  • 循环嵌套层数

这些特征虽然简单,但组合起来能反映代码的复杂度。我曾经发现,重入漏洞的合约平均函数调用次数比正常合约高30%左右。

4.2 CFG控制流图特征

AST只能看到静态结构,看不到执行路径。CFG就是干这个的。

CFG把代码拆成基本块(basic block),然后用有向边连接。每个基本块是一段顺序执行的代码,边代表跳转。

4.2.1 路径特征

我重点关注:

  • 路径数量:条件分支越多,路径爆炸,越容易出问题
  • 循环结构:循环里的外部调用是重入漏洞的高发区
  • 可达性分析:某些函数是否永远无法到达?或者某些危险操作是否在特定条件下才触发?
我的经验:CFG特征对逻辑漏洞特别有效。比如“先转账后更新余额”这种模式,在CFG上表现为一条清晰的路径:调用外部合约 → 更新状态。你只要检测这条路径的顺序,就能抓到重入。

4.2.2 控制流复杂度

我常用一个指标叫圈复杂度(Cyclomatic Complexity)。公式很简单:

M = E - N + 2P

其中E是边数,N是节点数,P是连通分量数。圈复杂度越高,代码越难测试,漏洞概率越大。我在项目中设过一个阈值:圈复杂度超过15的合约,直接标为高风险。

4.3 操作码序列特征

操作码是EVM直接执行的指令。相比源代码,操作码更底层,也更难被混淆。

我处理操作码特征时,通常走这几步:

  1. 反编译:把字节码反编译成操作码序列
  2. 序列化:把操作码转成数字ID(比如SLOAD=0x54,SSTORE=0x55)
  3. 特征提取:用N-gram或者序列模型提取模式

4.3.1 N-gram特征

N-gram就是连续N个操作码的组合。比如2-gram:

PUSH1, CALL → 这个组合在重入漏洞中很常见
SLOAD, ISZERO → 这个组合在条件检查中常见

我一般用3-gram或4-gram,太短了没区分度,太长了稀疏性太强。

注意:操作码序列特征对混淆有一定的抵抗力。因为无论你怎么混淆源代码,最终生成的EVM操作码序列是确定的。但缺点也很明显——它丢失了变量名、注释等高层信息。

4.3.2 频率特征

统计每种操作码出现的次数。比如:

  • CALL操作码出现次数
  • SSTORE操作码出现次数
  • DELEGATECALL操作码出现次数

这些频率特征虽然简单,但能快速识别出“频繁调用外部合约”或“频繁写存储”的合约。

4.4 语义嵌入(CodeBERT)

前面三种特征都是手工设计的。说白了,靠的是工程师的经验。但有些漏洞模式太隐蔽,手工特征根本抓不到。

这时候就需要语义嵌入了。我用的最多的是CodeBERT

4.4.1 什么是CodeBERT

CodeBERT是一个预训练模型,专门用来理解代码语义。它把代码片段映射成一个固定长度的向量(比如768维)。这个向量里包含了代码的语义信息——比如“这个函数是转账的”、“这个变量是余额”。

4.4.2 怎么用

我一般这么操作:

  1. 切片:把合约代码切成函数级别的片段
  2. 编码:用CodeBERT把每个函数编码成向量
  3. 聚合:把所有函数的向量平均或拼接,得到合约级别的表示
优势:CodeBERT能捕捉到语义相似性。比如“transfer”和“send”在语义上是相近的,CodeBERT会把它们映射到相近的向量空间。而手工特征很难做到这一点。

4.4.3 实际效果

我在一个项目里对比过:只用AST+CFG特征,F1分数大概在0.82左右。加上CodeBERT语义嵌入后,F1提升到了0.89。提升最明显的是那些逻辑复杂、模式多变的漏洞,比如访问控制漏洞。

避坑指南:CodeBERT虽然强,但计算成本高。我建议把它作为补充特征,而不是替代手工特征。另外,CodeBERT对代码长度有限制(一般是512个token),长合约需要切片处理。

4.5 特征融合策略

四种特征各有优劣。我习惯把它们融合起来用:

特征类型 优势 劣势 适用场景
AST 语法结构清晰 对语义不敏感 语法漏洞、模式匹配
CFG 执行路径分析 计算复杂度高 逻辑漏洞、重入
操作码 底层、抗混淆 丢失高层信息 字节码分析、混淆检测
CodeBERT 语义理解强 计算成本高 复杂逻辑、未知漏洞

我常用的融合方式是:AST+CFG作为基础特征,操作码作为补充,CodeBERT作为增强。这样既保证了效率,又兼顾了效果。

特征工程知识体系 智能合约特征 AST抽象语法树 CFG控制流图 操作码序列 语义嵌入(CodeBERT) 结构特征 + 统计特征 路径特征 + 圈复杂度 N-gram + 频率特征 函数编码 + 语义向量 特征融合 → 模型输入

嗯,特征工程这块就讲这么多。四种特征各有各的脾气,你得根据实际场景来选。我个人建议,刚开始做的时候先用AST+CFG打底,等模型跑通了再慢慢加操作码和CodeBERT。别一上来就堆特征,容易过拟合。

专注资料整理