x64dbg-脱壳基础
9unk Lv5

目标程序

程序下载:

CRACKME

CRACKME UPX

解压密码:9unk

任务目标:学习了解脱壳步骤,以及理解 OEP、IAT 。

脱壳简介

一个未加过壳的程序修改起来很方便,但是如果一个加过壳的或者自修改的程序,要想修改就比较困难,我们在调试器默认分析的入口点处修改程序时不起作用的,只有当壳把原程序区段解密完成后修改才能起作用。加过壳的程序,原程序代码段通常是被加密过的,我们想修改它就不那么容易了。

加壳程序给目标程序加壳的原理通常是加密/压缩原程序各个区段,并且给目标程序添加一个或者多个区段作为原程序的引导代码(壳代码),然后将原程序入口点修改为外壳程序的入口点。如果我们将加过壳的程序用 x64dbg 加载的话, x64dbg 会停在壳的解密例程的入口点处,由此开始执行。

壳的解密过程(外壳程序)首先会定位加密/压缩过的原程序的各个区段,将其解密/解压,然后跳转至 OEP(程序未加壳时的入口点)处开始执行。

我们对比加壳前后的文件进行对比,可以发现加壳程序给原程序添加了额外的代码保护原程序,但是体积反而变小了。

3.jpg

我们先将不加壳的 crackme 加载到 x64dbg 中,我们可以看到其入口点是 401000,也就是说,运行它,将从 401000 地址处开始执行。

1.jpg

我们再运行加过壳的 crackme ,首先会从解密入口(409C80)开始执行,如下图所示。

2.jpg

我们继续尝试在加壳程序中定位到 401000 位置,会发现找不到原程序的代码任何踪迹。这里加壳程序将原程序的代码段加密/压缩保存到其他地方,并且将原程序代码段清空。

4.jpg

通常情况下,大部分加壳程序会待在加壳程序中创建自己的区段,从自己的区段开始执行解压/解密程序,解压/解密程序会将对原程序各个区段进行解压/解密。

我们在入口点一直向下拉,可以看到加壳程序的解密过程,知道看到 jmp crackme up.401000 说明此时程序已经解压/解密结束,加壳程序准备跳转到 OEP 处。

6.jpg

我们在跳转 OEP 处设置断点,并运行加壳程序。

7.jpg

8.jpg

我们继续跳转到 401000 处,可以看到此时程序已经解压修复完毕

9.jpg

以上就是加壳程序执行流程,大概步骤如下:

  1. 执行解压/解密程序

  2. 解压/解密原程序的各个区段的数据

  3. 跳往 OEP 处

  4. 执行原程序代码

通常脱壳的基本步骤

  1. 寻找 OEP

  2. 转储

  3. 修复 IAT(修复导入表)

  4. 检查目标程序是否存在 AntiDump 等组织程序被转储的保护措施,并尝试修复这些问题。

OEP 寻踪总结

单步跟踪法

主要使用 “F8” 和 “F4” 这两个快捷键,一步一步分析每一条汇编背后所代表的意思,将壳代码读懂,从而找到原始 OEP 然后脱壳。

ESP 定律法

一般加壳程序在运行时,会先执行壳代码,然后在内存中恢复还原原程序,再跳转到原始OEP,执行原程序的代码。这些壳代码首先会使用 PUSHAD 指令保存寄存器环境,在解密各个区段完毕,跳往 OEP 之前会使用 POPAD 指令恢复寄存器环境。

操作步骤

  1. 执行 pushad 指令,此时寄存器的值存储到栈顶,也就是 esp 的位置

1.jpg

  1. 右击 esp 寄存器,选择在内存中跳转。此时才内存窗口可以看到当前存储的寄存器值

2.jpg

  1. 对内存窗口中的第一个数据设置硬件访问断点,当程序执行 POP 指令进行断点时就会自动断下

3.jpg

  1. “F9” 运行程序后,程序断在如下位置。下面有个 jmp 指令,跳转到 OEP 处。

4.jpg

内存镜像法

在加壳程序执行时,会先将源程序的 “CODE” 和 “DATA” 区段解压\解密并载入内存,然后再载入 “rsrc” 资源到内存中,最后跳到 OEP 执行解密后的程序。内存镜像法就是在 rsrs 先设置一个内存执行断点,当程序停下来的时候说明程序已经解压\解密完成。此时再到 “DATA” 区段设置内存执行断点,程序下一次会停在 OEP 入口点。

