UPX脱壳


简介

upx 壳是一种压缩壳,在 CTF 比赛中比较常见。

针对 upx 壳有专业的脱壳工具,一般我们可以直接使用工具脱壳。

但是在比赛中往往不会让你那么简单就把壳脱掉,出题人会通过修改加壳程序的一些特征导致脱壳工具无法使用。

这个时候我们可以通过恢复修改掉的地方让工具继续奏效。

当然我们也可以直接手脱,手动脱壳我们就需要找到加壳程序的 OEP 然后 dump 文件内存。

OEP 原始程序入口点。EP(Entry Point),意即程序的入口点。而 OEP 是程序的原始入口点,一个正常的程序只有 EP,只有入口点被修改的程序(加壳),才会拥有OEP。

工具脱壳

UPX(Ultimate Packer for eXecutables)是一个开源的可执行文件压缩器,用于减小可执行文件的大小,同时保持其功能。UPX 支持多种操作系统,包括 Linux、Windows 和 macOS。

upx 脱壳工具使用请参考这篇文章。

upx 脱壳工具使用:【逆向】UPX工具使用及加壳_upx.exe-CSDN博客

常规upx工具脱壳

直接使用 upx 脱壳工具进行脱壳

1
2
3
4
#exe文件
upx -d demo.exe
#elf文件
upx -d demo

例题:[SWPUCTF 2023 秋季新生赛]UPX

  • 分析

exeinfo 查看发现有 upx 壳。

  • 脱壳

直接使用 upx -d 命令进行脱壳。

可以看到脱壳成功。

exeinfo 查看确认一下

确认脱壳成功

ida 打开查看反编译代码,发现 flag

upx魔改壳

常规 upx壳 只要利用 upx 脱壳工具直接执行 upx -d 命令即可脱壳。

不过只要做一些简单的修改就可以让 upx 工具失效。

这种魔改壳在 CTF 中很常见。

  1. 修改区段名

​我们先来查看一下未修改前的文件信息。

image-20240529102451097

可以看到图中显示了 upx1 和 3.91 upx 这两个特征标识。

image-20240529102654744

然后查看一下加壳后的区段窗口,发现只有三个区段。最明显的特征upx0upx1.

我们只需要修改它,就可以让 upx 脱壳工具无法脱壳。

我们利用 010 Editor 编辑文件将upx改成rpx

image-20240529103143695

image-20240529102913143

尝试脱壳

​结果:

image-20240529103042290

​可以看到失败了。

​只要改回原样就可以进行脱壳。

  1. 修改标识

image-20240529103242207

​可以看到 3.96.UPX! 特征码。我们可以修改 3.96 版本号开始24个字节的内容。都不会对程序运行产生影响。

​修改标识之后,upx 脱壳工具就无法脱壳。不过可以利用 UPX Unpacker for Dummies 工具进行脱壳也可以直接手脱。

后面我们会讲如何手动脱壳。

例题:[LitCTF 2024]hello_upx

  • 分析

exe文件,查壳发现存在upx壳。

image-20240630175611

直接使用脱壳机,脱壳失败。

image-20240630175643

提示信息文件被修改,用010_Editor打开

发现有四处地方被修改

image-20240630175933

这里我们列出正常 upx 加壳文件的区段信息进行对比

image-20240630175910

UPX0UPX1是加 UPX 壳后的两个区段名。其中 UPX1 区段包含了需要解压的数据块。
.rsrc是程序资源信息区段名,这个区段含有原资源段的完整头部以及图标、Manifest、版本等未被压缩的资源,当然还有 UPX 自身需要的导入信息等(如果程序自身不含资源段,加壳后就是 UPX2)

分析修改

  1. upx0被改成了小写
  2. upx1被改成了小写
  3. upx2被改成了小写
  4. upx!被改成了小写
  • 恢复

将upx0、upx1、upx2和upx!全部修改为大写

image-20240630192214.png

  • 脱壳

image-20240630180900

可以看到脱壳成功。

分析代码逻辑

image-20240630181003.png

  • exp

根据代码逻辑构造exp

1
2
3
4
5
6
7
data=[0x4C, 0x68, 0x72, 0x40, 0x50, 0x41, 0x75, 0x70, 0x2B, 0x63, 0x59, 0x25, 0x61, 0x58, 0x51, 0x65, 0x20, 0x4E, 0x5A, 0x1E, 0x60, 0x4E, 0x5E, 0x4F, 0x65]
flag=""

for i in range(len(data)):
flag+=chr((data[i]+i))
print(flag)
#LitCTF{w3lc0me_t0_l1tctf}

手脱

手动脱壳的目标:找到原始程序入口地址(OEP)。

EXE

自己写的文件加壳,手脱。

入口不是pushad,只能一步一步单步步过。

pushad意味着 upx 壳解压缩代码的入口。
64位程序中没有puahad,而是用几个push汇编代码替代。32位程序中存在pushad

遵循单步定律,向下跳转允许实现,向上跳转不允许实现。

向下的红色小箭头就是向下的跳转,线为红色即是跳转成立,线为白色就是跳转不成立。
同理向上的红色小箭头就是向上的跳转。

