任务一
前面在恶意代码分析(一)中已经提到了加壳与脱壳,以及一些判断文件是否加壳的手段和使用upx脱壳,这个专题来继续研究加壳与脱壳
编写一个简单的C程序:
#include <stdio.h>
#include <windows.h>
int main() {
MessageBoxA(NULL, "Hello, Malware Analysis!", "Test", MB_OK);
return 0;
}
使用各种编译器将源程序编译
可用用MinGW-w64 或 Visual Studio Build Tools,当然Dev-C++、Code::Blocks、Visual Studio也是可
以的,这里我就用dev了。编译运行后效果如下图

接下来可以来记录编译生成的程序的各种信息
大小:

当然也可以powershell:
Get-Item .\original.exe | Select-Object Name,Length,CreationTime,LastWriteTime

MD5:
certutil -hashfile original.exe MD5

字符串:
strings64.exe -nobanner -a original.exe > strings.txt

节表信息:
丢进DIE

导入表信息:

接着对original.exe进行一个UPX加壳
upx -9 original.exe -o packed.exe

运行一下packed.exe,仍然弹出

说明加壳后的程序仍可正常执行
查看加壳后文件大小:
dir original.exe packed.exe

压缩率 = (原始文件大小 – 加壳后文件大小) / 原始文件大小 × 100%
压缩率为27%
DIE检查一下

有显示UPX加壳
任务二
对比original.exe和packed.exe的字符串
strings64.exe -nobanner -a original.exe > original_strings.txt

然后搜索关键字符串
findstr /i "Hello Malware Analysis Test MessageBoxA USER32 KERNEL32"
original_strings.txt > result.txt

然后可以提取 packed.exe 字符串
strings64.exe -nobanner -a packed.exe > packed_strings.txt

然后搜索:
findstr /i "Hello Malware Analysis Test MessageBoxA USER32 KERNEL32 UPX" packed_strings.txt > result_packed.txt

加壳后一般会看到:
UPX0
UPX1
UPX!
但是原来的:
Hello, Malware Analysis!
Test
MessageBoxA
可能看不到,或者可见信息明显减少
DIE看看

可以发现对比没有加壳的,新增 UPX0、UPX1 节,原来的 .text、.rdata、.data 等节消失、被合并或被压缩进 UPX 节,节表结构明显变化
IDA看看

pushad
…
循环解压
…
popad
jmp 原始入口点
或者类似
mov
lea
add
sub
xor
rep movsd
jmp
这些通常是:
- 保存寄存器现场
- 解压原始代码和数据
- 修复导入表
- 跳转到原始入口点 OEP
一般来说,UPX 常见入口代码特征是:
程序入口点位于 UPX1 或 UPX 相关节中
入口处不是业务逻辑,而是解压 stub
控制流看起来不像正常 C 程序
无法直接看到 MessageBoxA 调用
所以加壳之后对于分析难度增加一般有四个方面,首先是字符串被压缩。原始程序里能直接看到的字符串,加壳后可能看不到,或者可见性明显下降。其次是导入表变少或不完整。原始程序导入表可能能看到:
USER32.dll -> MessageBoxA
KERNEL32.dll -> ExitProcess
加壳后,导入表可能只剩下壳运行所需的一些 API,也可能看不到原始的MessageBoxA
接着是节表结构改变,原始节:.text .rdata .data .rsrc等等之类,加壳后就只有UPX0,UPX1 UPX2
最后就是IDA 中控制流的不直观了。IDA 打开original.exe ,入口点附近比较容易找到正常程序逻辑,但是 IDA 打开packed.exe ,入口点是壳代码,解压循环,跳转,内存写入,导入表修复
刚刚我们是给样本加了UPX的壳,现在我们自动脱了UPX的壳
upx -d packed.exe -o unpacked.exe

直接运行unpacked.exe,跳出弹窗,说明脱壳后程序功能正常

接着我们来对比一下压缩,解压缩,原样本的各种信息

可以看到packed.exe 比 original.exe 小,unpacked.exe 跟 original.exe一样,因为我们的样本比较小,加的壳也比较简单。一般来说unpacked.exe 会接近 original.exe,因为解压缩中途会丢失东西
计算一下MD5和sha256


