8-BitQuest
菜单
[恶意软件分析]

暗云:篡改驱动人生并改写 MBR 的 Bootkit

· 3 分钟阅读 · @admin

暗云 Bootkit 像素封面

暗云通过补丁后的驱动人生模块注册系统服务,在 svchost.exe 中改写 MBR。感染后的引导代码在系统启动阶段挂钩 nt!ZwCreateSection,进入内核后再保护磁盘上的 MBR,并下载后续模块。

样本哈希:

  • bc5dea3605878631b8feeeb53bfb5b17
  • d3394d7d60f560c0889cebcb5ab53c11

公开分析可参考 FreeBuf:暗云。下面按样本里的执行阶段整理,截图保留在对应步骤后面。

1 补丁后的驱动人生

第一处被 Patch 的位置:

补丁在这里补上若干必要函数,作为后续代码的传入参数。恶意代码执行完后,再返回原来的函数继续执行。

1.1.1 Shellcode 1

接下来在资源中查找名为 Code 的未加密代码,申请内存后把这段代码拷进去执行。

Shellcode 1:

1.1.2 Shellcode 2

接着执行 Shellcode 2。它通过哈希取得 VirtualAlloc 和 RtlDecompressBuffer 的地址,申请内存,再解密并执行下载代码。

Shellcode 2:

获取函数地址的方法:

加密代码就接在函数后面:

1.1.3 Shellcode 3

解密后的代码大小核对一致,才执行下载逻辑。真正进入下载代码之前,会先跑 Shellcode 3 的头部,整理执行空间并修正导入表。

1.1.4 注册服务的 DLL

这一段接在 Shellcode 3 后面。它执行 DLL 入口,而这个入口里只有一个函数,作用是注册被篡改的驱动服务模块。

执行前会先检查当前文件名是不是指定格式,文件名里要带两个下划线。

然后收集这台机器的信息:

再把信息发到指定服务器:

随后调用 URLDownloadToFile,下载用于注册篡改驱动服务的模块。

下载下来的模块先解密再执行。

这里的解密算法比较固定,后面暗云在内核里下载代码时也用同一段算法。

解密后修正重定位和 IAT,再执行入口函数。

这个模块会下载被 Patch 的驱动人生组件并执行,把它注册成系统服务,最后挂进 svchost.exe,由服务去改写 MBR。

1.2 svchost 服务改写 MBR

注册出来的服务如下:

被 Patch 文件的 MD5 为 35a4bbc13ec88961f5ec7b674479089e。

服务把代码映射到内存里执行:

然后下载 shellcode,写入暗云的 MBR。流程和前面注册服务时一样:Shellcode 2 调用 RtlDecompressBuffer,Shellcode 3 负责加载并执行修改 MBR 的代码。

写入暗云代码:

写入前的原始 MBR:

写入后的 MBR:

原始 MBR 被备份到第二个扇区。

2 感染后的模块

感染后的 MBR 会先加载第 3 扇区之后的 60 个扇区,解密并执行,同时挂钩 int 15h,给自己留下第二次执行机会。等系统通过 int 15h 查询内存信息时,再挂钩 BILoadImageEx,从中找到并挂钩 nt!ZwCreateSection。

2.1 ZwCreateSection 挂钩

挂钩 ZwCreateSection:

进入挂钩函数后,先把钩子恢复回去。

然后映射存放代码的物理页面,再调用后面的 shellcode。

2.2 Shellcode 1:磁盘保护与下载

这一段主要做两件事:挂钩磁盘以保护被改写的 MBR,以及联网下载后续要注入的 shellcode。

2.2.1 修正导入表

进入入口函数后,先修正导入表。

2.2.2 设置线程回调

这段代码先判断系统版本,阻止 360 急救箱的设备驱动加载,并备份感染后的 MBR。接着注册线程回调,为后面挂钩磁盘做准备。

对抗 360 驱动的部分:

2.2.3 线程回调

线程回调里判断新创建的是不是 csrss.exe。如果是,就再创建一个系统线程,用来下载 shellcode,并执行注入 svchost.exe 的代码。

挂钩 DR0 的下层设备:

2.2.4 磁盘挂钩

先取得 SystemRoot 的文件对象。

再取得磁盘对象。

然后在同一目录下创建对象:

这个目录树下面的对象是:

从该事件对象的命名信息里取出对象目录:

遍历全部 HashBuckets:

查找 Dr0 设备:

取得底层设备和驱动:

接着按系统和磁盘类型分别挂钩。

磁盘驱动:

不同操作系统的处理不一样。

XP:

Windows 8 和 Windows 10 会保存 SCSI 派遣函数头部 20 字节,一旦发现被 Patch 就恢复。

Windows 7 单独判断挂钩点。

Atapi / msahci:

storahci:

storport:

挂钩时会置上标志,并保存原始函数地址。挂钩函数对外返回一份正常的 MBR。

挂钩之前会先算校验和,用来检查自身代码是否完整。

参与校验的代码偏移和大小:

代码开头就是导入表。这里算出一个 checksum 保存起来,后面的自检会用到它。