操作步骤

  1. 载入 UPX 程序,使用快捷键 “Alt+M” 进入到内存视图

5.jpg

  1. 对 “.rsrc” 区段设置内存访问断点

6.jpg

7.jpg

  1. “F9” 运行upx程序到 “rsrc” 区段,此时前面两个区段已经解密好了。
    8.jpg

  2. 再次到内存视图,使用 “F2” 对 “CODE” 区段设置内存执行断点。

9.jpg

  1. 继续 “F9” 执行代码,此时可以看到程序停在了 OEP 入口点

10.jpg

一步到达 OEP

方法一

使用快捷键 “Ctrl+B” 搜索十六进制字符串 “E9 ?? ?? ?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00”,即可找到跳转 OEP 的位置。

11.jpg

12.jpg

13.jpg

方法二

使用快捷键 “Ctrl+F” 搜索 popad。找的 popad 需要满足,在程序返回时,壳程序希望恢复现场环境的地方。也就是靠近 jmp 和 return 的地方。

14.jpg

15.jpg

16.jpg

IAT(Import Address Tables)

什么是 IAT

我们知道每个 API 函数在对应的进程空间中都有其相应的入口地址。如下图展示了 win10 下 crackme.exe 程序的 MessageBoxA API 函数的入口地址。

10.jpg

可以看到我机器上的 MessageBoxA 这个 API 函数的地址是 75EE1930 ,对于部分人的机器来说 MessageBoxA 的入口地址是一样的,而另一部分人的机器上的 MessageBoxA 入口地址是不一样的,这取决于大家机器的操作系统版本(DLL版本也会更新),以及打补丁的情况。

DLL 版本的更新,其中包含的 API 函数入口地址也会改变

就拿这个 crackme 中的 MessageBoxA API 的入口地址来说,这个程序在我的操作系统上可以正常运行,但是将程序运行在其他不同版本的操作系统上运行,程序可能就会出错。为了解决这个兼容问题,操作系统就必须提供一些措施确保该 crackme 可以在其他版本的 windows 操作系统,以及不同 DLL 版本下也能正常运行。这时 IAT(Import Address Tables:输入函数地址表)就应运而生了。

我下面再展示一个 winxp 下 crackme.exe 程序的 MessageBoxA API 函数的入口地址。

11.jpg

可以看到在 winxp 系统下,MessageBoxA API 函数的入口地址是 77D507EA 。

IAT 定位

接下来探讨如何在脱壳过程中定位 IAT。这里用得是未加壳的 crackme.exe

首先通过右键汇编窗口选择 –> 搜索 –> 当前模块 –> 跨模块调用,看看主模块中调用了哪些模块以及 API 函数。

12.jpg

13.jpg

可以看到下面这 3 个地方调用了 MessageBoxA 函数

14.jpg

左键双击第一个 MessageBoxA ,x64dgb 自动跳到相应的汇编指令处。这里 jmp.&MessageBoxA 用的是尖括号,表示这是一个直接调用。

15.jpg

我们直接使用空格键,查看调用的地址是 0040143A

16.jpg

我们定位到 0040143A 处看到这是一个间接跳转

17.jpg

18.jpg

我们继续按空格查看间接跳转的地址,如下图所示我们可以看到这边的地址是 004031AC

19.jpg

我们看到 jmp dword ptr ds:[0x004031AC](004031AC 这个内存单元中保存的数值才是 MessageBoxA 真正的入口地址)。我们可以看到很多类似的间接 jmp 。

这就是为了解决各操作系统之间的兼容问题而设计的,当程序需要调用某个 API 时,都是通过间接跳转来调用的,读取某个地址中保存的 API 函数地址,然后调用它。

我们现在在数据窗口中定位到 004031AC 地址处,看看内存单元中存放的是什么。

20.jpg

这里我们可以看到 004031AC 中保存的是 75EE1930,这一片区域包含了该程序调用的所有 API 函数的入口地址,这一块区域我们称之为 IAT(导入函数地址表),这里就是解决系统间接调用 API 兼容问题关键所在。程序在不同版本的操作系统上都是调用间接跳转到 IAT 表中,在 IAT 中读入真正的 API 函数入口地址,最后调用它。这样就确保了不同版本系统调用都是正确的 API 函数。

