EMBER恶意软件基准数据集

开篇

之前在搜集二进制恶意样本时,遇到了EMBER数据集。本来以为是一个二进制样本集,了解后才发现是从样本中提取的数据,并不包含原始样本,因此也就没有深入研究。为了满足下好奇心,这次抽时间来学习下,弄明白为什么会有这么一份纯数据集,它存在的目的是什么,用来解决什么问题。

EMBER最初由 Endgame 在 2018 年发布,全称是Endgame Malware BEnchmark for Research,后来 Endgame 被 Elastic 收购,这个项目也随之易主,全称就改成了:Elastic Malware Benchmark for Empowering Researchers。从名字可以看出,它的本质是一份基准数据集。

该数据集主要用于信息安全领域的机器学习研究------即提供一个规模庞大、开放且足够通用,能够涵盖多种典型用例的良性/恶意样本数据集。

在机器学习的计算机视觉领域有 MNIST、CIFAR、ImageNet,语音领域有 TIMIT,情感分析则有 Sentiment140------这些公开基准数据集,是各自领域能"科学地比较、可复现地进步"的前提。一个新方法好不好,大家放在同一个数据集上一跑,高下立判。

而在恶意软件检测领域,直到 2018 年,还没有这样一个公认的、公开的良性/恶意基准数据集。后果就是,各家用各家的私有样本、私有特征、私有切分,论文里报出的 99.x% 之间毫无可比性。

EMBER数据集的根本贡献,就是来填这个坑的,给恶意软件检测,造一个二进制检测机器学习领域的ImageNet。

EMBER数据集按发布顺序,共有三个版本,EMBER2017、EMBER2018、EMBER2024,每一个版本都是上一个版本的升级。接下来我们按版本顺序来学习下这个数据集。

EMBER2017

EMBER2017是发布的第一个版本,它面向的问题是:不运行程序,仅根据 Windows PE 文件的静态特征,来判断它是恶意文件还是良性文件。

EMBER2017 包含从 110 万个 Windows PE 文件中提取的特征:

数据划分 恶意 良性 未标注 合计
训练集 30 万 30 万 30 万 90 万
测试集 10 万 10 万 0 20 万

该数据集依据 VirusTotal 多引擎检测结果来选择样本:良性样本没有任何一个引擎报毒,恶意样本需要有超过 40 个引擎报毒。

EMBER2017使用 LIEF二进制解析库来解析 PE 文件,单个样本的所有特征保存成一条JSON Line。特征被分为如下八组,下面是一条真实数据示例:

