# 浅谈被加壳ELF的调试

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-05-20T08:33:00Z
Canonical: https://wepostx.com/topics/824

> 乌云历史资料归档
>
> **原始作者：** 腾讯电脑管家
> **原始编号：** superkieran-wooyundrops:571
> **原始发布时间：** 2015-05-20 16:33
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 ELF格式简介：

注：本文只讨论如何调试被加壳的ELF文件，包括调试中的技巧运用及调试过程中可能遇到的问题的解决方法，不包含如何还原加固的DEX

本文将以某加壳程序和某加固为目标

ELF全称：Executable and Linkable Format，是Linux下的一种可执行文件格式。 此种文件格式和WINDOWS一样，常见分为两种类型：

- 可执行文件(Executable File)，对应PE子类型：EXE

- 共享目标文件(Shared Object File)，后缀名：.so，对应PE子类型：DLL

# 0x01 ELF文件加载大概流程：

- 通过Section Header或者Program Header加载需要的镜像数据 和WINDOWS PE加载机制不同，ELF有些文件数据是不会加载到内存镜像中

- 加载SO NEED LIB和SYM 类似PE的Import_table

- 执行重定位(如果有) 类似PE的Reloc_table

- 执行INIT_ARRAY段或者INIT段（如果有，数组中的地址不等于0xffffffff，0表示结束）

类似PE的TLS，已知的被加壳的ELF，壳代码都出现在这两个段里 PE中的TLS也常常出现在壳中，例如Vmprotect, Execryptor等

- 执行入口点代码（如果有）

所有的上述加载流程代码全部包含在linker.so里，可参看安卓源码或逆向linker.so

# 0x03 ARM CPU简介： 1.指令集简介：

ARMCPU采用RISC（精简指令集）架构（X86是CISC，复杂指令集），指令等长，相对CISC架构更加省电，执行效率更高

2. ARM指令集三种类型：

ARM(4字节等长)，THUMB(2字节等长)，THUMB2(4字节等长)。这三种指令集可以在同一执行程序中切换，切换原则为：

ARM THUMB，THUMBARM（PC的最高位确定指令集类型：1为THUMB；0为ARM）

THUMBTHUMB2（由27-31位决定）

thumb2其实就是thumb的扩展，其目的是为了一条4字节指令完成多条2字节指令

*|3.寄存器: *|

通用寄存器：r0-r15

特殊寄存器：r13 = SP（栈地址）, r14 = LR（函数返回地址）, r15 = PC（当前指令流地址）

还包括CPSR，APSR，浮点等寄存器，具体可参照ARM指令集手册

# 0x04 加壳SO的调试

1.ELF代码执行顺序

上面介绍了，加壳的SO的壳代码都在INIT_ARRAY段和INIT段

那我们先看下一个被加壳以后的SO，在linux下面用readelf -a命令查看ELF信息

![原文图片](/media/2016/06/5359e7ece1f1697f937ad6b2adb26b98)

![原文图片](/media/2016/06/95b09bc7b588a0dc9d628215e653b8b4)

在ELF-HEADER里我们看到有Entry，地址为0x22a8

在动态段里看到了INIT_ARRAY数组，数组地址为0x21000，大小是12 BYTES

用IDA看看数组的内容

![原文图片](/media/2016/06/1e369c704cc9ac83bc3d6c073ac0222d)

上面说了，-1为无效，0代表结束，那INIT有效地址仅是0x2418这个地址

也就是说，这个SO加载起来以后，会先从0x2418这个地址开始执行，执行完成后再去执行Entry

我们再来看下某加固的ELF信息

![原文图片](/media/2016/06/f36e6cbcb1cc3947847131a744ec27d4)

![原文图片](/media/2016/06/f8053e2b8360772da857b3edd6d1480c)

在ELF-HEADER里我们看到有Entry，地址为0x3860

![原文图片](/media/2016/06/fd92a3b91a51fee3d18ef6bea752b7e2)

DA打开某加固的文件时，会提示错误，不能打开，后面在Anti Anti Debugger中会讲到为什么

在动态段里看到了INIT_ARRAY数组，数组地址为0x28CA4，大小是8 BYTES