挂钩完成后注册 DPC,用来保护自己的钩子和代码。

DPC 会把当前代码的校验和与之前保存的 checksum 比较,一旦发现被修改就蓝屏,错误码为 E5。

自身挂钩的磁盘钩子如果被恢复超过五次,同样蓝屏。对 SCSI Dispatch,还会检查函数头部是否被挂钩,发现挂钩就把它恢复。

2.2.5 系统线程里的网络下载

线程入口会先等大约一分钟,应该是在等网络就绪,同时查找 nt 的基址,作为参数传给后面的 Shellcode 2。

样本自己实现了一套内核态的 DNS、TCP 和 UDP,并且优先走 UDP。连接的域名是 ms.maimai666.com。

域名解析:

优先使用 53 端口。

解析到 IP 之后,先取数据包长度。

接收时以 0x200 为单位收包。

UDP 方式还会对收到的数据做校验。

下载完成后调用入口函数执行。

2.3 Shellcode 2

这里调用 RtlDecompressBuffer 解开尾部代码,并填好函数导入表,然后调用 PsCreateSystemThread 去注入代码。

线程函数:

要查找的进程名:

申请的内存地址:

注入 APC:

2.4 Shellcode 3

代码这时已经注入到应用层,负责加载尾部数据里的 DLL,并修正导入表。

导入表和重定位都处理完之后,调用入口函数。

2.5 注入的 DLL

这个函数唯一的作用就是下载代码并运行。

再按类型决定怎么执行:

数据文件的格式:

解密算法和前面那一段完全一样。

2.6 注入 svchost 后的后续行为

样本会创建傀儡进程来执行代码。

后面还会加载 Lua 脚本引擎,从这个地址拉脚本:

http://www.acsewle.com:8877/lcdn.db

拉下来的脚本用于 DDoS 和刷流量。其中一份刷流量脚本如下:

3 Bootkit 引导流程

暗云的 MBR 分成三部分:第 1 个扇区是木马自己的 MBR,第 2 个扇区是它备份的原始 MBR,第 3 个扇区开始是后续代码,以加密形式存放。整个 bootkit 支持 XP 之后的系统,包括 32 位、64 位,以及 Windows 10 14393。前面的流程相同,后面再按实际系统分开实现。

第 1 个扇区的作用是预留 67KB 空间,供驻留代码使用。

它把 MBR 代码拷到 0x8F000,从第 3 个扇区起读取 60 个扇区到 0x7E00,再把这段代码解密到 0x8F200,然后跳到高端内存执行。

接着检测是否支持 int 15h。支持就挂钩 int 15h,否则读取第 2 个扇区里备份的 MBR,跳回原来的引导流程。

在 int 15h 的挂钩里检测 bootloader 的特征码,再写入下一步挂钩。

bootloader 加载时,这段挂钩代码得到执行机会,搜索 bootmgr!Archx86TransferTo64BitApplicationAsm,把下一步挂钩写进去。

该处挂钩执行时,再搜索 winload!OslArchTransferToKernel,写入再下一步的挂钩。

这一处挂钩被执行时,先找到 NTOS 基址,然后枚举 NTOS 的节表。

找到有空余空间的节之后,把代码拷进去,并挂钩 ZwCreateSection,再跳到 NTOS 的节空间。这样系统第一次执行 ZwCreateSection 时,挂钩代码就能拿到执行机会。

这个调用位于 Phase1Initialization 之中。代码先恢复 ZwCreateSection 的挂钩,再分配内存,把物理地址上的数据拷进这块内存并跳过去执行。之后又分配两块内存:前面放代码,后面放数据,其中包括要用到的函数指针表。

然后跳到刚分配的代码内存继续执行,先检查当前是不是受支持的系统。

接着创建用于对抗 360 急救箱的占坑对象,备份木马 MBR,供后面保护自身时核对,并设置线程创建回调 PsSetCreateThreadNotifyRoutine。

线程回调里等待 csrss.exe 被创建,然后挂钩磁盘以保护自身 MBR,创建内核线程和 DPC,并做后续的网络操作、接收云控指令。这些都做完后,通过 ExQueueWorkItem 抹掉自己的线程回调。

64 位下前面的部分基本相同,从 winload 之后才走 64 位流程。

同样是枚举 NTOS 节表,找有空余空间的节。和 32 位不同的是,节属性必须同时带有 IMAGE_SCN_MEM_READ、IMAGE_SCN_MEM_EXECUTE 和 IMAGE_SCN_CNT_CODE。

挂钩代码本身也不一样:

然后会搜索 INIT 节里的 PatchGuard 特征,把它废掉。

PatchGuard 初始化的位置被改成直接返回:

48 31 C0    xor rax, rax
48 FF C0    inc rax
C3          retn
90          nop

这样 PatchGuard 不会再因为木马的挂钩而触发蓝屏。Windows 10 上这一处可能没处理干净,所以 Windows 10 x64 仍可能出现 PatchGuard 蓝屏。

系统调用 ZwCreateSection 时,木马获得执行机会。

后面的操作和 32 位基本一样。

更多文章