Malware Analysis 脱壳与C2提取
1.加密壳的特征
- 程序只有很少的导入DLL和函数
- 存在很多混淆的字符串
- 存在直接的系统调用
- 不规范的节区名字
- 不常见的可执行节区(应该只用.text/.code节是可执行的)
- 意外的可写节区
- raw-size和virtual-size之间差异很大
- 存在0大小的节区
- 缺少网络通信相关的API
- 缺乏用于恶意软件功能的基本 API(例如,勒索软件中的 Crypt* 功能)
- 不常见的文件格式和头
- OEP指向 .text/.code 之外的其他部分
- 资源部分(.rsrc 部分)很大,代码中存在 LoadResource( ) 函数
- 存在叠加层
- 在IDA中的彩条中发现很大的数据和未被探索的代码
2.虚拟化壳的特征
- 通常是64位的
- IAT表被移除或者只有一个导入函数
- 大多数字符串都被加密了
- 存在内存完整性校验和保护
- 指令被虚拟并且被翻译成RISC指令
- 虚拟指令在内存中加密存储
- 混淆是基于栈的,因此使用静态方法处理虚拟化代码非常困难
- 大部分虚拟化代码都是多态的,所以有很多虚拟指令指的是同一条原始指令
- 有数千行push指令,其中许多都是没用的
- 使用无条件跳转实现代码重排
- 存在控制流平坦化、反调试、反虚拟机等技术
- 并非所有 x64 指令都是虚拟化的,因此您会发现包含虚拟化和非虚拟化(原始)指令混合的二进制代码
- 大多数时候,函数的序言和尾声都没有被虚拟化
- 原始代码可以被分散存放,因此指令和数据将混合在一起
- 引用导入函数的指令可能被清零或被替换成NOP,因此这些位置将被动态恢复。有时,这些引用位置不为0,被修改为指向一张假的IAT表,此时为IAT混淆
- 在 shellcode 和常见恶意软件中使用的 API 名称是经过hash的
- 从原始寄存器到虚拟寄存器的转换通常是一对一的,但并非总是如此。此外,还有一个上下文切换组件负责将寄存器和标志信息传输到虚拟机上下文中
- 虚拟机处理程序来自data段
- 许多原始API被重定向到转发调用的存根代码处
- 使用了如常量展开、基于模式的混淆、间接控制、函数内联、代码复用,不透明谓词等混淆技术
3.调试已加壳程序注意事项
- 使用反反调试插件来过掉常规反调试,甚至使用内核调试器
- 注意主机名、账户名、以及样本名(程序检测文件名是否是hash)
- 为虚拟机分配100GB以上的磁盘空间
- 为虚拟机拍摄开机时间超过20分钟的快照
- Process Hacker、Process Explorer、Process Monitor等知名工具的恶意代码检查(建议在使用前重命名这些可执行二进制文件)
4.脱壳后的修复工作
- 修复PE文件头
- 修复OEP,可能是0或者是错误的RVA
- 修复IAT
- 修复ImageBase
- 检查dump到的数据是已映射的还是未映射的
5.恶意代码中常用的注入状态机
5.1 远程线程注入
1 |
|
5.2 自注入
1 |
|
5.3 反射式注入
1 |
|
5.4 APC注入
1 |
|
5.5 傀儡进程
1 |
|
5.6 AtomBombing
1 |
|
5.7 Process Doppelgänging
1 |
|
5.8 Process Herpaderping
1 |
|
5.9 全局钩子注入
1 |
|
5.10 额外窗口内存注入
1 |
|
5.11 Propagate注入
1 |
|
6.脱壳手段
6.1 关键API设置断点
- CreateProcessInternalW( )
- VirtualAlloc( ) | VirtualAllocEx( )
- VirtualProtect( ) | ZwProtectVirtualMemory( )
- WriteProcessMemory( ) | NtWriteProcessMemory( )
- ResumeThread( ) | NtResumeThread( )
- CryptDecrypt( ) | RtlDecompressBuffer( )
- NtCreateSection( ) + MapViewOfSection( ) | ZwMapViewOfSection( )
- UnmapViewOfSection( ) | ZwUnmapViewOfSection( )
- NtWriteVirtualMemory( )
- NtReadVirtualMemory( )
注意事项:
1.需要执行到程序模板后设置断点,越过系统初始化部分。
2.使用反调试插件,有时候还需要忽略全部异常。
3.在VirtualAlloc( )函数设置断点时,建议设置在返回代码(ret 10)处,之后设置内存写入断点来监控这块内存。
4.可以使用Intel PIN工具来追踪OEP
6.2 设置DLL加载断点
在每个DLL加载的过程中去检索内存
6.3 自动化脱壳工具
内存扫描工具2:Releases · hasherezade/pe-sieve (github.com)
内存转储:hasherezade/mal_unpack: Dynamic unpacker based on PE-sieve (github.com)
6.4 付费脱壳
7.示例分析
7.1 脱壳
先用PE bear看一下,导入表中不存在网络相关的API吗。可能被隐藏了。
再使用PEStdudio查看一下节表,.data段的raw size与virtual size差别很大,很可疑。
因为目标程序是DLL,可以使用rundll32.exe以及指定导出函数的方式进行调试。
命令行参数:”C:\Windows\SysWOW64\rundll32.exe” C:\Users\mas\Desktop\8ff43b6ddf6243bd5ee073f9987920fa223809f589d151d7e438fd8cc08ce292\sample_1.bin,#1
表示调用第一个导出函数
在内存相关API下断点。(VirtualAlloc ret 0x10、VirtualProtect、ResumeThread)
F9运行,会在VirtualAlloc处首次断下,将eax所指的地址放到内存窗口1进行观察;继续F9,还会在此函数断下,继续将地址放入内存窗口2观察;继续F9,重复上述步骤;继续F9,会在VirtualProtected处断下,并且内存窗口3中的内容如下:
可以看见是 aPLib 压缩格式,将此内存区域的内存dump出来。
拖入010editor,搜索字符串MZ,可以发现存在PE文件。
将MZ前的数据全部删除,并保存,此时得到一个完整的PE文件,拖入PE Bear中查看导入表,可以发现网络相关的API都出现了。
7.2 分析解密算法
此时基本的脱壳已经完成,接下来寻找这个程序的C2配置,在IDA中打开dump_1.binv并点击ida中的Unexplored区域头部。
此处根据经验猜测 byte_10004010 为8字节密钥,unk_10004018 为加密后的数据,查看byte_10004010处的交叉引用,定位操作代码。
向上分析一下,可以判断sub_100011A4函数是申请堆内存,如图所示:
sub_10001214函数就是memcpy函数,如图所示:
此时代码变成如下:
sub_10002131函数中进行加解密操作,分析结果如下:
解密算法为:
使用SHA1对8字节的key进行hash,得到rc4的5字节密钥,再使用此密钥进行rc4解密得到C2配置。
信息整理如下:
1 |
|
7.3 编写自动化脚本
编写python3脚本实现自动提取C2配置:
1 |
|
执行结果如下:
7.4 额外收获
- 调试dll,使用rundll32.exe加参数的方式。
1 |
|
- python导入第三方库
1 |
|
- unbuntu解压带密码的zip
1 |
|