upx壳与脱壳技巧

Posted by Qfrost on 2020-02-17
Estimated Reading Time 8 Minutes
Words 2.2k In Total
Viewed Times

0x00 UPX Introduction

假期在家里做了一下各类upx壳和脱壳方法的整理,这里做一个分享。
广义来说壳主要分两种:压缩壳与加密壳 即缩小文件体积和加密代码以提高逆向难度。 UPX、ASPack等壳均属于压缩壳,其可将文件体积缩小50%-70%。而本文讲的UPX因其逆向难度过于简单,故妄图用UPX达到保护代码的效果几乎不存在,但脱壳的思想确实是值得思考的。
其实现压缩的方法是在程序原先代码之前再插入一段代码,因此在加壳程序运行前这段代码会先被运行,其作用则是解压缩加壳程序的代码到某一内存段或临时文件上,在全部解压缩完毕后跳至该内存段执行程序的真正代码,通常的,称这个入口为OEP。因为upx壳具有先解压缩后执行程序的特点,那么我们在加壳程序运行至解压缩完毕时暂停程序(即动调断点到OEP),将已经完成解压缩的代码段或临时文件抠出来生成一个新的文件,那么这个文件将不再带有壳了。

0x01 ESP Law

ESP脱壳定律是最著名的最常用的脱壳方法,其核心思想是利用堆栈平衡原理,在程序进入OEP时需恢复栈,从而断点。换成人话讲就是…可以看一下52的这个帖子,我认为讲的非常通俗:
https://www.52pojie.cn/thread-394116-1-1.html

