简介:这是一套面向恶意代码检测的机器学习源码项目,项目聚焦Android应用smali指令序列,通过提取3-gram操作码特征,并利用TF与TF-IDF两种加权方式构建高区分度特征集,随后训练二分类模型并输出预测结果、TPR/FPR指标及ROC曲线,完整覆盖了从特征工程到模型评估的学术实验链路。源码经过严格调试,运行环境清晰,适合计算机、人工智能、大数据等专业学生在课程设计、期末大作业或毕业设计中学习参考,也可作为相关技术人员的实战示例。压缩包内共22个文件,核心为9个Python脚本,分别承担smali解析、特征构建、模型训练、预测与可视化等任务;配套6个CSV数据集及特征文件、4个TXT预测/输出记录、README说明以及Git版本配置,整体仅46KB,轻量便于快速部署。目前已有294人学习使用,读者可基于提供的数据集和脚本直接复现实验,再替换自有样本进行验证,既能加深对特征选择与分类评估的理解,也能作为毕设框架进一步扩展。
1. 机器学习检测恶意代码:这条流水线到底能解决什么
杀毒软件特征码更新永远追不上样本变种,这是我拿到这个基于机器学习检测恶意代码的源码包后最直接的感受。它不是一个能上生产环境的引擎,而是一条完整跑通的静态检测流水线:apk 反编译成 smali,提取 opcode 指令序列,切 3-gram 后统计频率,用 TF 和 TF-IDF 各筛 top50 特征,训练分类器,最后用 TPR/FPR 和 ROC 曲线量化效果。代码经过调试,适合做毕业设计、课程设计的学生复现,也适合安全从业者快速了解机器学习检测的落地姿势。
这份资源的完整之处在于把样本标注、特征提取、模型评估整条链都留全了:VirusShare.csv 和 yingyongbao.csv 对应恶意与正常样本,处理脚本、特征 CSV、预测输出、绘图脚本都在包内。适合计科、大数据、数学等专业学生当毕设骨架,也需要一点 sklearn 基础。下面按我拆包的顺序,把怎么用、参数在哪里、容易翻车的地方一次说清楚。
2. 源码包解剖:smali、3-gram 与 TF/TF-IDF 特征管线
2.1 从 apk 到 smali:smali.py 在链路中的位置
smali 是 dex 的可读汇编表示,每条指令对应一个 opcode。静态检测的基本逻辑是:恶意代码的指令序列和正常应用存在统计差异,把 opcode 抽出来就能当样本特征。smali.py 就是干这个的,它把 apk 或已解压的 smali 目录批量处理成 opcode 序列文本。包内没有附带完整的 apk 数据集,所以你得自己准备样本目录,再用 smali.py 或手动反编译工具产出中间文件。
要理解这个包,先把静态检测的基本盘捋一遍。恶意样本反编译出来的 smali 里,经常出现反射调用、动态加载、敏感 API 访问的连续指令模式;正常应用更多是 UI 生命周期、网络请求这类规律序列。3-gram 抓的是“连续三条指令的局部模式”,相当于把程序行为片段当成词,再交给分类器学。smali.py 在这条链里其实是可以替换的一环——你用 apktool 或 baksmali 手工解包也行,只要最终能产出 opcode 序列文本,后面的脚本都不关心来源。
我一般会在跑特征提取前单独验证一下 smali.py 的输出格式:每行一个 opcode,还是整行带参数。这个决定后面正则怎么写。包内 README 如果没写清楚,先拿一个小样本目录跑一遍看输出。路径上注意别带中文,Windows 下解压 zip 经常带出乱码目录,脚本读不到文件时先怀疑这个。
2.2 为什么同时保留 TF 和 TF-IDF 两套脚本
这是这个毕设项目最有价值的地方。TF 只统计 opcode 3-gram 在当前样本里的出现次数,TF-IDF 在 TF 基础上乘了逆文档频率,能惩罚那些在所有样本里都出现的通用指令组合,比如 move、invoke 开头的常见序列。恶意样本里真正有区分度的特征往往只出现在少数样本中,TF-IDF 会把它们的权重顶上去;但 TF-IDF 对样本覆盖度敏感,样本数少时容易选到长尾噪声。两套脚本并存,就是让你能跑对比实验,毕设里自然多出一节“两种特征权重的对比分析”。
TF 路线对应的分类器常见做法是多项式朴素贝叶斯或随机森林,TF-IDF 路线也一样。这两套脚本只负责特征矩阵,不限定后面的分类器,所以工程里才会另外配 TPR/FPR 脚本。特征维度只有 50,模型迭代很快,你可以反复换分类器重新评估。我见过不少直接把特征 CSV 丢给 sklearn 默认随机森林就跑出 0.95 AUC 的情况,所以别把特征构建看成黑匣子,它只是把原始指令变成表格,真正出效果的是特征选择和数据质量。
2.3 文件清单:拿到包先认清这些产物
| 文件 | 类型 | 在链路中的位置 |
|---|---|---|
| smali.py | 预处理脚本 | apk/dex → smali → opcode 序列 |
| opcode_3-gram_TF_top50.py | 特征构建 | opcode 序列 → TF 3-gram top50 特征 CSV |
| opcode_3-gram_TFIDF_top50.py | 特征构建 | opcode 序列 → TF-IDF 3-gram top50 特征 CSV |
| opcode_3-gram_TF_top50_test.py / opcode_3-gram_TFIDF_top50_test.py | 测试集特征 | 测试集样本 → 对应特征 CSV |
| TF_top50_3gramfeature.csv / TFIDF_top50_3gramfeature.csv | 训练特征 | 喂给分类器 |
| TF_top50_3gramfeature_test.csv / TFIDF_top50_3gramfeature_test.csv | 测试特征 | 喂给已训练模型 |
| out_x_test_.txt / out_pridict_.txt | 预测输出 | 预测类别与概率 |
| TF_3-gram_top50_tpr_fpr.py / TFIDF_3-gram_top50_tpr_fpr.py | 评估 | 阈值扫描算 TPR/FPR |
| plot_roc_gram_TF_top50_3-gram.py / plot_roc_gram_TFIDF_top50_3-gram.py | 可视化 | 画 ROC 曲线 |
CSV 里的列是选出来的 50 个 3-gram 字符串,行是样本,值是 TF 或 TF-IDF 权重。如果某一行全 0,说明该样本的有效 opcode 太少,这类样本在训练阶段应该直接过滤,而不是留着当噪声。实际样本集里经常有加固应用,smali 里抠不出几条指令,全 0 行就是典型代表。另外注意 out_pridict 是原作者把 predict 拼错了,不是新变量,找函数时别按 pridict 去搜。
2.4 从文件命名反推运行顺序
命名规律很直白:训练脚本不带 _test,测试脚本带 _test;TF 和 TFIDF 各一套。运行顺序基本是 smali.py → opcode 3-gram 训练脚本 → 测试脚本 → tpr_fpr → plot_roc。README 里如果没有更细的说明,按这个顺序不会错。特征文件名里的 top50 对应每类特征保留 50 维,维度不高,用文本编辑器就能直接打开 CSV 看前几行长什么样。
这一步还有个好处:能提前发现脚本之间的依赖。比如 opcode_3-gram_TF_top50.py 读的是 smali.py 的输出目录,如果目录名对不上,脚本会报 FileNotFoundError。先用命令列一下目录结构:
find . -maxdepth 2 -type f | sort逻辑说明:这个命令把所有文件按路径列出来,方便和 README 描述的流程对照。参数说明:如果你在 Windows 下没有 find 命令,直接用资源管理器或者dir /s /b效果一样。重点是确认特征 CSV、训练样本和测试样本三个目录都存在且路径正确。
3. 把 opcode 提出来:smali.py 与 3-gram 构建脚本的实测用法
3.1 环境准备与依赖版本
Python 3.6 以上就能跑,需要 pandas、numpy、scikit-learn、matplotlib。sklearn 建议 0.24 以上,太老的版本 TfidfVectorizer 行为有些差异。装依赖就一条命令:
pip install numpy pandas scikit-learn matplotlib逻辑说明:numpy 和 pandas 负责 CSV 特征矩阵读写,scikit-learn 提供特征向量化和分类器,matplotlib 画 ROC。参数说明:如果机器上同时有 Python 2,用python3 -m pip install显式指定解释器,否则 import 报错时先怀疑环境问题,别急着改代码。
我知道很多人一提到机器学习检测就想到深度学习。这个包用的是传统机器学习模型,原因很实际:特征维度 50,样本量就几千,深度学习在这个规模下容易过拟合,而且毕设答辩时解释随机森林的决策边界比解释神经网络容易得多。先跑通传统模型,再往深度学习扩,是性价比最高的路线。包内的 tpr_fpr 脚本也是为传统分类器设计的,换深度学习还得自己改评估逻辑。
3.2 smali.py 的核心逻辑与调用方式
smali.py 的作用是批量把 smali 文件转成 opcode 序列,核心逻辑通常是逐行正则取指令助记符。常见写法长这样:
import re OPCODE_RE = re.compile(r'^\s*([a-zA-Z][\w\-/]+)') def extract_opcodes(smali_file): ops = [] with open(smali_file, 'r', encoding='utf-8', errors='ignore') as f: for line in f: stripped = line.lstrip() if stripped.startswith(('#', '.')): continue m = OPCODE_RE.match(line) if m: ops.append(m.group(1)) return ops逻辑说明:跳过 .method、.line 这类 directive 行和注释行,剩下的行取第一个 token 作为 opcode。参数说明:errors='ignore'是为了容忍 smali 文件里的非法 UTF-8 字符,代价是吞掉个别乱码行,影响不大;正则里[\w\-/]+覆盖了 invoke-virtual、move-result 这类带连字符的指令名。如果你发现某个样本提取出的 opcode 数量异常少,先检查它是不是加固应用,这类样本的 smali 里根本没有真正的指令体。
这个脚本一般还会把每个样本的 opcode 写入单独 txt。常见用法是命令行传一个根目录:
python smali.py --input ./smali_samples --output ./opcode_seq逻辑说明:--input 指向所有样本的 smali 根目录,--output 指向 opcode 序列的输出目录。参数说明:我这里用 --input/--output 指代参数名,包内脚本的具体参数以 README 为准;如果它不支持命令行参数,直接改脚本顶部的路径常量也一样。跑完以后抽查两个样本的输出文件,确保不是空文件再往下走。
3.3 3-gram 构建:滑窗、特征词与稀疏矩阵
拿到一个样本的 opcode 列表后,3-gram 就是把相邻三条指令拼成一个特征词。这一步最容易写错的是边界:长度小于 3 的序列必须返回空列表,否则后面会索引越界。
def make_3gram_words(opcodes): if len(opcodes) < 3: return [] return [' '.join(opcodes[i:i+3]) for i in range(len(opcodes) - 2)]逻辑说明:窗口每次滑动一条指令,生成 len(opcodes)-2 个特征词,比如 10 条指令产生 8 个 3-gram。参数说明:如果你想对比 2-gram 或 4-gram,把滑窗大小改成对应数字即可,但特征维度会随窗口长度指数增长,4-gram 的词典经常冲到几十万,内存不够时先降窗口。拼出来的 3-gram 词条直接用空格分隔,是为了配合 CountVectorizer 的默认 token 切分,不用额外写 analyzer。
包内脚本应该是把这层逻辑封装在 opcode_3-gram_TF_top50.py 里,读取 opcode 序列目录后统一生成训练特征矩阵。这一步的产物就是 TF_top50_3gramfeature.csv,打开后你会看到每一列都是类似invoke-virtual move-result move-object这样的指令三元组。
3.4 完整命令链与产物校验
按包内脚本命名,训练和测试要分开跑,顺序如下:
python opcode_3-gram_TF_top50.py python opcode_3-gram_TF_top50_test.py python opcode_3-gram_TFIDF_top50.py python opcode_3-gram_TFIDF_top50_test.py python TF_3-gram_top50_tpr_fpr.py python plot_roc_gram_TF_top50_3-gram.py逻辑说明:先跑训练集特征,再跑测试集特征,然后计算 TPR/FPR,最后画 ROC。参数说明:如果你的样本量很大,比如超过一万个,建议在脚本里加一个sample(n=5000)的抽样步骤,否则特征矩阵构建会非常慢。
跑完后检查四件事:CSV 是否生成;CSV 行数是否和样本数一致;CSV 是不是 50 列特征加 1 列标签;txt 预测结果里是 0/1 标签还是概率值。这一步花五分钟,能避免后面画 ROC 时才发现数据错位。预测结果文件如果只有 0/1 而没有概率,ROC 曲线会退化成单点,几乎没法看。
提示:如果发现脚本直接读取的样本目录和 smali.py 的输出目录不一致,优先检查脚本顶部的路径常量,而不是去看代码逻辑。这类路径问题占了毕设排错的大头。
4. TF 与 TF-IDF 的 top50 之争:特征筛选脚本与 ROC 评估
4.1 两种权重的计算逻辑差异
TF 在单个样本内部统计 3-gram 出现次数,TF-IDF 乘上逆文档频率后,可以理解为:如果某个 3-gram 在 100 个样本里都出现,它的区分能力就弱,权重被压低。公式写出来就是:
TF-IDF = TF × log(总样本数 / (包含该词的样本数 + 1))
加 1 是平滑项,防止分母为 0。在恶意代码场景里,move、invoke 这类通用指令组合的 3-gram 出现在绝大多数样本里,IDF 接近 0,TF-IDF 排名会自然把它们压到后面;而 TF 排名会把这些通用组合顶到前列。所以同一批样本,TF top50 和 TF-IDF top50 很可能只有一半重合。跑对比实验时,这个差异本身就是值得写进论文的点。
4.2 top50 筛选手法与常见翻车点
脚本名里的 top50 代表每类权重取前 50 个特征。常见做法不是手写排序,而是直接用 CountVectorizer 或 TfidfVectorizer 的 max_features=50 参数,按词频降序截断。但这里有个隐藏问题:max_features 是按词频选的,不一定是按区分度选的,所以直接用的时候,TF 路线选出来的很可能全是通用指令,模型 AUC 偏低。我一般会先把 max_features 放到 500,训练完看特征重要性或卡方检验,再手工挑区分度高的,而不是死磕 50。
| 对比维度 | TF | TF-IDF |
|---|---|---|
| 统计范围 | 当前样本内 | 样本内 × 全局逆频率 |
| 对通用指令的压制 | 无 | 有 |
| 特征稳定性 | 高 | 对样本覆盖度敏感 |
| 典型翻车点 | top50 全是 move/invoke | 样本太少时选到长尾噪声 |
| 适用场景 | 样本量大、类别均衡 | 样本量中等、想突出恶意特有模式 |
表格里的最后一行是关键:如果你的恶意样本只有几百个,TF-IDF 选出来的 top50 可能全是只在两三个样本里出现的怪序列,看起来 AUC 高,实际是过拟合。这种时候 TF 反而更稳。
4.3 TPR/FPR 脚本与 ROC 绘图的配合
包内 tpr_fpr 脚本的作用是对预测概率做阈值扫描:阈值从 1 到 0 滑动,每个阈值下算一次真正率和假正率。ROC 曲线其实就是把所有阈值下的 (FPR, TPR) 点连起来。核心代码:
from sklearn.metrics import roc_curve, auc fpr, tpr, thresholds = roc_curve(y_true, y_score) roc_auc = auc(fpr, tpr) print(f"FPR={fpr:.3f} TPR={tpr:.3f}")逻辑说明:y_true 是测试集真实标签,y_score 是模型输出概率,不是预测类别。参数说明:如果直接把 0/1 预测结果传进 roc_curve,曲线会退化成只有一个点,AUC 虽然能算但失去意义——所以务必确认 out_pridict 开头那个文件装的是概率而不是类别。plot_roc_gram_TF_top50_3-gram.py 里应该已经封装了这层逻辑,跑之前看一眼它读的是哪个 txt。
怎么读图:曲线越靠近左上角越好,AUC 0.9 以上基本能过毕设答辩;0.8 到 0.9 之间要检查特征筛选是不是选了太多通用指令;低于 0.7 先别调分类器,回头查特征 CSV 有没有全 0 行,或者样本标签是不是反了。
提示:如果 out_pridict 文件里全是 0/1 而不是概率,先回脚本里把 predict 换成 predict_proba,再重新出一次结果。
5. 避坑指南:样本不均衡、空 smali 与 n-gram 内存告警的五个现场
5.1 VirusShare 样本下载频繁断流
现象:批量拉恶意样本时,下载到一半连接断开,CSV 里的 hash 和本地文件对不上。
原因:VirusShare 对普通接口有频率限制,连续请求会被临时限流。
解决:控制并发数,加随机延时;或者直接用官方提供的批量导出功能,下载完先按 hash 校验文件完整性再进目录。这一步值得花时间,因为后面所有脚本的数据源都依赖这个目录。包内的 VirusShare.csv 拿到的第一件事就是确认样本能不能按 hash 找到,找不到就先补齐再往下走。
5.2 正负样本比例失衡,模型学成“全都预测恶意”
现象:训练集恶意 3000 个、正常 500 个时,模型预测结果几乎全 1,TPR 很高但 FPR 也很高,ROC 曲线贴近对角线。
原因:分类器优化的是整体准确率,多数类占比大,全猜多数类损失最小。yingyongbao.csv 对应的正常样本往往数量不够,容易造成这种局面。
解决:把正常样本扩到和恶意样本接近,或者对少数类给class_weight='balanced'。我一般是先下采样恶意样本到 1:1.2,保证训练时两类都够数。还有一种做法是正常样本直接从应用宝多抓几个版本,但注意同应用不同版本的 opcode 序列相似度高,重复样本会虚高 AUC。
5.3 加壳应用 smali 里抠不出指令
现象:某样本 smali 目录存在,但 extract_opcodes 返回空列表或只有十来个指令,特征 CSV 对应行全 0。
原因:加固应用本质是隐藏真实 dex,baksmali 只能解出壳的入口,没有业务逻辑,自然没有有效 opcode。
解决:特征构建前过滤 opcode 数量低于 50 的样本,并记录这些样本的 hash。毕设里还可以单开一节“加壳样本的检测局限”,答辩反而加分。千万别把这些全 0 行留在训练集里,它们会让模型学到一个“全 0 就是恶意”的假规律,换数据就翻车。
5.4 3-gram 词典爆炸,内存直接吃满
现象:把全部样本的 3-gram 直接塞进 CountVectorizer,fit 时报 MemoryError,或者跑十分钟不出结果。
原因:3-gram 组合数接近 opcode 类型数的三次方,几千个样本轻松产生几十万维特征,稀疏矩阵虽然省内存,但 fit 时的词频统计字典很占内存。
解决:先设置 max_features=5000 粗筛,再从中取 top50;或者干脆按脚本设定从 TF 排序截断,不要全量构建词典。脚本名里的 top50 本身就是给内存压力做减法,别绕开它。如果你确实想跑全量 3-gram,把训练样本限制在前 1000 个以内,否则就是拿内存换时间。
5.5 预测结果文件和样本顺序对不上,AUC 跌到 0.5
现象:ROC 画出来 AUC 只有 0.5 左右,像随机猜,但训练时准确率不低。
原因:测试集样本在 shuffle 之后,out_x_test txt 里的行顺序和特征 CSV 的行顺序不一致,或者读文件时矩阵转置了。原作者还把 predict 拼成了 pridict,复制文件名时容易改错,导致读到了旧文件。
解决:先读 CSV 确认行数,再读 txt,逐行打印前 5 个样本的 hash 和预测值比对。比对没问题再画图,这是最省时间的排查路径。和顺序错位并列的另一个问题是:测试集和训练集有重复样本,AUC 虚高,毕设答辩时容易被一眼看穿。用 pandas 求一下两边的 hash 交集,把交集样本去掉再评估。
6. 进阶:换分类器、调窗口,把 ROC 曲线变成论文里的说服力
6.1 换分类器与调 top-N:在同一个评估脚本上低成本迭代
包内评估脚本的核心是 ROC 那一段,分类器本身可以随便换。把模型替换成随机森林,观察 AUC 变化:
from sklearn.ensemble import RandomForestClassifier clf = RandomForestClassifier(n_estimators=300, max_depth=10, random_state=42) clf.fit(X_train, y_train) y_prob = clf.predict_proba(X_test)[:, 1]逻辑说明:predict_proba 第二列才是正类概率,ROC 需要的是概率不是标签。参数说明:n_estimators 超过 300 后 AUC 增益很小,max_depth=10 防止在 top50 特征上过拟合。建议同时把 max_features 从 50 调到 100、200,画三条 ROC 对比,AUC 提升开始小于 0.005 就停,这就是特征维度上限。
6.2 多折交叉验证:单次划分的结果不算数
毕设答辩最常被问的就是“你这 0.95 是不是碰巧”。用五折交叉验证把 ROC 画成带均值方差的曲线,说服力完全不同:
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for train_idx, test_idx in skf.split(X, y): X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] # 每折训练并记录 fpr/tpr,最后统一画均值曲线逻辑说明:分层抽样保证每折里正负样本比例和全量一致。参数说明:shuffle=True 是必须的,样本本来按来源 CSV 排列,不洗牌会让每折都偏向某一类。画图时把五条 ROC 画成浅色细线,均值画成深色粗线,比单条曲线好看也经得起问。
我自己第一次跑这份资源时,就是栽在 5.5 那个顺序错位上,花了两个晚上才发现是读取顺序的问题。从那以后,我每次跑评估都强制先校验行号、再用五折交叉验证出图,已经成了习惯。希望这套拆解帮你在复现时少走弯路。
本文还有配套的精品资源,点击获取