工作来源
DIMVA 2021
工作背景
很少有调查研究如何在大范围内自动检测和识别特定的高级攻击组织使用的恶意软件样本。此前,安全社区已经广泛研究了如何根据代码相似性检测未知攻击。最近,社区也开始探索使用现代机器学习方法来检测更复杂的恶意软件攻击,例如 APT。但现有方法不足以处理现代恶意软件中的代码重用和攻击归因。
这主要有两个原因:首先,关于 APT 样本的相关数据很少。因此,在实际部署中,这种限制使得常见的机器学习技术在查找特定类型的恶意软件方面的效率明显降低。其次,由于恶意软件作者大量使用规避技术,当前的静态代码相似性分析方法在定位以前未知的威胁上失灵,这使得几乎所有现有的静态分析方法在实践中的效果都乏善可陈。
已有方法大多依赖于通过静态分析提取的二进制代码的特定语法特征,例如控制流图(CFG)。但无法应对反分析技术对静态分析的巨大影响。
此前还有研究根据开发者的编码风格对用户进行跟踪,在编译过程中不会丢失源码级别的作者编码风格。最近的工作依赖于编码风格特征,并利用机器学习来开发更强大的代码作者归属机制。例如:
Abuhamad 提出可以有效利用深度神经网络对大规模代码进行特征化的系统。
Caliskan-Islam 依靠随机森林和 AST 的语法特征跟踪开发者。
恶意软件聚类的三大类方法:① 依赖静态特征,例如 MutantX-S。② 依赖运行时特征。③ 依赖执行前后的特征,例如 DUET。但恶意软件聚类的目标是识别未知、混淆的家族,不是用于确定归因。
几乎所有基于恶意软件的攻击(包括高级、有针对性的攻击,如 APT)都遵循经过实践证明的模式来开发实际的 Payload。恶意软件很少从头开始编写,并且通常依赖于现有的代码或特定的、独特的代码(例如 Mimikatz)。当然,分析恶意软件样本时的一项主要挑战就是缺乏源代码。
为了能够构建有效、准确的代码相似性检测方法,适用于现实世界的恶意软件二进制文件,第一步,使用动态分析技术执行代码,然后创建运行样本的进程快照。第二步,从内存快照中重构相应的高级源代码,并自动定位极有可能用于恶意行为的代码段。
工作设计
需要提取给定的 Payload 再进行反编译,开发用于相似性计算反编译代码的有效编码机制,并构建无监督模型以对未知样本运行代码分析。整体架构如下所示:
反编译
对给定的文件在 Lastline 沙盒中执行,在分析的关键时刻生成多个进程 Dump。关键时刻如下所示:
执行创建新进程的 API 时,如 CreateProcess;执行创建新文件的 API 时,如 CreateFile;执行权限提升 API 时,如 AdjustTokenPrivileges
执行在原始 PE Image 之外时
原始 PE Image 更改时
向量化
依赖于孪生神经网络(SNN)进行向量编码,SSN 由两个或多个具有相同架构的子网络组成,SNN 在很多领域表现都很好。神经网络的结构如下所示,同时接受两个输入并为输入计算两个向量,然后利用距离度量来估计两个向量之间的相似性。每个子网络都是一个 LSTM 网络,这种 RNN 网络在异常检测和预测时间序列数据上都很成功。
为了向量化函数并将它们作为输入提供给 SNN,方案将每个函数映射到其抽象语法树(AST)节点。最后,为了比较两个 LSTM 的最终隐藏状态,利用曼哈顿距离度量将孪生神经网络修改为 MaLSTM 的变种。
为了发现相似的函数,首先提取每个函数向量(即 AST 节点)的 n-gram,再通过局部敏感哈希对所有提取的函数 n-gram 进行哈希计算,利用局部敏感哈希将相似的函数映射到相同的桶中。研究发现单独依赖局部敏感哈希通常会引入误报,特别是在反编译代码中提取的函数有较大差异的情况下,需要结合其他技术处理误报。
聚类
每个样本经过 SNN 转换为一组编码函数,与数据库中的集群进行比较。因为恶意样本和良性样本也会共享大量的代码,例如,静态链接的标准库函数、全局变量初始化代码和导入解析代码等。如果函数被分类到包含恶意样本和良性样本函数的混合集群中,就会被认为是需要被过滤掉的噪音。
在沙盒中运行样本并重构源码,提取了 534 个函数,共计 45902 行代码。有 434 个(81%)函数被认为是噪音,共计 36957 行代码。其余 100 个函数中大部分被归类到 Turla 的集群中,进一步调查可以确认是 Turla 的样本。
工作准备
实验环境
SCRUTINIZER 是用 Python 实现的,编程利用名为 Dask 的并行计算库来提高脚本的执行速度。
实验在 20 核的 Intel Xeon CPU、276 GB 内存的 Ubuntu 服务器上进行。
实验数据
训练数据集与测试数据集互不重叠,良性样本和恶意样本共计 44015 个,提取了 1734992 个函数。
训练数据集:良性样本 31475 个、恶意样本 12540 个。
测试数据集:良性样本 2500 个、恶意样本 500 个。
从 12540 个真实的恶意软件样本中运行和提取运行时内存快照。然后构建工具分析进程快照并重构实际恶意 Payload 的源代码,并检索相应的函数代码片段。
工作评估
函数向量化
首先使用 Ghidra 处理脚本反编译每个文件,接着利用 Clang 将每个函数映射到一个扁平的 AST 向量。为了使 AST 构造更加精确,将向量中的每个函数或 API 调用(AST 中的 CALL EXPR 节点)替换为函数或 API 调用的具体名称,以此区分系统函数调用和开发者编写的函数调用。
接着训练 MaLSTM 来识别相似和不同的函数对。脚本处理时需要过滤不共享公共原型(即函数名称和参数)和输出的相似函数。其次,对函数集合的子集进行了多次哈希与手动检查,总共从恶意样本和良性样本中收集了 1105000 对相似和不相似的函数。
平均值、中位数和标准差如下所示:
随机选择了 1000 个函数,并进行手动检查。可以确认 128 位的向量效果更好。
聚类分析
首先处理数据集中的所有样本,提取它们的函数和对应的 AST 向量。然后使用 MaLSTM 再将它们聚类成组,并且过滤掉噪音函数。
利用 HDBSCAN 算法进行聚类。与此同时,为了加快聚类效率,再使用主成分分析(PCA)将特征维度从 128 减少到 8 维。
总共发现了 1734992 个类,每个类别都包含编码相似的函数。其中,91% 的类别是完全良性的,3.2% 是完全恶意的,5.88% 的类别是混合的。最大的类别共有 14406 个相似的特征向量,而类的平均大小约为 5 个。
将相似的函数聚类在一起后,根据反编译代码中观察到的函数的出现频率为每个类别生成一个字典标签。一个函数的出现频率如下所示:
实际部署
利用 3000 个未知文件来测试 SCRUTINIZER 的有效性。根据经验分析,每个样本平均包含 485 个函数。根据标签,如果更多的在良性样本中发现,就认为该函数是噪音,需要被过滤。
平均来说,SCRUTINIZER 能够过滤 56% 的代码。而未知样本中中位数是有 199 个函数,有 126 个(63%)函数被过滤掉,相当于删除了大约 11476 行(56%)代码。
当恶意函数数量超过良性函数时,并且样本与任何先前已知样本的相似度超过阈值,就会将未知样本标记为恶意软件。实验结果表明,过滤掉常见的函数后,准确率从 82% 提高到 92%。同时,误报率从 10% 下降到 1.2% 左右。如下所示,ROC 也从 0.88 提升到 0.95。
如下所示,系统判断的样本相似与实际的样本归属的列表。其中一些样本已经被其他安全研究人员判定了归属,这也就可以确认 SCRUTINIZER 能够通过函数级相似推理未知样本的代码重用。
案例一
海莲花最初的攻击可追溯到 2014 年,在 2020 年又借疫情话题发送钓鱼邮件攻击中国应急管理部等单位。样本(fcd7227891271a65b729a27de962c0cb)中发现了 377 个不同的函数,共 18125 行代码。SCRUTINIZER 过滤掉 247 个函数,达 8720 行(48%)代码。其余的函数被划分到 109 个不同类中,其中有 4 个函数被分到同一类中,即该样本包含 105 个和建模阶段观察到的海莲花样本中的函数不同的函数。CrowdStrike 将该样本识别为 Ocean Buffalo,FireEye 也判断该样本为为 APT32。
案例二
该样本(276c28759d06e09a28524fffc2812580)被认为与 Barium 和 Turla 相似,样本中发现了 971 个函数,共计 74309 行代码。被过滤掉 767 个函数,达 59976 行(81%)代码。其余 204 个函数被划分到 163 个不同类中,共有 22 个函数被认为与 Barium 和 Turla 相似。进一步调查发现该样本包含一个名为 Mimikatz 的开源黑客工具,该工具同时被这两个攻击组织使用。
工作思考
这项工作总体来说还是不错的,通过查找代码相似度分数低于 15% 的样本,就能够发现海莲花的多个恶意软件变种。尽管方法在大规模应用上仍然存在瓶颈,但仍可适用于去粗取精后的少量重点样本的辅助分析,这和卡巴斯基之前分享过的思路也类似,值得一看。
准确率:与任何其他机器学习方法一样,SCRUTINIZER 的准确性取决于用于构建检测模型的数据量。然而,收集大量样本相关的数据并非易事,反编译是容易出错的部分,可能导致准确性降低。此外,使用的 AST 构建方法也会产生误报。在实际应用中,需要不断更新处理方法,以将误报率保持在可管理的范围内。
分析效率:动态分析很很昂贵的,且沙盒要做的非常好,比如能处理逻辑炸弹等。
为 1000 个反编译样本中所有函数的相似性进行测试大约需要 4 个小时。每个子网络在 5 个 epoch 中并行训练,大约一百万对函数的训练过程大约需要 36 分钟,这也是十分耗时的。
查看官方仓库获取更多信息
https://github.com/OMirzaei/SCRUTINIZER/blob/master/README.md