IAT 填充函数入口地址原理

从上面的演示中已经理解了一个程序是如何定位到调用函数的入口地址:程序运行 –> 调用间接跳转 –> 间接跳转定位到 IAT 导入表(真正的函数入口地址)

但是函数入口地址又是如何填充到 IAT 中的呢?间接跳转的地址(如:004031AC )在不同版本的系统中是不一样的吗?

我们选中 4031AC 中保存的内容,右键 –> 复制 –> 文件偏移。将文件偏移地址复制出来(这里是 FAC)。

21.jpg

然后再使用十六进制编辑器查看该偏移地址(FAC)的内容

22.jpg

我们可以看到可执行文件对应的偏移内容是 60 33 00 00 ,当程序运行起来的时候,0FAC 这个文件偏移对应的虚拟地址会被填充为 30 19 EE 75,也就是说该 crackme 进程空间中的 004031AC 地址处会被填入正确的 API 函数地址。

我们看到 0FAC 文件偏移处的值是 3360,该数值其实是 RVA(相对虚拟地址),其指向对应的 API 名称。这里 3360 加上映像基地址即 403360,我们定位到 403360 处看看是什么。

23.jpg

这里我们可以看到指向的是 MessageBoxA 这个字符串,也就是说明操作系统可以根据这个指针,定位到相应的 API 函数名称,然后通过调用 GetProcAddress 获取相应的 API 函数的地址,然后将该地址填充到 IAT 中,覆盖原来的 3360 。这样就能保证在程序执行前,IAT 中被填充了正确的 API 函数地址。

总结

IAT 填充过程:程序中某段 FOA(文件偏移地址) 存放着一段指针数据,指向存放 API 字符串的虚拟地址(RVA)–> 然后通过调用 GetProcAddress 获取相应的 API 函数的地址 –> 将获取的 API 地址填充到 IAT 中(将原来的 API 字符串虚拟地址替换成真正的 API 地址)。

IAT 调用过程:程序运行 –> 调用间接跳转 –> 间接跳转定位到 IAT 导入表(真正的函数入口地址)

IAT 修复

IAT填充的这个过程很复杂,但是整个过程都是由系统自动完成的,在程序开始执行前,IAT 就已经被填入了正确的 API 函数地址。

也就是说,为了确保操作系统将正确的 API 填充到 IAT 中,应该满足一下几点条件:

  1. 可执行文件各 IAT 项所在的文件偏移处必须是指针,指向一个字符串

  2. 该字符串为 API 名称

如果满足这两项,就可以确保程序启动时,操作系统会将正确的 API 函数地址填充到 IAT 中。

加壳程序定位 IAT

我们下打开 Crackme UPX 这个程序

24.jpg

我们定位到 4031AC 处(原程序 MessageBoxA 入口地址)

25.jpg

值是空的,那么 403360 指向的字符串呢?

26.jpg

值也是空的,那么程序运行起来就会出错。

我们现在定位,并设置断点运行程序到 OEP 处

27.jpg

接着来看看 IAT

28.jpg

程序的解密子程序已经把正确的 API 函数地址填充到原程序的 IAT 中,如果现在将程序 dump 出来的话,运行会出错,因为 dump 出来的程序启动所需的数据时不完整的。

我们定位到 403360 处,会发现 API 字符串是空的

29.jpg

我们现在把程序 dump 出来看看程序能否正常运行

  1. 进入 OEP 入口(401000)

30.jpg

  1. 打开 Scylla 插件(快捷键:Ctrl+I)

31.jpg

32.jpg

reference

一步到达OEP脱壳

史上脱UPX最快 最方便 最简单的方法一步就到OEP

小Z带你学习什么是两次内存镜像法和什么是内存断点

参考自:从零开始 OlllyDBG-三十三章 [Ricardo Narvaja]

  • 本文标题:x64dbg-脱壳基础
  • 本文作者:9unk
  • 创建时间:2021-03-09 15:26:28
  • 本文链接:https://9unkk.github.io/2021/03/09/x64dbg-tuo-ke-ji-chu/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!