我们还看到了INIT段，地址是0x11401

我们在这里，总结下执行的顺序：

根据linker的代码，当INIT段和INIT_ARRAY段都存在的情况下，先运行INIT段，再运行INIT_ARRAY段，否则单独运行对应指向的函数，最后执行ENTRY

![原文图片](/media/2016/06/93df40d534feb54d9e10b47a88fb3a1d)

2. 自己准备SO_LOADER

调试SO和PE_DLL其实道理是一样的，都需要一个宿主进程，这里要写一个SO_LOADER，参看代码如下，可通过NDK编译。（代码是王晨同学早期提供的）

![原文图片](/media/2016/06/c31a8465d8703b740c77846e1e833213)

3.环境准备：

我这里选用IDA6.6做为调试器。

第一步：拷贝调试器到安卓手机上

![原文图片](/media/2016/06/c4e57f506dc134933e391c749ce5f6f8)

命令：adb push android_server /data/local/tmp/and

这里为什么把android_server 改名成and呢~~~，其实就是为了避免被检测出调试器，后面我在Anti Anti Debugger中会详细说一下关于这部分的内容

第二步：启动调试器

adb shell回车，进入/data/local/tmp/目录，启动调试器，启动后画面

![原文图片](/media/2016/06/a78ac3831a7bd5ab98f61d1bd4c674ba)

第三步：重定向调试端口

adb forward tcp:23946 tcp:23946

至此手机端设置完毕，下面来看看IDA里如何设置

IDA加载我们自己写的so_loader，在854C处，按F2下断点

![原文图片](/media/2016/06/b7f09534c213b4b96a292b92dae0284e)

选择菜单栏里面的Debuger-Select Debugger，选择Remote Arm linux/Andoid debugger

![原文图片](/media/2016/06/eadafd25bb4acffcfee2d764bfd28f6d)

点击OK，然后F9运行

![原文图片](/media/2016/06/d17420f4611d35e1377e81ad682bc10a)

在配置里面，Hostname里面填入127.0.0.1，点击OK

如果你的手机里面没有这个文件，会提示你COPY，点击确定即可，如果有这个文件，会出现下面的选择，一般选择USE FOUND就可以了，如果你要调试的程序有修改，选择COPY NEW覆盖一个新的进去

![原文图片](/media/2016/06/3b9dd69175020d9ec76bcb2081294dd3)

然后一路OK，就出现调试状态了~~，当前PC就是刚才我们F2设置的断点

![原文图片](/media/2016/06/2f6b26ec42e515423f660c7d83638543)

4.如何断住SO的INIT_ARRAY段和INIT段

上面说了，SO的加载在linker.so里完成，我们要做的，就是把断点设置在linker.so里面

先找代码，IDA打开linker.so，在string窗口里找

call_constructors_recursive,双击并查看引用

![原文图片](/media/2016/06/bd1567afd3318713bbc0a8be3e3318f6)

![原文图片](/media/2016/06/d7d42c8f41f2eba5d26635a4d5a6e8b0)

双击第二个引用处，然后往上找blx r3(init段的调用)，b.w xxxxxxxx(init_array段的调用)

![原文图片](/media/2016/06/b3b05e052ce367d122f56fe2c5172038)

![原文图片](/media/2016/06/6396ef8aae5a1632efd2986ecbc46791)

好了，现在我们找到了地址0x54d0, 0x3af0这两个地方，回到刚才调试的IDA里面，

选择菜单栏debugger-Debugger windows-module list打开进程模块列表

![原文图片](/media/2016/06/866bfa27834f3c26744991c650a28332)

linker.so的base是40002000,分别对应的两个地址就是

```text
0x40002000+0x54d0 = 0x400074d0 0x40002000+0x3af0 = 0x40005af0
```

我们在IDA View-PC窗口GO 过去

![原文图片](/media/2016/06/b7ffa83874af24b961f61c843f6be308)

在0x74d0处，按C键，变成代码.奇怪，为什么没有反应！！而且在最下面的output window有如下提示

![原文图片](/media/2016/06/17702815421dc0bed4ecba3273be62a0)