original.exe 和 packed.exe 的哈希一定不同,original.exe 和 unpacked.exe 的哈希也通常不同。因为加壳再恢复后不一定完全恢复原始字节文件的字节级结构,比如说:
PE 头可能被重新构造
节表信息可能和 original.exe 不完全一致
节的 Raw Size、VirtualSize、对齐方式可能不同等等之类的
DIE对比一番

可以看到全都恢复了节表,各种节头都有了
字符串也对比一下
Strings>strings64.exe -nobanner -a C:\Users\27906\Desktop\tempcode\c_c++_temp\unpacked.exe > unpacked_strings.txt
进行搜索:
Strings>findstr /i "Hello Malware Analysis Test MessageBoxA USER32 KERNEL32" unpacked_strings.txt > result_unpacked.txt

packed.exe 中原始字符串可见性较低 unpacked.exe 中 Hello、Test、MessageBoxA 等字符串重新变
得更容易看到
任务三:脱壳后分析
脱壳后导入表分析
用 PE-bear、Detect It Easy 或 CFF Explorer 打开,unpacked.exe。进入Imports / 导入表,重点看有没有:
USER32.dll -> MessageBoxA
KERNEL32.dll

这里多提一下,如果是用 MinGW 编译,也可能看到:
msvcrt.dll
libgcc
KERNEL32.dll
USER32.dll
如果用 Visual Studio 静态链接,导入表可能会有差异,但关键还是看MessageBoxA 是否恢复可见
用IDA Pro 分析

可以看到unpacked.exe中的入口点应该更像正常程序初始化逻辑,可能经过 C Runtime 启动函数,再进入main或WinMain
用字符串定位 MessageBoxA 逻辑,在 IDA 里按Shift + F12 打开字符串窗口,搜索Hello, Malware Analysis!

然后按X查看交叉引用

找到引用这些字符串的函数后,通常就能看到类似
push 0
push offset Caption ; "Test"
push offset Text ; "Hello, Malware Analysis!"
push 0
call MessageBoxA

对比加壳前和加壳后:

任务四:手动脱壳原理探究
方法一:ESP / RSP 定律法
用 x64dbg 打开 packed.exe,打开 x64dbg:File -> Open -> packed.exe。加载后程序会停在入口点,现在看到的入口点一般不是原始程序逻辑,而是 UPX 壳代码

记录入口点处的 ESP / RSP
在右侧寄存器窗口找到:RSP RIP
记录下来RSP = 000000000062F4A8 RIP 0000000000423F1A
在栈地址上设置硬件断点
在寄存器窗口RSP里右键设置断点于RSP
运行直到断点触发
按F9,让程序运行;如果断点触发,说明程序已经运行到壳代码后半段,通常接近 OEP,这时不要继续直接 F9,应该单步观察

使用:F7:步入 F8:步过,观察附近代码
64 位程序,可能没有popad ,但要找的是:
从 UPX 壳代码区域跳转到原始代码区域的 JMP
判断方法:
当前代码在 UPX1 或壳代码区域;
JMP 目标跳到了另一个内存区域;
跳转后代码更像正常程序初始化逻辑;
跳转后能通过字符串交叉引用找到 MessageBoxA 逻辑。
通过寻找,找到了OEP所在位置

现在已经停在 OEP,内存中原始代码已经解压好了
可以看到
OEP VA = 0000000000401500
ImageBase = 0000000000400000
OEP RVA = 00001500
在 x64dbg 里打开 Scylla
选择当前调试进程packed.exe

修复导入表,依次点:
IAT Autosearch
Get Imports
Dump 保存为dump2.exe
Fix Dump 选择dump2.exe 输出一个新的.exe文件 这就是我们手动脱壳后的产物
如果 Get Imports 之后看到:
USER32.dll -> MessageBoxA
KERNEL32.dll

接下来就来验证手动脱壳。首先先双击手动脱壳后的样本

看起来没问题,接着用DIE去对比自动脱壳的.exe 看导入表,基本上已经恢复了之前设置的函数

用 strings 验证字符串,执行:
strings64.exe -nobanner -a dump2_SCY.exe > dump2_SCY_strings.txt
搜索:
findstr /i "Hello Malware Analysis Test MessageBoxA USER32" dump2_SCY_strings.txt > result.txt

这个壳就算是手动脱下来成功了