复制代码
{ // 一条 EMBER2017 样本记录的开始
  "sha256": "000185977be72c8b007ac347b73ceb1ba3e5e4dae4fe98d4f2ea92250f7f580e", // 原始 PE 文件的 SHA-256 哈希,用作样本唯一标识
  "appeared": "2017-01", // 该文件首次被发现的大致月份
  "label": -1, // 样本标签:0 表示良性,1 表示恶意,-1 表示未标注

  "general": { // 文件的通用静态特征
    "size": 33334, // 文件在磁盘上的实际大小,单位为字节
    "vsize": 45056, // PE 文件加载到内存后的虚拟大小,单位为字节
    "has_debug": 0, // 是否包含调试信息:0 表示没有,1 表示有
    "exports": 0, // 文件导出的函数数量
    "imports": 41, // 文件导入的函数数量
    "has_relocations": 1, // 是否包含重定位信息
    "has_resources": 0, // 是否包含 PE 资源,例如图标、对话框或版本信息
    "has_signature": 0, // 是否包含 Authenticode 数字签名
    "has_tls": 0, // 是否包含线程局部存储 TLS 结构
    "symbols": 0 // 文件中包含的符号数量
  }, // 通用静态特征结束

  "header": { // PE 文件头特征
    "coff": { // COFF File Header 中的字段
      "timestamp": 1365446976, // PE 头中的编译时间戳,采用 Unix 时间戳形式
      "machine": "I386", // 目标处理器架构,这里表示 32 位 x86
      "characteristics": [ // PE 文件的整体属性
        "LARGE_ADDRESS_AWARE", // 程序能够处理较大的虚拟地址空间
        "EXECUTABLE_IMAGE" // 文件是一个可执行映像
      ] // COFF 属性列表结束
   }, // COFF File Header 结束

    "optional": { // PE Optional Header 中的字段
      "subsystem": "WINDOWS_CUI", // 运行子系统,这里表示 Windows 控制台程序
      "dll_characteristics": [ // 与加载安全机制有关的 DLL 属性
        "DYNAMIC_BASE", // 支持地址空间布局随机化 ASLR
        "TERMINAL_SERVER_AWARE" // 程序声明兼容 Windows Terminal Server
      ], // DLL 属性列表结束
      "magic": "PE32", // PE 类型,PE32 表示 32 位可执行文件
      "major_image_version": 1, // 映像主版本号
      "minor_image_version": 2, // 映像次版本号
      "major_linker_version": 11, // 生成该文件的链接器主版本号
      "minor_linker_version": 0, // 生成该文件的链接器次版本号
      "major_operating_system_version": 6, // 所需操作系统主版本号
      "minor_operating_system_version": 0, // 所需操作系统次版本号
      "major_subsystem_version": 6, // 所需子系统主版本号
      "minor_subsystem_version": 0, // 所需子系统次版本号
      "sizeof_code": 3584, // PE 中代码节的总大小
      "sizeof_headers": 1024, // DOS Header、PE Header 和节表的总大小
      "sizeof_heap_commit": 4096 // 程序启动时需要提交的堆空间大小
    } // Optional Header 结束
  }, // PE 文件头特征结束

  "imports": { // 文件的导入表,按 DLL 名称分组
    "KERNEL32.dll": [ // 从 Windows KERNEL32.dll 导入的函数
      "GetTickCount" // 获取系统启动后经过的毫秒数
    ] // KERNEL32.dll 导入函数列表结束
  }, // 导入表结束

  "exports": [], // 文件导出的函数列表;空数组表示没有导出函数

  "section": { // PE 节表信息
    "entry": ".text", // 程序入口点所在节的名称
    "sections": [ // PE 文件包含的所有节
      { // 第一个节的信息
        "name": ".text", // 节名称;.text 通常用于存放可执行代码
        "size": 3584, // 节在磁盘文件中占用的大小
        "entropy": 6.368472139761825, // 节内容的信息熵,较高时可能表示压缩、加密或加壳
        "vsize": 3270, // 节加载到内存后的虚拟大小
        "props": [ // 节的属性和访问权限
          "CNT_CODE", // 表示该节包含程序代码
          "MEM_EXECUTE", // 表示该节具有执行权限
          "MEM_READ" // 表示该节具有读取权限
        ] // 节属性列表结束
      } // 第一个节的信息结束
    ] // PE 节列表结束
  }, // PE 节表信息结束

  "histogram": [3818, 155, ..., 377], // 256维字节直方图;依次统计 0x00 至 0xFF 各字节值出现的次数

  "byteentropy": [0, 0, ..., 2943], // 256维字节---局部熵联合直方图,用于描述字节分布与压缩或加密程度

  "strings": { // 从文件中提取的可打印字符串统计特征
    "numstrings": 170, // 长度至少为 5 的可打印字符串数量
    "avlength": 8.170588235294117, // 可打印字符串的平均长度
    "printabledist": [15, ..., 6], // 96维可打印字符直方图,统计 ASCII 0x20 至 0x7F 的出现次数
    "printables": 1389, // 所有已提取字符串包含的可打印字符总数
    "entropy": 6.259255409240723, // 可打印字符分布的信息熵
    "paths": 0, // 类似 C:\ 的 Windows 文件路径数量
    "urls": 0, // 以 http:// 或 https:// 开头的 URL 数量
    "registry": 0, // 以 HKEY_ 开头的 Windows 注册表键数量
    "MZ": 1 // 字符串 MZ 的出现次数,可能表示文件中嵌入了其他 PE 文件
  } // 字符串统计特征结束
} // 一条 EMBER2017 样本记录结束

