MAS系列1
MAS系列网址:https://exploitreversing.com/
分析样本哈希(SHA256):8ff43b6ddf6243bd5ee073f9987920fa223809f589d151d7e438fd8cc08ce292
脱壳相关概念
加壳的动机
- 便于逃避AV的检测
- 由于需要规避许多反分析技术(反调试器和反vm技巧),因此很难对其进行动态脱壳
- 整个恶意软件或只有脱壳的代码可是多态的
大多数恶意软件使用自定义的壳,使代码在安全防御监控下无法检测到。另外还有一些特殊的加壳程序(也称为保护程序),例如Themida,Arxan, VMProtect, Agile.NET和许多其他的会虚拟化指令并实施各种反取证和混淆技术的加壳程序,下面是他们的一些特征
- 已用于64位二进制文件
- IAT被去除或者最多导入一个函数
- 大多数字符串被加密
- 内存完整性受到检查和保护,因此无法从内存中dump出原始原始程序,因为原始指令并没有解码
- 指令被虚拟化,并且有的转换为RISC指令
- 虚拟指令在内存中被加密
- 使用基于堆栈的混淆,使用静态的方法处理虚拟化指令是非常困难的
- 大多数虚拟指令是多态的,因此有许多虚拟指令引用同一原始指令
- 有非常多假的push指令,当然,其中包含许多死代码(dead code)和无用代码(useless code)。
- 这些保护程序使用无条件跳转实现代码重新排序
- 所有的这些现在的加壳器使用代码平坦化,许多反调试技术,和反虚拟机技术
- 并非所有 x64 指令都是虚拟化的,因此会发现包含虚拟化和非虚拟化(原生)指令混合的二进制代码
- 大多数时候,函数的序言(prologues)和尾部(epilogues)并没有虚拟化。
- 原始代码段(code section)可以被拆分或分散在程序中,所以指令和数据可能会混合在一起。
- 引用导入函数的指令可能会被清零或被NOP替换,这种情况下,这些引用会动态恢复。有时相同的引用不会被清零,而是替换为使用 RVA 跳转到相同导入地址的跳转指令,正如有名的“IAT 混淆”。
- 在 shellcode 和常见的恶意软件中使用的 API 是经过 hash 处理的。
- 从原生寄存器(native register)到虚拟寄存器(virtualized register)的转换通常是一对一的,但也并非总是如此。此外,还有一个上下文切换组件,负责将寄存器和标志信息转移到虚拟机上下文。
- 虚拟机处理函数(handlers)来自数据段
- 许多原生(native)API 被重定向到桩代码(stub code)来转发函数调用
- 例如常量展开(constant unfolding)、基于模式的混淆(pattern-based obfuscation)、间接跳转(control indirection)、内联函数(inline function)、代码重用(code duplication)以及不透明谓词(opaque predicate)等混淆被经常使用。
在脱壳之前或脱壳时,我们可以观察并考虑以下问题:
- 恶意软件真的加壳了吗
- 加壳的证据
- 恶意软件是否执行了自我注入或者远程注入
- 恶意软件是否执行了自我覆盖
- Payload被写在哪里
- Payload如何被执行
- 怎么证明脱壳完毕
- 是否存在多层加壳
我们如何知道恶意软件是否真的加壳?有2,3个点可以证明这件事情
- 二进制样本只有几个导入的DLL和函数
- 有许多混淆的字符串
- 存在特殊的系统调用
- 非标准 段名字
- 没有通用的可执行段(只有.text/.code段可以被执行)
- 额外的可写段
- 高熵(High entropy)段(高于7.0,但只能作为一个弱指标)
- 段的原始大小和虚拟大小存在实质性的差异
- 大小为0的段
- 没有与网络通信的相关API
- 缺少恶意软件功能的必要API(比如勒索软件中的Crypt*函数)
- 不寻常的文件格式和文件头
- 程序入口点执行其他的段,而不是.text/.code段
- 特别大的资源端被LoadResource()加载
- 多层加壳的出现
- 在IDA Pro上打开后在彩条上看到大量的数据或者未探索的代码
仅仅出现1个并不能确定加壳,出现多个就基本上可以确定加壳了,此外,还有一些其他的因素可以考虑
- 大多数样本使用LoadLibrary() 和GetProcAddress()动态解析API(反射性代码注入除外)
- 网络APIS也可能被动态解析
- 在第一次分析时很难检测到不正确的标头
- 大的资源段可能不是壳,因为其可能仅包含GUI控件或者数字证书
- 可能会混合使用加密/混淆字符串和明文,因此很难确定二进制文件是否加壳
使用调试器脱壳可能有一系列需要理解和绕过的挑战
- 反调试技术(时间检查【time checking】、CPUID、堆检查【heap checking】、调试标志位检查【debugging flag checking】、NtSetInformationThread()等),所以推荐在 x64dbg/x32dbg 使用 ScyllaHide 等反调试插件。
- 反虚拟机技术用于检查 VMware,VirtualBox,Hyper-V 和 Qemu 工件。
- 文件名,主机名以及用户检查(避免使用哈希作为文件名)。
- 虚拟机上可用磁盘大小(推荐至少 100 GB)。
- 虚拟机上的处理器数量(两个或更多)。
- 正常运行时间(尝试保持正常运行时间超过20分钟)
- 许多无意义的调用(不会使用返回结果)和不存在的 API(假 API)。
- 异常处理被用于反调试。
- 软件断点被清除以及寄存器(DR#)被操控(反断点技术)。
- 使用经典算法(crc32,conti,add_ror13…)的哈希函数。
- Process Hacker、Process Explorer、Process Monitor 等知名工具的检查(建议在使用前重命名这些可执行文件)。
不幸的是,反虚拟机技术和反调试技术不可能总被插件处理,我们必须设法使用调试器绕过。在这种情况下,我们可以使用 WinDbg 等不同的调试器来调试恶意软件(这些恶意软件只针对 ring3 调试器,不针对内核调试器,如 GuLoader 恶意软件)。
在脱壳后,我们也可能需要修复生成的二进制文件,因为可能存在以下问题
- DOS/PE头可能已在内存中破坏或者被压缩库修改
- 许多情况下,当从内存中提取二进制文件时,需要清理它,因为在它的 DOS 头(MZ 签名)和 PE 头之前有一些垃圾字节
- 入口点(Entry-Point,EP)可能被清零或错误。
- 脱壳后二进制文件的导入表可能被破坏,因为内存被转储为文件,但其地址指的是虚拟地址(映射版本而非未映射版本),因此显示的地址为未对齐段或不存在的段。
- 基地址是错误的
- PE格式的字段存在一些不一致
- 可能很难确定原始入口点(Original Entry Point,OEP),它通常使用间接调用(如call [eax]或jmp [eax])完成脱壳器代码到原代码的转换。另外,未解析 API 的存在可能是恶意代码尚未到达 OEP 的证据。注意:OEP 是可执行文件在加壳之前的入口点,在加壳之后,一个新的 EP 与加壳器相关联
- 互斥锁(mutexes)被用于两层脱壳器之间的一个“解锁密钥”。这种情况下,在没有第一阶段发生的情况下第二阶段的脱壳是不会发生的,如果发生了,则证明互斥锁的存在。
- 代码可能执行自我覆盖
- 脱壳代码的第一阶段不会从任意目录运行,只能从特定目录运行。
- 你可以提取诱饵二进制文件(decoy binary),在许多案例中,恶意软件开发者会打包一个或多个无用的可执行文件作为诱饵,以此来消耗分析人员的时间,因此,明智的做法是不要相信第一次就从内存中得到了脱壳后正确的二进制文件
下面是一些解决这些问题的办法:
- 从另一个可执行文件(或从样本自身)复制一个完整的 PE 头,并考虑脱壳后的二进制文件是未映射(.text 段通常从 0x400 开始)还是映射的(.text 段通常从 0x1000 开始)。
- 通过修改相应的原始地址(Raw Address)和原始大小(Raw Size)来对齐脱壳后二进制文件的段,此操作通常会修复导入表,并且可能会查看到导入函数。需要注意:一些脱壳后的二进制文件在对齐它们的段之前是不会显示导入表的。然而,还有一些恶意软件在你脱壳之后导入表依旧没有任何函数,这并不意味你的脱壳过程出错,而是可能使用了动态解析 API 的技术。
- 重建 IAT 并强制修改 OEP。
- 如果在查找 OEP 时遇到了问题,请记住 OEP 可能在 IAT 解析之后出现。这种情况下,一种可能的方法是检查 IAT 是否已解析(检查 x64dbg 上的模块间调用)或在恶意软件执行时的关键 API 上设置断点(例如勒索软件使用的CryptoAcquireContext()),因为当执行到这些关键 API 时,IAT 肯定已经完成了解析,然后寻找无条件跳转到特定内存或间接调用(如call [eax])。另一种有趣的方法是使用调试器的图形可视化功能(x64dbg 中的 g),并在最后的代码块检查这些转换点(间接调用或内存地址的无条件跳转)
- 调整基地址以匹配从内存转储的段基地址
- 为了检测执行自我覆盖的恶意软件,可以尝试在 .text/.code 段设置断点,这样可以在代码段被写或执行时触发断点。
- 在两阶段脱壳情况下,第一个脱壳得到的二进制文件可能是 DLL,根据上下文将 DLL 文件转换为可执行文件可能很有用,并且有很多方法可以完成此任务,但我最喜欢的方法是编辑 PE 头以更改Characteristics 字段,并将导出函数的入口作为程序入口。
代码注入技术回顾
主要的代码技术有以下几种
- DLL 注入: 这个老的技术用来强制一个进程加载一个DLL, 主要涉及到的APIs:
OpenProcess()
,VirtualAllocEx()
,WriteProcessMemory()
,CreateRemoteThread | NtCreateThread | RtlCreateUserThread
- PE 注入: 在这种技术中,编写恶意代码强制在远进程或自我进程中(自我注入)执行,
OpenThread()
,SuspendThread()
,VirtualAllocEx()
,WriteProcessMemory()
,SetThreadContext()
,ResumeThread() | NtResumeThread()
。 - Reflective Injection
- APC Injection
- Process Hollowing: 简而言之,该技术被恶意软件用来 ”挖空”进程的全部内容,并在其中插入恶意内容。包含的APIs有:
CreateProcess()
,NtQueryProcessInfomation
,GetModuleHandle()
,Zw/NtUnmapViewOfSection()
,VirtualAllocEx()
,WriteProcessMemory()
,GetThreadContext()
- AtomBombing
- Process Doppelgänging
- Process Herpaderping
- Hooking Injection
- Extra Windows Memory Injection
- Propagate Injection
(没写详细的就是遗忘的或者没接触过的,之后再补
脱壳方法
脱壳的分类非常复杂,这部分主要讲述脱壳技术,但是一般来说,有一些方法能够对恶意软件进行脱壳,例如使用调试器、自动化工具、Web服务或者编写自己的脱壳代码,具体使用哪种方法根据情况而定
调试器 + 特定函数断点脱壳
这是使用最多的方法是,包括将恶意软件加载到调试器中,并在众所周知的API上设置软件断点,其中大多数与内存管理和操作有关,并查找要从内存中提取的可执行文件和ShellCode,在以下API上插入软件断点:
- CreateProcessInternalW()
- VirtualAlloc() | VirtualAllocEx()
- VirtualProtect() | ZwProtectVirtualMemory()
- WriteProcessMemory() | NtWriteProcessMemory()
- ResumeThread() | NtResumeThread()
- CryptDecrypt() | RtlDecompressBuffer()
- NtCreateSection() + MapViewOfSection() | ZwMapViewOfSection()
- UnmapViewOfSection() | ZwUnmapViewOfSection()
- NtWriteVirtualMemory()
- NtReadVirtualMemory()
脱壳过程中,我们可能会遇到一些问题(例如,恶意软件使用了反调试技术)和其他困难。因此,下面有一些在脱壳前和脱壳后需要注意的:
- 在恶意软件到达其入口点后设置断点(系统断点后)
- 如前所述,建议使用反调试插件,并在少数情况下忽略从 0x00000000 到 0xFFFFFFFF 范围的所有异常
- 有时忽略异常可能不是一个好主意,因为恶意软件可能使用异常来脱壳,另外,还存在使用中断和异常调用 API 的恶意软件(本文之外)
- 使用 MSDN 了解列出的 API 以及参数是成功脱壳恶意软件的关键。
- 如果使用VirtualAlloc(),建议在其退出时(ret 10)设置断点,此外,有时通过设置写内存断点更容易跟踪分配的内容。
- 某些情况下,恶意软件将 payload 提取到内存中,但它会破坏 PE 头,因此需要使用 HxD 等十六进制编辑器重建整个 PE 头
- 提取的 payload 可能采用映射或未映射的格式。如果是映射格式,那可能导入表是损坏的,需要通过 PEBear 手动对齐段,或者使用 pe_unmapper 之类的工具进行修复,可能还需要修复基地址和入口点。
- 要重建损坏的 IAT,建议使用 Scylla(内嵌在 x64dbg),但需要输入 OEP,找到 OEP 的方法之一是查找代码转换的指令(如jmp eax,call eax,call [eax]等)。
- 一些脱壳后的恶意软件在 IAT 中没有任何函数,所以有两种可能:段未对齐(映射版本)或是脱壳后的恶意软件有动态解析函数的功能。
- 在 x64dbg 上使用 g 键可能有助于在块中可视化代码,并找到可能的 OEP。
- 查找 OEP 的另一个方法是通过 PIN 等代码跟踪工具。
- tiny_tracer 之类借助 PIN 的工具可以更轻松地检测,并且可用于了解恶意软件调用的函数(对于脱壳和了解反分析技术非常有用),还能寻找可能的 OEP。
- 在许多情况下,脱壳后的代码可能是恶意软件的第一阶段,因此,有必要重复步骤来对下一阶段脱壳。
- 一些恶意软件样本执行自我覆盖,因此可以在 .text 段设置断点来检测脱壳的执行。
- 根据提取的二进制文件(如 shellcode),它可能需要特定的上下文环境才能运行,因此有必要将其注入正在运行的进程(如 explorer.exe)中执行进一步的分析。
- 如何检查提取的文件是否是最终的恶意软件?这里没有一个准确的答案,通过从 DLL 如 WS2_32.dll(Winsock)和 Wininet.dll 查找网络函数,明文字符串,加密函数(主要是勒索软件),以及其他证据来判断。在重新对齐段或修复 IAT 之后,可以将提取到的代码加载到 IDA Pro 进行分析。
调试器 + DLL加载时断点
这是一种古老且简单的恶意软件脱壳技术,方法是在每个加载的 DLL 时停止调试器并检查内存中可能提取的 PE 格式文件的内存(注意:不要只关注 RWX 段,因为许多恶意软件会在 RW 段,并且在将执行上下文传递到可执行文件前,会通过VirtualProtect()将段的权限更改为 RWX)。毫无疑问,虽然会消耗一些时间,但大多数情况下十分有效。x64dbg 有配置选项中可以设置每个 DLL 加载时命中断点(Options => Debugging Options => Events => DLL load)。
自动脱壳
恶意软件分析人员可以使用自动化工具完成脱壳,Aleksandra Doniec (Hasherezade) 提供了出色的工具来实现这一目标:
- hollows-hunter:https://github.com/hasherezade/hollows_hunter/releases
- pe-sieve:https://github.com/hasherezade/pe-sieve/releases
- mal_unpack:https://github.com/hasherezade/mal_unpack/releases
使用Process Hacker
另一种从内存中提取二进制文件的简单(且有局限性)方法是通过 Process Hacker,方法是双击正在运行的进程,转到 Memory 选项,查找感兴趣的区域或基地址(RWX),双击并保存。当然,在自我注入的情况下更容易找到恶意二进制文件或 payload。但远程注入的情况下,需要逆向恶意程序来了解要注入的目标进程,诸如 explorer.exe 或 svchost.exe 等目标。重申一次,这是一种简单且有限的方法。
使用公开或付费的服务
你可以使用互联网上的服务 Unpacme,它提供自动脱壳服务,有一个免费的公共计划(每月 10 次)和其他付费计划。此外,它还提供了一个 API(https://api.unpac.me/)来将你的自定义程序与 Unpacme 服务连接
写脱壳代码
尽管这种方法听起来比较耗时,通常可以使用 Python 完成脱壳,主要是在 shellcode 或 处理恶意软件线程使用多种反虚拟机和反调试的情况中。此外,在处理类似恶意软件时,还具有自动脱壳的优势。
二进制脱壳
分析样本: 8ff43b6ddf6243bd5ee073f9987920fa223809f589d151d7e438fd8cc08ce292
首先使用PEBear进行检查,可能有助于收集第一批有价值的信息
发现在IAT中并没有关于网络通信,加解密的API,所以猜测加壳了。
.data段的VirtualSize远远大于RawSize,而且这个段的属性是可写的,这是另一个加壳的标记。
开始脱壳,在下面这些经典函数处下断点
- VirtualAlloc
- VirtualProtect
- ResumeThread
经过几次CTRL + F9,观察VirtualAlloc分配的内存处的数据,发现了M8Z,这是一个aPLib压缩,直接dump下来
搜索MZ头,发现已经解压缩完了,然后将MZ之前的去掉,拖到CFF Explorer里
可以看到很多加解密的函数,以及网络通信的函数,到这里位置,第一步的Unpack就结束了
逆向解密代码
现在我们有了解压缩的二进制文件,所以让我们在IDA Pro中打开它。有很多方法可以找到加密配置,但更简单(而且有点不准确)的方法之一是在IDA Pro上查找处理数据的函数或Unexplored 颜色条区域(实际上,未探索的区域比下面所示的要大得多):
- byte_10004000
- byte_10004010
- byte_10004018
根据我分析恶意软件的经验,我已经知道4010这块局域的8字节是Microsoft Crypto API中几个API的重要依据,因此这表明我们分析对了,在许多著名的恶意软件样本中发现的另一种模式是struct key + encrypted data,所以即使我没有关于这种情况的任何进一步指示,我也可以假设“byte_10004010”是某个密钥(长度为 8 字节),尽管 有时它不是最终密钥,因为恶意威胁使用 KDF(Key Derivation Functions)从提供的密码生成最终密钥。 根据我们的分析,“unk10004018”指的是可能加密的数据。
这些都只是猜测,只有去分析过之后,才会对这个位置的数据有一个更好的了解。
来到交叉引用的函数的位置
尽管CryptAcquireContextA()
是一个被遗弃的函数,但他仍然被恶意软件威胁广泛使用。此API用于通过CSP(加密服务提供程序)获取密钥容器的句柄,它使用默认的密钥容器名称和用户默认的提供程序,因为这两个参数都为零,看第5个参数,0XF0000000指的是 CRYPTO_VERIFYCONTEXT
,这在使用临时密钥或不需要访问永久私钥的应用程序中很常见。这个恶意样本正在使用密钥派生。
所以加密的步骤为
- byte_10004010 = C58B00157F8E9288
- byte_10004010 –>SHA1(20bytes)
- SHA1 → CryptHashData( ) → CryptDeriveKey( ) → RC4 key (5 bytes)
写解密代码解密数据
1 | import binascii |
我们设法从unpack的Hancitor二进制文件中提取并解密了Hanitor C2配置。编写解密器脚本的优点是,我们可以对所有遵循二进制模式的Hanctors样本使用它
使用malwoverview去查找一些其他的Hancitor样本。
随便找一个下载下来,利用相似的方式脱壳,即在VirtualAlloc上下断点
发现这里有MZ头,dump下来后,去除前面那部分,然后打开IDA
发现是相同的结构,用之前的脚本提取下
1 | 解密数据如下: |
发现解密成功,然后好奇用bindiff对比以下上面2个样本unpack后的样本
发现基本一模一样,另外解密过程也可以用网站
https://gchq.github.io/CyberChef/
总结
在这一篇文章中,我展示了如何通过编写Python3脚本从Hancitor样本中提取和解密C2数据,此外我还介绍了一些概念和基础,如代码注入和脱壳,这些概念和基础将在本系列的下一篇文章中有用。