隐藏的威胁:揭开 OnionCrypter 的秘密
恶意软体作者的目标之一是让他们的创作能在防毒软体下侦测不到。一个可能的解决方案是加密器Crypter。加密器对程式进行加密,让它看起来像无意义的数据,并为这个加密程式创建一个称为 stub 的外壳。这个 stub 看似无害的程式,可能还会执行一些没有害处的任务,但它的主要任务是解密有效负载并运行它。
为什么这引人入胜?
本篇博客讨论的加密器使用了多种有趣的技术,使得分析师和正确的侦测变得困难。这个加密器的一个关键技术是多层加密。因此我们称它为“OnionCrypter”。重要的是,这个名字反映了这个加密器所使用的许多层次,绝对与 TOR 浏览器或网路无关。
这篇博客涵盖了 OnionCrypter 用来复杂化分析的多种技术,并详细说明了其结构。这对恶意软体分析师有所帮助,因为像这样的样本在一开始可能会让人感到困惑和压倒,对人类和动态分析沙箱来说都是如此。
更有趣的是,我们发现自2016年以来,OnionCrypter 已被超过30个不同的恶意软体家族使用。这其中包括一些著名的、最普遍的家族,如 Ursnif、Lokibot、Zeus、AgentTesla 和 Smokeloader等。在过去三年里,我们已经保护全球近400000名用户免受这个加密器保护的恶意软体的侵害。它的广泛使用及使用的时间长,使其成为一个关键的恶意软体基础设施组件。我们相信,OnionCrypter 的作者很可能将其作为一种加密服务提供。根据第一层的独特性,我们也可以安全地假设 OnionCrypter 的作者提供了一个独特的 stub 档案选项,以确保加密的恶意软体无法被侦测。类似这样的服务经常以 FUD完全不可侦测加密器的名义进行广告。
尽管 OnionCrypter 用于保护多种不同家族的恶意软体,但它自身也形成了一个恶意软体家族。OnionCrypter 已经存在了好几年,因此它并不是全新的东西,然而有趣的是,由于多层和第一层的独特性,没有人将这个加密器检测为一个恶意软体家族。在从 VirusTotal 下载了这个加密器的数千个样本之后,我们能够确认,来自所有防毒软体的侦测主要基于检测这个加密器中的加密内容。即使防毒软体识别样本为一个加密器且里面包装了一些其他的恶意软体,它们也会将样本辨认为多达十几个不同的恶意软体家族。
统计
根据超过15000个样本的数据最古老的样本可以追溯到2016年,我们建立了使用这个加密器的恶意软体家族的统计。下图显示了多个恶意软体作者使用 OnionCrypter 的情况。
恶意软体家族在样本中的出现
透过相同的数据,我们能进一步呈现这个加密器在其存在期间的流行程度。
OnionCrypter的流行程度
这些数据可以进一步解释。高峰期暗示在那段时间内可能出现了一个新的恶意软体活动,该活动利用 OnionCrypter 的服务并在全球范围内广泛传播。经仔细检查后,我们确认最高峰与2019年夏季的 BetaBot 恶意软体家族的传播相关,这是一个传播勒索软体和其他恶意软体的家族。
BetaBot活动在2019年夏季期间使用OnionCrypter
分析
OnionCrypter 是一款用 C 开发的 32 位软体。OnionCrypter 的架构由三个层次组成。每一层次将在单独的部分进行讨论,并介绍其中的技术。
OnionCrypter 程式结构
层次 1
这是 OnionCrypter 的外层。尽管第一层通常包含至少几百个函数,但总有一个长函数我们称之为主函数,这里包含大量的垃圾代码,但也有以下几个重要功能,是 OnionCrypter 的关键部分:
创建一个命名事件物件分配内存将数据加载到内存中解密加载的数据将执行权转交给解密后的数据找到这个函数最简单的方法是检查对 CreateEventA API 函数的交叉引用。
独特性
在多个样本中找到这个主函数后,第一个障碍就是独特性。每个被分析的样本都有独特的主函数。差异可以很大,比如垃圾代码中的 API 函数调用完全不同,或很小,比如在看似相同的循环中使用不同的寄存器和局部变数。因此,如果有人想要覆盖大多数样本,创建静态检测规则会相当复杂。
蚂蚁加速npv下载在查看了一些样本后,可以相对简单地估算出哪个函数是主函数。主函数通常相当长,因为垃圾代码的缘故,并且往往因为循环展开的原因。在垃圾代码的展开循环之间,内存分配或解密可能进行在一小部分代码中。
IDA Pro中主函数的总览从左到右260003293D1785571FEF5A2CF54E89B7AF0C1FBD5B970D2285F21BFC65E2981C05AAB2F7D5D432CBEB970BC5471B3FAE1E45F23E0933CC673BE923F7609F53AE17C2E36EE4387365AC00A84E91B59CE4D31D3BA04624902512810B7797A2356B81C479BF71196724055F1AF30CA05C9162B7D32E7B3363B7F93D1AAF0161E7608B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
在很多情况下,一个或多个睡眠调用synchapih中的 sleep 函数会出现在垃圾代码中。这些睡眠调用加上多次迭代的循环可能会将执行时间增加几分钟。这可能会导致一些简单的动态分析沙箱无法正常运行。即使沙箱能够检测最终的有效负载并用 Yara 规则扫描,通常也需要将时间限制设置为3分钟或更多。
IDA Pro中的垃圾代码示例8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
UPX 假冒者
最常见的打包工具之一是 UPX,该工具能压缩程序并隐藏它们的原始代码。几个样本的第一层被修改得看起来像是被 UPX 打包,即使它们实际上并没有。乍一看可以看到样本有与 UPX 完全相同的段,甚至在使用 “Detect It Easy” 等工具进行分析时,该工具会错误地告诉你样本是 UPX 打包的。
这可能会使缺乏经验的分析师感到困惑,但更糟糕的是,这可能会混淆分析工具。有多种工具能自动解包 UPX 打包的程序,并提取原始代码以供进一步分析。当这样的工具对 UPX 假冒样本进行解包时,结果将是随机损坏的数据。在这样的数据上,任何静态检测都是不可能的,而损坏的样本不会在动态分析箱中运行。
例外情况
大多数样本在调试期间都会引发例外。在大多数情况下,这发生在主函数的起始部分。处理这些例外会延长手动分析的时间,并肯定会使动态分析变得更加困难。确定例外发生的地方是很好的主意,因为即使有些样本只抛出几个例外,其他样本在循环中却会抛出许多,如果逐一处理可能会耗时过长。
最常见的例外情况包括:
Microsoft C 例外码 0xE06D7363这个例外通常是由一些垃圾代码中使用的奇特函数引起的。一些导致此例外的函数包括:SCardEstablishContextSCardConnectASCardTransmit引用的内存指令在 XYZ。内存无法被读取。例外码 0xC0000005未知的例外码 0x6EF来自函数 GetServiceDisplayNameA我们还发现 OnionCrypter 结合了抛出例外的函数与有关鼠标光标位置的数据。OnionCrypter 使用一个循环来获取光标位置X 和 Y 坐标,使用 GetCursorPos 函数并将其与上一轮循环中位置的值进行比较。如果 X 或 Y 坐标没有改变,程序将调用更多抛出例外的函数,等待几秒钟,然后开始下一轮循环。正常用户预期在此期间会移动鼠标,但对于不断按 F9 键以跳过抛出例外部分的沙箱或分析师来说,这是不预期的。因此我们认为,抛出例外是一种反调试技巧,以使分析师的手动工作更加艰辛。
命名事件物件
OnionCrypter 使用了命名的事件物件,这些事件物件被硬编码到代码中,并在主函数中创建,以避免有效负载的多次执行。这项功能对隐藏在内部的恶意软体至关重要,因为多次在一个设备上同时执行特定恶意软体可能会导致一些意外或不希望的行为例如,不必要在一台设备上同时运行同一个勒索软体。经过深入分析,我们能将多个事件物件与这个特定软体联系起来。
创建命名事件物件8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
为了促进新事件物件名称的提取并自动化处理,创建了一个 IDAPython 脚本。在最常见的事件物件名称中,包括:
milsinsvetlifecicledparamescueevnStrollsMenulapkieventdoroga内存分配
在主函数执行的某个时间点,OnionCrypter 必须创建一个内存空间来加载和解密数据。这里展示了另一种独特性。OnionCrypter 使用以下函数之一进行分配:
GlobalAllocVirtualAllocHeapAlloc在其他恶意软体家族中,属于同一家族的加密器样本通常会在所有样本中使用相同的内存分配函数。而在这里,有三种不同的函数。这使得分析变得复杂,这又是另一种反分析技巧,用来隐藏有效负载,因为只挂钩一个函数并监控分配的内存是不够的。更糟的是,挂钩所有这些函数可能会是找到分配内存的一种很慢的方法,因为重要的分配发生在垃圾代码的某些部分。同时在执行垃圾代码过程中,分配函数可能会被调用多次以分配微不足道的内存。特别是当这些函数在循环中使用时,监控所有分配的位置可能会让人感到不堪重负。一个可能的解决方案是,知道加密数据的分配内存的所有三个属性read/write/execute都设置为 true。通过在主函数中的巧妙设置断点并监控内存段,能够找到标记有 read/write/execute 标志的内存段被创建的时刻。
解密第二层
在内存分配之后,数据被移动到创建的空间并进行解密。解密循环要么在主函数中以内联方式实现,要么呼叫一个单独的函数。只需在分配的内存上设置一个 R/W 断点,就可以轻松找到解密循环。即使在这里,每个样本都是相当独特的。尽管所有样本都是按字节读取数据并用另一个值进行 XOR,但解密算法的实现完全不同,如下图所示。
IDA Pro中的解密循环结构左侧 75E692519607C2E58A3E4F5606D17262D4387D8EEA92FAB9C11C64C4A6035FBC右侧 8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
左侧的解密算法是作为主函数的一部分实现的,这个算法相当简单它用一个字节作为密钥值,对所有加密数据的字节进行 XOR 操作。有趣的是,这个算法如此幼稚,以至于如果密钥最初设置为零,第二层将会完全不进行 (解)密。
另一边则是相对复杂的解密算法。它是一个独立的函数,接收指向加密数据的指针、加密数据的长度和密钥种子值作为参数。解密从加密数据的开头开始,对每个加密字节进行密钥值的 XOR 操作。与先前的解密算法不同,这是一个流密码,它生成了一个密钥流。密钥流由密钥值组成,其中新密钥值是从上一迭代中使用的密钥值生成。
将执行权传递给第二层
即使在这里,也有一些创造性的方式来启动解密后代码的执行。最简单且最常见的方式是将指向解密代码的指针加载到寄存器中并调用它。
当没有对寄存器进行呼叫时,事情可能会变得更加有趣。一些样本使用“Enum” 函数,如 EnumSystemLanguageGroupsA 传递执行权。最初,该函数枚举由操作系统安装或支持的语言组,但该函数的一个参数是应用程序定义的回调函数的指针。该回调函数应处理 EnumSystemLanguageGroupsA 函数提供的枚举语言组信息。取而代之的是提供一个指向回调函数的指针,实际上是提供一个指向解密代码的指针,这样一来,解密的代码就会被执行。
将执行权传递给第二层909A94BCB5C0354D85B8BDB64D4EE49093CCA070653F73B99C201136B72CB94A
其余的“Enum”函数,例如 CertEnumSystemStore 或 EnumDisplayMonitors 等,也使用类似的技术。由于这些函数的数量以及其合法使用的可能性,基于这项技术来检测 OnionCrypter 是不可行的。
将执行权传递给第二层,第2次846DCC9BCDC5C6103B2979FF93F4E1789B63827413B2FE56B1362129DF069DAF
已知 OnionCrypter 使用的函数列表:
EnumSystemLanguageGroupsACertEnumSystemStoreEnumDisplayMonitorsEnumObjectsEnumFontFamiliesAEnumTimeFormatsAEnumDesktopsAEnumerateLoadedModulesEnumDateFormatsAEnumPropsAEnumFontsAEnumSystemGeoIDEnumWindowStationsWEnumResourceTypesAacmFormatEnumAEnumSystemCodePagesW层次 2
第二层是一个 shell 代码,其最终任务是解密另一层。这个过程并不简单。迷你结构中发生的事情的概览如下图所示,但“解密层 3”的内容则隐藏了相当复杂的解密过程。第三层是分部分解密的,但在第二层的一个子层中的 shell 代码进行解密。而且,即使是这些 shell 代码也分成小部分进行解密,然后组合在一起形成解密序列。
第二层 shell 代码的主要结构
查找 DLL 和函数
首先,OnionCrypter 加载到 kernel32dll 的指针。它使用 TIB线程信息块来查找进程信息块,里面有指向包含当前进程中所有加载模块的信息的结构PEBLDRDATA的指针。通过搜索这个结构,OnionCrypter 找到了 kernel32dll 的基地址。
加载模块列表8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
当 OnionCrypter 取得 kernel32dll 的基地址后,它将加载已知的导出表的地址。接著,OnionCrypter 循环遍历包含 DLL 函数名称的名称指针表。OnionCrypter 对每个函数名称计算 CRC32,并将该数字与作为硬编码参数获得的数字进行比较。当匹配时,迭代器值用于在序号表中查找该函数的序号。通过该序号可以在导出地址表中查找函数的地址。即便这种方法是众所周知的,OnionCrypter 尝试通过使用预先计算的 CRC32 数字而非函数名称的字符串来隐藏其所加载的内容。
通过函数名称的 CRC32 加载 DLL 函数指针的示例8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
作为第一个函数,OnionCrypter 加载了 GetModuleHandleA,利用该函数可以加载 advapi32dll 和 ntdlldll。在随后的步骤中,程序从 DLL 中加载多个函数并将它们存储在跟 shell 代码运行相同的内存区域中。固定存储是在这里创建的。
在 shell 代码内存中存储载入的函数8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
解密下一层
现在运行在第二层的 shell 代码开始解密第三层。解密结构相当复杂。在最上层有大量内存分配和循环。在这个循环里,小块数据被解密并拷贝到更大的内存中,但事情并没有那么简单。
在数据块被解密之前,程序首先进行一次大小为0x1000字节的 VirtualAlloc 并设置 RWX 标志。然后,程序开始以16字节为单位解密数据片段并将其组合起来。随之而来的是大量的内存分配,以至于挂钩分配函数是无用的而且让人不快。
解密并连接完16字节的数据片段之后,数据会被复制到虚拟分配的内存中。结果发现,这些数据是另一个只有解密循环的 shell 代码。这个 shell 代码被调用并解密第二层中的一些数据。然后,已解密的数据经过另一个函数的转换后再次拷贝到返回的内存中。
解密下一层代码的主要结构
OnionCrypter 可以选择使用 RtlCompressBuffer 函数压缩数据或仅部分数据。这种压缩在加密之前进行。在解密过程中,解压缩数据块是在解密后进行的,但在它们与其他块合并之前。
当所有块都解密并合并完后,执行权会传递给存储解密数据的地方,然后加密器开始执行第三层。
层次 3
这一层与之前的层次相似。刚开始时,使用与之前相同的技巧来加载一些重要的 API 函数。这次 shell 代码加载的函数比之前更多。
在 shell 代码内存中存储载入的函数8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
即使这些函数指针已经加载,也不一定会被使用。在某些样本中会使用 RtlDecompressBuffer,而有些则不会。这最可能的原因是 OnionCrypter 提供了如“附加压缩”或“睡眠”的选项,这些选项可以在加密时由用户选择。
数据的解密过程与前一层相同。在解密后,OnionCrypter 在循环中调用 VirtualProtect 函数,并将从程序本身的基地址开始的内存权限更改为 R/W/X。在这个更改后,OnionCrypter 拷贝解密数据并覆盖自己,包括 PE 标头及其随后的段。然后程序使用 VirtualProtect 将内存权限更改回看似合法的权限。
最终,OnionCrypter 在新的 PE 标头中找到入口点并将执行权传递给那里。这就是现在已注入到加密过程中的有效负载开始运行的时刻。
自我注入前后 PE 标头信息8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
自我注入前后的段标头8B85A4D9DF1140D25F11914EC4E429C505BD97551EDE19197D2B795C44770AFE
结论
OnionCrypter 是一个存在已久的恶意软体家族。结合这个加密器的流行程度,以及样本拥有如此独特的第一层,我们可以合理地假设这个加密器并不是一次性开发出来的。相反,通过对多个样本及其捕获日期的分析,能够看到 OnionCrypter 的某些部分有多个版本。
在所有样本中,OnionCrypter 的主要特征保持不变:
三层架构独特的第一层,伴随大量垃圾代码第一层的“主”函数的存在第二层和第三层的通用功能和特性另一方面,以下是来自不同版本样本中可能出现差异的一些内容:
第二层的解密算法 可以发现用于解密第二层的解密算法既有简单的,也有更复杂的,如前面几个部分所描述的。作者不太可能先使用复杂的算法,然后再更改为简单的算法,以使分析变得更简单。因此,OnionCrypter 这一部分可能会随著新版本的升级而更新。“主”函数的位置 在较旧的样本中,“主”函数通常是非常容易找到的,因为它就是应用程序的用户提供的入口点 WinMain 函数。这在新版本中有所改变,因为大多数最近捕获的样本中都有相对简单且短小的 WinMain 函数,而“主”函数可以作为其他函数之一出现。第二层和第三层的结构 尽管这些层在 OnionCrypter 的所有样本中都存在并且始终服务于同一目的,但它们的实现可能会有所不同。例如,有些版本加载的 DLL 函数就比较少。此外,在一些旧版本中,DLL 函数的加载并不是独立的函数。根据分析,内部层可能已进行稍微重构,使这些层次变得更加复杂,添加新功能并使解密过程变得更加复杂和混淆。最终有效负载的注入 尽管大多数样本使用上面章节中提到的自我注入技术,但也有情况下,解密后的有效负载被注入进一个处于挂起状态的新进程中。这一技术与自我注入相似,但是通过组合函数 CreateProcessInternalW、VirtualProtectEx、WriteProcessMemory 和 ResumeThread 来实现的。这篇博客涵盖了在旧版本和新版本 OnionCrypter 中发现的技术。对于解密和有效负载执行的整个过程是对于最复杂和最难分析的版本的具体描述。
疑似安全指标 (IoC)
散列: https//githubcom/avast/ioc/tree/master/OnionCrypter/samplessha256最常见事件名称的列表: https//githubcom/avast/ioc/tree/master/OnionCrypter/eventnamestxt附录
项目库: https//githubcom/avast/ioc/tree/master/OnionCrypter提取样本中事件名称的 IDAPython 脚本: https//githubcom/avast/ioc/tree/master/OnionCrypter/extras/extracteventnamespy标签:分析、加密器、恶意软体、混淆、逆向
分享:XFacebook