2016年12月28日星期三

Android 逆向学习笔记 (七)- ELF 简单介绍

现在越来越多的安卓 app 为了保证软件的安全性,会使用 NDK 来进行 Native 开发,最后会产生一个 .so 这样的 ELF 文件,通过动态加载这个 ELF 文件调用隐藏在 .so 文件里的方法。

不过有的时候经过处理的 ELF 文件直接拖入 IDA 会报错,像下面这样,如果强行在 IDA 里查看的话会丧失很多有用信息,比较麻烦。



可以看 IDA 载入 ELF 的时候报了两个错, 1. SHT 入口位置不对,2. SHT 表大小或者偏移量不对。归纳来说就是 IDA 加载 ELF 的 SHT( Section Header Table) 内容出了错误。不过为什么出了这样的错误对安卓程序来说没有影响呢?

这是因为 Java 加载 .so 这样的 ELF 文件时不参照 Section Header Table 里的内容,所以开发者可以索性直接全部删除这个片段的内容,加大分析难度。这个时候如果需要分析的话最好可以先修复 ELF 文件结构。

ELF Structure

ELF 的主要有下面三个索引表片段。

1. ELF Header : 在 ELF 文件的最开始,保存了文件的路线图,描述该文件的组织情况。
2. Program Header Table : 告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件(e_type = 1)不需要这个表。
3. Section Header Table : 包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

下面简单介绍一下各个索引表片段的内容,使用工具有 readelf(OS X - greadelf)和 xxd。
-h 选项代表显示 ELF Header 的内容,从这里可以看到 Size of this header: 52 (bytes),即 ELF Header 的长度为 52 = 0x34,使用 xxd 查看 ELF 前 52 bytes 的内容。
这里对照上图查看:
0x00000000: 457f 464c 01     01   0001      0000 0000 0000 0000
                      .ELF          |32    |LE|Version |Padding
0x00000010: 0003    0028             0001 0000 0000 0000 0034 0000
                      e_type |e_machine |e_version |e_entry   |e_phoff
0x00000020: 118c 0002 0000 0500 0034         0020               0007            0028
                      e_shoff     |e_flags      |e_ehsize |e_phentsize |e_phnum   |e_shentsize
0x00000030: 0016         0015
                      e_shnum  |e_shstrndx

具体含义不一一说明,可以看到整个 ELF 的结构在 ELF Header 就比较清楚了,这里跳过 Program Header Table 不表,因为我比较关注的是 Section Header Table

通过 ELF Headere_shoff 可以看到 SHT 的偏移量为 0x02118c,所以这里直接往 0x2118c 看。


可以看到 SHT 节头表起始为空节里没有数据,继续往下看第一个节。


0x000211b4 : 000b 0000     000b 0000 0002 0000 0114 0000
                        s_name_off  |s_type      |s_flags     |s_addr
0x000211c4 : 0114 0000 0740 0000  0002 0000 0001 0000
                       s_offset     | s_size       |s_link       |s_info
0x000311d4 : 0004 0000    0010 0000
                       s_addralign |s_entsize

对照下图查看 Section Header Table 的结构也比较清楚了,

ELF Fix

因为 SHT 在安卓引用 .so 文件时无关紧要,所以有两种保护情况:
1. 将 ELF Header 里和 SHT 有关的内容修改成错误的内容,但是不动 SHT。
2. 直接将 ELF 中的 SHT 删除。

第一种情况因为保留了正确的 SHT 所以只需要修复 ELF Header 就行,这时有下面几个需要注意的点:
1. e_shentsize = 0x28 = 40
2. e_shnum = (total_size - e_shoff) / e_shensize
3. e_shstrndx = e_shnum - 1

如果上述无法修复,那么就需要和 SHT 被删除的情况一样处理,可以使用附件中给出的一个小工具,此时修复原理比较复杂,可以在参考链接中查看具体内容。

* 附件 Github note-7。

参考链接:
Read More

2016年12月13日星期二

ANDROID 逆向实例(四)- com.qtfreet.crackme001