那么我们实际演示一下吧,在Windows下编译这个代码(因为懒,逻辑和flag用了hgame中的一题的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// gcc -s -no-pie -m32 -static -o pe32.exe src.cpp   UPX无法处理40Kb以下的二进制文件 需使用静态编译    -s参数可以删除符号表
#include<stdio.h>
unsigned char ciper[43] = {0x68, 0x68, 0x63, 0x70, 0x69, 0x80, 0x5B, 0x75, 0x78, 0x49,
0x6D, 0x76, 0x75, 0x7B, 0x75, 0x6E, 0x41, 0x84, 0x71, 0x65,
0x44, 0x82, 0x4A, 0x85, 0x8C, 0x82, 0x7D, 0x7A, 0x82, 0x4D,
0x90, 0x7E, 0x92, 0x54, 0x98, 0x88, 0x96, 0x98, 0x57, 0x95,
0x8F, 0xA6, 0x00};
int main(){
char flag[43]; // hgame{Unp@cking_1s_R0m4ntic_f0r_r3vers1ng}
printf("Input your flag:\n");
scanf("%s", flag);
for(int i=0;i<42;++i){
if(flag[i] + i != ciper[i]){
printf("Wrong!\n");
// printf("%d %d", i, flag[i]);
return 0;
}
}
printf("Success!\n");

}

然后将文件丢到Linux中用其upx工具进行加壳(当然也可以在Windows下下载一些用于给pe文件加upx壳的工具,效果是一样的)

1
upx pe32.exe -o pe32_upx.exe

然后将文件取出拖入IDA可以看到,和编译前是完全不一样的
pe32_1.png

接下来就用OD加载开始脱壳,载入后开头一句pushad,执行这条语句后鼠标在ESP处右键 HW break [ESP],在这里实际上就是对0x61FF54下了一个硬件断点(详情可看上面52的讲解)
pe32_2.png

然后F9运行程序后就可以看到程序在某处执行了popad指令后停下了,然后再往下运行几行可以看到一个大跳转
pe32_3.png

在前面已经说过,upx壳程序在解压完毕后,会jmp到解压出来的代码段上运行程序真正的代码,因此在使用esp定律后,第一个大跳转往往就是OEP,这里我们可以看到EIP来到了这里
pe32_4.png

这里便是OEP了。关于OEP的判断大有文章可做,这里不做多解释,为了验证这里是OEP这一判断的正确性,我们可以用IDA打开那个没有加壳的程序
pe32_5.png

可以看到,没有加壳的程序入口处的汇编指令和我们当前OD断下的位置的指令是一样的,可以证明,这里就是OEP。

那么接下来就是要dump程序了,这里我踩了好多坑QAQ
OD -> Plugins -> Ollydump -> 脱壳在当前调试进程
pe32_6.png

这样我们就将脱壳后的文件dump下来了,但是这个程序的IAT是损坏的,我们需要用ImportREC去修复它。不要关闭OD,以管理员身份运行ImportREC,在进程列表中找到那个未被脱壳的程序进程后在OEP处填上正确的OEP(这里是14A0, 因为我们要减去基址)后 按图点击以下3个按钮。
pe32_7.png
然后在无效函数处右键删除无效指针 最后点击修复抓取文件,选中我们刚刚dump下来的pe文件即可。可以看到于其同目录生成了一个unpack_.exe文件,且这个文件双击后可以正常运行。脱壳完毕!
pe32_8.png

这里关于是否要重建输出表是个很玄学的问题,而且我用这里例子调试发现重建后表的程序,修复的时候用双击打开的exe修复IAT可运行 ;而用OD断点的exe修复不可运行
不重建的程序与重建的刚好相反,用双击打开的exe不行但用od断点的可以。然后我有尝试使用别的例子,发现有些例子重建表后不论怎么修复都无法运行。本人太菜想不通为什么,望知道的师傅解答。

PS:如果OD载入开头不是pushad,那可能是tlscallback被调试器拦截了,需自行下断运行至pushad处。也可使用IDA进行动调,其不会拦截tls (当然,这也是其不专业的地方,若是出题人在tls里动手脚,那IDA直接无解了) IDA下硬件断点也很简单,Ctrl+alt+B切至断点列表插入一个断点即可,后续和上述步骤相同
pe32_9.png

0x02 单步跟踪法

对于64位的程序,是不存在esp定律的,从而也就无法使用其进行脱壳。因此对于64位的程序,往往使用单步跟踪法。其原理也是先跑完程序的解密解压缩部分,后找到OEP进行dump。
单步跟踪法的操作更为简单,先F8一直往下跑,在某处call跑飞的话记下该处的位置重启程序,再运行到那里时再F7步入。但是对于一些循环,要熟练使用F4 F9等键进行快速跑过。对于一般的upx加壳程序,往往只需要步入三四次即可成功到达OEP。这里使用2020 hgame的unpack这题作为例子。

这是一道非常典型的elf64位的upx加壳题,出题人修改了分区表,使其不能被upx自动脱壳工具识别,因此需要手动脱壳。

将程序载入IDA,配置好远程动调,启动调试直接步入第一个函数,然后一路F8就可以看到程序断在了这个位置,且远程提示我们输入flag,说明OEP存在与断死的这个函数内,我们需要F7步入后分析(因为提示我们输入flag必然在OEP之后)。所以我们记下该处位置,Ctrl+F2重启程序
PS:最好不要直接在该处下断点,测试发现下断点后再次运行程序似乎对别的机器码也会造成影响,导致代码识别错乱
elf64_1.png

同样的方法,可以看到第二次程序断死在了这里
elf64_2.png

步入后程序来到了这有三处小循环的地方,用好F2和F9成功跑过循环就可以看到程序call了[r15],其将程序带到了一个新的区段上 在这里已经可以看到程序的文件头了,执行ret后程序就到了OEP(0x400890)
elf64_3.png
elf64_4.png

接下来就是dump了,非常感谢一个师傅给我分享了elf64的dump脚本,这里贴上,只需要这时运行此脚本就可以dump下程序了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <idc.idc>
#define PT_LOAD 1
#define PT_DYNAMIC 2
static main(void)
{
auto ImageBase,StartImg,EndImg;
auto e_phoff;
auto e_phnum,p_offset;
auto i,dumpfile;
ImageBase=0x400000;
StartImg=0x400000;
EndImg=0x0;
if (Dword(ImageBase)==0x7f454c46 || Dword(ImageBase)==0x464c457f )
{
if(dumpfile = fopen("D:\\DumpFile","wb"))
{
e_phoff=ImageBase+Qword(ImageBase+0x20);
Message("e_phoff = 0x%x\n", e_phoff);
e_phnum=Word(ImageBase+0x38);
Message("e_phnum = 0x%x\n", e_phnum);
for(i=0;i<e_phnum;i++)
{
if (Dword(e_phoff)==PT_LOAD || Dword(e_phoff)==PT_DYNAMIC)
{
p_offset=Qword(e_phoff+0x8);
StartImg=Qword(e_phoff+0x10);
EndImg=StartImg+Qword(e_phoff+0x28);
Message("start = 0x%x, end = 0x%x, offset = 0x%x\n", StartImg, EndImg, p_offset);
dump(dumpfile,StartImg,EndImg,p_offset);
Message("dump segment %d ok.\n",i);
}
e_phoff=e_phoff+0x38;
}

fseek(dumpfile,0x3c,0);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);

fseek(dumpfile,0x28,0);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);
fputc(0x00,dumpfile);

fclose(dumpfile);
}
else Message("dump err.");
}
}
static dump(dumpfile,startimg,endimg,offset)
{
auto i;
auto size;
size = endimg-startimg;
fseek(dumpfile,offset,0);
for ( i=0; i < size; i=i+1 )
{
fputc(Byte(startimg+i),dumpfile);
}
}

dump后的程序载入IDA中,就可以看到非常清楚的逻辑了 脱壳完毕!
elf64_5.png