特征向量化

有了二进制样本的大量特征了,还需要将这些特征向量化,即把每个特征都转换成有意义的数字表示,才能用于机器学习算法的处理。

对于 JSONL 中的一条记录,EMBER 会先把它解析成字典,再对 8 组特征分别做数值转换、归一化或哈希编码,最后拼接成一个 2351 维向量。

总向量构成如下图所示:

各组特征的具体处理方式如下。

histogram(字节直方图):把计数转换成比例

假设样本的部分 histogram 数据是:

复制代码
[45521, 13095, 12167, ...]

全部计数之和是 3101705,因此该部分数据向量化之后是一个256维的浮点数向量:

复制代码
第 0 维 = 45521 / 3101705 ≈ 0.01467612
第 1 维 = 13095 / 3101705 ≈ 0.00422187
第 2 维 = 12167 / 3101705 ≈ 0.00392268

byteentropy 采用同样的归一化操作。

strings:部分归一化,部分直接保留

strings 字段里已经是提取好的统计信息。按下面的顺序直接转换为数组:

比如:

复制代码
[14573, 5.97207, 87031,1046/87031, 817/87031, ...,6.56990, 3, 0, 0, 51]

其中 96 维字符频率需要除以printables字段值来做归一化,其他统计值直接转换为 float32。

generals:按固定顺序排列数值

general 使用下面的固定顺序:

比如:

复制代码
[3101705, 380928, 0, 0, 156, 0, 1, 0, 0, 0]

这里没有做归一化操作,所有特征使用原数值。

imports和exports:使用特征哈希

imports以下面的数据为例:

复制代码
{
    "KERNEL32.dll": ["SetFileTime", "CompareFileTime"]
}

哈希函数为每个名称确定一个位置及正负号,然后向该位置累加 +1 或 -1。不同名称可能落到同一个位置,因此同一个桶里的值会累加或抵消。

比如单独处理 "kernel32.dll" 时,会向 DLL 向量的第 117 个桶贡献 +1。

exports 是128维,用来处理导出函数名,如果文件没有导出函数,则得到全零的128维向量。

header和section:数值与哈希组合

EMBER2018

EMBER2018 不只是"把样本年份换成 2018",它同时调整了数据规模、样本选择、特征定义和附加元数据 。最核心的变化是:样本选择标准更严格、特征从 v1 升级为 v2、向量从 2351 维增加到 2381 维

对比项 EMBER2017 EMBER2018
PE 样本总数 110 万 100 万
训练集 90 万 80 万
有标签训练样本 60 万 60 万
未标注训练样本 30 万 20 万
测试集 20 万 20 万
测试集类别 10 万良性 + 10 万恶意 10 万良性 + 10 万恶意
特征版本 v1 v2
特征维数 2351 2381
LIEF 库版本 0.8.3 0.9.0
数据目录特征 新增 30 维
导入序号处理 旧方式 更新后的处理方式
样本选择难度 标签边界比较清晰 刻意选择得更难
家族元数据 原始版本主要是二分类标签,即 benign/malicious 增加更丰富的 avclass 元数据

EMBER2018 的 100 万样本具体为:

复制代码
训练集 800,000
├── 恶意      300,000
├── 良性      300,000
└── 未标注    200,000

测试集 200,000
├── 恶意      100,000
└── 良性      100,000

也就是总共:

复制代码
恶意样本:400,000
良性样本:400,000
未标注:  200,000

新增 Data Directories 特征

EMBER2018 特征版本 v2 最大的结构变化,是在header.optional中新增数据目录特征:

复制代码
"datadirectories": [
  {
    "name": "EXPORT_TABLE",
    "size": 0,
    "virtual_address": 0
  },
  {
    "name": "IMPORT_TABLE",
    "size": 120,
    "virtual_address": 8192
  }
]

EMBER v2 对前 15 个数据目录各提取:

复制代码
size
virtual_address

因此新增15×2=30个特征维度。

复制代码
EMBER v1:2351 维
EMBER v2:2351 + 30 = 2381 维

数据目录可以补充"某种 PE 结构是否存在、规模多大、位于哪个 RVA"等信息。例如 CLR Runtime Header 可以帮助识别 .NET PE,Certificate Table 可以反映签名结构。

元数据中增加 AVClass 恶意家族

EMBER2018 的原始 JSONL 元数据中增加了avclass字段,这就把 EMBER 从一个纯二分类基准,变成了也能做多分类家族识别的数据集:

复制代码
{
  "sha256": "...",
  "appeared": "2018-05",
  "label": 1,
  "avclass": "ramnit" // 基于多引擎检测结果生成的家族共识标签
}

avclass是从VT报告上多个杀毒引擎检测结果聚合出的恶意软件家族名。注意,该字段仅作为元数据存在,不进入官方 2381 维二分类特征向量。

官方同时重新发布了 EMBER2017 v2

发布 EMBER2018 时,官方还使用 v2 特征定义重新处理了 2017 数据集对应的样本。因此实际有三个下载包:

数据年份 特征版本
EMBER2017 v1
EMBER2017 v2
EMBER2018 v2

EMBER2024

EMBER2024 和 EMBER2018 相比是一次大改版,不是简单的数据更新。

不再是纯 PE 数据集

这是最大的变化。EMBER2017/2018 只有 Win32 PE,EMBER2024 覆盖 6 种文件格式,总计约 320 万样本:

格式 每周采集样本数 训练集 测试集
Win32 30000 1560000 360000
Win64 10000 520000 120000
.NET 5000 260000 60000
APK 4000 208000 48000
PDF 1000 52000 12000
ELF 500 26000 6000
合计 50500 2626000 606000

数据量比 EMBER2018 大了一个数量级(100 万 → 320 万),全量约 46.7 GB,其中 Win32 训练集单独就有 23.7 GB。

样本来自 VirusTotal,首次上传时间在 2023-09-24 到 2024-12-14 之间。采样方式很规整:每周恰好取 50,500 个文件,各文件格式和恶意/良性数量都有固定配额,前 52 周采集的样本为训练集,后 12 周采集的样本为测试集。

这个设计比 EMBER2018 更严谨------每周配额固定,意味着时间维度上样本分布均匀,做 concept drift(概念漂移)分析时不用再去校正采样偏差。

EMBER2024首创使用了6315 个"逃逸样本"作为Challenge set:上传到 VirusTotal 时被约 70 个 AV 引擎全部漏报,但事后被确认为恶意。

以往所有基于 VT 标签构建的数据集(包括 EMBER2018)都有一个根本性的循环论证问题:标签来自 AV引擎的判断,那么模型学到的上限就是"复现 AV引擎 的判断",AV引擎集体漏掉的东西数据集里根本不存在。 Challenge set 专门把这部分捞出来单独成集,用来测模型在 AV 盲区上的表现。

EMBER2024 不再用 AVClass,改用 ClarAVy(AVClass和ClarAVy都是整理多个杀毒软件结果标签的工具,但ClarAVy针对 AV 标签做了更好的共识建模)。

ClarAVy提供了 7 类标签/标签组:

此外,PE样本的特征提取器直接使用pefile库替换掉了LIEF,同时,PE 新增特征组:DOS header、Rich header、Authenticode 签名、PE 解析告警(parsing warnings)。

Rich header 和 parsing warnings 这两个加得很到位------前者能反映编译工具链指纹,后者本身就是强信号(畸形 PE 往往是刻意构造的)。

