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中的一题的):
|
|
然后将文件丢到Linux中用其upx工具进行加壳(当然也可以在Windows下下载一些用于给pe文件加upx壳的工具,效果是一样的)
upx pe32.exe -o pe32_upx.exe
然后将文件取出拖入IDA可以看到,和编译前是完全不一样的
接下来就用OD加载开始脱壳,载入后开头一句pushad,执行这条语句后鼠标在ESP处右键 HW break [ESP],在这里实际上就是对0x61FF54下了一个硬件断点(详情可看上面52的讲解)
然后F9运行程序后就可以看到程序在某处执行了popad指令后停下了,然后再往下运行几行可以看到一个大跳转
在前面已经说过,upx壳程序在解压完毕后,会jmp到解压出来的代码段上运行程序真正的代码,因此在使用esp定律后,第一个大跳转往往就是OEP,这里我们可以看到EIP来到了这里
这里便是OEP了。关于OEP的判断大有文章可做,这里不做多解释,为了验证这里是OEP这一判断的正确性,我们可以用IDA打开那个没有加壳的程序
可以看到,没有加壳的程序入口处的汇编指令和我们当前OD断下的位置的指令是一样的,可以证明,这里就是OEP。
那么接下来就是要dump程序了,这里我踩了好多坑QAQ
OD -> Plugins -> Ollydump -> 脱壳在当前调试进程
这样我们就将脱壳后的文件dump下来了,但是这个程序的IAT是损坏的,我们需要用ImportREC去修复它。不要关闭OD,以管理员身份运行ImportREC,在进程列表中找到那个未被脱壳的程序进程后在OEP处填上正确的OEP(这里是14A0, 因为我们要减去基址)后 按图点击以下3个按钮。
然后在无效函数处右键删除无效指针 最后点击修复抓取文件,选中我们刚刚dump下来的pe文件即可。可以看到于其同目录生成了一个unpack_.exe文件,且这个文件双击后可以正常运行。脱壳完毕!
这里关于是否要重建输出表是个很玄学的问题,而且我用这里例子调试发现重建后表的程序,修复的时候用双击打开的exe修复IAT可运行 ;而用OD断点的exe修复不可运行
不重建的程序与重建的刚好相反,用双击打开的exe不行但用od断点的可以。然后我有尝试使用别的例子,发现有些例子重建表后不论怎么修复都无法运行。本人太菜想不通为什么,望知道的师傅解答。
PS:如果OD载入开头不是pushad,那可能是tlscallback被调试器拦截了,需自行下断运行至pushad处。也可使用IDA进行动调,其不会拦截tls (当然,这也是其不专业的地方,若是出题人在tls里动手脚,那IDA直接无解了) IDA下硬件断点也很简单,Ctrl+alt+B切至断点列表插入一个断点即可,后续和上述步骤相同
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:最好不要直接在该处下断点,测试发现下断点后再次运行程序似乎对别的机器码也会造成影响,导致代码识别错乱
同样的方法,可以看到第二次程序断死在了这里
步入后程序来到了这有三处小循环的地方,用好F2和F9成功跑过循环就可以看到程序call了[r15],其将程序带到了一个新的区段上 在这里已经可以看到程序的文件头了,执行ret后程序就到了OEP(0x400890)
接下来就是dump了,非常感谢一个师傅给我分享了elf64的dump脚本,这里贴上,只需要这时运行此脚本就可以dump下程序了
#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中,就可以看到非常清楚的逻辑了 脱壳完毕!