UPX是一个压缩壳,在CTF中的应用大部分也仅仅是对程序套个壳,增加一下对反编译的时间,有时还会魔改一下但问题也不是很大改回来就行。
魔改标志位
这是一个正常壳映射到内存中的示意图
再来看看压缩壳的思路
| PE头 |
+------------------------+
| .text节 | <- 程序代码
+------------------------+
| .data节 | <- 数据
+------------------------+
| .rdata节 | <- 只读数据
+------------------------+
| 其他节 |
+------------------------+
+------------------------+
| 新PE头 |
+------------------------+
| UPX0节 | <- 压缩后的数据 +------------------------+
| UPX1节 | <- 解压代码
+------------------------+
| 预留节 | <- 解压目标空间
+------------------------+
graph TD
A[程序启动] --> B[进入壳入口点]
B --> C[获取关键数据结构]
C --> D[定位压缩数据]
D --> E[申请内存空间]
E --> F[解压数据]
F --> G[修复PE结构]
G --> H[修复重定位]
H --> I[修复导入表]
I --> J[跳转到OEP]
1.初始化阶段
-
获取当前进程信息
-
定位关键数据结构
-
初始化解压环境
2.内存准备
[申请内存]
VirtualAlloc(NULL,需要的大小,MEM_COMMIT |
MEM_RESERVE,PAGE_EXECUTE_READWRITE)
[设置内存属性]
VirtualProtect(内存地址, 大小, 新属性, &旧属性)
3.解压过程
源数据(压缩) -> 解压算法 -> 目标位置(解压)
+----------------+ +----------+ +----------------+
| UPX0节压缩数据 | --> | 解压算法 | --> | 预留节解压空间 |
+----------------+ +----------+ +----------------+
4.修复阶段
[PE头修复]
-
修正SizeOfImage
-
修正SectionAlignment
-
修正FileAlignment
[重定位修复]
for each 重定位项:
if 类型 == IMAGE_REL_BASED_HIGHLOW:
*(DWORD*)重定位地址 = 新基址 + (*(DWORD*)重定位地址 - 旧基址)
[导入表修复]
for each 导入DLL:
LoadLibrary(DLL名)
for each 导入函数:
GetProcAddress(函数名)
填写IAT
5.执行转换
[跳转到OEP]
push OEP地址
ret
5.内存布局变化
加壳前:
高地址
| 其他节 |
+------------------+
| .data |
+------------------+
| .text |
+------------------+
| PE头 |
+------------------+
低地址
加壳后:
高地址
+------------------+
| 预留节 | <- 解压目标
+------------------+
| UPX1 | <- 解压代码
+------------------+
| UPX0 | <- 压缩数据
+------------------+
| PE头 |
+------------------+
低地址
解压后:
高地址
+------------------+
| 原始程序 | <- 已解压
+------------------+
| UPX1 |
+------------------+
| UPX0 |
+------------------+
| PE头 |
+------------------+
低地址
6.关键数据结构
typedef struct {
DWORD OEP; // 原始入口点
DWORD ImageBase; // 映像基址
DWORD ImportTable; // 导入表RVA
DWORD RelocationTable; // 重定位表RVA
DWORD CompressedData; // 压缩数据位置
DWORD CompressedSize; // 压缩数据大小
DWORD UncompressedSize; // 解压后大小
} UPX_SHELL_DATA;
这是UPX壳的完整工作流程,实际实现时还需要考虑:反调试保护,代码混淆,异常处理,资源处理,内存对齐,安全性检查。 脱壳步骤:ESP定律脱壳,本质就是利用堆栈平衡,当子程序结束时要返回父程序时会ESP要在保证在RET这条指令之前,ESP指向的是我们压入栈中的地址,也就是说当UPX在自解压完成后我们只要找到这个地址就行, 示例: 压栈地址
当自解压完成后也会到达这里所以在这下个内存断点
在内存窗口中转到
运行到这,之后只要dump出来就行
这就完成了手工脱壳了,比较简单。
注:鼎星安全有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
原文始发于微信公众号(鼎新安全):细讲UPX壳
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论