非 PE 格式(APK / ELF / PDF)只使用4个通用特征组:general、histogram、byteentropy、strings。

Win32 / Win64 / .NET / ELF 数据集额外附带 capa 分析结果:capabilities、MITRE ATT&CK TTPs、Malware Behavior Catalogue 映射。

另有一份补充数据集:16356790 个来自数据集恶意样本的函数数据,含原始字节和反汇编。这个对做函数级建模、二进制相似度、或者 LLM-on-assembly 方向的人价值很大。

GBDT

GBDT(Gradient Boosting Decision Tree,梯度提升决策树)就是使用决策树作为基础模型的 GBM(Gradient Boosting Machine,梯度提升机)可以理解成一个"不断改错的决策树团队",由多颗深度较小的决策树串联而成。

一棵决策树会根据特征不断提问,例如:

  • 文件的整体熵是否较高?
  • 是否导入了可疑函数?
  • 是否存在异常节区?
  • 文件是否带有数字签名?

最后,决策树给出一个判断结果。但一棵树的能力有限,因此 GBDT 会连续训练许多棵树。

第一棵树先做一个比较粗略的判断;第二棵树重点检查第一棵树判断不准的样本;第三棵树继续修正前面留下的问题。最终结果是所有树的结果逐步累加:

在分类任务中,GBM 会把最终分数转换成概率。例如,模型输出恶意概率为 0.97,就可以判定为恶意文件。

EMBER使用的LightGBM(Light Gradient Boosting Machine)是 GBDT算法 的一种高效实现。它没有改变"多棵树逐步改错"的基本思想,而是让训练过程更快、占用内存更少。

LightGBM 适合 EMBER,主要是因为 EMBER 的特征类型比较多。树模型可以直接学习"某个特征是否超过阈值"以及"多个特征组合出现时是否可疑",不需要假设特征和结果之间是简单的线性关系。

总结

EMBER 的核心价值在于它通过字节统计、字符串、导入函数、节区结构等多组互补特征,取得了较好的恶意文件识别效果。在合适的数据划分和阈值设置下,EMBER 能够在保持较高检出率的同时,将误报率控制在较低水平。对于安全产品来说,这种平衡非常重要。

与直接处理原始字节的端到端模型相比,EMBER 的特征工程和 GBDT 结构更容易分析误报原因、调整检测阈值和进行模型更新,因此在实际部署中更容易保持稳定的检测效果。当然,EMBER 并不能在所有场景下都绝对优于端到端模型,其性能还会受到数据分布、标签质量、时间变化和阈值选择的影响。

未来,EMBER 类方法可以进一步融合动态行为、调用序列和威胁情报,提升对新型样本的检出能力。

相关推荐
deepseek231 小时前
WSO2 Agent Manager 正式可用:企业 Agent 从能跑到可治理,沙盒、身份与 MCP 如何落地
人工智能·ai agent·mcp·企业治理
挖掘狂人1 小时前
连猫都没见过,它怎么认出了猫?一篇啃透机器学习核心算法
人工智能·深度学习·机器学习
武子康1 小时前
CLAUDE.md 越写越长,哪些规则该放到子目录?
人工智能·llm·agent
米软科技1 小时前
高校信息化团队实践:利用AI低代码缩短零散业务交付周期
人工智能·科技·低代码·汽车·制造
0+1111 小时前
算法 --二分查找
c++·算法·leetcode
径硕科技JINGdigital1 小时前
Amazon Bedrock能够为企业生成式AI应用提供哪些安全与合规支持?
大数据·人工智能
vortex51 小时前
Claude Code 高级使用教程:从 Worktree 并行到 Dynamic Workflows
人工智能
lie..1 小时前
30天从零开始学AI应用开发(Day 4):Python 极速入门(上):够用就行,别啃书
人工智能·python·大模型
恣睢s1 小时前
MISC杂项——Base加密加密
网络安全