一直单步步过,直到发现这样的多个push指令为止。

很明显这里就是 UPX 解压缩代码的入口点。

根据esp定律下断点寻找OEP

f8 单步一下执行一下,让rsp发送变化

查看寄存器窗口,发现rsp产生了变化

右击rsp,选中在栈中转到。

右击栈顶,选中断点,选择硬件访问断点,选择4字节。

设置硬件断点后 f9 运行。

之后发现下面有一个比较大幅度的jmp跳转,并且已经显示main特征。

判断这个特征会跳转到 OEP。

选中jmp指令,然后 f4 运行到此处。

之后直接单步执行就会跳到 OEP。

OEP:

接下来 dump 文件

点击 Scylla 插件,然后点击 dump 之后将文件保存。

但是这样 dump 后的文件是无法运行的,所以我们要修复文件。

  • 修复文件

先点击 Scylla 中的 IAT Autosearch,然后点击 Get Imports 。

,在 Imports 列表中右键 delete 删掉带有红叉的。

之后点击 Fix Dump 选中之前的 Dump 文件修复即可。

脱壳之后运行程序测试。

成功执行程序,dump文件成功。

exeinfo 确认脱壳

发现仍然保留特征信息

ida打开确认

确认已脱壳

ELF

例题

自己写一段代码编译加壳。

1
2
3
4
5
6
7
8
#include<stdio.h>
int main() 1 {
char buf[10]={0};
puts("input:");
read(0,buf,10);
printf("%s",buf);
return 0;
}

编译,这里需要静态编译,要不然文件太小加不了壳。

加壳,可以看到加壳成功

exeinfo 扫一下,可以看到显示 upx 壳

  • 寻找OEP

ida打开文件

直接在start启动函数位置下断点,然后动态调试。

发现ret指令直接 F4 然后 F7 进入,直到发现endbr64指令(即源程序代码开头)。

下翻,翻到ret指令直接 f4 运行到这里,然后 f8 单步。

重复以上过程,如果没找到ret指令就执行最近的jmp跳转。

当看到下面这个syscall汇编中断代码时我们就快到 OEP 了。

之后 f4 运行到ret位置,之后继续单步。

  • 跳转到 OEP

看起来都是数据,按快捷键 c 将数据解释为代码。

之后看到endbr64,elf64 位程序的入口。

然后查看程序代码,发现将byte_401775函数地址送入rdi

由它来调用初始化程序,进而调用main函数。

这里的byte_401775就是原程序main函数。

进入main函数查看

按快捷键c解释为代码,然后在endbr64处按快捷键p创建函数。

之后就可以f5反编译了。

main函数反编译代码

CTF 中一般就可以在这里分析程序代码解题了。

不过接下来我们要 dump 文件。

  • dump内存到文件

先回到 OEP 这里。

确保rip执向endbr64汇编代码。

alt+f7 快捷键打开并运行脚本(脚本放后面了)。

dump64 位程序选用 64 位文件。

执行,等待执行完毕。

执行完毕

dump 文件的存放路径在脚本中设置。

执行文件

可执行,dump文件成功。

exeinfo扫一下,确认脱壳。

显示无壳,则脱壳成功。

idc dump内存文件代码

感谢大佬的代码

  • 64位程序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
#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("G:\\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);
}
}
  • 32位程序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
#include <idc.idc>
#define PT_LOAD 1

#define PT_DYNAMIC 2
static main(void)
{
auto ImageBase,StartImg,EndImg; //基址 08048000
auto e_phoff;
auto e_phnum,p_offset; //paddr 0xc 地址,pmemsz ox14大小,p_offset 0x4
auto i,dumpfile;
ImageBase=0x08048000;
StartImg=0x08048000;
EndImg=0x0;
Message("%8x\n",Dword(ImageBase));
if (Dword(ImageBase)==0x7f454c46 || Dword(ImageBase)==0x464c457f )
{
if(dumpfile=fopen("G:\\dumpfile","wb"))//这里可以更改路径
{
e_phoff=ImageBase+Word(ImageBase+0x1c);
e_phnum=Word(ImageBase+0x2c);
for(i=0;i<e_phnum;i++)
{
if (Dword(e_phoff)==PT_LOAD || Dword(e_phoff)==PT_DYNAMIC)

{ p_offset=Dword(e_phoff+0x4);
StartImg=Dword(e_phoff+0xc);
EndImg=Dword(e_phoff+0xc)+Dword(e_phoff+0x14);

dump(dumpfile,StartImg,EndImg,p_offset);
Message("dump LOAD%d ok.\n",i);
}

e_phoff=e_phoff+0x20;
}
fseek(dumpfile,0x30,0);
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);
}
}

后言

参考链接:手动去upx特征_upx -d-CSDN博客
参考链接:linux 下 upx 脱壳笔记
参考链接:[三叶草二进制招新培训](三叶草二进制招新培训9 - UPX脱壳_哔哩哔哩_bilibili)
参考链接:如何用x64dbg UPX手动脱壳(64位)