这里就是我说的很重要的问题了，上面我提到了，被调试的程序可以在3种指令集之间切换，这时的IDA并不知道当前要变成代码的地址是ARM还是THUMB，这时我们需要对照静态的来看，或者你对指令集绝对的熟悉，看到BYTE CODE就知道是哪种指令集

![原文图片](/media/2016/06/c8681d47142ae71f1c7fb12e2568f175)

静态中，显然IDA给的是2字节指令，那必然是THUMB，我们需要把当前地址改成THUMB

方法：按键盘的ALT+G，呼唤出窗口

![原文图片](/media/2016/06/81fddf4ea12818d96ee97f0379a80a7f)

T,DS不用管，我们只需要把VALUE改成1就是THUMB指令集了，变成1点击OK以后

在原来的地址上出现了CODE16，这时我们再去C一次

![原文图片](/media/2016/06/ebcc27f380d154adfdbb5ba1e98f1602)

C完以后，就出现代码了！！！ARM和THUMB就是这么切换的，切记，切记

![原文图片](/media/2016/06/7ca53acafa2f6ee1bb32a03e819c2b8e)

再看另外一个地址,0x400a5af0,按照同样的方法再来一次

![原文图片](/media/2016/06/e09b4c50a413f95c5ae4a90e01089e01)

问题又来了，奇怪了，为什么下面不是指令？！这个是IDA的BUG，6.6版本对THUMB2指令在调试状态的解析就是有问题。。。。，不过没关系，我们往下看

![原文图片](/media/2016/06/6c1fff5ecf6363ca1542f75decba91f0)

这段代码才是最重要的，执行每一个init_array中地址的函数，就在blx r2这句。 至此，如何断住INIT段和INIT_ARRAY段，就讲完了，剩下的大家就自己调试吧

# 0x05 Anti Anti Debugger

1.Anti IDA

其实这种问题，是IDA解析ELF和linker解析ELF不一致造成的，IDA更加严格

用ida打开某加固的so,提示

![原文图片](/media/2016/06/0d056d8b3d089af5bbf0f189fbdccd46)

这个提示就是说，有个数据描述是无效的，我们来看看，是哪个。

SHT说的就是Section Table，来看看ELF头部数据如下

![原文图片](/media/2016/06/1fe7e3781fd915e01994d7dc10b81dd2)

shoff就是这个值，我们用16进制编辑器过去看看

![原文图片](/media/2016/06/41b4e0d9af34de703e2db350510bad1c)

全是0，显然这里有问题，我们首先要把这个值清0，保存文件。

再次加载，还有问题，提示如下：

![原文图片](/media/2016/06/77b20e0d7f358d14224f1c6b54efbbe2)

这次通过调试IDA的ELF插件，发现当PROGRAM HEADER中的物理偏移大于文件大小时，就会出现该错误。

![原文图片](/media/2016/06/f60c35ef60797405a3a1f1c42535b31e)

显然，Program Header中的第一组数据，p_offset超过了文件大小，根据ELF结构，定位到该数据偏移，改成0，IDA加载成功

2.Anti Debugger

通过调试该加壳程序，总结他用到的方法：

方法1：检测父进程的文件名

调用getppid，获取父进程的id, open("/proc/ppid/cmdline")获取父进程名称，检测常用调试器的名字，这就是上面我COPY文件时，为啥要把android_server变成随意一个文件名的原因了

对策：修改getppid的返回值，随便给一个可以用的就行了

其实还有其他方法可以获取ppid，比如open("/proc/pid/status")，read这个handle的内容，在里面寻找ppid也行

方法2：异常陷阱

和WINDOWS的方法类似，设置一个trap，检测调试器

对策：IDA默认所有的trap都交给调试器处理，所以我们需要修改对应的设置。菜单选择debugger-debugger options,点击edit exceptions按钮

![原文图片](/media/2016/06/2991fa783130b4aed312ba63cc7657a7)

在trap上，右键编辑改成如下即可

![原文图片](/media/2016/06/01ab3cba8c00ce198e698339bb17cbeb)

当然，检测调试器，还有很多方法，见招拆招就可以，这里就不详述了

## Replies
