借助 x64dbg 的 UPX 手工脱壳

背景介绍

比赛过程中,一部分 Re 题会被加 UPX 壳。正常情况下只需要用 upx -d /path/to/file 即可脱壳,但偶尔有题目使用魔改的 UPX 无法直接脱壳,这时就需要手工脱壳。本文记录了一种借助 x64dbgScylla 的手工脱壳方法。

魔改的UPX

准备

首先准备一个 Test 程序,初始情况下用 IDA 打开情况如下:

原始代码

现在使用 upx -9 /path/to/file 进行加壳,结果如下:

加壳后的DIE页面

使用 IDA 打开时会提示 Some imported modules will not be visible because the IAT is located outside of memory range of the input file. ,忽略后的结果如图:

加壳后的IDA页面

这样的代码完全无法阅读,字符串、函数信息等都已经没了。

下面开始手工脱壳。

脱壳

寻找入口点

首先要先在加壳后的程序中定位到原程序的入口点。使用 x64dbg 打开后直接运行一步,发现停在了 pushad 上。该指令将所有寄存器的值压栈,而在 UPX 的执行流程里,这一步之后会加载 UPX 的解压代码用于将原始程序解压。

upx 的工作原理其实是这样的:首先将程序压缩。所谓的压缩包括两方面,一方面在程序的开头或者其他合适的地方插入一段代码,另一方面是将程序的其他地方做压缩。压缩也可以叫做加密,因为压缩后的程序比较难看懂,主要是和原来的代码有很大的不同。最大的表现也就是他的主要作用就是程序本身变小了。变小之后的程序在传输方面有很大的优势。其次就是在程序执行时,实时的对程序解压缩。解压缩功能是在第一步时插入的代码完成的功能。联起来就是:upx 可以完成代码的压缩和实时解压执行。且不会影响程序的执行效率。

upx 和普通的压缩,解压不同点就算在于 upx 是实时解压缩的。实时解压的原理可以使用以下图形表示:

graph LR;
  1-->2-->3-->4-->5-->6;

假设 1 是 upx 插入的代码,2,3,4 是压缩后的代码。5,6 是随便的什么东西。程序从 1 开始执行。而 1 的功能是将 2,3,4 解压缩为 7,8,9。7,8,9 就是 2,3,4 在压缩之前的形式:

graph LR;
  1-->7-->8-->9-->5-->6;

连起来就是:

graph LR;
  1==>2-.->3-.->4-.->5;
  7==>8==>9==>5==>6;
  2-->解密-->7;
  3-->解密-->8;
  4-->解密-->9;

参考文献:upx 加壳原理_zacklin 的专栏 - CSDN 博客

所以我们只要跟踪到这部分栈被弹出,就意味着解压完成。之后的代码就是原始代码。

那么如何跟踪这部分栈呢?只需要先 F7 步进, pushad 执行完成后对当前栈顶的内存地址下一个硬件断点即可。

硬件断点

然后 F5 继续执行。再次中断处如图:

中断

可以看到 EIP 上方就是 popad ,说明 UPX 的解压过程已经结束,现在在进行一些清理工作。

看到下方 00D0180000D01806 的代码,它们在将缺失的栈段空间补齐,会循环若干次。故在 00D01809 这个 jmp 上下断点。执行到该语句后再 F7 步进一次,到达其跳转后的地点:

入口点

这里就是我们要找的入口点。

脱壳

找到入口点以后使用 x64dbg 自带的 Scylla 进行脱壳。

直接使用 IAT Autosearch 功能,期间可能会提示使用 IAT Search Advanced ,选 “是” 继续。然后 Get Imports ,最后直接 Dump

Dump 出来的文件使用 IDA 打开,可能依旧会提示 IAT表 缺失,不管它,直接看 main 函数。

脱壳后代码

Done 😃


借助 x64dbg 的 UPX 手工脱壳

https://potat0.cc/posts/20210617/UPX_Manual_Unpack/

作者

Potat0

发布于

2021年6月17日

更新于

2021年7月22日

许可协议