- 对本书的赞誉
- 前言
- 基础篇
- 第 1 章 Android 中锁屏密码加密算法分析
- 第 2 章 Android 中 NDK 开发
- 第 3 章 Android 中开发与逆向常用命令总结
- 第 4 章 so 文件格式解析
- 第 5 章 AndroidManifest.xml 文件格式解析
- 第 6 章 resource.arsc 文件格式解析
- 第 7 章 dex 文件格式解析
- 防护篇
- 第 8 章 Android 应用安全防护的基本策略
- 第 9 章 Android 中常用权限分析
- 第 10 章 Android 中的 run-as 命令
- 第 11 章 Android 中的 allowBackup 属性
- 第 12 章 Android 中的签名机制
- 第 13 章 Android 应用加固原理
- 第 14 章 Android 中的 so 加固原理
- 工具篇
- 第 15 章 Android 逆向分析基础
- 第 16 章 反编译神器 apktool 和 Jadx
- 第 17 章 Hook 神器 Xposed
- 第 18 章 脱壳神器 ZjDroid
- 第 19 章 Native 层 Hook 神器 Cydia Substrate
- 操作篇
- 第 20 章 静态方式逆向应用
- 第 21 章 动态调试 smali 源码
- 第 22 章 IDA 工具调试 so 源码
- 第 23 章 逆向加固应用
- 第 24 章 逆向应用经典案例分析
- 第 25 章 Android 中常见漏洞分析
- 第 26 章 文件加密病毒 Wannacry 样本分析
14.1 基于对 so 中的 section 加密实现 so 加固
在前面章节中已经详细介绍了 so 文件格式,有了这个知识作为基础,下面就来讲解如何对 so 文件进行加密。
14.1.1 技术原理
加密过程:找到 so 文件中一个 section 的起始地址和大小就可以对这个 section 进行加密了。
解密过程:因为对 section 进行加密之后,肯定需要解密,不然运行肯定报错,这里的重点是什么时候去进行解密。对于一个 so 文件,当被加载到程序之后,在运行程序之前可以从哪个时间点来突破?这里就需要一个知识点:__attribute__((constructor));关于这个属性的用法这里就不做介绍了,网上有相关资料,它的作用是优先于 main 方法之前执行,类似于 Java 中的构造函数,当然 C++中的构造函数是基于这个属性实现的,在之前第 4 章介绍 ELF 文件格式的时候,有两个 section 会引起注意,如图 14-1 所示。
图 14-1 ELF 文件的段信息
用这个属性实现的函数存在两个 section,在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行某些初始化代码的机会。这些初始化函数的被调用顺序是不一定的,不过所有共享目标,初始化都会在可执行文件得到控制之前发生。类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit()机制执行。动态链接器对终止函数的调用顺序是不确定的。共享目标通过动态结构中的 DT_INIT 和 DT_FINI 条目指定初始化/终止函数。通常这些代码放在.init 和.fini 节区中。这个知识点很重要,后面在进行动态调试 so 的时候,还会用到这个知识点,所以一定要理解。这里找到了解密的时机,自己定义一个解密函数,然后用上面的这个属性声明就可以了。
14.1.2 实现方案
编写一个简单的 native 代码,需要做两件事:
·将核心的 native 函数定义在自己的一个 section 中,这里会用到这个属性:__attribute__((section(“.mytext”)));其中.mytext 是定义的 section。
·需要编写解密函数,用属性:__attribute__((constructor));声明。
这样一个 native 程序包含两个重要的函数,使用 ndk 编译成 so 文件。
编写加密程序,在加密程序中需要做的是:
·通过解析 so 文件,找到.mytext 段的起始地址和大小,思路是:找到所有的 section,然后获取它的 name 字段,再结合 String Section,遍历找到.mytext 字段。
·找到.mytext 段之后进行加密,最后再写入到文件中。
14.1.3 代码实现
前面介绍了原理和实现方案,下面就开始编写对应的代码吧。
1.native 程序
先来看看大致代码逻辑:
下面来分析一下代码:
1)定义自己的段:
其中 getString 返回一个字符串,提供给 Android 上层,然后将 getString 函数定义在.mytext 段中。
2)获取 so 加载到内存中的起始地址:
读取设备的 proc/<uid>/maps 中的内容,因为这个 maps 中是程序运行的内存映像,如下所示:
只有获取到 so 的起始地址,才能找到指定的 section,然后进行解密。
3)解密函数:
获取到 so 文件的头部,然后获取指定 section 的偏移地址和 size:
读者可能会有困惑?为什么是这样获取偏移地址和大小的呢,其实这里做了一点工作,就是在加密的时候顺便改写了 so 的头部信息,将偏移地址和大小值写到了头部中,这样加大破解难度。
text_addr 是起始地址+偏移值,就是 section 在内存中的绝对地址,nsize 是 section 占用的页数。
然后修改这个 section 的内存操作权限:
这里调用了一个系统函数:mprotect,其参数如下:
·需要修改内存的起始地址,必须需要页面对齐,也就是必须是页面 PAGE_SIZE(0x1000=4096)的整数倍。
·需要修改的大小,即占用的页数*PAGE_SIZE。
·权限值。
最后读取内存中的 section 内容,然后进行解密,再将内存权限修改回去。然后使用 ndk 编译成 so 即可,这里用到了系统的打印 log 信息,所以需要设置共享库,看一下编译脚本 Android.mk:
2.加密程序
获取到上面的 so 文件,下面就来看看如何进行加密的:
需要解析 so 文件的头部信息、程序头信息、段头信息,如下所示:
关于如何解析这里就不详细说明了,不了解的读者可以去查看前面第 4 章介绍 so 文件格式内容。
获取这些信息之后,下面就来开始寻找 section 信息了,只需要遍历 section 列表,找到名字是.mytext 的 section 即可,然后获取偏移地址和大小,对内容进行加密,回写到文件中。
下面来看看核心方法:
section 中的 sh_name 字段的值是这个 section 的 name 在 StringSection 中的索引值,这里 offset 就是 StringSection 在文件中的偏移值。当然需要知道的一个知识点就是:StringSection 中的每个 name 都是以'\0'结尾的,所以只需要判断字符串到结束符就可以了,判断方法如下:
加密完成之后,需要做的是回写到 so 文件中,当然还需要做一件事,就是将加密的.mytext 段的偏移值和 pageSize 保存到头部信息中:
看到这里读者可能会困惑,这样修改了 so 的头部信息的话,在加载运行 so 文件的时候不会报错吗?其实这就要看看 Android 底层是如何解析 so 文件,然后将 so 文件映射到内存中,下面来看看系统是如何解析 so 文件的。
在 linker.h 源码中有一个重要的结构体 soinfo,下面列出一些字段:
另外,linker.c 中也有许多地方可以佐证。其本质还是 linker 基于装载视图解析的 so 文件。基于上面的结论,再来分析 ELF 头的字段:
·e_ident[EI_NIDENT]字段包含魔数、字节序、字长和版本,后面填充 0。对于安卓的 linker,通过 verify_elf_object 函数检验魔数,判定是否为.so 文件。那么,我们可以向位置写入数据,至少可以向后面的 0 填充位置写入数据。遗憾的是,我在 fedora 14 下测试,是不能向 0 填充位置写数据,链接器报非 0 填充错误。
·对于安卓的 linker,对 e_type、e_machine、e_version 和 e_flags 字段并不关心,是可以修改成其他数据的(仅分析,没有实测)。
·对于动态链接库,e_entry 入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
·so 装载时,与链接视图没有关系,即 e_shoff、e_shentsize、e_shnum 和 e_shstrndx 这些字段是可以任意修改的。被修改之后,使用 readelf 和 ida 等工具打开,会报各种错误,相信读者已经见识过了。
·既然 so 装载与装载视图紧密相关,自然 e_phoff、e_phentsize 和 e_phnum 这些字段是不能动的。
从上面可以知道,so 文件中有些信息在运行时是没有用的,有些东西是不能改的。在上面加密完成之后,可以验证一下,使用 readelf 命令查看一下,如图 14-2 所示。
图 14-2 查看 so 文件内容
从上面的内容可以看到,so 文件内容已经显示错乱了,加密成功。再用 IDA 查看一下,如图 14-3 和图 14-4 所示。
图 14-3 IDA 打开 so 文件(一)
图 14-4 IDA 打开 so 文件(二)
会有错误提示,但是点击 OK,还是成功打开了 so 文件,使用 ctrl+s 查看 section 信息的时候,如图 14-5 所示。
图 14-5 查看加密之后的 section 信息
也没有看到 section 信息,可以看一下没有加密前的效果,如图 14-6 所示。
图 14-6 so 加密之前的 section 信息
既然加密成功了,那么下面验证一下能否运行成功。
3.Android 测试 demo
在获取加密之后的 so 文件后,用 Android 工程测试一下:
运行结果如图 14-7 所示。
图 14-7 运行效果
提示:案例下载地址为 http://download.csdn.net/detail/jiangwei0910410003/9288051
14.1.4 总结
加密流程:
1)从 so 文件头读取 section 偏移 shoff、shnum 和 shstrtab。
2)读取 shstrtab 中的字符串,存放在 str 空间中。
3)从 shoff 位置开始读取 section header,存放在 shdr 中。
4)通过 shdr->sh_name 在 str 字符串中索引,与.mytext 进行字符串比较,如果不匹配,继续读取。
5)通过 shdr->sh_offset 和 shdr->sh_size 字段,将.mytext 内容读取并保存在 content 中。
6)为了便于理解,不使用复杂的加密算法。这里,只将 content 的所有内容取反,即*content=~(*content)。
7)将 content 内容写回 so 文件中。
8)为了验证第二节中关于 section 字段可以任意修改的结论,这里,将 shdr->addr 写入 ELF 头 e_shoff,将 shdr->sh_size 和 addr 所在内存块写入 e_entry 中,即 ehdr.e_entry=(length<<16)+nsize。当然,这样同时也简化了解密流程,还有一个好处是:如果将 so 文件头修正放回去,程序是不能运行的。
解密时,需要保证解密函数在 so 加载时被调用,那函数声明为:init_getString__attribute__((constructor))。(也可以使用 c++构造器实现,其本质也是用 attribute 实现的。)
解密流程:
1)动态链接器通过 call_array 调用 init_getString。
2)Init_getString 首先调用 getLibAddr 方法,得到 so 文件在内存中的起始地址。
3)读取前 52 字节,即 ELF 头。通过 e_shoff 获得.mytext 内存加载地址,ehdr.e_entry 获取.mytext 大小和所在内存块。
4)修改.mytext 所在内存块的读写权限。
5)将[e_shoff,e_shoff+size]内存区域数据解密,即取反操作:*content=~(*content)。
6)修改回内存区域的读写权限。
这里是对代码段的数据进行解密,需要写权限。如果对数据段的数据解密,是不需要更改权限而直接操作的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论