4、特征工程:AST抽象语法树特征、CFG控制流图特征、操作码序列特征、语义嵌入(CodeBERT)
好,咱们进入第四章。特征工程。
说实话,这一章是整个模型训练里最吃经验的部分。你模型结构再花哨,喂进去的特征是垃圾,出来的一定是垃圾。我在做第一个智能合约审计模型时就吃过这个亏——特征选得太粗糙,准确率死活上不去。后来老老实实把AST、CFG、操作码、语义嵌入全撸了一遍,效果才起来。
今天咱们就把这四类特征掰开揉碎讲清楚。
4.1 AST抽象语法树特征
AST是什么?说白了,就是编译器把源代码解析成一棵树。每个节点是一个语法结构,比如函数定义、变量声明、表达式。
我习惯把AST特征分成两类:结构特征和统计特征。
4.1.1 结构特征
结构特征关注的是树本身的形状。比如:
- 树深度:嵌套越深,逻辑越复杂,风险越高
- 节点类型分布:比如函数调用节点、赋值节点、条件节点的数量
- 子树模式:某些危险模式在AST上是有固定子树的,比如“tx.origin 检查”
举个例子,检测重入漏洞时,我关注的是“外部调用”节点后面跟着“状态更新”节点。这种模式在AST上表现为一个特定的子树结构。
4.1.2 统计特征
统计特征更粗暴一些——直接数数。比如:
- 函数数量
- 变量声明数量
- require/assert语句数量
- 循环嵌套层数
这些特征虽然简单,但组合起来能反映代码的复杂度。我曾经发现,重入漏洞的合约平均函数调用次数比正常合约高30%左右。
4.2 CFG控制流图特征
AST只能看到静态结构,看不到执行路径。CFG就是干这个的。
CFG把代码拆成基本块(basic block),然后用有向边连接。每个基本块是一段顺序执行的代码,边代表跳转。
4.2.1 路径特征
我重点关注:
- 路径数量:条件分支越多,路径爆炸,越容易出问题
- 循环结构:循环里的外部调用是重入漏洞的高发区
- 可达性分析:某些函数是否永远无法到达?或者某些危险操作是否在特定条件下才触发?
4.2.2 控制流复杂度
我常用一个指标叫圈复杂度(Cyclomatic Complexity)。公式很简单:
M = E - N + 2P
其中E是边数,N是节点数,P是连通分量数。圈复杂度越高,代码越难测试,漏洞概率越大。我在项目中设过一个阈值:圈复杂度超过15的合约,直接标为高风险。
4.3 操作码序列特征
操作码是EVM直接执行的指令。相比源代码,操作码更底层,也更难被混淆。
我处理操作码特征时,通常走这几步:
- 反编译:把字节码反编译成操作码序列
- 序列化:把操作码转成数字ID(比如SLOAD=0x54,SSTORE=0x55)
- 特征提取:用N-gram或者序列模型提取模式
4.3.1 N-gram特征
N-gram就是连续N个操作码的组合。比如2-gram:
PUSH1, CALL → 这个组合在重入漏洞中很常见
SLOAD, ISZERO → 这个组合在条件检查中常见
我一般用3-gram或4-gram,太短了没区分度,太长了稀疏性太强。
4.3.2 频率特征
统计每种操作码出现的次数。比如:
- CALL操作码出现次数
- SSTORE操作码出现次数
- DELEGATECALL操作码出现次数
这些频率特征虽然简单,但能快速识别出“频繁调用外部合约”或“频繁写存储”的合约。
4.4 语义嵌入(CodeBERT)
前面三种特征都是手工设计的。说白了,靠的是工程师的经验。但有些漏洞模式太隐蔽,手工特征根本抓不到。
这时候就需要语义嵌入了。我用的最多的是CodeBERT。
4.4.1 什么是CodeBERT
CodeBERT是一个预训练模型,专门用来理解代码语义。它把代码片段映射成一个固定长度的向量(比如768维)。这个向量里包含了代码的语义信息——比如“这个函数是转账的”、“这个变量是余额”。
4.4.2 怎么用
我一般这么操作:
- 切片:把合约代码切成函数级别的片段
- 编码:用CodeBERT把每个函数编码成向量
- 聚合:把所有函数的向量平均或拼接,得到合约级别的表示
4.4.3 实际效果
我在一个项目里对比过:只用AST+CFG特征,F1分数大概在0.82左右。加上CodeBERT语义嵌入后,F1提升到了0.89。提升最明显的是那些逻辑复杂、模式多变的漏洞,比如访问控制漏洞。
4.5 特征融合策略
四种特征各有优劣。我习惯把它们融合起来用:
| 特征类型 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| AST | 语法结构清晰 | 对语义不敏感 | 语法漏洞、模式匹配 |
| CFG | 执行路径分析 | 计算复杂度高 | 逻辑漏洞、重入 |
| 操作码 | 底层、抗混淆 | 丢失高层信息 | 字节码分析、混淆检测 |
| CodeBERT | 语义理解强 | 计算成本高 | 复杂逻辑、未知漏洞 |
我常用的融合方式是:AST+CFG作为基础特征,操作码作为补充,CodeBERT作为增强。这样既保证了效率,又兼顾了效果。
嗯,特征工程这块就讲这么多。四种特征各有各的脾气,你得根据实际场景来选。我个人建议,刚开始做的时候先用AST+CFG打底,等模型跑通了再慢慢加操作码和CodeBERT。别一上来就堆特征,容易过拟合。