现在很多 apk 在开发中为了保证代码不被轻易逆向出来会对关键函数采用 ndk 的方式用 c 语言和 java 代码进行交互,所以学会分析 .so 文件(加载流程、方式等)对安卓逆向来说是比较重要的。

这里借助一个 CrackMe.apk 实例来帮忙分析。反编译 apk 文件得到 smali 代码,提取关键代码如下:

# direct methods
.method static constructor ()V
    .locals 2

    .prologue
    .line 15
    sget v0, Landroid/os/Build$VERSION;->SDK_INT:I

    const/16 v1, 0x14

    if-ge v0, v1, :cond_0

    .line 16
    const-string v0, "qtfreet"

    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

    .line 20
    :cond_0
    return-void
.end method

...

# virtual methods
.method public native check(Ljava/lang/String;)Z
.end method

分析后可以看到调用了 .so 文件里定义的 check 函数,然后这个 check 函数又和 button 的 OnClick 事件绑定,可以判断 check 则是判断用户输入 key 是否正确的函数。

用 IDA 对 libqtfreet.so 进行分析,首先先看看有没有 JAVA_com_qtfreet_crackme001_check 这样类似的函数,如果存在的话就说明 .so 文件里面没有对 check 函数进行动态注册,JAVA_com_qtfreet_crackme001_check 就是 smali 里面调用的 check 函数原型,直接分析即可。

如果没有发现上面类似函数的话则说明 smali 调用的 check 函数进行了动态注册,此时需要找到注册的位置。

这里需要注意一个问题,就是安卓加载 .so 文件的流程。安卓对 .so 文件加载时首先会查看  .init 或 .init_array 段是否存在,如果存在那么就先运行这段的内容,如果不存在的话那么就检查是否存在 JNI_Onload 存在则执行。

另外,因为 .init 或者 .init_array 在 IDA 动态调试的时候是不会显示出来的,所以需要静态分析出这两段的偏移量然后动态调试的时候计算出绝对位置,然后在 make code(快捷键:c),这样才可以看到该段内的代码内容。

查看 .init_array 段的地址有两种办法:
1.可以使用 IDA 加载 .so 文件,新建 “Segments” 视图,这里会列出不同类型的代码段信息,如下图所示。



2.可以使用二进制工具 readelf 来查看 .so 文件的结构,在 OS X 上面可以使用 greadelf 代替。


可以看到这里两个都指示 .init_array 存在,并且偏移量为 0x20790,IDA 定位到该位置查看如图。


定义了一些东西,这个不必太在意具体是什么内容后面用到的话再看就行了, .init_array 执行完之后就跳到 JNI_Onload 里面执行,IDA 跳到 JNI_Onload 之后 F5 查看伪代码。

这里会有一些小问题,就是 jni.h 头文件里的结构体因为 IDA 里面缺乏参考所以不能正确的对应到他们的类型,需要手动修改数据类型(IDA 快捷键 Y),例如 JNI_Onload(int a1) 这里的 a1 就应该修改成 JavaVM*,后面的一些则需要修改成 JNIEnv*。修改好之后的代码如下图所示:


看到高亮的 RegisterNatives,进去第三个参数 off_21458 位置查看保存的内容。


可以看到确实是 check 函数进行动态注册的地方,并且可以知道 check 真正的函数代码在 sub_8A64+1 位置(具体为什么可以参考 RegisterNative 源码),IDA 跟进 F5。


可以看到这里最后执行了一个 memcmp 所以可以确定 sub_8A64 开始是关键的判断代码,直接将 memcmp 的返回值修改即可成功破解 crackme。如果需要逆出 key 则需要结合之前的初始化变量和函数来看,可以自己试一试逆向 key 不过多阐述了。

ps: 文中省略了一些 Anti Anti-Debug 的内容,因为这个 crackme 的反调试并不复杂而且通过单一函数实现,可以直接将反调试函数 “movs r0,r0” 掉。

* crackme.apk 可以在 Github 上找到。
Read More