前面发了文章 “APT 恶意 DLL 分析及 C2 配置提取(子 DLL 篇)”,这次发布 母 DLL 篇的分析及它们的关联。
0x00 前言:
在前面写过了 APT 恶意 DLL 分析及 C2 配置提取(子 DLL 篇),那是用于恶意样本 C2 配置提取学习的,这里分析的是其母 DLL 篇,主要是想知道子 DLL 和母 DLL 是怎么关联起来的,所以一并分析了。因为子 DLL 和 母 DLL的手法和操作重点不一样,所以拆成两篇来写,避免混乱。建议先看子 DLL 篇的分析过程,因为样本的发展阶段在那里提及,能有一个总览的效果。
0x01 外层 DLL 分析:
样本 IOC:
HASH | 值 |
---|---|
MD5 | e5fcf505c25e66116f288a8ae28d2c8a |
SHA1 | e597f6439a01aad82e153e0de647f54ad82b58d3 |
SHA256 | 63996a39755e84ee8b5d3f47296991362a17afaaccf2ac43207a424a366f4cc9 |
关键行为预览:
动态获取手法分析:
动态获取 dll 基址:
母体 DLL 的动态获取手法都经过反复多次的数学运算混淆,但是底层手法还是一样的,获取 PEB 结构----> LDR 结构---->InLoadOrderModuleList链遍历---->比较 BaseDllName---->最终获取 DllBase。
关键的偏移我们提取出来就是 0xC---->0xC---->0x30遍历---->0x18,在下面的混淆中我们用这种方法来识别出其目的是遍历 PEB 结构获取 DLL 基址。
应用到题目中:
动态获取导出函数基址:
同理的,在 DLL 基址上获取导出函数,用的是 PE 结构特性,具体结构体就不截图了,常规的步骤还是如《Windows PE 权威指南》中所说:
步骤1 定位到 PE 头。
步骤2 从 PE 文件头中找到数据目录表,表项的第一个双字值是导出表的起始 RVA。
步骤3 从导出表中获取 NumberOfNames 字段的值,以便构造一个循环,根据此值确定循环的次数。
步骤4 从 AddressOfNames 字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配; 如果匹配成功,则记录从 AddressOfNames 开始的索引号。
步骤5 通过索引号再去检索 AddressOfNameOrdinals 数组,从同样索引的位置映射找到函数的地址(AddressOfFunctions)索引。
步骤6 通过查询 AddressOfFunctions 指定函数地址索引位置的值,找到虚拟地址。
步骤7 将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。
关键的偏移提取出来就是:
DOS头(DLL 基址)---->0x3c(指向 PE 头)---->0x
78(数据目录表)---->0x0(数据目录表第一项就是导出函数表,IMAGE_EXPORT_DIRECTORY 结构)---->0x18 (定位 NumberOfNames 字段构成循环)---->0x20(定位 AddressOfNames 字段,函数名称地址表RVA)---->0x24(定位 AddressOfNameOrdinals 字段,函数序号地址表)---->0x1c(定位 AddressOfFunctions 字段,导出函数地址表 RVA)
PE 文件内存映像操作:
这次的分析过程中发现样本把解密后文件格式的 DLL 通过 PE 结构特征进行 PE 文件内存的映像操作,本着学习的目的,所以手法还是跟踪探讨下。
涉及知识:
分析中需要一些对 PE 文件结构的前置知识,以下是我的一些笔记积累:
节的属性:
1:为了保证程序执行的安全,保障内核的稳定,Windows 操作系统通常对不同用途的数据设置不同的访问权限。比如,代码段中的字节码在程序运行的时候,一般不允许用户进行修改,数据段则允许在程序运行过程中读和写,常量只能读等。
2:内存中的节和文件中的节会出现很大的不同。例如 “.data?” 的数据在磁盘中不存在,但在内存中存在。而 “.reloc” 重定位表数据却恰怡相反,在磁盘数据中存在但在内存中被抛弃。
节的对齐:
1: 文件对齐:(200h)
为了提高磁盘利用率,通常情况下,定义的节在文件中的对齐单位要远小于内存对齐的单位 ,通常会以一个物理扇区的大小作为对齐粒度的值,即 512 字节,十六进制表示为 200h。
2: 内存对齐:(1000h、2000h)
由于 Windows 操作系统对内存属性的设置以页为单位,所以通常情况下,节在内存中的对齐单位必须至少是一个页的大小。对 32 位的 Windows XP 系统来说,这个值是 4KB(1000h),而对于 64 位操作系统来说,这个值就是 8KB (2000h)。
PE 内存映像:(为运行,就是 OD 中解析的 exe 文件,在进程内)
是指将 PE 文件按照一定的规则装载到内存中,装入后的整个文件头内容不会发生变化,但 PE 文件的某一部分如节的内容会按照字段中的对齐方式在内存中对齐,从而使得内存中的 PE 映像与装载前的 PE 文件不同。
其目的是为了运行。为了配合操作系统的运行,方便调度,提高运行效率。
要用上的 PE 结构体:(32 位,文件头和节表)
定位并提取节表字段信息:
申请、划分并填充内存映像空间:
节属性分配:
手法总结:
解密出内层核心 DLL(文件结构)---->0x3c 处 e_lfanew 字段定位 PE 头---->ASCII 4555 确认 PE 标识
定位到 PE 头后:(从这里开始以 PE 头为起始偏移)
0x4 定位 IMAGE_FILE_HEADER.Machine 字段确定值为14c 的Inter 386 架构---->0x38 定位IMAGE_OPTIONAL_HEADER32.SectionAlignment字段获取内存中节的对齐粒度(后面映射时使用)---->0x14定位IMAGE_FILE_HEADER.SizeOfOptionalHeader字段获取扩展头大小---->扩展头大小加上 IMAGE_FILE_HEADER 的 0x18 大小就定位到节表项了。
定位到节表项后:(从这里开始以节表头为起始偏移)
0xc 定位 IMAGE_SECTION_HEADER.VirtualAddress 字段获取节区的 RVA 地址(在内存映像中用到)---->0x10 定位到 IMAGE_SECTION_HEADER.SizeOfRawData 字段获取节在文件中对齐后的尺寸(在内存映像中用到)----> 加 0x28 遍历下一个节表继续获取相应字段(节表项40字节大小)
最后就是空间申请,划分,填充,段属性分配了。
行为分析:
开辟空间,填充数据并解密成内层核心 DLL(文件格式):
检索系统信息:
进入内核 DLL 中,检查参数,开启线程:(这里我重新调的,所以地址和前面对不上)
设定参数重新执行,直接来到母体的 DllRegisterServer 函数:
0x02 母 DLL 与子 DLL 关联梳理:
母 DLL 与子 DLL 的联系现在理清了,母 DLL 在解密子 DLL 并进行 PE 内存映像操作后进入子 DLL 的入口点中获取并凭借路径和参数,开启新的进程来运行母 DLL 的 DllRegisterServer 导出函数。但其实际上是一个过渡,过渡到子 DLL 同样的导出函数中进行操作。
0x03 函数链顺序划分:
分配空间:(混淆操作)
malloc---->free
PE 内存映像操作:
VirtualAllocExNuma---->memcpy---->malloc---->“data_operation”——"PE 文件"---->VirtualAlloc(文件头和每个节表各一次)---->VirtualProtect(除 .reloc 节表外各一次)---->VirtualFree(内存映射中释放 .reloc 段)
检索系统信息:
GetNativeSystemInfo
比较参数:
GetCommandLineW---->“截取逗号后字符”---->lstrcmpiw(对比 L"DllRegisterServer")
拼接参数,开启线程运行:
SHGetFolderPathA---->GetModuleFileName---->L"%s\rundll32.exe \"%s\",DllRegisterServer"---->sprintfw(L"C:\Windows\SysWOW64\rundll32.exe \"C:\Users\xxx\Desktop\1.dll\",DllRegisterServer")---->CreateProcessW
- 本文作者: 沐一·林
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1821
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!