版权信息

书名：Android应用安全防护和逆向分析

作者：姜维

排版：KK

出版社：机械工业出版社

出版时间：2017-11-01

ISBN：9787111584452

— · 版权所有 侵权必究 · —

对本书的赞誉

移动应用安全已经是个不可忽视的问题，本书结合多个案例进行实际操作讲解，介绍很多实用工具和命令，是一本非常好的安全逆向工具书籍。姜维研究安全逆向这么久，将自身多年的丰富经验用书本的形式展现给读者也是非常不易。我仔细看过这本书，感觉真的很不错，对学习安全逆向的同学很有帮助，值得购买。

——周鸿祎，360公司创始人、董事长兼CEO，知名天使投资人

本书作者的技术搏客在CSDN上有超过500万人次的总访问量！

为什么这么多人会关注Android安全技术？因为不仅Android安全技术越来越重要，而且要成为真正的开发高手需要从正反两个方面来学习：从正面学习Android编程类别API，逐层学习App开发，从反面（逆向研究APK代码）学习Android系统如何运行的，其他软件是如何实现特定功能的，这样获得的Android知识更加系统全面。作者在书中有非常详细的案例讲解，也提供了大量的工具源码，是Android开发人员逆向学习研究的极好工具手册，可以帮助Android开发者成为更全面的Android高手。

——蒋涛，CSDN创始人，极客帮基金创始人

Android系统应用面越来越广，从移动手机到智能硬件，都有很高的覆盖率，其引发的安全问题危害也越来越大。作者以实际操作讲解的同时，还特别重视一些原理的介绍，可以帮助读者快速提升Android安全技术水平。

——段钢，看雪学院创始人

安全领域是一个冒险的世界，作者结合自己在这个领域的丰富经验，用生动的案例带领读者在这个世界中探险。本书章节安排精心合理，技术点循序渐进，使用了大量带有标注的配图，清晰且直观，属于难得一见的用心之作。

——泮晓波，顶象技术移动安全实验室负责人、合伙人

本书的作者在信息安全领域的多年经验，加上纯粹的技术分享精神，注定了这是一本值得广大安全爱好者期待的好书，本书完整地向你诠释——掌握了安全技术的底层原理，你就掌握了安全的一切。

——丰生强（网名：非虫），国内知名安全专家

随着移动应用的普及，移动应用的安全也随之成为一个严肃而重要的问题。要解决该问题所需要学习的内容非常多，对初学者而言，一本合适的入门书籍将起到承前启后的重要作用。而本书就恰恰是这样的一本书籍，它既涵盖了入门者所必须掌握的基础知识，又精选了许多安全领域中的典型实例并对它们开展了详细讲解。最后，希望作者能再接再厉，对本书内容进行持续更新。

——邓凡平

前言

随着移动应用的广泛使用，不可忽视的一个问题就是信息安全。本书从Android应用安全和逆向两个方面来介绍移动开发中如何做好安全。

我本来不是从事安全逆向工作的，但是一个偶然的机会使我发现逆向研究非常有趣，因为在逆向出别人的App那一刻会觉得无比自豪。

第一次逆向是因为遇到一个问题需要去解决，记得当时想查看一个App的内部资源信息，尝试使用apktool反编译程序，惊奇地发现原来别人的App反编译之后会有这么多东西，逆向真的很有趣。而后在开发中无法实现或者没有相应的资源时就去反编译别人的App，查看对应的代码怎么实现。后来就反编译那些有阻碍的应用，慢慢摸索，克服困难，每研究成功一个案例，便是对自身技能的一次提高。从第一次使用apktool工具，到使用Jadx可视化工具，再到用IDA工具调试，一步一个脚印走过来，经验逐渐丰富，技能也慢慢提升了。

逆向研究需要一种逆向思维，我没有接受过专业训练，只是在业余时间看相关书籍，找几个样本研究，从简单到难，在这个过程中慢慢学到很多技术。本书就是我这几年学习与探索的总结。

本书涉及内容有点多，但是没有一章是多余的，每章内容都是干货。本书包括26章，分为四篇。

基础篇

基础篇包括第1～7章，主要介绍Android技术中与逆向相关的基础知识，为后续章节的学习做准备。

第1章通过对Android中锁屏密码加密算法的分析，带领读者进入安全世界，这方面内容不算复杂，但是需要阅读Android源码来得到算法分析，其中一个知识点是如何通过查看Android源码来帮助解决开发过程中遇到的问题，这是所有Android开发人员必备的技能。通过找到锁屏密码入口，一步一步跟踪最终得到密码加密算法，以及加密之后的内容存放在哪里。这是进入安全逆向分析世界的大门。

第2章主要介绍Android中NDK开发知识。为了安全考虑，现在很多应用把一部分功能做到了Native层，所以如果不知道NDK开发技巧，就无法进行后续的逆向操作。这一章从搭建环境到每个方法的使用，详细讲解了Android中NDK开发的技巧。

第3章主要介绍Android中开发以及逆向需要用到的命令，每个命令都有特定的案例和用法，这些命令不仅仅用于逆向，也能够帮助开发人员提升开发效率，所以了解和掌握这些命令是至关重要的。

第4～7章主要剖析Android中编译之后的apk包含的四类主要文件格式，这部分内容可能有点枯燥，但是至关重要，因为在安全防护或者逆向分析中都有很重要的意义。

防护篇

防护篇包括第8～14章，主要介绍安全防护的相关内容，是本书的核心内容之一。

第8章介绍现在一些应用主要用到的安全防护策略，如混淆、签名校验、反调试检测等，每个安全策略都给出了详细介绍。

第9章介绍Android开发中经常用到的一些权限，介绍如果对这些特殊权限操作不当会带来什么样的安全问题，以及如何预防。

第10章介绍Android中的run-as命令以及如何分析系统安全策略，详细介绍了App、shell、system这三种身份，并介绍了一些技巧，比如如何对应用进行升级权限、降低权限等操作。

第11章讲解Android配置文件中的allowBackup属性引发的安全问题，以及如何应对。本章用一个案例来分析如何导出沙盒数据查看应用中的密码信息，修改密码信息然后再进行还原，全程无需root权限即可完成。

第12章介绍Android中的应用签名机制，讲解应用的签名信息是如何保存的、如何验证的，签名机制的流程，以及如何预防安全问题。

第13章介绍在Android中对apk进行加固的策略，以及如何对恶意者分析apk文件的操作进行防护，还涉及Android中的动态加载机制，并通过动态加载技术实现apk文件的解密功能。

第14章介绍在Android中如何对so文件加固，如何做到安全防护功能。

工具篇

工具篇包括第15～19章，主要介绍逆向分析需要用到的几个工具，本书从实际应用出发，详细介绍每个工具的具体用法，特别是在使用的过程中遇到问题时的处理方法。

第15章介绍逆向工作中用到的工具，以及如何开启设备的调试总开关，这个技能在逆向调试的时候非常重要。

第16章主要介绍Android中反编译神器apktool和Jadx，详细介绍了这两个工具如何使用，以及使用过程中遇到问题时的处理方法。

第17章主要介绍Android中一款Hook神器Xposed，详细介绍这个工具的用法，以及遇到问题时的处理方法。

第18章主要介绍一款脱壳工具，它是基于Xposed工具编写的，可以自动脱壳。

第19章主要介绍Android中另外一款Hook神器Cydia Substrate，详细介绍了这个工具的用法以及遇到问题时的处理方法。

操作篇

操作篇包括第20～26章，主要介绍Android中的逆向操作技巧，包括静态方式和动态方式逆向，用一个经典的加固应用作为逆向案例分析了现阶段脱壳的大致流程，还介绍了Android开发中会遇到的系统漏洞，并分析了一个经典的病毒样本。

第20章主要介绍如何利用静态方式逆向应用，用一个案例讲解了静态防护逆向应用的流程。

第21章主要介绍如何使用Eclipse动态调试smali代码来逆向应用，用一个案例分析整个操作流程。

第22章主要介绍使用IDA工具动态调试so源码，同时也介绍了一些应对反调试检测的方法。

第23章主要介绍如何逆向那些经过加固的应用，用一个案例详细介绍了每一步操作，最后总结了现在脱壳操作的大致流程。

第24章主要总结之前介绍的逆向知识，用一个经典案例作为收尾，讲解了现阶段逆向应用的大体流程和思路。

第25章介绍Android开发中会遇到的系统漏洞——一个是解压文件漏洞，一个是录屏授权漏洞。如果这两个漏洞不做修复，会导致应用沙盒数据篡改以及用户隐私数据的丢失等。该章详细介绍了漏洞的产生原因以及如何进行修复。

第26章介绍Android中一个非常经典的文件加密病毒样本，通过静态方式分析了该病毒样本的工作原理，总结了处理该类病毒的方法。

什么人适合阅读本书

阅读本书需要有一定的Android开发基础。有的读者可能会觉得第1章内容就有点深，本书第1章的目的在于把读者带入安全世界，看不懂没关系，可以从第2章开始看下去，毕竟应用开发领域和安全逆向领域有很多不一样的地方。本书最大的特点在于非常实用，用案例讲解详细操作步骤，跟着每一步具体操作，才能真正看明白。可以把本书作为一本参考书，没看懂不要急，多操作几遍试试。

欢迎联系我

本书用到了很多工具和案例样本、代码，几乎所有代码都给出了具体下载地址，如果在操作过程中发现下载失败或者链接失效，请联系我，可以加我的微信peter_jw212，也可以关注微信公众号“编码美丽”进行留言，或者访问我的博客http://blog.csdn.net/jiangwei0910410003，以及我的个人网站http://www.wjdiankong.cn。有任何问题都可以通过这些渠道联系我，我将尽力给出详细解答。

有的读者读完这本书之后，可能发现有些内容对新技术不再生效了，比如加固和脱壳技术。我在此要说明一下，这些技术是时刻都在变的，所谓道高一尺，魔高一丈，攻防技术每一天都可能改变。所以本书选择了最基本的入门技术进行讲解，因为只有掌握了这些技术，才能继续学习并产生灵感来应对日后变化的技术。我认为最基本的入门技术也是最重要的技术。

致谢

这本书的写作历时一年多，真心觉得很不容易。如果觉得本书写得好，就请推广点赞；如果发现本书有错误的地方，还请批评指正。毕竟第一次写书没有那么完美，期待读者的指正和批评。最后想感谢一些人，他们在我写书过程中给予了技术和精神上的支持。非常感谢非虫大神（《Android软件安全和逆向分析》等书作者）对本书第4章、第7章的so和dex文件格式解析技术的支持；感谢看雪论坛的MindMac大神对第5章的arsc文件格式解析技术的支持；感谢看雪论坛的ThomasKing大神对第14章加固技术的支持；感谢我的同学汪恒和殷传宝对我从开始写作到出版这一路上的陪伴和鼓励。

基础篇

第1章　Android中锁屏密码加密算法分析

第2章　Android中NDK开发

第3章　Android中开发与逆向常用命令总结

第4章　so文件格式解析

第5章　AndroidManifest.xml文件格式解析

第6章　resource.arsc文件格式解析

第7章　dex文件格式解析

第1章　Android中锁屏密码加密算法分析

为了安全，Android设备在解锁屏幕时会有密码输入，那么这个密码存放在哪里？是否为明文存储？如果是加密存储，那么加密算法是什么？这就是本章要介绍的内容。本章的目的是带领读者踏入移动安全的大门。

1.1　锁屏密码方式

Android中现在支持的锁屏密码主要有两种：一种是手势密码，也就是常见的九宫格密码图；一种是输入密码，分为PIN密码和复杂字符密码，而PIN密码就是数字密码，比较简单。当然现在也有一个高级的指纹密码，这不是本章分析的范围，本章只分析手势密码和输入密码。

1.2　密码算法分析

在设置锁屏密码界面，用工具获取当前的View类，然后一步一步跟入，最终会跟到一个锁屏密码工具类：LockPatternUtils.java。每个版本可能实现逻辑不一样，这里用5.1版本的源码进行分析。

1.2.1　输入密码算法分析

找到源码之后，首先来分析一下输入密码算法：

可以看到有一个方法passwordToHash，参数为用户输入的密码和当前用户对应的id，一般设备不会有多个用户，所以这里的userId是默认值0。下面就是最为核心的加密算法了：原文密码+设备的salt值，然后分别进行MD5和SHA-1操作，转化成hex值再次拼接，得到的就是最终保存到本地的加密密码内容。而这里最重要的是如何获取设备对应的salt值，这可以一步一步跟踪代码：

查看getSalt方法，首先根据字段key为lockscreen.password_salt从一个地方获取salt值，如果发现这个值为0，就随机生成一个，然后将其保存到那个地方，最后将salt转化成hex值即可。现在需要找到这个地方，继续跟踪代码：

猜想应该是保存到一个数据库中了，继续跟踪代码：

通过在ServiceManager中获取一个服务来进行操作，在Android中，像这种获取服务的方式最终实现逻辑都是在XXXService类中，这里是LockSettingsService.java类，找到这个类，查看它的getLong方法：

其实到这里就可以看出，非常肯定是数据库中保存的，继续跟踪代码：

果然发现是保存到一个数据库中，继续查看LockSettingsStorage.java类：

看到了，数据库名字叫作locksettings.db，那么它保存在哪里呢？继续看这个类：

可以看到有两个key文件，它们就是用来保存加密之后的手势密码和输入密码的，将信息保存到本地，下次开机解锁需要读取这个文件内容进行密码比对。看到一个目录是system，所以数据库和这两个key文件很可能保存到目录/data/system/下了，为了再证实一下，直接用find命令去根目录下搜索这个数据库文件也是可以的。最终确定是该目录：

这里可能会提示找不到find命令，这时需要安装busybox工具，才能使用这个命令。找到这个数据库文件就好办了，直接取出来，然后用SQLite工具进行查看即可，当然也可以直接在手机中查看。为了方便还是取出来看，如图1-1所示。

图1-1　数据库信息

这里看到了表格字段，并且获取到这个值了，那么下面就要用这个值来验证上面的分析是否正确。首先给设备设置一个简单的输入密码，这里直接输入简单的“1234”，然后会在/data/system目录下生成一个密码加密key文件/data/system/password.key，将该文件导出来，如图1-2所示。

图1-2　加密文件

下面就用简单的Java代码手动实现这个算法，看看分析是否正确。加密算法不用自己写，直接从上面的源码中拷贝出来就可以了。代码如下：

这里的salt值是我们从数据库中得到的，不过要记得进行hex转化：

然后用“1234”原文密码去生成加密之后的信息：

运行结果如图1-3所示。

图1-3　运行结果

可以发现内容和上面的password.key内容完全一致，也就验证了上面的分析完全正确。到这里就分析完了输入密码的加密算法，总结一点就是：MD5（输入明文密码+设备的salt）.Hex+SHA1（输入明文密码+设备的salt）.Hex就是最终的加密内容。而这里最重要的是如何获取设备的salt值，可以用反射机制进行获取，新建一个简单的Android项目：

这样就不用去查看数据库获取salt值了，方便快捷，打印的日志信息如下：

这是数据库中的long类型值转化成hex之后的值。

1.2.2　手势密码算法分析

下面来分析手势密码，代码依然在LockPatternUtils.java中：

这个算法比较简单，就是九宫格图案转化成字节数组，然后用SHA1加密即可。关于九宫格不再多说了，从0开始顺时针计数到8，类似图1-4所示。

图1-4　九宫格

看一下代码，有行和列之分。比如L形状的手势密码应该是00 03 06 07 08，这样组成五个字节。这里为了验证手势密码是否正确，设置一个简单的手势密码，如图1-5所示。

图1-5　简单的手势密码

然后在/data/system目录下生成一个密码文件/data/system/gesture.key，取出来用二进制工具查看，不然可能看到的是乱码，这里用的是010Editor工具查看，如图1-6所示。

图1-6　手势密码加密内容

为了最大化地还原算法，依然把源码拷贝出来，然后定义一个手势九宫格类，构造出这个手势的点数据：

这是源码的加密算法，下面再构造出手势点数据：

手势点数据应该是00 01 02 05 08，打印看结果，如图1-7所示。

图1-7　运行结果

从运行结果发现，一模一样，这样就完美地分析完了手势密码加密算法。

这里再总结一下两种方式锁屏密码算法。

第一种：输入密码算法

对输入的明文密码+设备的salt值进行MD5和SHA1操作，之后转化成hex值进行拼接即可，最终加密信息保存到本地目录/data/system/password.key。

第二种：手势密码算法

将九宫格手势密码中的点数据转化成对应的字节数组，然后直接进行SHA1加密即可。最终加密信息保存到本地目录/data/system/gesture.key。

1.3　本章小结

读完本章是不是迫不及待地想动手尝试一下？在操作之前一定要记住，先得到设备的salt值，然后要注意源码版本。

第2章　Android中NDK开发

本章主要介绍Android中的NDK开发技术相关知识，因为后续章节特别是在介绍安全应用防护和逆向应用的时候，会涉及NDK的相关知识，而且考虑到项目的安全性开发，把一些重要的代码放到底层也是很重要的，同时能提高执行效率。

2.1　搭建开发环境

在搭建环境之前必须先去官网下载NDK工具包，官网地址是http://wear.techbrood.com/tools/sdk/ndk/，选择相应平台的NDK版本即可。

2.1.1　Eclipse环境搭建

第一步：配置NDK路径，如图2-1所示。

图2-1　配置NDK路径

第二步：新建Android项目，如图2-2所示。

图2-2　新建Android项目

点击Add Native Support，出现如图2-3所示的lib命令。

点击“Finish”，再次观察项目多了jni文件夹，如图2-4所示。

图2-3　命令lib

图2-4　添加了jni文件夹

在jni下面就可以开始编写native层的代码。

第三步：使用javah生成native的头文件，如图2-5所示。

图2-5　生成native头文件

注意：javah执行的目录，必须是类包名路径的最上层，然后执行：

注意没有后缀名java。

第四步：运行项目，点击工具栏中的小锤子图标如图2-6所示。

图2-6　运行项目

运行结果如图2-7所示。

图2-7　运行结果

2.1.2　Android Studio环境搭建

去官网下载NDK工具，然后使用Android Studio中进行新建一个简单项目，然后创建JNI即可，如图2-8所示。

图2-8　创建jni

第一步：在项目中新建jni目录，如图2-9所示。

第二步：用javah命令生成native的头文件，如图2-10所示。

第三步：配置项目的NDK目录，如图2-11所示。

图2-9　新建jni目录

图2-10　生成native头文件

图2-11　配置NDK目录

选择模块的设置选项Open Module Settings，如图2-12所示。

图2-12　模块的设置选项

在其中设置NDK目录即可。

第四步：配置Gradle中的ndk选项，如图2-13所示。

图2-13　配置gradle

这里只需要设置编译之后的模块名，即so文件的名称，以及产生哪几个平台下的so文件，需要用到的lib库，这里用到了Android中打印log的库文件。

第五步：编译运行生成so文件

在build目录下生成指定的so文件，拷贝到项目的libs目录下即可，如图2-14所示。

图2-14　项目的libs目录

2.2　第一行代码：HelloWorld

本节开始介绍JNI技术，先输出一个Hello World。具体流程如下，在Java中定义一个方法，在C++中实现这个方法，在方法内部输出“Hello World”，然后再回到Java中进行调用。

第一步：在Eclipse中建立一个类：JNIDemo。

命令如下：

其中sayHello就是要在C++中实现的方法。

第二步：使用javah命令将JNIDemo生成.h的头文件。

命令如下：

注意：

·首先要确保配置了Java的环境变量，不然javah命令不能用。

·案例的Java项目是放在E：\workspace中的，所以首先进入项目的bin目录中，然后使用javah命令生成头文件。

·javah后面的类文件格式是类的全名（包名+class文件名），同时不能有.class后缀。

命令执行成功后会在bin目录中生成头文件com_jni_demo_JNIDemo.h，参见图2-15。

图2-15　javah命令参数说明

注意：如果包含native方法的类，引用其他地方的类，那么进入bin\classes\目录下会出现问题提示找不到指定的类，这时候需要切换到源码目录src下运行即可。

第三步：使用VC6.0生成.dll文件。

首先创建一个dll工程，如图2-16～图2-18所示。

在.cpp文件中输入如下代码：

图2-16　VC6.0生成.dll文件

图2-17　VC6.0生成.dll文件

图2-18　VC6.0生成.dll文件

这个方法的声明可以在上面生成的com_jni_demo_JNIDemo.h头文件中找到，这个就是Java工程中的sayHello方法的实现：

这里编译会出现以下几个问题：

1）会提示找不到相应的头文件，如图2-19所示。这时需要将jni.h、jni_md.h文件拷贝到工程目录中，这两个文件的具体位置参见图2-20。

Java安装目录中的include文件夹下，jni_md.h文件在win32文件夹中，找到这两个文件后，将其拷贝到C++的工程目录中。

图2-19　编译之后的头文件

图2-20　jni.h文件

2）当拷贝到这两个文件之后，编译还是提示找不到这两个文件：主要原因是#include<jni.h>是从系统目录中查找jni.h头文件的，而这里只把jni.h拷贝到工程目录中，所以需要在com_jni_demo_JNIDemo.h头文件中将#include<jni.h>改成#include"jni.h"。同理，在jni.h文件中将#include<jni_md.h>改成#include"jni_md.h"。

3）同时还有一个错误提示：e：\c++\jnidemo\jnidemo.cpp（9）：fatal error C1010：unexpected end of file while looking for precompiled header directive，这是指预编译头文件读写错误，这时候还要在VC中进行设置：项目→设置→C/C++。在分类中选择预编译的头文件，选择不使用预补偿页眉，如图2-21所示。

这样，编译成功，生成JNIDemo.dll文件在C++工程中的Debug目录中。

注意：因为之前开发都是使用VC工具，所以这里使用了VC 6.0来进行C++代码的编写和运行，其实可以直接使用Eclipse或在Android Studio中也可以进行编写，这样会更方便。

第四步：将JNIDemo.dll文件添加到环境变量中，如图2-22所示。

注意：在用户变量中的path设置，用分号隔开：“；E：\C++\Debug”，这样就将.dll文件添加到环境变量中了。

第五步：在Eclipse中调用sayHello方法，输出“Hello World”。代码如下：

图2-21　预编译头文件

图2-22　将JNIDemo.dll文件添加到环境变量中

System.loadLibrary方法是加载JNIDemo.dll文件的，一定要注意不要有.dll后缀名，只需要文件名即可。

注意，运行的时候会报错，如图2-23所示。

图2-23　运行错误

这个提示是没有找到JNIDemo.dll文件，这时需要关闭Eclipse，然后再打开，运行就没有错了。原因是Eclipse每次打开的时候都会去读取环境变量的配置，刚才配置的path没有立即生效，所以要关闭Eclipse，然后重新打开一次即可。

注意：这里因为使用了VC编辑器进行native代码的编写，所以需要配置dll文件操作，但是现在更多的是习惯直接在Eclipse/Android Studio中配置C++环境直接编写了，这样更方便。

2.3　JNIEnv类型和jobject类型

上一节介绍的是一个简单的应用，说明JNI是怎么工作的，这一节介绍本地方法sayHello的参数及其使用。

首先来看一下C++中的sayHello方法的实现：

2.3.1　JNIEnv类型

JNIEnv类型实际上代表了Java环境，通过JNIEnv*指针就可以对Java端的代码进行操作。例如，创建Java类中的对象，调用Java对象的方法，获取Java对象中的属性等。

JNIEnv类中有很多函数可以用，如下所示：

·NewObject：创建Java类中的对象。

·NewString：创建Java类中的String对象。

·New<Type>Array：创建类型为Type的数组对象。

·Get<Type>Field：获取类型为Type的字段。

·Set<Type>Field：设置类型为Type的字段的值。

·GetStatic<Type>Field：获取类型为Type的static的字段。

·SetStatic<Type>Field：设置类型为Type的static的字段的值。

·Call<Type>Method：调用返回类型为Type的方法。

·CallStatic<Type>Method：调用返回值类型为Type的static方法。

更多的函数使用可以查看jni.h文件中的函数名称。

2.3.2　jobject参数obj

如果native方法不是static，obj就代表native方法的类实例。

如果native方法是static，obj就代表native方法的类的class对象实例（static方法不需要类实例的，所以就代表这个类的class对象）。

2.3.3　Java类型和native中的类型映射关系

Java和C++中的基本类型的映射关系参见表2-1。

表2-1　Java和C++中的基本类型的映射关系

具体的说明可以查看jni.h文件。

2.3.4　jclass类型

为了能够在C/C++中使用Java类，jni.h头文件中专门定义了jclass类型来表示Java中的Class类。

JNIEnv类中有如下几个简单的函数可以取得jclass：

·jclass FindClass（const char*clsName）：通过类的名称（类的全名，这时候包名不是用点号而是用/来区分的）来获取jclass。如：jclass str=env->FindClass（"java/lang/String"）；获取Java中的String对象的class对象。

·jclass GetObjectClass（jobject obj）：通过对象实例来获取jclass，相当于Java中的getClass方法。

·jclass GetSuperClass（jclass obj）：通过jclass可以获取其父类的jclass对象。

2.3.5　native中访问Java层代码

在C/C++本地代码中访问Java端的代码，一个常见的应用就是获取类的属性和调用类的方法，为了在C/C++中表示属性和方法，JNI在jni.h头文件中定义了jfieldId、jmethodID类型来分别代表Java端的属性和方法。在访问或者设置Java属性的时候，首先就要先在本地代码取得代表该Java属性的jfieldID，然后才能在本地代码中进行Java属性操作，同样，需要调用Java端的方法时，也是需要取得代表该方法的jmethodID才能进行Java方法调用。

使用JNIEnv的如下方法：

·GetFieldID/GetMethodID

·GetStaticFieldID/GetStaticMethodID

来取得相应的jfieldID和jmethodID。

下面来具体看一下这几个方法。

GetFieldID方法如下：

方法的参数说明：

·clazz：这个方法依赖的类对象的class对象。

·name：这个字段的名称。

·sign：这个字段的签名（每个变量，每个方法都是有签名的）。

怎么查看类中的字段和方法的签名呢？使用javap命令，如下所示。

GetMethodID也能够取得构造函数的jmethodID，创建一个Java对象时可以调用指定的构造方法，后续将向大家介绍，如：

签名的格式见表2-2。

表2-2　签名的格式

下面来看一例子：

上面的native代码中，首先取得property字段，因为property字段是int类型的，所以在签名中传入“I”，取得方法function的ID时：

签名为（Iljava/util/Date；[I）I。

关于GetStaticFieldID/GetStaticMethodID这两个方法的用法大同小异，区别在于这两个方法是获取静态字段和方法的ID。

2.4　JNIEnv类型中方法的使用

前面说到JNIEnv类型，下面通过例子来看一下这些方法的使用。第一个例子是在Java代码中定义一个属性，然后再从C++代码中将其设置成另外的值，并且输出来。

2.4.1　native中获取方法的Id

先来看一下Java代码：

再来看一下C++代码：

编译成功后，在Eclipse运行后的结果如图2-24所示。

图2-24　运行成功效果图

第一个0是在C++代码中的cout<<number<<endl。

第二个100是在Java中的System.out.println（jniDemo.number）。

JNIEnv提供了众多的Call<Type>Method和CallStatic<Type>Method，还有CallNonvirtual<Type>Method函数，需要通过GetMethodID取得相应方法的jmethodID来传入到上述函数的参数中。

调用示例方法的三种形式如下：

第一种是最常用的方式。第二种是当调用这个函数的时候有一个指向参数表的va_list变量时使用的（很少使用）。第三种是当调用这个函数的时候有一个指向jvalue或jvalue数组的指针时用的。

jvalue在jni.h头文件中定义是一个union联合体，在C/C++中，union可以存放不同类型的值，但是当你给其中一个类型赋值之后，这个union就是这种类型了，比如你给jvalue中的s赋值的话，jvalue就变成了jshort类型了，所以可以定义一个jvalue数组（这样就可以包含多种类型的参数了）传递到方法中，如下所示：

假如现在Java中有这样的一个方法：

1）在C++中使用第一种方式调用function方法：

obj是方法funtion的对象。id_function是方法function的id，可以通过GetMethodID（）方法获取。然后就是对应的参数，这和Java中的可变参数类似。最后一个char类型的参数L'a'为什么前面要加一个L呢？原因是Java中的字符是Unicode双字节的，而C++中的字符是单字节的，所以要变成宽字符，即前面加一个L。

2）在C++中使用第三种方式调用function方法：

例子：C++中调用Java中的方法。

Java代码如下：

这时候用javap获取max方法的签名，如下所示。

max方法的签名是（DD）D。在C++中的代码如下：

编译成动态文件后，到Eclipse中执行sayHello方法，运行结果如图2-25所示。

图2-25　运行成功效果图

可见，成功地输出了最大值。

2.4.2　Java和C++中的多态机制

JNIEnv中有一个特殊的方法CallNonvirtual<Type>Method，如下所示：

首先来了解一下，上面调用的function是子类的function方法，但是在C++中就不一样了：

这段C++代码中执行的是父类的function方法，如果想执行子类的function方法怎么办呢？就需要将父类的function方法定义成virtual虚函数：

所以，C++和Java对于继承后执行的是父类还是子类的方法是有区别的，在Java中所有的方法都是虚拟的，所以总是调用子类的方法，因此CallNonVirtual<Type>Method方法就出来了，这个方法可以帮助调用Java中父类的方法。

在JNI中定义的CallNonvirtual<Type>Method能够实现子类对象调用父类方法的功能，如果想要调用一个对象的父类方法，而不是子类的方法，就可以使用CallNonvirtual<Type>Method。要使用它，首先要获得父类及其要调用的父类方法的jmethodID，然后传入到这个函数就能通过子类对象调用被覆写的父类方法了。

例如：在Java中定义Father类：

定义一个子类Child，继承Father类，重写父类中的function方法：

在JNIDemo代码，定义Father类型的属性：

再来看一下C++中的代码：

编译成功.dll文件，回到Eclipse中运行结果参如图2-26所示。

图2-26　运行结果

其中：

·Child：function是调用env->CallVoidMethod（...）方法的。

·Father：function是调用env->CallNonvirtualMethod（...）方法的。

这样就能够控制到底调用哪个类的function方法了。

2.5　创建Java对象及字符串的操作方法

首先来看一下C/C++中怎么创建Java对象，然后再介绍如何操作Java字符串。

2.5.1　native中创建Java对象

在JNIEnv中有两种方法创建Java对象，下面分别介绍。

第一种方法创建Java对象

代码如下：

参数如下：

·clazz：是需要创建的Java对象的Class对象。

·methodID：是传递一个方法的ID，想一想Java对象在创建的时候，需要执行什么方法呢？对，没错那就是构造方法。

·第三个参数：是构造函数需要传入的参数值（默认的构造方法是不需要传入这个参数的）。所以在创建Java对象之前要做的工作就是要获取这个对象的class对象，然后再获取该对象的构造方法。想要获取方法的id，就需要方法的签名，因为构造方法没有返回值，所以认为类的默认构造方法的返回值类型的签名始终是“（）V”（因为默认的构造方法是没有参数的），方法的名称始终为“<init>”。

在C++中构造Java中的Date对象，并且调用它的getTime（）方法打印当前时间。

Java中的代码不需要改变，主要是在C++代码中改写：

编译成.dll文件，在Eclipse中运行结果如图2-27所示。

图2-27　运行结果

第二种方法创建Java对象

用AllocObject函数创建一个对象，可以根据传入的jclass创建一个Java对象，但是状态是非初始化的，在这个对象之前绝对要用CallNonvirtualVoidMethod来调用该jclass的构造函数，这样就可以延迟构造函数的调用。这种方法用得很少，下面只对代码做简单的说明。

Java中的代码不做任何修改，C++代码修改如下：

2.5.2　native中操作Java字符串

首先来了解一下Java和C/C++中字符串的区别。在Java中，使用的字符串String对象是Unicode（UTF-16）码，即每个字符不论是中文还是英文还是符号，一个字符总是占两个字节。Java通过JNI接口可以将Java的字符串转换到C/C++中的宽字符串（wchar_t*），或传回一个UTF-8的字符串（char*）到C/C++；反过来，C/C++可以通过一个宽字符串，或一个UTF-8编码的字符串来创建一个Java端的String对象。

接下来看一下JNIEnv中的一些C++方法。

1）获取字符串的长度：

参数j_msg是一个jstring对象。

2）将jstring对象拷贝到const jchar*指针字符串：

这是在Java 1.2出来的函数，这个函数把Java字符串的内容直接拷贝到C/C++的字符串数组中，在调用这个函数之前必须有一个C/C++分配出来的字符串（具体看下面的例子），然后传入到这个函数中进行字符串的拷贝。

由于C/C++中分配内存开销相对小，而且Java中的String内容拷贝的开销可以忽略，更好的一点是此函数不分配内存，不会抛出OutOfMemoryError异常。

参数j_msg是一个jstring对象，start是拷贝字符串的开始位置，len是拷贝字符串的长度，jstr是目标指针字符串。

3）生成一个jstring对象：

参数：jstr是字符串指针，size是字符串长度。

这个方法可以认为是将字符串指针jstr转换成字符串对象jstring。

4）将jstring对象转换成const jchar*字符串指针。有两个方法：GetStringChars和GetStringUTFChars方法。

GetStringChars方法如下：

返回一个UTF-16编码的宽字符串（jchar*）。

参数如下：

·j_msg是字符串对象。

·copied是指传入的是一个jboolean指针，用来标识是否对Java的String对象进行了拷贝，如果传入的这个jboolean指针不是NULL，则它会给该指针所指向的内存传入JNI_TRUE或JNI_FALSE标识是否进行了拷贝，传入NULL表示不关心是否拷贝字符串，也就不会给jboolean*指向的内存赋值。

其对应的释放内存指针的方法：

参数：j_msg是jstring对象，jstr是字符串指针。

GetStringUTFChars方法如下：

这个方法是可以取得UTF-8编码的字符串（char*）。参数的含义和GetStringChars方法是一样的。这个方法也有对应的一个释放内存的方法：

参数的含义和上面的ReleaseStringChars方法的参数的含义是一样的。

提示：这两个函数分别都会有两个不同的动作：

·开辟一个新内存，然后在Java中的String拷贝到这个内存中，然后返回指向这个内存地址的指针。

·直接返回指向Java中String的内存的指针，这个时候千万不要改变这个内存的内容，这个将会破坏String在Java中始终是常量的这个原则。

5）将jstring对象转化成const jchar*字符串指针：

参数j_msg是字符串对象，copied同上面的解释，这里就不多说了。

这个方法的作用是为了增加直接传回指向Java字符串的指针的可能性（而不是拷贝），JDK 1.2出来了新的函数GetStringCritical/ReleaseStringCritical。

在GetStringCritical/ReleaseStringCritical之间是一个关键区，在这个关键区域之间不能调用JNI的其他函数，否则将造成关键区代码执行期间垃圾回收器停止运作，任何触发垃圾回收器的线程也会暂停，其他的触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。就是说在关键区域中千万不要出现中断操作，或在JVM中分配任何新对象；否则会造成JVM死锁。虽然这个函数会增加直接传回指向Java字符串的指针的可能性，不过还是会根据情况传回拷贝过的字符串。不支持GetStringUTFCritical，没有这样的函数，由于Java字符串用的是UTF-16，要转成UTF-8编码的字符串始终需要进行一次拷贝，所以没有这样的函数。

这个方法和第四个方法是一样的。其对应的释放内存指针的方法如下：

下面来看一下实例：在Java中定义一个String属性，通过控制台输入值，然后定义一个本地方法callCppFunction，在C++中这个方法的实现就是：获取到Java中这个字符串属性，将其进行倒序操作，然后再从Java中输出。

先来看一下Java代码：

再来看一下C++代码：

这里使用了三种方式实现功能。要注意的是，还有一个方法是将const jchar*转换成wstring，因为reverse方法接受的参数是wstring。在Eclipse中的运行结果如图2-28所示。

图2-28　Eclipse中的运行结果

2.6　C/C++中操作Java中的数组

在Java中数组分为两种：

·基本类型数组。

·对象类型（Object[]）的数组（数组中存放的是指向Java对象中的引用）。

一个能用于两种不同类型数组的函数是GetArrayLength（jarray array）。

2.6.1　操作基本类型数组

首先来看一下怎么处理基本类型的数组，有如下几种方法。

1.Get<Type>ArrayElements方法

这类函数可以把Java基本类型的数组转换到C/C++中的数组，有两种处理方式，一种是拷贝一份传回本地代码，另一种是把指向Java数组的指针直接传回到本地代码中，处理完本地化的数组后，通过Release<Type>ArrayElements来释放数组。

2.Release<Type>ArrayElements方法

用这个函数可以选择将如何处理Java和C++的数组，是提交，还是撤销等，内存释放还是不释放等。

mode可以取下面的值：

·0：对Java的数组进行更新并释放C/C++的数组。

·JNI_COMMIT：对Java的数组进行更新但是不释放C/C++的数组。

·JNI_ABORT：对Java的数组不进行更新，释放C/C++的数组。

3.GetPrimittiveArrayCritical方法

4.ReleasePrimitiveArrayCritical方法

也是JDK1.2出来的函数，为了增加直接传回指向Java数组的指针而加入的函数，同样也会有同GetStringCritical一样死锁的问题。

5.Get<Type>ArrayRegion方法

在C/C++预先开辟一段内存，然后把Java基本类型的数组拷贝到这段内存中，这个方法和之前拷贝字符串的GetStringRegion方法的原理是类似的。

6.Set<Type>ArrayRegion方法

把Java基本类型数组中的指定范围的元素用C/C++数组中的元素来赋值。

7.<Type>ArrayNew方法

指定一个长度然后返回相应的Java基本类型的数组。

2.6.2　操作对象类型数组

JNI没有提供把Java对象类型数组（Object[]）直接转到C++中的Object[]数组的函数，而是通过Get/SetObjectArrayElement这样的函数来对Java的Object[]数组进行操作。由于对象数组没有进行拷贝，所以不需要释放任何资源。NewObjectArray可以通过指定长度和初始值来创建某个类的数组。

下面来看个例子：操作两种类型的数组。

Java中的代码：

C++中的代码：

在Eclipse编译运行，结果如图2-29所示。

图2-29　在Eclipse中运行的结果

2.7　C/C++中的引用类型和ID的缓存

2.7.1　引用类型

从Java虚拟机创建的对象传到本地C/C++代码时会产生引用，根据Java的垃圾回收机制，只要有引用存在就不会触发该引用所指的Java对象的垃圾回收。下面介绍C/C++中的引用类型。

1.局部引用

局部引用是最常见的引用类型，基本上通过JNI返回来的引用都是局部引用，例如使用NewObject就会返回创建出来的实例的局部引用，局部引用只在该native函数中有效，所有在该函数中产生的局部引用，都会在函数返回的时候自动释放，也可以使用DeleteLocalRef函数手动释放该引用。那么，既然局部引用能够在函数返回时自动释放，为什么还需要DeleteLocalRef函数呢。

实际上局部引用存在是防止其指向的对象被垃圾回收，尤其是当一个局部引用指向一个很庞大的对象，或是在一个循环中生成了局部引用。最好的做法就是在使用完该对象后，在该循环尾部把这个引用释放掉，以确保在触发垃圾回收器的时候能够回收。

在局部引用的有效期中，可以传递到别的本地函数中，要强调的是它的有效期仍然只在一次的Java本地函数调用中，所以千万不能用C++全局变量保存它或者把它定义为C++静态局部变量。

2.全局引用

全局引用可以跨越当前线程，在多个native函数中有效，不过需要编程人员手动来释放该引用，全局引用存在期间会防止在Java的垃圾回收器的回收。

与局部引用不同，全局引用的创建不是由JNI自动创建的，全局引用是需要调用NewGlobalRef函数，而释放它需要使用ReleaseGlobalRef函数。

3.弱全局引用

弱全局引用是Java 1.2新出来的功能，与全局引用相似，创建和删除都需要由编程人员来进行，这种引用与全局引用一样可以在多个本地代码中有效，也跨越多线程有效。不一样的是，这种引用将不会阻止垃圾回收器回收这个引用所指向的对象，使用NewWeakGlobalRef和ReleaseWeakGlobalRef来产生和解除引用。

关于引用的一个函数如下：

上述的六种方法很好理解，这里就不做解释了。

这个函数是用来比较两个引用是否相等，但是对于弱全局引用还有一个特别的功能，如果把NULL传入要比较的对象中，就能够判断弱全局引用所指向的Java对象是否被回收。

缓存jfieldID/jmethodID，取得jfieldID和jmethodID的时候会通过该属性/方法名称加上签名来查询相应的jfieldID/jmethodID。这种查询相对来说开销大，我们可以将这些FieldID/MethodID缓存起来，这样就需要查询一次，以后就是用缓存起来的FieldID/MethodID了。

2.7.2　缓存方法

1.在用的时候缓存

在native代码中使用static局部变量来保存已经查询过的id，这样就不会在每次函数调用时查询，而只要第一次查询成功后就保存起来了。不过在这种情况下就不得不考虑多线程同时调用此函数时可能会招致同时查询的危机，不过这种情况是无害的，因为查询同一个属性方法的ID通常返回的是一样的值：

static jfieldID fieldID_string=NULL；这段代码只执行一次。

2.在Java类初始化时缓存

更好的一个方式是在任何native函数调用前把ID全部存起来，可以让Java在第一次加载这个类的时候首先调用本地代码初始化所有的jfieldID/jmethodID，这样就可以省去多次确定ID是否存在的语句。当然，这些jfieldID/jmethodID是定义在C/C++的全局，使用这种方式还是有好处的，当Java类卸载或者重新加载的时候，也会调用该本地代码来重新计算ID的。

在Java中使用静态代码块进行初始化。

2.8　本章小结

本章主要介绍了Android中的NDK开发，其实Android中的NDK就是Java中的JNI，两者没有本质区别，特别是在语法和开发流程上几乎是一样的。后续章节有很多地方会用到这里的相关知识，建议读者能够自己独立编写出一个native的案例，为后面的学习做准备。

第3章　Android中开发与逆向常用命令总结

Android中开发和逆向用到的命令很多，本章介绍一些隐蔽但却非常好用的命令，可以帮助快速找到问题，这些命令都是我在开发实践中总结出来的，大家不妨了解一下。

3.1　基础命令

基础命令是用得最多的，下面介绍两个基础命令。

1.cat命令

cat命令主要用于查看文件内容，这个命令的重要性不言而喻，有时候想查看文件信息，当然可以借助软件查看，但是这个命令非常便捷，更重要的是它可以结合grep过滤内容信息：

记住一点：Linux中的过滤命令是grep，Windows中的过滤命令是findstr。

2.echo/touch命令

echo和touch命令可以方便地写文件，下面看一下这两个命令的结合使用：

这里可以看到用echo和touch命令写内容到文件中，然后用cat命令读取文件信息。这里还用到了内容重定向符号“>”和“>>”，这两个符号也是非常有用的，有时候在执行一条命令时可能输出的结果非常多，这时就需要借助重定向命令把结果输出到文本中。后面还会提到这个用途。

3.2　非shell命令

我把常用命令分为非shell命令和shell命令，当然这样分类有点不合常规。为了好理解，我把需要提前用adb shell命令运行的命令叫做shell命令，直接用adb shell运行的命令叫做非shell命令。本节介绍非shell命令，下一节介绍shell命令。

1.adb shell dumpsys activity top

说明：可以查看当前应用的activity信息。

用法：运行需要查看的应用。

案例：

延伸：如果直接运行adb shell dumpsys也是可以的，只是会把当前系统中所有应用运行的四大组件都会打印出来，而这时候会发现打印的内容非常多，就需要借助之前说到的信息重定向了，具体做法如下。

这里还借助了Windows中的start命令，可以直接利用系统默认程序打开文本内容。而且我在以往逆向应用的时候，很多时候都用到这个命令来找到突破口。

2.adb shell dumpsys package

说明：可以查看指定包名应用的详细信息（相当于应用的AndroidManifest.xml中的内容）。

用法：adb shell dumpsys package[pkgname]

案例：

这里就是相当于把应用的清单文件打印出来而已。

3.adb shell dumpsys meminfo

说明：可以查看指定进程名或者进程id的内存信息。

用法：adb shell dumpsys meminfo[pname/pid]

案例：

利用这个命令可以查看进程当前的内存情况，和后面的top命令可以结合使用，分析应用的性能消耗情况。

4.adb shell dumpsys dbinfo

说明：可以查看指定包名应用的数据库存储信息（包括存储的SQL语句）。

用法：adb shell dumpsys dbinfo[packagename]

案例：

这里可以清晰地看到应用执行过的SQL语句信息，对应用逆向的时候具有一定用途。毕竟可以查看应用操作数据库信息了。

5.adb intall

说明：安装应用包apk文件。

用法：adb install[apk文件]。

案例：

注意：如果应用已经安装了，需要使用adb install-r[apk文件]，相当于升级安装。这个命令就不多说了，非常简单。

6.adb uninstall

说明：卸载应用。

用法：adb uninstall[packagename]

案例：

和上面的命令类似。

7.adb pull

说明：将设备中的文件放到本地。

用法：adb pull设备目录文件本地目录。

案例：

注意，在操作的时候可能遇到文件权限问题，用chmod改一下权限即可。

8.adb push

说明：将本地文件放到设备中。

用法：adb push本地目录文件设备目录。

案例：

注意，在操作的时候可能遇到文件权限问题，用chmod改一下权限即可。

9.adb shell screencap

说明：截屏操作。

用法：adb shell screencap-p截图文件路径。

案例：

这个命令对于测试人员非常有用，有时候想快速截取手机屏幕，只要快速打开，就可以利用这个命令写一个简单的脚本文件，内容如下：

这样就一步到位，很快打开一个截图图片。这个是Windows中的bat命令格式。

延伸：一些恶意软件利用设备root之后，运行该命令就可以获取用户当前屏幕信息，对于盗取账号非常危险。

10.adb shell screenrecord

说明：录屏操作。

用法：adb shell screenrecord视频保存路径。

案例：

这个命令其实和上面截屏命令差不多，只不过这个是录制屏幕，对于测试人员来说更加重要，有时候想复现问题步骤，那么就可以采用这个命令进行录屏功能。

延伸：现在很多录屏软件（在5.0之前版本）可以在root之后用这个命令进行录屏功能。

11.adb shell input text

说明：输入文本内容。

用法：adb shell input text[需要输入文本框内容]

案例：

让需要输入内容的文本框获取焦点：

注意：这个命令也可以模拟物理按键、虚拟键盘、滑动、滚动等事件。

延伸：这个命令对于需要输入一大堆信息到文本框中的情况非常有用，比如在PC端有一段内容，想输入到手机的某个搜索框中，那么可以通过把这段内容发送到手机，然后再复制操作。但是有了这个命令就非常简单，先让想要输入的文本框获取焦点，然后运行这个命令即可。

12.adb forward

说明：设备的端口转发。

用法：adb forwrad[（远程端）协议：端口号][（设备端）协议：端口号]

案例：

这个命令在IDA调试中非常有用。

13.adb jdwp

说明：查看设备中可以被调试的应用的进程号。

用法：adb jdwp

案例：

这个命令或许用途不是很多，但是在调试的时候还是有点用途。

14.adb logcat

说明：查看当前日志信息。

用法1：adb logcat-s tag

案例：adb logcat-s fb

用法2：adb logcat|findstr pname/pid/keyword

案例：adb logcat|findstr cn.wjdiankong.demo

这个命令大家都不陌生，也是重中之重，有的同学或许会好奇，为何不用AS查看日志了，但是有时候AS不能满足需求，比如想开多个日志窗口，这时候可以打开多个cmd窗口利用这个命令查看日志信息即可。用得最多的就是-s参数，可以直接查看对应的tag日志信息，利用findstr进行信息过滤：

这里因为是在Windows下，所以用findstr进行信息过滤了，当然这个命令也可以直接在adb shell运行之后，比如：

这样就可以用grep进行信息过滤了。

3.3　shell命令

我所说的shell命令运行的前提是先运行adb shell，而这些命令和非shell命令都是互通的，所谓互通就是要想在设备中运行shell命令，就优先运行adb shell一下。比如查看当前应用信息，可以这么做：

也可以直接在外部运行，比如清空应用数据：

1.run-as

说明：可以在非root设备中查看指定debug模式的包名应用沙盒数据。

用法：run-as[package name]

案例：run-as cn.wjdiankong.demo

后面章节会详细介绍这个命令的原理。在开发中有时候对一个非root手机想看debug应用的沙盒数据，那么这个命令就可以帮助进行这项操作了。

2.ps

说明：查看设备的进程信息，或者指定进程的线程信息。

用法：ps|grep过滤内容

ps-t[pid]查看pid对应的线程信息

案例：

这个命令的重要程度不必多说，可以结合grep进行信息过滤。

3.pm clear

说明：清空指定包名应用的数据。

用法：pm clear[packagename]

案例：pm clear cn.wjdiankong.demo

有时候想清空一个应用的数据，可能需要去设置页面进行操作。可以不用那么麻烦，直接用这个命令即可。

4.pm install

说明：安装设备中的apk文件，功能和adb install一样。

用法：pm install[apk文件]

案例：

这个命令与adb install命令一样。

5.pm uninstall

说明：卸载设备中的应用，功能与adb uninstall一样。

用法：pm uninstall[packagename]

案例：

这个命令和adb uninstall命令一样。

6.am start

说明：启动一个应用。

用法：am start-n[包（package）名]/[包名].[活动（activity）名称]

案例：

注意：可以用debug方式启动应用（am start-D-n…）。特别在反编译调试应用的时候，可能需要用debug方式启动应用。

7.am startservice

说明：启动一个服务。

用法：am startservice-n[包（package）名]/[包名].[服务（service）名]

案例：

和上面命令类似，启动服务。

8.am broadcast

说明：发送一个广播。

用法：am broadcast-a[广播动作]

案例：

和上面的命令类似，发送一个广播。有时候定义了一个广播，可能需要测试，就可以借助这个功能模拟发送一个广播。

9.netcfg

说明：查看设备的ip地址。

用法：netcfg

有时候想查看设备的ip地址，去设置页面太费劲了，可以直接用这个命令即可：

10.netstat

说明：查看设备的端口号信息。

用法：netstat

有时候想查看设备的端口号信息，这个命令也是非常重要的，例如：

11.app_process

说明：运行Java代码。

用法：app_process[运行代码目录][运行主类]

案例：

这个命令主要用于Android中一些特殊开发场景中，想启动一个jar包，不过这个jar包有要求：需要dx命令把dex文件转化成jar包功能，实际上它不是一个正常的jar包了，而是一个包含了classes.dex文件的压缩文件了。

12.dalvikvm

说明：运行一个dex文件。

用法：dalvikvm-cp[dex文件][运行主类]

案例：

有时候为了测试一个dex文件功能可以用到这个命令，与上面的命令有很大相似之处，只是运行的文件不一样。

13.top

说明：查看当前应用的CPU消耗信息。

用法：top[-n/-m/-d/-s/-t]

-m//最多显示多少个进程

-n//刷新次数

-d//刷新间隔时间（默认5秒）

-s//按哪列排序

-t//显示线程信息而不是进程

案例：

这个命令在分析应用性能的时候非常有用，可以用grep过滤想要分析的应用信息，查看它的当前CPU使用率。

14.getprop

说明：查看系统属性值。

用法：getprop[属性值名称]

案例：

这个命令可以查看设备的信息，比如设备版本号、系统属性等，后面章节会介绍在root设备之后，还可以去修改这些系统属性。比如debug开关，让所有的应用都处于可调试状态。

3.4　操作apk命令

有两个命令可直接获取apk的信息，下面分别介绍。

1.用aapt操作apk命令

说明：查看apk中的信息以及编辑apk程序包。

用法：aapt dump xmltree[apk包][需要查看的资源文件xml]

案例：

依然用重定向把输出信息保存到文件中，然后再打开查看，可以看到非常详细的apk清单文件了。所以有时候想查看一个apk中的清单文件，也没必要进行复杂的反编译工作。

2.用dexdump操作dex命令

说明：可以查看一个dex文件的详细信息。

用法：dexdump[dex文件路径]

案例：

这个命令用于查看apk中dex文件的详细信息，有时候也没必要反编译，可以直接用这个命令即可。

3.5　进程命令

这里介绍用cat命令查看当前应用进程的信息，在逆向中可能用得到。

1.查看当前进程的内存加载情况

可以使用如命令

查看当前进程的内存映射信息，比如加载了哪些so文件，dex文件等：

2.查看进程的状态信息

可以利用如下命令

查看当前进程的状态信息，比如熟知的TracerPid：

3.查看当前应用使用的端口号信息

可以使用如下命令

获取当前应用使用到的端口号信息：

注意：上面介绍的shell命令也可以在手机中直接运行，需要去下载一个“终端模拟器”应用，直接在里面输入这些shell命令即可，这样不用电脑就可以进行操作了。

3.6　本章小结

本章分析了Android开发和逆向中常用的一些命令，这些命令对于后续操作起到非常高效的助推作用。当然还有其他命令，只是没有那么好用，也就没有介绍了。

第4章　so文件格式解析

本章详细介绍so文件的内容，so文件的格式解析非常重要，不仅对Android中的底层开发非常有用，而且对Android的安全更重要。在这个内容基础之上，后面的章节中会进一步介绍如何对so文件进行加密做到应用的安全防护，以及逆向分析应用的so代码逻辑。

4.1　ELF文件格式

Android中的so文件就是ELF文件，所以要了解so文件，必须先来了解一下ELF文件的格式。如何详细了解ELF文件？最好的方法就是手动写一个工具类来解析一个ELF文件。ELF文件格式见图4-1。

图4-1　ELF文件格式（见书后彩图，图片由@非虫授权）

这张图已经很详细了，后面用Java代码来解析ELF文件的时候，就是按照这张图来进行的。

4.2　解析工具

这里还需要介绍一个工具readelf，因为这个工具在解析ELF文件的时候也非常有用，而且是检查解析ELF文件的模板。不过Window下这个命令不能用，因为这个命令是Linux的，所以还得做个工作，就是安装Cygwin。

提示：Cygwin下载地址：http://pan.baidu.com/s/1C1Zci

下载Cygwin之后，需要改一个东西才能用，如图4-2所示。

图4-2　下载Cygwin工具

改一下这个文件：

将路径要改成你本地cygwin64中的bin目录的路径，不然运行错误。改好之后，直接运行Cygwin.bat就可以了。

关于readelf工具我们这里不做太详细的介绍，只介绍本章要用到的命令。

1.readelf-h xxx.so

查看so文件的头部信息，如下所示：

2.readelf-S xxx.so

查看so文件的节（Section）头的信息，如下所示：

3.readelf-l xxx.so

查看so文件的程序段头信息（Program），如下所示：

4.readelf-a xxx.so

查看so文件的全部内容，如下所示：

还有很多命令及用法，这里就不细讲了。

4.3　解析ELF文件

上面介绍了ELF文件格式资料、解析ELF文件的工具，下面就来实际操作一下，用Java代码动手解析一个libhello-jni.so文件。这个libhello-jni.so文件可以下载。

提示：libhello-jni.so下载地址：

http://download.csdn.net/detail/jiangwei0910410003/9204087

1.定义ELF文件中各个结构体内容

这需要参考elf.h这个头文件的格式，这个文件在Android源码目录中：

在看elf.h文件中定义的数据结构时，要记得每个字段的占用字节数就可以了。

有了结构定义，下面就来看看如何解析吧。在解析之前需要将so文件读取到byte[]中，定义一个数据结构类型：

2.解析ELF文件的头部信息

文件的最开始几个字节给出如何解释文件的提示信息。这些信息独立于处理器，也独立于文件中的其余内容。ELF头部信息数据结构如下表示：

这里介绍几个重要的字段，后面修改so文件的时候也会用到：

·e_phoff：是程序头（Program Header）内容在整个文件的偏移值，可以用这个偏移值来定位程序头的开始位置，用于解析程序头信息。

·e_shoff：是段头（Section Header）内容在这个文件的偏移值，可以用这个偏移值来定位段头的开始位置，用于解析段头信息。

·e_phnum：是程序头的个数。

·e_shnum：是段头的个数。

·e_shstrndx：是String段在整个段列表中的索引值，用于后面定位String段的位置。

参照图4-1可以很容易解析下面代码：

按照对应的每个字段的字节个数，读取字节就可以了。

3.解析段头信息

每个段头部可以用如下数据结构描述：

这个结构中字段就不做解释了。后面会手动构造这样一个数据结构，到时候再详细说明每个字段含义。

按照这个结构，解析也简单了：

这里需要注意的是，看到的SectionHeader一般都是多个的，所以这里用一个List集合来保存。

4.解析程序头信息

可执行文件或者共享目标文件的程序头部是一个结构数组，每个结构描述了一个段或者系统准备程序执行所必需的其他信息。目标文件的“段”包含一个或者多个“节区”，也就是“段内容”（Segment Contents）。程序头部仅对可执行文件和共享目标文件有意义。可执行目标文件在ELF头部的e_phentsize和e_phnum成员中给出其自身程序头部的大小。

程序头部的数据结构如下：

按照这个结构来进行解析，如下所示：

当然还有其他结构的解析工作，这里就不一一介绍了，因为这些结构在后面的介绍中不会用到，这里只需了解一下。

4.4　验证解析结果

解析工作做完了，为了验证解析工作是否正确，需要给每个结构定义打印函数，也就是重写toString方法即可，如下所示：

然后再使用readelf工具来查看so文件的各个结构内容，对比就可以知道解析是否成功了。

提示：解析代码下载地址：https://github.com/fourbrother/parse_androidso

4.5　本章小结

本章主要介绍了Android中的so文件格式解析内容，了解so文件格式，对应用安全防护和逆向应用都有很重要的意义。比如，现在为了应用的安全，可以对so文件进行加密操作，这就需要对so文件格式有非常深入的了解；在逆向应用的时候，碰到so加固混淆时，可以分析so文件格式来确定哪一块数据结构被加密和防护了，从而更深入地分析问题和解决问题。

第5章　AndroidManifest.xml文件格式解析

本章介绍Android的AndroidManifest.xml文件格式的内容。为什么要介绍这个内容呢？在应用安全防护中，可以利用对应用的AndroidManifest.xml文件做混淆，从而达到安全防护功能，同时在做逆向分析的时候，为了能够反编译成功，有时候必须要了解AndroidManifest.xml文件格式，需要进行文件修复才可行。

5.1　格式分析

首先来脑补一个知识点，Android中的apk程序其实就是一个压缩包，可以用压缩软件进行解压的，如图5-1所示。

图5-1　apk压缩包

可以看到这里有三个文件：AndroidManifest.xml、classes.dex、resources.arsc，这三个文件都非常重要，本章介绍第一个文件，后续两章分析后两个文件。

只要反编译过apk的人都知道apktool工具的工作原理就是解析这三个文件格式。因为Android在编译成apk之后，这个文件有自己的格式，用普通文本格式打开的话是乱码，所以需要解析成能看懂的内容。就像前一章解析so文件一样。

既然编译成apk之后格式变了，那么就说明谷歌给AndroidManifest定义了一种文件格式，只要知道这种格式的话，就可以详细地解析这个文件了，如图5-2所示。

这张图详细解析了AndroidManifest.xml文件的格式，但是光看这张图可能印象不深，所以要结合一个案例来解析一个文件，才能理解透彻。下面就用一个案例来解析一下吧。

随便找一个简单的apk，用压缩文件打开，解压出AndroidManifest.xml就可以了，然后开始读取内容进行解析。

图5-2　Android Mainfest.xml文件格式（见书后彩图）

5.2　格式解析

通过前一节的格式分析，大致知道了编译之后的AndroidManifest.xml文件格式，本节通过代码案例来详细解析文件中的各个模块。

5.2.1　解析头部信息

任何一个文件格式都会有头部信息，而且头部信息也很重要，同时，头部一般都有固定格式头部信息还有以下这些字段信息：

1）文件魔数：四个字节。

2）文件大小：四个字节。

下面就开始解析所有的字段Chunk内容了，其实每个Chunk的内容都有一个相似点，就是头部信息：ChunkType（四个字节）和ChunkSize（四个字节）。

5.2.2　解析String Chunk

String Chunk主要用于存放AndroidManifest文件中所有的字符串信息，如图5-3所示。

说明如下：

·ChunkType：StringChunk的类型，固定四个字节：0x001C0001。

·ChunkSize：StringChunk的大小，四个字节。

·StringCount：StringChunk中字符串的个数，四个字节。

·StyleCount：StringChunk中样式的个数，四个字节，但是在实际解析过程中，这个值一直是0x00000000。

·Unknown：位置区域，四个字节，在解析的过程中，这里需要略过四个字节。

·StringPoolOffset：字符串池的偏移值，四个字节，这个偏移值是相对于StringChunk的头部位置。

·StylePoolOffset：样式池的偏移值，四个字节，这里没有Style，所以这个字段可忽略。

·StringOffsets：每个字符串的偏移值，它的大小应该是StringCount*4个字节。

·SytleOffsets：每个样式的偏移值，它的大小应该是SytleCount*4个字节。

后面就是字符串内容和样式内容了。

图5-3　String Chunk结构

下面介绍代码，由于代码的篇幅有点长，所以这里分段说明。后面会给出整个项目代码下载地址的。

1）首先需要把AndroidManifest.xml文件读入到一个byte数组中：

2）下面来解析头部信息：

按照上面说的格式解析即可：

3）解析StringChunk信息：

这里需要解释几点：

1）在上面的格式说明中，有一个Unknow字段，四个字节，所以需要略过。

2）在解析字符串内容的时候，字符串内容的结束符是0x0000。

3）每个字符串开始的前两个字节是字符串的长度。

有了每个字符串的偏移值和大小，那么解析字符串内容就简单了，如下所示：

可以看到0x000B（高位和低位相反）就是字符串的大小，结尾是0x0000：

一个字符对应的是两个字节，而且这里有一个方法：

逻辑就是过滤空字符串：在C语言中是NULL，在Java语言中就是00，如果不过滤的话，会出现下面的这种情况：

每个字符是宽字符，很难看，其原因是每个字符后面多了一个00，所以过滤之后就可以了，如下所示：

上面解析了AndroidManifest.xml中所有的字符串内容。这里需要用一个全局的字符列表来存储这些字符串的值，后面会用索引来获取这些字符串的值。

5.2.3　解析ResourceId Chunk

ResourceId Chunk主要是用来存放AndroidManifest中用到的系统属性值对应的资源ID，如图5-4所示，比如android：versionCode中的versionCode属性，android是前缀，后面会说到。

图5-4　ResourceId Chunk结构

·ChunkType：ResourceId Chunk的类型，固定四个字节0x00080108。

·ChunkSize：ResourceId Chunk的大小，四个字节。

·ResourceIds：ResourceId的内容，这里大小是ResourceId Chunk大小除以4，减去头部的大小8个字节（ChunkType和ChunkSize）。

解析代码如下：

解析结果如下：

这里解析出来的ID到底是什么呢？

Android中的ID值

在写Android程序的时候，都会发现有一个R文件，那里面存放着每个资源对应的ID，那么这些ID值是怎么得到的呢？

Package ID相当于一个命名空间，限定资源的来源。Android系统当前定义了两个资源命令空间，其中一个是系统资源命令空间，其Package ID等于0x01；另外一个是应用程序资源命令空间，其Package ID等于0x7f。所有位于[0x01，0x7f]之间的Package ID都是合法的，而在这个范围之外的都是非法的。前面提到的系统资源包package-export.apk的Package ID就等于0x01，而在应用程序中定义的资源Package ID的值都等于0x7f，这一点可以通过生成的R.java文件来验证。

Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等若干种，每一种都会被赋予一个ID。

Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意，不同类型的资源的Entry ID有可能是相同的，但是由于它们的类型不同，仍然可以通过其资源ID区别开来。

关于资源ID的更多描述，以及资源的引用关系，可以参考frameworks/base/libs/utils目录下的README文件。

可以得知系统资源对应ID的XML文件在这里：frameworks\base\core\res\res\values\public.xml，代码如下：

用上面解析到的ID去public.xml文件中查询一下，得到如下结果：

查到了，是versionCode，这个系统资源ID存放的文件public.xml是很重要的，后面在讲解resource.arsc文件格式的时候还会用到。

5.2.4　解析Start Namespace Chunk

这个Chunk主要包含一个AndroidManifest文件中的命令空间的内容，Android中的XML都是采用Schema格式的，所以肯定有Prefix和URI的，如图5-5所示。

图5-5　Start Namespace Chunk

XML格式有两种：DTD和Schema。

说明如下：

·Chunk Type：Chunk的类型，固定四个字节0x00100100。

·Chunk Size：Chunk的大小，四个字节。

·Line Number：在AndroidManifest文件中的行号，四个字节。

·Unknown：未知区域，四个字节。

·Prefix：命名空间的前缀（在字符串中的索引值），比如：android。

·Uri：命名空间的URI（在字符串中的索引值），比如：http://schemas.android.com/apk/res/android。

解析代码如下：

解析的结果如下：

这里的内容就是上面我们解析完String之后对应的字符串索引值。这里需要注意的是，一个XML中可能会有多个命名空间，所以这里用Map存储Prefix和URI对应的关系，后面在解析节点内容的时候会用到。

5.2.5　解析Start Tag Chunk

这个Chunk主要是存放AndroidManifest.xml中的标签信息，是最核心的内容，当然也是最复杂的内容，如图5-6所示。

图5-6　Start Tag Chunk结构

说明如下：

·Chunk Type：Chunk的类型，固定四个字节：0x00100102。

·Chunk Size：Chunk的大小，固定四个字节。

·Line Number：对应于AndroidManifest中的行号，四个字节。

·Unknown：未知领域，四个字节。

·Namespace Uri：这个标签用到的命名空间的URI，比如用到了android这个前缀，那么就需要用http://schemas.android.com/apk/res/android这个URI去获取，四个字节。

·Name：标签名称（在字符串中的索引值），四个字节。

·Flags：标签的类型，四个字节，比如是开始标签还是结束标签等。

·Attribute Count：标签包含的属性个数，四个字节。

·Class Atrribute：标签包含的类属性，四个字节。

·Atrributes：属性内容，每个属性算是一个Entry，Entry是大小为5的字节数组[Namespace，URI，Name，ValueString，Data]，在解析的时候需要注意第四个值，要做一次处理：需要右移24位。所以这个字段的大小是：“属性个数×5×4个字节”。

解析属性代码如下：

可以看到，第四个值需要额外的处理一下，就是需要右移24位。解析完属性之后，可以得到一个标签的名称、属性名称、属性值：

看解析的结果：

标签manifest包含的属性如下：

这里有几个问题需要解释一下：

1）为什么看到的是三个属性，但是解析打印的结果是5个？

因为系统在编译apk的时候，会添加两个属性：platformBuildVersionCode和platform-BuildVersionName。

这是发布的设备版本号和版本名称：

这个是解析之后的结果。

2）当没有android这样的前缀时，NamespaceUri是null。

3）当dataType不同，对应的data值也不同，如下所示：

这个方法就是用来转义的，后面在解析resource.arsc的时候也会用到这个方法。

4）每个属性理论上都会含有一个NamespaceUri，这也决定了属性的前缀Prefix默认都是Android，但是有时候会自定义一个控件，这时候就需要导入NamespaceUri和Prefix了。所以一个XML中可能会有多个Namespace，每个属性都会包含NamespaceUri。

到这里就算解析完大部分的工作了，至于EndTagChunk，与StartTagChunk非常类似，这里就不再详解了，下面代码也加了解释：

在解析的时候，需要做一个循环操作：

因为Android中在解析XML的时候提供了很多种方式，但是这里没有用任何一种方式，而是用纯代码编写的，所以用一个循环来遍历解析Tag。其实这种方式类似于用SAX解析XML，这时候本节开头提到的Flag字段就大有用途了。这里还做了一个工作就是将解析之后的XML格式化一下，如下所示：

难度不大，这里也就不继续解释了。有一个地方需要优化，就是可以利用LineNumber属性来精确到格式化后文件的行数，不过这个工作量有点大，这里就不做了。有兴趣的同学可以考虑一下，格式化完之后的结果如下：

这里就把之前的16进制的内容解析出来了。

还有一个问题，就是看到还有很多@7F070001这类的东西，这其实是资源Id，这需要解析完resource.arsc文件之后，才能对应上这个资源，参见下一章。

当发现可以解析AndroidManifest文件了，那么同样也可以解析其他的XML文件：

这时解析其他XML时报错了，定位代码发现是在解析String Chunk的地方报错了，修改如下：

因为其他的XML中的字符串格式和AndroidManifest.xml中的不一样，所以这里需要单独解析一下：

修改之后就可以了。

在反编译的时候，有时候只想反编译AndroidManifest内容，所以ApkTool工具就有点繁琐了，不过网上有个已经写好了的工具AXMLPrinter.jar，这个工具很好用：

将xxx.xml解析之后输出到demo.xml中即可。

提示：AXMLPrinter.jar工具下载地址：

http://download.csdn.net/detail/jiangwei0910410003/9415323

源代码下载地址：

http://download.csdn.net/detail/jiangwei0910410003/9415342

AXMLPrinter.jar工具项目见图5-7。

从项目结构可以发现，它用的是Android中自带的Pull解析XML的，主函数是：

图5-7　AXMLPrinter项目

到这里还需要告诉大家一件事，对上面的解析工作有一个更简单的方法，那就是aapt命令。关于这个aapt是干什么的，网上有很多资料，其实就是将Android中的资源文件打包成resource.arsc即可，参见图5-8。

图5-8　aapt工具

类型为res/animator、res/anim、res/color、res/drawable（非Bitmap文件，即非.png、.9.png、.jpg、.gif文件）、res/layout、res/menu、res/values和res/xml的资源文件均会从文本格式的XML文件编译成二进制格式的XML文件。

要从文本格式编译成二进制格式的原因如下：

·二进制格式的XML文件占用空间更小。这是由于所有XML元素的标签、属性名称、属性值和内容所涉及的字符串都会被统一收集到一个字符串资源池中去，并且会去重。有了这个字符串资源池，原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值，从而可以减少文件的大小。

·二进制格式的XML文件解析速度更快。这是由于二进制格式的XML元素里面不再包含有字符串值，因此就避免了进行字符串解析，从而提高速度。

将XML资源文件从文本格式编译成二进制格式，解决了空间占用以及解析效率的问题，但是对于Android资源管理框架来说，这只是完成了其中的一部分工作。Android资源管理框架的另外一个重要任务就是要根据资源ID来快速找到对应的资源。

那么下面用aapt命令查看一下。aapt命令在AndroidSdk目录中，如图5-9所示。

图5-9　aapt命令的目录

看到路径了：Android SDK目录/build-tools/下，这个目录下全是Android中构建apk的所有工具，再看一下这些工具的用途，参见图5-10。

说明如下：

·aapt.exe生成R.java类文件。

·aidl.exe把.aidl转成.java文件（如果没有aidl，则跳过这一步）。

·javac.exe编译.java类文件生成class文件。

图5-10　构建apk的工具

·dx.bat命令行脚本生成classes.dex文件。

·aapt.exe生成资源包文件（包括res、assets、androidmanifest.xml等）。

·apkbuilder.bat生成未签名的apk安装文件。

·arsigner.exe对未签名的包进行apk签名。

原来可以不借助任何IDE工具也是可以打出一个apk包的。

继续看aapt命令的用法：

将输入的结果定向到demo.txt中，如图5-11所示。

图5-11　aapt命令输出结果

看到解析出来的内容就是上面解析的AndroidManifest.xml内容，所以这也是一个解析方法。当然aapt命令是系统提供给一个很好的工具，可以在反编译的过程中借助这个工具。这里记得有一个aapt命令就好了，它的用途还有很多，可以单独编译成一个resource.arsc文件来，后面会用到这个命令。

提示：项目下载地址：https://github.com/fourbrother/parse_androidxml

5.3　本章小结

本章主要介绍Android编译之后的AndroidManifest.xml文件格式。可以使用网上的一个小工具AXMLPrinter。但这里我也写了一个工具解析。那么本章内容仅仅是为了解析AndroidManifest吗？肯定不是，写本章内容是为后面介绍反编译apk做准备，其实现在有很多人都发现了，在使用apktool来反编译apk的时候经常报出一些异常信息，其实那就是加固的人用来对抗apktool工具的，导致反编译失败。所以有必要了解apktool的源码和解析原理，这样才能遇到反编译失败时，能定位到问题，修复apktool工具即可。

apktool的工具解析原理其实就是解析AndroidManifest.xml、resource.arsc、classes.dex这三个文件。还有其他的布局、资源xml等，那么针对这几个问题，本章专门解析XML文件的格式，后面章节还会继续解析resource.arsc和classes.dex文件的格式。

第6章　resource.arsc文件格式解析

前一章介绍了如何解析AndroidManifest文件格式，本章介绍resource.arsc文件格式解析。resource.arsc格式解析很重要，现在很多应用为了缩减应用包的大小开始对资源进行混淆，就用到这个文件格式；应用安全防护中可以通过对资源混淆来做到安全应用，也用到这个文件格式。本章将从资源文件id格式开始，详细解析资源文件的数据结构头、字符串、正文等。

6.1　Android中资源文件id格式

在使用apktool工具进行反编译的时候，会发现有一个文件res/values/public.xml，如图6-1所示。

图6-1　public.xml文件

查看一下public.xml文件内容，如图6-2所示。

可以看到，这个文件保存了apk中所有id类型和对应的id值。这里面的每个条目内容都包括如下内容：

·type：类型名

·name：资源名

·id：资源的id

类型包括如下几种：drawable、menu、layout、string、attr、color、style等，所以会在反编译之后的文件夹中看到这几个类型的文件。

上面介绍了如何使用apktool查看资源文件的内容，下面介绍如何来解析resource.arsc文件。

解压一个apk得到对应的resource.arsc文件。按照惯例，每个文件的格式描述都是有对应的数据结构。resource也不例外：frameworks\base\include\androidfw\ResourceTypes.h，这就是resource中定义的所有数据结构。下面用一张图详细表示出resources.arsc文件格式，如图6-3所示。

图6-2　public.xml文件

图6-3　resources.arsc文件格式（见书后彩图）

6.2　数据结构定义

资源文件的项目如图6-4所示。

图6-4　资源文件的数据结构

可见到这里定义了很多的数据结构，下面分别介绍。

6.2.1　头部信息

Resources.arsc文件格式是由一系列的chunk构成，每一个chunk均包含如下结构的ResChunk_header，用来描述这个chunk的基本信息：

参数说明：

·type：当前chunk的类型。

·headerSize：当前chunk的头部大小。

·size：当前chunk的大小。

6.2.2　资源索引表的头部信息

Resources.arsc文件的第一个结构是资源索引表头部，其结构描述了Resources.arsc文件的大小和资源包数量：

参数说明：

·header：就是标准的Chunk头部信息格式。

·packageCount：被编译的资源包的个数。

6.2.3　资源项的值字符串资源池

紧跟着资源索引表头部的是资源项的值字符串资源池，这个字符串资源池包含了所有在资源包里面定义的资源项的值字符串，字符串资源池头部的结构如下：

参数说明：

·header：标准的Chunk头部信息结构。

·stringCount：字符串的个数。

·styleCount：字符串样式的个数。

·flags：字符串的属性，可取值包括0x000（UTF-16）、0x001（字符串经过排序）、0X100（UTF-8）和它们的组合值。

·stringStart：字符串内容块相对于其头部的距离。

·stylesStart：字符串样式块相对于其头部的距离。

紧接着头部的是两个偏移数组，分别是字符串偏移数组和字符串样式偏移数组。这两个偏移数组的大小分别等于stringCount和styleCount的值，而每一个元素的类型都是无符号整型。整个字符中资源池结构如图6-5所示。

图6-5　字符串资源池结构

字符串资源池中的前两个字节为字符串长度，长度计算方法如下代码：

如果字符串编码格式为UTF-8则字符串以0X00作为结束符，UTF-16则以0X0000作为结束符。

字符串与字符串样式有一一对应的关系，也就是说，如果第n个字符串有样式，则它的样式描述位于样式块的第n个元素。字符串样式的结构包括如下两个结构体：ResStringPool_ref和ResStringPool_span。一个字符串可以对应多个ResStringPool_span和一个ResStringPool_ref。ResStringPool_span在前描述字符串的样式，ResStringPool_ref在后固定值为0XFFFFFFFF作为占位符。样式块最后会以两个值为0XFFFFFFFF的ResStringPool_ref作为结束。

6.2.4　Package数据块

接着资源项的值字符串资源池后面的部分就是Package数据块，这个数据块记录编译包的元数据，头部结构如下：

参数说明：

·header：Chunk的头部信息数据结构。

·id：包的id，等于Package Id，一般用户包的值Package Id为0X7F，系统资源包的Package Id为0X01。这个值很重要的。

·name：包名。

·typeString：类型字符串资源池相对头部的偏移。

·lastPublicType：最后一个导出的Public类型字符串在类型字符串资源池中的索引，目前这个值设置为类型字符串资源池的元素个数。在解析的过程中没发现它的用途。

·keyStrings：资源项名称字符串相对头部的偏移。

·lastPublicKey：最后一个导出的Public资源项名称字符串在资源项名称字符串资源池中的索引，目前这个值设置为资源项名称字符串资源池的元素个数。在解析的过程中没发现它的用途。

Package数据块的整体结构参见图6-6。

图6-6　Package数据块结构

其中Type String Pool和Key String Pool是两个字符串资源池，结构和资源项的值字符串资源池结构相同，分别对应类型字符串资源池和资源项名称字符串资源池。

再接下来的结构体可能是类型规范数据块或者类型资源项数据块，可以通过它们的Type来识别，类型规范数据块的Type为RES_TABLE_TYPE_SPEC_TYPE，类型资源项数据块的Type为RES_TABLE_TYPE_TYPE。

6.2.5　类型规范数据块

类型规范数据块用来描述资源项的配置差异性。通过这个差异性描述，就可以知道每一个资源项的配置状况。知道了一个资源项的配置状况之后，Android资源管理框架在检测到设备的配置信息发生变化之后，就可以知道是否需要重新加载该资源项。类型规范数据块是按照类型来组织的，也就是说，每一种类型都对应有一个类型规范数据块。其数据块头部结构如下：

参数说明：

·header：Chunk的头部信息结构。

·id：标识资源的Type ID。Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等若干种，每一种都会被赋予一个ID。

·res0：保留，始终为0。

·res1：保留，始终为0。

·entryCount：等于本类型的资源项个数，指名称相同的资源项的个数。

ResTable_typeSpec后面紧跟着的是一个大小为entryCount的uint32_t数组，每一个数组元素都用于描述一个资源项的配置差异性。

6.2.6　资源类型项数据块

资源类型项数据块用来描述资源项的具体信息，这样就可以知道每一个资源项的名称、值和配置等信息。资源类型项数据同样是按照类型和配置来组织的，也就是说，一个具有n个配置的类型一共对应有n个类型资源项数据块，其数据块头部结构如下：

参数说明：

·header：Chunk的头部信息结构。

·id：标识资源的Type ID。

·res0：保留，始终为0。

·res1：保留，始终为0。

·entryCount：等于本类型的资源项个数，指名称相同的资源项的个数。

·entriesStart：等于资源项数据块相对头部的偏移值。

·resConfig：指向一个ResTable_config，用来描述配置信息、地区、语言、分辨率等。

ResTable_type后接着是一个大小为entryCount的uint32_t数组，每一个数组元素都用于描述一个资源类型项数据块的偏移位置。紧跟在这个偏移数组后面的是一个大小为entryCount的ResTable_entry数组，每一个数组元素都用来描述一个资源项的具体信息。ResTable_entry的结构如下：

ResTable_entry根据flags的不同，后面跟随的数据也不相同，如果flags为1，则ResTable_entry是ResTable_map_entry。ResTable_map_entry继承自ResTable_entry，其结构如下：

ResTable_map_entry其后跟随count个ResTable_map类型的数组，ResTable_map的结构如下：

如果flags为0，则ResTable_entry其后跟随的是一个Res_value，描述一个普通资源的值，Res_value结构如下：

参数说明：

·size：ResValue的头部大小。

·res0：保留，始终为0。

·dataType：数据的类型，可以从上面的枚举类型中获取。

·data：数据对应的索引。

这里看到了有一个转化的方法，在解析AndroidManifest文件的时候也用到了这个方法。

6.3　解析代码

因为篇幅的原因，这里就不把所有的代码都粘贴出来了，后面会列出代码下载地址，首先读取resource.arsc文件到一个字节数组，然后开始解析。

6.3.1　解析头部信息

头部信息代码如下：

解析结果如下：

6.3.2　解析资源字符串内容

解析资源字符串代码如下：

这里有一个核心的方法parseStringPoolChunk：

这里在得到一个字符串的时候，需要得到字符串的开始位置和字符串的大小即可，这点和解析AndroidManifest.xml文件中的字符串原理是一样的。就是一个字符串的头两个字节中的最后一个字节是字符串的长度。这里在解析完字符串之后，需要用一个列表将其存储起来，后面要用到，需要通过索引来取字符串内容。

解析结果如下：

6.3.3　解析包信息

解析包信息代码如下：

这里看到有一个特殊的地方，就是最后两行，这是后面需要重点解析的两个内容，一个是资源值字符串的偏移值，另一个是资源类型字符串的偏移值。

解析结果如下：

6.3.4　解析资源类型的字符串内容

解析资源类型的字符串代码如下：

这里也是用parseStringPoolChunk方法进行解析的，同样也需要用一个字符串列表存储内容。

解析结果如下：

6.3.5　解析资源值字符串内容

解析资源值字符串内容代码如下：

这里也是使用parseStringPoolChunk方法来解析，解析完之后需要用一个字符串列表保存，后面需要使用索引值来访问。

解析结果如下：

6.3.6　解析正文内容

这里说到的正文内容就是ResValue值，是开始构建public.xml中的条目信息，和类型的分离不同的XML文件，所以这部分内容的解析工作有点复杂，如下所示：

这里有一个循环解析，有两个方法，一个是isEnd方法，另一个是isTypeSpec方法。

如果仔细看前面的图6-3，就可以看到后面的ResType和ResTypeSpec两个内容是交替出现的，直到文件结束。

所以isEnd方法就是判断是否到达文件结束位置：

还有一个方法就是判断是ResType还是ResTypeSpec，这可以通过Chunk中头部信息来区分的：

下面分别来解析ResTypeSpec和ResType这两个内容。

1.解析ResTypeSpec

主要得到Res的每个类型名：

解析结果如下：

2.解析ResType

主要得到每个res类型的所有条目内容：

看到这里，发现解析很复杂，和在讲解数据结构的时候一样，需要解析很多内容：ResValue、ResTableMap、ResTableMapEntry、ResTableEntry、ResConfig。关于每个数据结构如何解析这里就不多说了，就是读取字节即可。这里有一个核心的代码：

判断flag的值来进行不同的解析操作。需要注意这点。

解析结果如下：

看到解析结果还是挺欣慰的，因为最难的地方解析成功了，这就是想要的结果。这里需要解释的是，有了这些值构建public.xml内容和各个类型的XML内容就很简单了。

注意，这里ResId的构造方法是：

可以看到一个int类型的resId，它的最高两个字节表示packId，系统资源id是0x01，普通应用资源id是0x7F。它的中间的两个字节表示resTypeId，类型id，这个值从0开始，比如例子中第一个类型是attr，那么它的resTypeId就是00。它的最低四个字节表示这个资源的顺序id，从1开始，逐渐累加1。

提示：项目下载地址：https://github.com/fourbrother/parse_androidarsc

上面就解析完了所有的resource.arsc文件，解析resource.arsc文件格式的目的有两个：

·在使用apktool工具进行反编译的时候，经常出现一些莫名的异常信息，最多的就是NotFound ResId 0x0000XXX这些内容。那么这时候就需要修复，当然可以得到apktool的源码来解决这个问题，还可以使用自己写的这套解析代码。

·解析resource.arsc文件之后，对resource.arsc文件格式如果有了解，可以对资源文件名进行混淆，从而减小apk包大小。因为META-INF文件夹下的三个文件体积很大，原因就是它们内部保存了每个资源名称，在项目中有时候为了不造成冲突，就把资源名起的很长，那么这样就会导致apk的包很大。同样resource.arsc文件也会很大，因为资源名都是需要保存的，但是Android中的混淆是不会对资源文件进行混淆的，所以这时候就可以通过这个思路来减小包apk的大小了。

在前面一章中已经介绍了关于aapt命令的用法了，这里依然可以利用aapt命令解析arsc文件，用aapt命令很简单：

将输入的结果定向到demo.txt中，如图6-7所示。

图6-7　用aapt命令输出结果

看到内容就是上面解析的AndroidManifest.xml内容，所以这也是一个解析方法。为何aapt命令最后说呢？因为了解AndroidManifest.xml格式能更好地掌握解析方法。以后记得有一个aapt命令就好了，它的用途很多，可以单独编译成一个resource.arsc文件来，后面会用到这个命令。

6.4　本章小结

Android中的资源文件最后编译成resource.arsc文件，本章介绍了这个文件的格式，在后续的安全防护应用和逆向应用中都有很重要的作用。

第7章　dex文件格式解析

前几章介绍了so文件、AndroidManifest文件、resources文件，本章将介绍dex文件格式的解析。了解dex文件格式对后面的应用安全防护非常有用。

7.1　dex文件格式

下面来看一下dex文件格式说明图，如图7-1所示。

图7-1　dex文件格式（见书后彩图，图片由@非虫授权）

dex的文件格式如图7-2所示。

图7-2　dex文件格式

7.2　构造dex文件

在讲解dex文件之前，需要先创建一个简单的例子来帮助来解析。不借助任何IDE工具就可以构造一个dex文件。用javac、dx命令即可。创建Java源文件，内容如下代码：

在当前工作路径下，编译方法如下：

1）编译成java class文件。执行命令javac Hello.java。编译完成后，目录下生成Hello.class文件。可以使用命令java Hello来测试下，会输出代码中的“Hello，Android！”的字符串。

2）编译成dex文件。编译工具在Android SDK的路径.：/build-tools/19.0.1/dx，其中19.0.1是Android SDK build_tools的版本，请按照在本地安装的build_tools版本来设置路径。建议该路径加到path环境变量中。

执行命令如下：

编译正常会生成Hello.dex文件。

3）使用ADB运行测试。

测试命令和输出结果如下：

重要说明：

·测试环境使用真机和Android虚拟机都可以，核心的命令是：

·-cp是class path的缩写，后面的Hello是要运行的class名称。网上有描述说输入dalvikvm--help可以看到dalvikvm的帮助文档，但是在Android 4.4的官方模拟器和自己的手机上测试都提示找不到Class路径，在Android老的版本（4.3）上测试还是有输出的。

·因为命令执行时，dalvikvm会在/data/dalvik-cache/目录下创建.dex文件，因此要求adb的执行Shell对目录/data/dalvik-cache/有读、写和执行的权限，否则无法达到预期效果。

7.3　解析数据结构

下面按照图7-1的思路来一一讲解各个数据结构。

7.3.1　头部信息Header结构

dex文件里的header除了描述.dex文件的文件信息外，还有文件里其他各个区域的索引。header对应为结构体类型，逻辑上的描述用结构体header_item来理解它。先给出结构体里面用到的数据类型ubyte和uint的解释，然后是结构体的描述，后面对各种结构描述的时候也是用这种方法。

代码定义如下：

查看Hex如下：

Header的大小固定为0x70，偏移地址从0x00到0x70，提取信息如下：

用一张图来描述各个字段的长度：

里面以_size和_off为后缀的描述为：data_size是以字节为单位描述data区的大小，其余的_size都是描述该区里元素的个数；_off描述相对于文件起始位置的偏移量。其余的6个是描述dex文件信息的，各项说明如下：

·magic value：这8个字节一般是常量，为了使dex文件能够被识别出来，它必须出现在dex文件的最开头的位置。数组的值可以转换为一个字符串如下：{0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00}="dex\n035\0"，中间是一个'\n'符号后面035是dex文件格式的版本。

·checksum和signature：checksum是文件校验码，使用alder32算法校验文件除去maigc、checksum外余下的所有文件区域，用于检查文件错误。signature使用SHA-1算法hash除去magic、checksum和signature外余下的所有文件区域，用于唯一识别本文件。

·file_size：dex文件的大小。

·header_size：header区域的大小，单位字节，一般固定为0x70常量。

·endian_tag：大小端标签，标准dex文件格式为小端，此项一般固定为0x1234 5678常量。

·link_size和link_off：这两个字段表示的是链接数据的大小和偏移值。

·map_off：map item的偏移地址，该item属于data区里的内值要大于等于data_off的大小。

结构如map_list描述：

定义位置：data区。

引用位置：header区。

map_list里先用一个uint描述后面有size个map_item，后续就是对应的size个map_item描述。

map_item结构有4个元素：type表示该map_item的类型，本节能用到的描述如下，详细DalvikExecutable Format里Type Code的定义；size表示再细分此item，该类型的个数；offset是第一个元素的针对文件初始位置的偏移量；unuse是用对齐字节的，无实际用处。

结构定义如下：

header→map_off=0x0244，偏移为0244的位置值为0x000d。每个map_item描述占用12字节，整个map_list占用12*size+4个字节。所以整个map_list占用空间为12*13+4=160=0x00a0，占用空间为0x 0244~0x 02E3。从文件内容上看，也是从0x 0244到文件结束的位置。

地址0x0244的一个uinit的值为0x0000000d，map_list->size=0x0d=13，说明后续有13个map_item。根据map_item的结构描述在0x0248~0x02e3里的值，整理出这段二进制所表示的13个map_item内容，汇成表格如下：

map_list→map_item里的内容，有部分item跟header里面相应item的offset地址描述相同。但map_list描述的更为全面些，又包括了HEADER_ITEM、TYPE_LIST，STRING_DATA_ITEM等，最后还有它自己TYPE_MAP_LIST。

至此，header部分描述完毕，它包括描述dex文件的信息，其余各索引区和data区的偏移信息，一个map_list结构。map_list里除了对索引区和数据区的偏移地址又一次描述，也有其他诸如HEAD_ITEM、DEBUG_INFO_ITEM等信息。

·string_ids_size和string_ids_off。这两个字段表示dex中用到的所有字符串内容的大小和偏移值，需要解析完这部分，然后用一个字符串池存起来，后面有其他的数据结构会用索引值来访问字符串池，这个池子也是非常重要的。

·type_ids_size和type_ids_off。这两个字段表示dex中的类型数据结构的大小和偏移值，比如类类型、基本类型等信息。

·proto_ids_size和type_ids_off。这两个字段表示dex中元数据信息数据结构的大小和偏移值，描述方法的元数据信息，比如方法的返回类型，参数类型等信息。

·field_ids_size和field_ids_off。这两个字段表示dex中字段信息数据结构的大小和偏移值。

·method_ids_size和method_ids_off。这两个字段表示dex中方法信息数据结构的大小和偏移值。

·class_defs_size和class_defs_off。这两个字段表示dex中类信息数据结构的大小和偏移值，这个数据结构是整个dex中最复杂的，它内部层次很深，包含了很多其他的数据结构，所以解析起来也很麻烦，所以后面会着重讲解这个数据结构。

·data_size和data_off。这两个字段表示dex中数据区域的结构信息的大小和偏移值，这个结构中存放的是数据区域，比如定义的常量值等信息。

头部包含的信息还是很多的，主要分为两个部分：

·魔数+签名+文件大小等信息。

·后面的各个数据结构的大小和偏移值都是成对出现的。

7.3.2　string_ids数据结构

string_ids区索引了dex文件所有的字符串。这个区里的元素格式为string_ids_item，可以使用结构体如下描述：

以_ids结尾的各个段里放置的都是对应数据的偏移地址，只是为一个索引，所以才会在dex文件布局里把这些区归类为“索引区”。

string_data_off只是一个偏移地址，它指向的数据结构为string_data_item，代码定义如下：

这里涉及LEB128（little endian base 128）格式，是基于1个字节的一种不定长度的编码方式。若第一个字节的最高位为1，则表示还需要下一个字节来描述，直至最后一个字节的最高位为0。每个字节的其余位用来表示数据。LEB128这种数据类型的出现其实是为了解决一个问题，那就是减少内存的浪费，它表示整型类型的数值，但是整型类型四个字节有时候在使用的时候有点浪费，它的原理如下所示：

图中使用两个字节表示。编码的每个字节有效部分只有低7位，每个字节的最高位用来指示是否是最后一个字节：

·非最高字节的第7位为0。

·最高字节的第7位为1。

将LEB128编码的数字转换为可读数字的规则是：除去每个字节的第7位，将每个字节剩余的7个位拼接在一起，即为数字。

比如：LEB128编码的0x02b0转换后的数字为0x0130。

转换过程如下：

0x02b0=>0000 0010 1011 0000=>去除最高位=>000 0010 011 0000=>按4位重排=>00 0001 0011 0000=>0x130。

底层代码位于：android/dalvik/libdex/leb128.h。

Java中也写了一个工具类：

这个方法是读取dex中Uleb128类型的数据，遇到一个字节最高位=0就停止读下个字节的原理来实现即可，还有一个方法就是解码Uleb128类型的数据：

原理是去除每个字节的最高位，接着拼接剩下的7位，然后重新构造一个整型类型的数据，位不够就从低位开始左移。

通过上面的Uleb128的解释来看，其实Uleb128类型就是1～5个字节来回浮动，为什么是5呢？因为它要表示一个4个字节的整型类型，但是每个字节要去除最高位，那么肯定最多只需要5个字节就可以表示4个字节的整型类型数据了。

下面回归正题，继续来看string_ids数据结构。根据string_ids_item和string_data_item的描述，加上header里提供的入口位置string_ids_size=0x0e和string_ids_off=0x70，可以整理出string_ids及其对应的数据如下：

string_ids_item和string_data_item里提取出的对应数据表格如下：

string里的各种标志符号，诸如L、V、VL、[等在dex文件里有特殊的意思。

string_ids的意思就是找到这些字符串。其实使用二进制编辑器打开dex文件时，一般工具默认翻译成ASCII码，总会有一大片熟悉的字符白生生地很是晃眼。刚才走过的分析流程，就是顺藤摸瓜找到它们是怎么来的。以后的一些type-ids、method_ids也会引用到这一片熟悉的字符串。

提示：后面的解析代码会看到，其实没必要用那么复杂地去解析Uleb128类型，因为会看到这个字符串和我们之前解析XML和resource.arsc格式一样，每个字符串的第一个字节表示字符串的长度，那么只要知道每个字符串的偏移地址就可以解析出字符串的内容了，而每个字符串的偏移地址是存放在string_ids_item中的。

到这里就解析完了dex中所有的字符串内容，用一个字符串池来进行存储即可。下面来继续看type_ids数据结构。

7.3.3　type_ids数据结构

type_ids数据结构中存放的数据主要是描述dex中所有的类型，比如类类型、基本类型等信息。type_ids区索引了dex文件里的所有数据类型，包括class类型、数组类型（array types）和基本类型（primitive types）。本区域里的元素格式为type_ids_item，结构描述如下：

type_ids_item里面descriptor_idx值的意思是string_ids里的index序号，是用来描述此type的字符串。

根据header里type_ids_size=0x07，type_ids_off=0xa8，找到对应的二进制描述区。00000a0：1a02，如下所示：

根据type_id_item的描述，整理出表格如下。

因为type_id_item→descriptor_idx里存放的是指向string_ids的index号，所以也能得到该type的字符串描述。这里出现了3个type descriptor：

·L表示class的详细描述，一般以分号表示class描述结束。

·V表示void返回类型，只有在返回值的时候有效。

·[表示数组，[Ljava/lang/String；可以对应到Java语言里的java.lang.String[]类型。

后面的其他数据结构也会使用到type_ids类型，所以这里解析完type_ids也是需要用一个池子来存放的，后面直接用索引index来访问即可。

7.3.4　proto_ids数据结构

proto的意思是method prototype，代表Java语言里的一个method的原型。proto_ids里的元素为proto_id_item，结构如下：

其中：

·shorty_idx：跟type_ids一样，它的值是一个string_ids的index号，最终是一个简短的字符串描述，用来说明该method原型。

·return_type_idx：它的值是一个type_ids的index号，表示该method原型的返回值类型。

·parameters_off：后缀off是offset，指向method原型的参数列表type_list；若method没有参数，值为0。参数列表的格式是type_list，结构从逻辑上如下描述。size表示参数的个数；type_idx是对应参数的类型，它的值是一个type_ids的index号，跟return_type_idx是同类东西。

header里proto_ids_size=0x03，proto_ids_off=0xc4，它的二进制描述区如下：

根据proto_id_item和type_list的格式，对照这它们的二进制部分，整理出表格如下：

可以看出，有3个method原型，返回值都为void，index=0的没有参数传入，index=1的传入一个。

String参数，index=2的传入一个String[]类型的参数。

注意：在这里会看到很多带idx结尾的字段，这一般都是索引值，所以要注意，区分这个索引值到底是对应的哪张表格，是字符串池，还是类型池等信息，如果弄混淆的话，解析就会出现混乱了。后面其他数据结构都是需要注意。

7.3.5　field_ids数据结构

filed_ids区里面存放的是dex文件引用的所有的field。这个区的元素格式是field_id_item，逻辑结构描述如下所示：

其中：

·class_idx：表示本field所属的class类型，class_idx的值是type_ids的一个index，并且必须指向一个class类型。

·type_idx：表示本field的类型，它的值也是type_ids的一个index。

·name_idx：表示本field的名称，它的值是string_ids的一个index。

header里field_ids_size=1，field_ids_off=0xe8。说明本dex只有一个field，这部分的二进制描述如下：

filed_ids只有一些元素，比较简单。根据filed_ids的格式，整理出表格如下。它是Java最常用的System.out标准输出部分：

注意：这里的字段都是索引值，一定要区分是哪个池子的索引值。另外，这个数据结构后面也要使用到，所以需要用一个池子来存储。

7.3.6　method_ids数据结构

method_ids是索引区的最后一个条目，它索引了dex文件里的所有method。method_ids的元素格式是method_id_item，结构跟fields_ids很相似，如下所示：

其中：

·class_idx：表示该method所属的class类型，class_idx的值是type_ids的一个index，并且必须指向一个class类型。

·name_idx：表示该method的名称，它的值是string_ids的一个index。

·proto_idx：描述该method的原型，指向proto_ids的一个index。

header里method_ids_size=0x04，method_ids_off=0xf0。本部分的二进制描述如下：

整理出表格如下：

对dex反汇编的时候，常用的method表示方法是如下这种形式：

将上述表格里的字符串再次整理下，method的描述分别为：

至此，索引区的内容描述完毕，包括string_ids、type_ids、proto_ids、field_ids、method_ids。每个索引区域里存放着指向具体数据的偏移地址（如string_ids），或者存放的数据是其他索引区域里面的index号。

7.3.7　class_defs数据结构

上面介绍了所有的索引区域，终于到了最后一个数据结构了，之所以放最后，是因为这个数据结构是最复杂的，层次太深了。下面我试着详细介绍一下。

1.class_def_item

从字面意思解释，class_defs区域里存放着class的定义。它的结构较dex区都要复杂些，因为有些数据都直接指向了data区里面。

class_defs的数据格式为class_def_item，结构描述如下：

参数介绍如下：

·class_idx：描述具体的class类型，值是type_ids的一个index。值必须是一个class类型，不能是数组类型或者基本类型。

·access_flags：描述class的访问类型，诸如public、final、static等。在dex-format.html里“access_flagsDefinitions”有具体的描述。

·superclass_idx：描述supperclass的类型，值的形式跟class_idx一样。

·interfaces_off：值为偏移地址，指向class的interfaces，被指向的数据结构为type_list。class若没有interfaces，值为0。

·source_file_idx：表示源代码文件的信息，值是string_ids的一个index。若此项信息缺失，此项值赋值为NO_INDEX=0xffff ffff。

·annotions_off：值是一个偏移地址，指向的内容是该class的注释，位置在data区，格式为annotations_direcotry_item。若没有此项内容，值为0。

·class_data_off：值是一个偏移地址，指向的内容是该class的使用到的数据，位置在data区，格式为class_data_item。若没有此项内容，值为0。该结构里有很多内容，详细描述该class的field、method、method里的执行代码等信息，后面有一个比较大的篇幅来讲述class_data_item。

·static_value_off：值是一个偏移地址，指向data区里的一个列表（list），格式为encoded_array_item。若没有此项内容，值为0。

header里class_defs_size=0x01，class_defs_off=0x 0110。则此段二进制描述为：

根据对数据结构class_def_item的描述，整理出表格如下：

其实最初被编译的源码只有几行，和class_def_item的表格对照下，一目了然。

2.class_def_item=>class_data_item

class_data_off指向data区里的class_data_item结构，class_data_item里存放着本class使用到的各种数据，下面是class_data_item的逻辑结构：

关于元素的格式Uleb128在string_ids里讲述过，不赘述。encoded_field的结构如下：

encoded_method的结构如下：

其中：

1）method_idx_diff：前缀methd_idx表示它的值是method_ids的一个index，后缀_diff表示它是于另外一个method_idx的一个差值，就是相对于encodeed_method[]数组里上一个元素的method_idx的差值。其实encoded_filed→field_idx_diff表示的也是相同的意思，只是编译出来的Hello.dex文件里没有使用到class filed所以没有仔细讲，详细的参考dex_format.html的官网文档。

2）access_flags：访问权限，比如public、private、static、final等。

3）code_off：一个指向data区的偏移地址，目标是本method的代码实现。被指向的结构是：

·code_item，有近10项元素，后面再详细解释。

·class_def_item→class_data_off=0x 0234。

名称为LHello；class里只有2个directive methods。directive_methods里的值都是Uleb128的原始二进制值。按照directive_methods的格式encoded_method再整理一次这2个method描述，得到结果如下表格所描述：

method一个是<init>，一个是main，这里需要用string_ids那块介绍到的一个方法就是解码uleb128类型的方法得到正确的value值。

3.class_def_item=>class_data_item=>code_item

到这里，逻辑的描述有点深了。先回想一下是怎么走到这一步的，code_item在dex里处于一个什么位置。

1）一个dex文件被分成了9个区，详细见图7-1所示。其中有一个索引区叫作class_defs，索引了dex里面用到的class，以及对这个class的描述。

2）class_defs区，其实是class_def_item结构。这个结构里描述了LHello；的各种信息，诸如名称、superclass、access flag、interface等。class_def_item里有一个元素class_data_off，指向data区里的一个class_data_item结构，用来描述class使用到的各种数据。自此以后的结构都归于data区了。

3）class_data_item结构，描述class里使用到的static field，instance field，direct_method，和virtual_method的数目和描述。例子Hello.dex里，只有2个direct_method，其余的field和method的数目都为0。描述direct_method的结构叫作encoded_method，是用来详细描述某个method的。

4）encoded_method结构，描述某个method的method类型，access flags和一个指向code_item的偏移地址，里面存放的是该method的具体实现。

5）code_item，一层又一层，简要地说，code_item结构里描述着某个method的具体实现。它的结构如下描述：

末尾的3项见图7-2所示，标志为optional，表示可能有，也可能没有，根据具体的代码来：

·registers_size：该段代码使用到的寄存器数目。

·ins_size：method传入参数的数目。

·outs_size：该段代码调用其他method时需要的参数个数。

·tries_size：try_item结构的个数。

·debug_off：偏移地址，指向该段代码debug信息存放位置，是一个debug_info_item结构。

·insns_size：指令列表的大小，以16bit为单位。insns是instructions的缩写。

·padding：值为0，用于对齐字节。

·tries和handlers：用于处理Java中的exception，常见的语法有try catch。

4.分析main method的执行代码并与smali反编译的结果比较

在7.2节里有2个方法，因为main里的执行代码是自己写的，分析它会熟悉很多。偏移地址是directive_method[1]→code_off=0x0148，二进制描述如下：

根据code_item的结构整理表格如下：

insns数组里的8个二进制原始数据，对这些数据的解析，需要参考官网的文档“Dalvik VM InstructionFormat”和“Bytecode for Dalvik VM”。

分析思路整理如下：

1）“Dalvik VM Instruction Format”里操作符op都是位于首个16位数据的低8位，起始的是op=0x62。

2）在“Bytecode for Dalvik VM”里找到对应的Syntax和format。

3）在“Dalvik VM Instruction Format”里查找21c，得知op=0x62的指令占据2个16位数据，格式是AA|op BBBB，解释为op vAA，type@BBBB。因此这8组16位数据里，前2个是一组。对比数据得AA=0x00，BBBB=0x0000。

4）返回“Bytecode for Dalvik VM”里查阅对sget_object的解释，AA的值表示Value Register，即0号寄存器；BBBB表示static field的index，就是之前分析的field_ids区里Index=0指向的那个东西，当时的fields_ids的分析结果如下：

对field常用的表述是：包含field的类型→field名称：field类型。此次指向的就是Ljava/lang/System；→out：Ljava/io/printStream。

5）综上所述，前2个16位数据0x 0062 0000解释为：

其余的6个16位数据分析思路与这个一样，依次整理如下：

6）最后再整理下main method，用容易理解的方式表示出来就是：

看起来很像smali格式语言，不妨使用smali反编译Hello.dex，看看smali生成的代码跟方才推导出来的有什么差异，结果如下：

从内容上看，二者形式上有些差异，但表述的是同一个method。这说明刚才分析的路子是没有跑偏的。另外一个method是<init>，若是分析的话，思路和流程跟main一样。走到这里，心里很踏实了。

7.4　解析代码

上面解析完了所有的数据结构区域，下面就来看看具体的解析代码，由于篇幅的原因，这里就不贴出全部的代码了，只贴出核心的代码。

7.4.1　解析头部信息

解析代码如下：

这里就是记录几个索引区的偏移值和大小信息，解析结果如下：

7.4.2　解析string_ids索引区

解析代码如下：

解析结果如下：

7.4.3　解析type_ids索引区

解析代码如下：

解析结果如下：

7.4.4　解析proto_ids索引区

解析代码如下：

解析结果如下：

7.4.5　解析field_ids索引区

解析代码如下：

解析结果如下：

7.4.6　解析method_ids索引区

解析代码如下：

解析结果如下：

7.4.7　解析class_def区域

解析代码如下：

解析结果如下：

要想看懂解析结果需要通过class_def的数据结构解析，需要借助“Bytecode for Dalvik VM”查阅具体的指令，然后翻译成具体的指令代码，关于这个指令表可以参考这里：http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html，所以具体解析并不复杂，这里就不详细解析了，解析思路可以参考class_def的数据结构解析。

提示：项目下载地址：https://github.com/fourbrother/parse_androiddex

其实Android中有一个工具可以解析dex文件，直接用aapt命令就可以查看了，只是这个工具是dexdump；这个命令也是在AndroidSDK目录下的build-tools下面，可以将打印的结果重定向到demo.txt文件中，如图7-3所示。

图7-3　用aapt命令输出结果

那么上面做的解析工作是不是就没有用了呢？当然不是，后面会看到，解析dex格式有很多用途。

7.5　本章小结

本章主要介绍了Android中的dex文件格式，关于dex文件格式的重要性不必多言，如果能深入了解dex文件格式，在后续的加固应用和逆向应用的时候非常有用，比如一些应用通过前期改变dex的数据结构来进行加固应用，然后在内存中恢复数据结构，要想逆向这样的应用就需要熟练地掌握dex的数据结构知识。有时候还想在内存中篡改指令来改变代码逻辑，这时候也需要通过分析dex的数据结构来找到相对应的内存指令位置，然后进行数据修改。

防护篇

第8章　Android应用安全防护的基本策略

第9章　Android中常用权限分析

第10章　Android中的run-as命令

第11章　Android中的allowBackup属性

第12章　Android中的签名机制

第13章　Android应用加固原理

第14章　Android中的so加固原理

第8章　Android应用安全防护的基本策略

本章主要介绍在开发一个应用的时候，可以通过哪些方式给应用做安全防护，包括以下几个方式：混淆、签名保护、手动注册native方法、反调试检测，通过这些方式能够给应用加上很强的一层保护壳，因此这几种方式在现代很多应用中都会使用到。

8.1　混淆机制

在应用中用混淆不仅是为了安全防护，也是为了减小应用安装包的大小，所以每个应用发版之前必须要添加混淆这项功能。现在混淆机制一般有两种：代码混淆和资源混淆。下面分别介绍。

8.1.1　代码混淆

在反编译apk之后，看到的代码类名、方法名以及代码格式看起来不像正常的Android项目代码，那么这时候就会增加阅读难度，增加破解难度，这就是经过混淆的代码，如图8-1所示。

图8-1　代码混淆结构图

现在的破解查看Java层代码有以下两种方式：

·直接先解压classes.dex文件，使用dex2jar工具转化成jar文件，然后再用jd-gui工具进行查看类结构。

·使用apktool工具直接反编译apk，得到smali源码，阅读smali源码。

不过代码混淆也不是很安全，在破解的过程中一般是找程序的入口，那么这些入口一般都是Application或者是MainActivity之处，因为这些Android中的组件类是不能进行混淆的，所以还是有入口可寻，能够找到入口代码，然后进行跟踪。

8.1.2　资源混淆

上面说到，对代码的混淆能够增加代码阅读难度。为了保护资源也是可以做混淆的，资源混淆原理这里就不多解释了，微信团队已经将这个功能开源，不了解的同学可以去GitHub查看：https://github.com/shwenzhang/AndResGuard。

当然资源混淆还有一个很大的好处就是减小apk包的大小，这不是本节讨论的知识点，这里讨论的是混淆资源增加破解查找资源的难度，先来看一下混淆资源之后的结果如下所示：

这里可以看到，一个混淆资源的应用，反编译之后查看它的string.xml内容，发现它的name全是简单的混淆字母，那么如果通过name的值来查找对应的字符串内容获取消息，将是很难受的一件事，因为这时候如果全局搜索一个name值的话，比如这里的name='a'，那么得搜出多少个这样的name，查找也很耗时间。

对于混淆资源也不是没有办法，因为一般在反编译之后的Java代码中，看到的获取资源值的时候，并不是资源的name值，而是资源对应的int类型的值，比如这样如下所示：

这里获取一个字符串的值，这些int类型的值，可以在反编译之后的res/values/pulblic.xml中可以找到：

比如这里的2131230929变成16进制就是0x0x7f0800d1，在public.xml中查找，找到了name='ev'的一项，然后再去string.xml中进行name查找：

还是找到了这个字符串的值，反编译之后的public.xml中记录了所有资源的id和整型对应值，混淆之后的代码中看到的都是资源id的整型值，那么这么一看混淆并没有什么用途。

从上面描述的可知，混淆对于破解并没有什么太大的阻碍，也只是一个障眼法，不过混淆有另外一个功能，就是减少apk包的大小，这也是每个应用都添加混淆的最主要原因。

8.2　签名保护

Android中的每个应用都有一个唯一的签名，如果一个应用没有被签名是不允许安装到设备中的，一般在运行debug程序的时候也是有默认的签名文件的，只是IDE帮开发者做了签名工作，在应用发版的时候会用唯一的签名文件进行签名。那么在以往的破解中可以看到，有时候需要在反编译应用之后，重新签名再打包运行，这又给很多二次打包团队谋取利益提供了一种手段。就是反编译市场中的包，然后添加一些广告代码，最后使用自家的签名重新打包发布到市场中，因为签名在反编译之后是获取不到的，所以只能用自己的签名文件去签名，但是在已经安装了应用设备再去安装一个签名不一致的应用会导致安装失败，这样也有一个问题就是有些用户安装了这些二次打包的应用之后，无法再安装正规的应用了，只有卸载重装。根据这个原理可以利用应用的签名是唯一的特性做一层防护。

为了防止应用被二次打包，在程序入口处添加签名验证，如果发现应用的签名不正确就立即退出程序，可以在应用启动的时候获取应用的签名值，然后和正确的签名值作比对，如果不符合就直接退出程序。下面做一个简单的案例测试一下，如下所示：

这里定义一个简单的工具类用于比较应用的签名，只是简单处理，正常情况下这里应该比对签名的MD5值，为了简单就忽略了，然后在程序的入口处做一次比对，如果不正确就退出程序如下所示。

得到上面的apk之后，下面来反编译，重新签名安装（关于这里如何反编译和签名，不做解释了，使用apktool和jarsigner工具即可，签名文件是自己的），然后运行如下所示：

发现程序根本运行不起来，一点击就闪退，这就说明做到了防止应用被二次签名打包。

但是这也不是最安全的，因为既然有签名比对方法的地方，那么只需要反编译apk之后，修改smali语法，把这个方法调用的地方注释即可，如下所示：

只需要使用#把这行代码注释，然后回编译重新打包安装即可。这里的难点是如何找到这个检测签名方法的地方，比如有的程序在native层做的，但是不管在哪里，只要是在代码中，就可以找出来的。

8.3　手动注册native方法

这个方法其实不太常用，因为它的安全措施不是很强大，但是也是可以起到一定的作用，在说这个知识点的时候，先来了解一下so加载的流程。

在Android中，当程序在Java层运行System.loadLibrary（"jnitest"）；这行代码后，程序会去载入libjnitest.so文件。与此同时，产生一个Load事件，这个事件触发后，程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行。与Load事件相对，当载入的.so文件被卸载时，Unload事件被触发。此时，程序默认会去在载入的.so文件的函数列表中查找JNI_OnUnload函数并执行，然后卸载.so文件。需要注意的是，JNI_OnLoad与JNI_OnUnload这两个函数在.so组件中并不是强制要求的，用户也可以不去实现，Java代码一样可以调用到C组件中的函数，之所以在C组件中去实现这两个函数（特别是JNI_OnLoad函数），往往是做一个初始化工作或“善后”工作。可以这样认为，将JNI_ONLoad看成是.so组件的初始化函数，当其第一次被装载时被执行（Window下的dll文件也可类似的机制，在_DLL_Main（）函数中，通过一个swith case语句来识别当前是载入还是卸载）。将JNI_OnUnload函数看成是析构函数，当其被卸载时被调用。由此看来，就不难明白为什么很多jni C组件中会实现JNI_OnLoad这个函数了。一般情况下，在C组件中的JNI_OnLoad函数用来实现给VM注册接口，以方便VM可以快速地找到Java代码需要调用的C函数。（此外，JNI_OnLoad函数还有另外一个功能，那就是告诉VM此C组件使用那一个JNI版本，如果未实现JNI_OnLoad函数，则默认是JNI 1.1版本。）

应用层的Java类通过VM而调用到native函数。一般是通过VM去寻找*.so里的native函数。如果需要连续调用很多次，每次都需要寻找一遍，会多花许多时间。此时，C组件开发者可以将本地函数向VM进行注册，以便能加快后续调用native函数的效率。可以这么想象一下，假设VM内部一个native函数链表，初始时是空的，在未显式注册之前，此native函数链表是空的，每次Java调用native函数之前会首先在此链表中查找需要调用的native函数，如果找到就直接使用，如果未找到，得再通过载入的.so文件中的函数列表中去查找，且每次Java调用native函数都是进行这样的流程。因此，效率就自然会下降。为了克服这个问题，我们可以通过在.so文件载入初始化时，即JNI_OnLoad函数中，先行将native函数注册到VM的native函数链表中去，这样一来，后续每次Java调用native函数时都会在VM中的native函数链表中找到对应的函数，从而加快速度。

通过上面的分析之后，so文件加载和卸载的时机，可以显示的手动注册自己的native方法，那么一般在定义native方法的时候，对应的native层的函数名是：Java_类名_方法名，样式如下：

所以就有两个问题：

·第一个问题是在IDA工具查看so文件的时候，去找到对应的native方法非常容易，因为知道了Java层的native方法名和类型，那么直接可以定位到这个native函数如图8-2所示。

·第二问题是恶意攻击者可以得到这个so文件之后，查看这个native方法的参数和返回类型，也就是方法签名，然后自己在Java层写一个demo程序，然后构造一个和so文件中对应的native方法，就可以执行这个native方法，如果有一个校验密码或者是获取密码的方法是个native的，那么这时候就会很容易被攻击者执行方法后获取结果。

图8-2　IDA查看so中的函数名

说得简单点，比如上面的这个isEquals例子：现在有一个攻击者，想执行这个应用的isEquals方法，那么它只需要解压apk文件，得到so文件，查看so文件中的函数，或者查看上层的Java代码，得到这个方法的返回值和签名，然后他就可以编写一个简单的程序，构造一个类：

然后在它内部定义一个native方法：

再使用System.loadLibrary加载应用的so文件，然后在适当的地方执行isEquals方法，这样就等于调用了so文件中的isEquals方法了。

从上面的两个问题可以看到，如果native层的函数遵从这样的格式，无疑是给破解提供了简单的一种方式。可以这么做，显示地注册JNI方法，只需要在native层的代码中调用如下三个函数即可。

第一个函数是（*env）->RegisterNatives（env，clazz，methods，methodsLenght）。这个函数就是手动注册一个native方法，属于JNIEnv*的，参数也比较简单。clazz就是需要注册native方法的那个类，是jclass类型，这可以使用JNIEnv的FindClass方法，传递类的名称即可获取这个对象，如下所示：

methods是一个结构体，定义如下：

·name是Java中函数的名字。

·signature用字符串描述了函数的参数和返回值。

·fnPtr是函数指针，指向C函数。

类似于这样的结构如下所示：

第二个函数是jint JNI_OnLoad（JavaVM*vm，void*reserved）。这个函数就是上面说到的，加载so的时候被调用到，同时可以在这里获取JVM参数的，一般在这个函数中主要就是执行上面的注册函数功能，这里还需要获取一个JNIEnv*变量，如下所示：

这里通过JVM来获取JNIEnv变量，然后调用注册函数，如下所示：

第三个函数是void JNI_OnUnload（JavaVM*vm，void*reserved）。这个函数和JNI_OnLoad是相对应的，是在so被卸载的时候调用。

通过上面的三个函数就可以手动显示地注册native方法了，那么就可以修改native层的函数名，不要按照之前的那种格式了，增加破解者寻找关键的native层函数的难度。

这里把isEquals函数名变成了jiangwei，如下所示：

然后再修改注册方法的结构体，如下所示：

编译运行，使用IDA查看，如图8-3所示。

图8-3　IDA工具查看函数名

这时候破解者不能按照常规的套路找到native层的函数了，那么上面的两个问题就可以避免了，增加安全性。

但是问题来了，现在的破解者，一般打开so文件的时候，如果找不到对应的native方法之后，就会去找JNI_OnLoad函数，然后通过分析ARM汇编代码，找到register函数，分析注册方法结构体，找到对应的native方法。那么这种方式还是不靠谱，也是只能糊弄一下小白破解者。不过通过这个例子也可以得知，在JNI_OnLoad中可以做很多事，比如上面说的签名机制校验，也可以在JNI_OnLoad中做一次，增加安全性，如下所示：

看看equal_sign函数功能，如图8-4所示。

图8-4　校验签名

在这个方法中，用JNIEnv变量调用了Java层的方法，来获取应用的签名，然后进行比对。

用这种签名校验方式来做安全性保证也是一个思路，至少native层的代码分析比smali代码分析难度大点，而且这种签名校验机制必须用静态方式去破解apk，也就是通过分析代码来破解，因为程序没有运行起来无法通过动态方式破解的。

8.4　反调试检测

反调试检测是为了应对现在很多破解者使用IDA进行动态方式调试so文件，从而获取重要的信息，知道IDA进行so动态调试是基于进程的注入技术，然后使用Linux中的ptrace机制，进行调试目标进程的附加操作。ptrace机制有一个特点，如果一个进程被调试了，在它进程的status文件中有一个字段TracerPid会记录调试者的进程id值，如图8-5所示。

图8-5　TracerPid字段

查看文件：/proc/[myPid]/status，在第六行，有一个TracerPid字段，就是记录了调试者的进程id。那么就可以这么做来达到反调试的功效了：轮询遍历自己进程的status文件，然后读取TracerPid字段值，如果发现它大于0，就代表着自己的应用在被人调试，所以就立马退出程序。原理知道了，代码实现也很简单，这里用pthread创建一个线程，然后进行轮询操作：

使用pthread_create创建一个线程，线程启动之后执行thread_function函数：

看看thread_funcation函数：

开始轮询，读取TracerPid字段的值，发现大于0，就立马退出程序。运行结果看看，如下所示：

看到了，当使用IDA工具进行调试的时候，程序立马退出，IDA的调试页面也退出了。

但是还是有问题，因为现在破解者们已经免疫了，知道会有这种检测，所以就会用IDA工具给JNI_OnLoad函数下断点，然后进行调试，找到检测轮询代码，使用nop指令，替换检测指令，就相当于把检测代码给注释了。所以知道的人多了，方法就不好用了。

8.5　本章小结

本章中主要介绍了在Android中开发应用的时候，可以从几个方面对应用的安全做一层防护，主要介绍了以下几种方式：

·使用Android中的混淆机制，包括代码混淆和资源混淆，这样能够增加反编译之后的内容阅读难度，同时可以减小应用包的大小。

·使用应用的签名保护机制，因为每个应用的签名都是唯一的，为了防止被破解，可以在核心的方法或者程序入口处添加签名校验功能，如果校验失败就可以直接退出程序，增加破解难度。

·手动注册native方法，这个方法也算是一种混淆机制，因为Android中native方法在native层对应的方法名都有固定的格式，为了防止被破解，可以去混淆这种方法的命名格式来增加破解难度。

·增加应用的反调试功能，这里借助一个知识点，如果一个进程被注入或者是调试了，那么在它的status文件中的TracerPid字段的值就不为0。可以借助这个知识点来增加反调试功能。

第9章　Android中常用权限分析

Android在开发过程中会申请一些权限，比如定位、网络等关系到用户隐私数据的都需要在AndroidManifest.xml中声明，而且现在移动设备生产商都添加了应用在使用这些隐私功能的时候给用户一个提示，让用户去决定是否允许本次操作。Android 6.0新增了这个功能，叫做运行时权限。本章不介绍在AndroidManifest.xml中申请基础权限，而是介绍在授权页面申请权限，现在开发者会利用这些权限去开发用户体验不错的产品，在这个过程中会产生一些隐私安全问题。

9.1　辅助功能权限

许多Android使用者因为各种情况导致需要以特殊方式与手机交互。例如有些用户由于视力上、身体上、年龄上的问题致使他们不能看完整的屏幕、使用触屏，或者无法很好地接收到语音信息。

Android提供了辅助（Accessibility）功能和服务帮助这些用户更加简单地操作设备，包括文字转语音（不支持中文）、触觉反馈、手势操作、轨迹球和手柄操作。辅助功能实质上就是监听应用窗口变化和事件。在使用辅助功能的时候必须声明以下权限：

在代码中直接打开相应的Intent跳转到授权页面：

开发场景：实现静默安装/卸载、应用锁、通知栏管理、抢红包等功能。

辅助功能授权页面如图9-1所示。

图9-1　辅助功能授权页面

存在的风险：可以监听设备当前的窗口变化，分析当前应用的View结构之后模拟点击。比如一个恶意的软件申请到了这个权限之后，就可以监听设备的应用启动情况：

·模拟一些社交App的登录窗口页面，其实是他们自己的钓鱼登录页，这样用户是无感知的，当用户输入用户名和密码时就很危险了。

·当监听到当前应用是社交App，而且是聊天记录页面时，就可以得知当前聊天记录。

·分析设备中的应用情况，在用户不知情的情况下，模拟点击任何一个应用进行操作，从而获取信息。

9.2　设备管理权限

在开发的过程中，很多开发者没有注意到设备管理权限，它的作用类似于iPhone中的“查找我的手机”功能，当设备丢失的时候，可以快速定位以及擦除设备数据等，在使用这个功能时需要申请以下权限：

在代码中直接打开相应的Intent跳转到授权页面：

设备管理权限还有一个隐形的作用，就是可以防止被卸载。有的应用开发者想让自己的App无法被用户卸载，可以申请这个权限。其实想想也是合理的，因为它是设备丢失之后进行联系的唯一可靠功能了，肯定不能被卸载。

授权页面如图9-2所示。

图9-2　设备管理授权页面

存在的风险：申请这个权限之后，有一个很大的弊端就是设备相当于给申请者管理了，他们可以修改设备的锁屏密码、擦除数据等，这对于用户来说是很危险的。

9.3　通知栏管理权限

通知栏管理权限算是辅助功能的一个缩减版，因为辅助功能是可以监听设备的通知栏情况的，而这个功能专门用来监听设备的通知栏消息。在使用的过程中需要申请下面的权限：

在代码中直接打开相应的Intent跳转到授权页面：

开发场景：管理设备的通知信息，可以帮助用户管理杂乱的通知栏消息。

授权页面如图9-3所示。

图9-3　通知栏管理授权页面

存在的风险：与辅助功能一样，申请这个权限之后用户设备的通知栏消息就会被它接管，如果通知栏有敏感消息的话，对用户来说是很危险的。

9.4　VPN开发权限

Android 4.0之后增加了VPN功能，开发者可以使用VpnService来开发VPN相关的功能。在使用的过程中，需要申请权限，如下所示：

在代码中直接打开相应的Intent跳转到授权页面：

授权页面如图9-4所示。

图9-4　VPN授权页面

开发场景：Android中VPN软件开发，可以免root管理设备中的应用联网。

存在的风险：如果申请了这个权限，就代表这个设备的网络访问消息会被申请者接管，网络访问消息的重要性不言而喻。

9.5　本章小结

本章主要介绍了Android中一些常用的特殊权限，现在的开发者可以利用这些权限开发出体验很好的产品，但是也会有一些潜在的风险。所以在开发过程中如果用到这几个权限的话，还是需要慎重。设备使用者在遇到这些权限安全提示的时候也要额外警惕！

第10章　Android中的run-as命令

Android中有一个run-as命令，这个命令的作用是可以用root身份运行命令，但是它有一个限制，就是必须携带指定应用包名参数，而且这个应用是debug模式的，也就是说，如果在开发中想看自己应用的沙盒数据，在设备没有root的情况下，可以使用这个命令进行查看。本章将分析这个命令及使用方法。

10.1　命令分析和使用

以前想看一个应用的沙盒数据（/data/data/xxx/目录内容），一般都是选择root之后去查看对应的数据库、XML等数据。但是如果没有root了也想看这些数据，怎么办呢？Android中提供了一个命令，那就是run-as，不过这个命令有一个缺陷，就是只有debug模式应用才能被查看。

命令的用法很简单：run-as[packagename]。其中packagename就是想查看的应用的包名，运行命令之后，就直接进入指定应用的目录：/data/data/packagename/。

现在稍微了解了Android中有这个命令可以查看debug应用的沙盒数据，但直接运行会遇到问题，运行结果如下所示：

为什么这个命令会报错呢？遇到这个情况有两种方式解决：一种是网上查资料，一种是自己去看源代码。为了问题解决的高效性，就去看源码吧。Android中的命令一般都在/system/bin和/system/xbin目录下，这些命令的源码都是放在Android源码目录/system/core/下，如图10-1所示。

图10-1　run-as命令源码的位置

run-as的源码如图10-2所示。

图10-2　run-as源码分析（一）

看到这里，原来run-as命令运行是有很多限制的。下面分别介绍。

第一个限制：运行的uid限制

运行命令的用户id只能是shell和root用户。下面可以验证一下，使用system的uid运行命令：

这里用于测试的设备已经root了。看到使用su可以随意设定uid，这里将uid变成了system，再运行run-as发现报错了，符合预期吧？

第二个限制：应用的安装必须合法

这里再仔细看看get_package_info这个函数的源码，它位于run-as.c同目录下的package.c，如图10-3所示。

这里通过map_file函数来获取PACKAGES_LIST_FILE文件的buffer内容，再来看一下PACKAGES_LIST_FILE的定义：

图10-3　run-as源码分析（二）

导出这个文件，查看内容：

这个文件记录所有安装应用的信息：包名，用户id（uid），是否为debug模式，对应的数据目录，是否是release版，组id（gid）。这里发现demo.systemapi应用是debug模式的。一般正式App都是非debug模式。

通过读取这些信息就可以构造出一个PackageInfo了，如图10-4所示。

图10-4　run-as源码分析（三）

注意，这里看到了packages.list文件是存储安装包的简略信息，和它同一目录下还有一个重要文件packages.xml，可以导出来看看，如下所示：

这里记录了安装应用的详细信息，还有一个重要信息就是应用的安装来源，有些App可以做到这点就是通过这个信息来获取的，还有签名信息、权限等。

当然可以使用Android中的dumpsys packageinfo命令来查看指定应用的详细信息，如下所示：

第三个限制：应用的uid必须合法

对于查看的应用的uid也是有相应限制的，如下所示：

这里AID_APP的定义在Android源码目录\system\core\include\private\android_filesystem_config.h下。

定义了一些uid信息，例如：

·AID_ROOT对应的是root用户，uid=0。

·AID_SYSTEM对应的是system用户，uid=1000。

·AID_SHELL对应的是shell用户，uid=2000。

可以看到，root用户的权限＞system用户的权限＞shell用户的权限＞第三方应用的权限。

不过这里所说的权限只是针对一些情况，不是全部。Android中有些API会做uid限制，即使用反射机制也是访问失败的，比如，一个API限制只能uid=1000的用户即system用户才能调用，那么这时你是root用户也是没办法的，不过可以使用su进行降权。

这三个用户的id最好记住，算是常识，如下代码有详细定义：

再往下看，第三方应用uid的定义如下所示：

这里的AID_APP是10000，安装的应用的uid都是从10000开始的，所以uid都是大于10000的，代码中做了一层uid的合法性判断限制。

注意：一般查看应用的uid时，得到最后的整数+10000就是应用的uid，比如demo.systemapi的uid就是：10000+100=10100。

如下代码可以获取到应用的uid：

这可以验证，查看上面提到的packages.list文件中的信息，如下所示：

第四个限制：应用是否为debug模式

之前说过，run-as有一个最大的限制就是查看应用的数据时，应用必须是debug模式的，所以在开发程序的时候千万要保证一点，就是debug包不能外泄，不然数据就等于外泄了。

言归正传，源码看得差不多了，看一下问题，在运行run-as的时候遇到的问题如下所示：

问题就出在如下代码上：

再看get_package_info函数：

再看map_file函数：

终于找到原因了，原来是/data/system/packages.list文件的读取权限是AID_SYSTEM，查看一下如下代码：

问题找到了，尝试使用su升级权限再查看一下，如下所示：

可惜的是，这样明显是不行的，原因很简单，run-as有限制，本身这个命令只能是root和shell用户才可以访问。这里也可以看到，shell用户也不比system用户权限低，所以要视情况而定来看root、system、shell这三个用户的权限范围。

那么再使用root用户去尝试一下，如下所示：

这是可以的，因为run-as的权限验证通过了。

从上面的几个限制可以看到，首先root的uid限制可以放行，然后是debug模式可以放行，packages.list文件的访问权限是system，但是这对于root用户来说没有限制，因为在文件的读取权限上root可以操作任何文件，demo.systemapi的uid可以放行。

注意：这里运行完run-as命令之后，进入/data/data/demo.systemapi目录下，看到变成了sh命令，#变成$了，这其实是run-as在启动一个shell程序，可以查看run-as的源码：

到这里就分析完了为什么运行run-as命令失败，失败的原因也找到了，解决的办法也有了，但是还是需要root，这貌似和开始说的有点出入，因为本身run-as命令就是可以在非root设备上查看debug模式下应用的沙盒数据，那现在root了，还有什么意义呢？其实不是的，run-as命令很特殊。找到一个没有root的设备，然后运行run-as命令，结果如下所示：

也是可以运行成功的，而且这个设备没有root。但是在上面已经root的设备上运行run-as却报错，为什么呢？下面就开始本章的主要内容，即为何没有root，run-as命令还能运行成功，进入指定的应用目录下。

是这样的：开始的时候为了讲解run-as源码和一些前提知识，做了一件事，就是修改了run-as命令的权限，如下所示：

查看run-as命令的本来权限是：

然后修改了它的权限chmod 0755 run-as，变成

这个操作是为了开始本章内容。

下面把run-as权限改回原来的值，然后再运行run-as，如下所示：

10.2　Linux中的setuid和setgid概念

那么问题来了，为何改了一个权限，就会导致run-as运行报错了呢？修改的命令是：

修改之后的权限是：

和修改之前只有一个区别，就是x变成了s，是怎么回事呢？

这就引出了本节的话题：关于Linux中的setuid和setgid问题。Linux中的setuid和setgid是什么呢？Android中有哪些场景会用到它们呢？下面来一一讲解。

1.Linux中的setuid和setgid概念

Linux/Unix下的可执行文件一旦被设置了setuid标记，使用该可执行程序的进程就将拥有该执行文件的所有者权限。普通用户执行这个命令，可使自己升级为root权限。

设置setuid的方法是：chmod 4755 program或chmod u+s program（setuid只对文件有效）。

设置setgid的方法是：chmod 2755 dir或chmod g+s dir（setgid只对目录有效）。

同时设置setuid、setgid的方法是：chmod 6755 program被设置粘着位，chmod 1777 file或chmod o+t file（粘着位只对文件有效）当一个目录被设置为“粘着位”（用chmod a+t），则该目录下的文件只能由以下人员删除：

·超级管理员

·该目录的所有者

·该文件的所有者

也就是说，即便该目录是任何人都可以写，但也只有文件的属主才可以删除文件。那么如何使用chmod命令修改文件的权限，和这两个函数有什么关系呢？修改文件权限的命令是chmod，后面跟字母组合或者数字组合。

注意：Android中的toolbox对chmod命令做了限制，chmod只能使用数字代替字母来修改文件的权限，看下面例子：

这里使用字母组合方式来修改文件权限报错了：Bad mode，因为Google自带的toolbox中chmod语法不支持+x、-x。

2.chmod命令的用法和参数详解

知道Linux中的文件权限构造之后，可以使用ll命令查看一个文件，格式类似下面这样：

下面解析一下这个格式所表示的意思。

这种表示方法一共有10位：

第9位表示文件类型，可以为p、d、l、s、c、b和-：

·p表示命名管道文件

·d表示目录文件

·l表示符号连接文件

·s表示socket文件

·c表示字符设备文件

·b表示块设备文件

·-表示普通文件

第8～6位、5～3位、2～0位分别表示文件所有者的权限、同组用户的权限、其他用户的权限，其形式为rwx：

·r表示可读，可以读出文件的内容，对应的数字是4。

·w表示可写，可以修改文件的内容，对应的数字是2。

·x表示可执行，可运行这个程序，对应的数字是1。

那么，可以知道rwx的组合就是7=1+2+4。

如果想修改文件的权限为所有者是rwx，同组用户的权限是r--，其他用户的权限是--x，命令如下：

这里看到：u代表所有者，g代表同组用户，o代表其他用户。

使用数字更简单：

·7代表所有者：1+2+4

·1代表同组用户：1

·4代表其他用户：4

如下所示，可以看到文件的权限：

下面再来看一下如何修改文件为setuid和setgid。设置之后文件相应被设置了SUID或SGID位，分别表现在所有者或同组用户权限的可执行位上。例如：

1）-rwsr-xr-x表示SUID和所有者权限中可执行位被设置。

2）-rwsr--r--表示SUID被设置，但所有者权限中可执行位没有被设置。

3）-rwxr-sr-x表示SGID和同组用户权限中可执行位被设置。

4）-rw-r-sr--表示SGID被设置，但同组用户权限中可执行位没有被设置。

其实，在Unix的实现中，文件权限用12个二进制位表示，如果该位上的值是1，表示有相应的权限：

第11位为SUID位，第10位为SGID位，第9位为粘着位，第8～0位对应于上面的三组rwx位。

-rwsr-xr-x的值为：1 0 0 1 1 1 1 0 1 1 0 1

-rw-r-sr--的值为：0 1 0 1 1 0 1 0 0 1 0 0

1）所有者的s位对应的数字是：4

2）所有组的s位对应的数字是：2

3）粘着位对应的数字是：1

给文件加SUID和SUID的命令如下：

1）chmod u+s filename设置SUID位

2）chmod u-s filename去掉SUID设置

3）chmod g+s filename设置SGID位

4）chmod g-s filename去掉SGID设置

也可以用八进制表示方法来设置，如果明白了前面的12位权限表示法，八进制方法也很简单。

使用数字来设置文件的SUID、SGID、粘着位：

第一位的7就代表1+2+4，即设置了三个位。

10.3　Android中setuid和setgid的使用场景

前面介绍了Linux中setuid、setgid的用法和作用，以及如何使用chmod命令来操作。下面就来看看Android中有哪些场景用到了setuid、setgid。

1.zygote降权处理

首先不得不说zygote这个进程，Android中应用启动的过程都是和这个进程息息相关的，每个应用启动都是来找zygote做事的。zygote启动之后，会进入一个轮询使用socket来监听ActivityManagerService发来的消息，比如应用的启动，然后zygote就fork出一个进程，开始运转。那么问题来了，在Linux中，父进程fork出一个子进程，默认子进程会拥有和父进程一样的uid，那么zygote是root用户的，按照常理，所有fork出来的应用都是root用户了，看上去很危险——每个应用都有root权限了。其实每个应用只有指定的权限，这是由于zygote使用setuid、setgid做了降权处理，如下所示：

下面来看一下源码：dalvik_system_Zygote.cpp。

提示：源码位置：Android源码\dalvik\vm\native\dalvik_system_Zygote.cpp

zygote中主要做处理的是forkAndSpecializeCommon函数，如下所示：

往下面看：

这里将fork出来的进程的gid和uid设置成上层传递过来的值，从而实现降权。

这里还有一个函数enableDebugFeatures，这个函数是干什么的，其实看到名字都知道，让一个应用具备可调试功能，在debug的情况下：

继续往下看：

注意，在gDvm.jdwpAllowed字段判断时可以看到，Java中的调试系统还是很复杂的，由jdb、jdbserver、jwdp组成，这里遵从的是C/S模式：

·jdb是客户端，需要调试的应用。

·jdbserver是服务器端，调试代码信息处理的那一端。

·jwdp是调试协议。

而且每个可以调试的应用启动之后，都会有一个jwdp线程来运作这些事，可以使用：

来查看进程的线程，如下所示：

这里介绍的内容是为了接下来分析Android中的调试工具gdb、gdbserver做准备的。

2.su工具原理

Android手机的root原理是：一个普通的进程通过执行su，从而获得一个具有root权限的进程。有了这个具有root权限的进程之后，就可以想干什么就干什么了。su所做的事情其实很简单，它再fork另外一个子进程来做真正的事情，也就是在执行su的时候后面所跟的那些参数。由于运行su的进程的uid是root，因此由它fork出来的子进程的uid也是root，于是子进程也可以想干什么就干什么了。不过，用来root手机的su还会配合另外一个称为superuser的App来使用。su在fork子进程来做真正的事情之前，会先启动superuser，询问用户是否允许fork一个uid是root的子进程。这样就可以对root权限进行控制，避免被恶意应用偷偷地使用。

下面来看看su的源码，如图10-5所示。

图10-5　su的源码

可以看到，这里就是用了setuid和setgid来设置指定的uid，如果没有指定uid，默认是root用户，uid=0。

3.run-as命令

上面说到使用chmod命令可达到setuid等效果，在Android中也有类似的场景，前面分析的run-as命令就是一个很好的例子，run-as命令本身的uid是root，gid是shell。

如果用chmod 6755 run-as设置完权限之后如下面的代码所示，就具备了这种功能，即只要运行run-as命令的用户就会立马升级到root用户，因为root用户可以进入任何应用的data/data/目录下：

现在也知道了前面为什么修改了run-as命令之后会报错，然后修改回来之后又可以了。

从这里可以看到Android中的run-as命令其实还是很危险的，虽然它本身有很多限制，但追根到底还是Linux中setuid的安全性最为值得考究，setuid的危险性还是很大的。

10.4　run-as命令的作用

现在知道了run-as命令能够在非root设备上查看debug模式的应用沙盒数据，其实它还有一个重要作用，就是为Android中的调试做基础。上面在分析zygote源码的时候提到了Java中的调试系统jdb，其实Android中的调试系统是gdb，通过gdb和gdbserver来调试App。具体来说，就是gdbserver通过ptrace附加到目标App进程去，然后gdb再通过socket或者pipe来链接gdbserver，并且向它发出命令来对App进程进行调试。需要注意以下关键点：

·每一个需要调试的apk在打包的时候都会带上一个gdbserver，因为手机上面不带有gdbserver工具。gdbserver用来通过ptrace附加到要调度的App进程去。

·要注意ptrace的调用。一般来说，只有root权限的进程可以调用。例如，如果我们想通过ptrace向目标进程注入一个so，那么就需要在root过的手机上向su申请root权限。但是，这不是绝对的。如果一个进程与目标进程的uid是相同的，那么该进程就具有调用ptrace的权限。gdbserver在调试一个App之前，首先要通过ptrace_attach来附加到该App进程去。ptrace_attach在执行实际操作之后，会调用__ptrace_may_access来检查调用进程的权限，如果调用进程与目标进程具有相同的uid和gid，那么权限检查通过。否则的话，就要求调用者进程具有执行ptrace的能力，这是通过另外一个函数ptrace_has_cap来检查的。如果调用进程的uid是root，那么ptrace_has_cap一定会检查通过。当然，通过了上述两个权限检查之后，还要接受内核安全模块的检查，这就不是通过uid或者capability这一套机制来控制的了。

·如何让gdbserver进程的uid与要调试的App进程的uid一样？因为在没有root过的手机上要想获得root权限是不可能的，因此只能选择以目标进程相同的uid运行这个方法。这时可以使用run-as工具。

到这里知道了，在Android中要调试一个程序，首先这个程序必须是debug模式的，也就是在AndroidManifest.xml中设置的属性，所以以往在使用动态方式破解apk的时候，首先是反编译，然后修改XML中的debug属性，然后才能进行代码关联调试，而且动态调试so的强大工具IDA也是利用附加到目标进程中才进行调试的，原理和这里的gdb一样。

10.5　调用系统受uid限制的API

前面说了几次关于降权的问题，再用一个例子来详细介绍Android中如何降权，这个例子是模拟一个系统的API，但是这个API只允许system用户调用，代码如下：

这里做了用户uid的判断，如果不是system用户，直接返回错误信息。为了简单，也是为了向用户演示Android中如何执行jar功能，这里再加一个入口类：

把项目导出jar，然后用dx命令进程转化成dex，因为Android中是不识别class文件的，它只识别dex文件：

然后将生成的classes.dex文件塞到jar中，直接使用压缩软件就可以了，如图10-6所示。

图10-6　压缩软件查看jar文件

然后把exec.jar导入设备的/data目录下，如下所示：

接着把exec.jar设置到类环境变量中，运行即可：

·export CLASSPATH jar的路径。

·exec/system/bin/app_process jar的目录jar中的入口类。

为了演示，例子都是在root用户下做的操作，如下所示：

运行成功，看到打印的log信息，权限拒绝了，因为只能允许system用户访问，下面就用su降权到system再运行，如下所示：

注意：每次切换用户的时候，一定要记得重新设置一下类变量，不然会运行失败的。

成功获取到值了。总结一下步骤：

1）导出可执行的jar文件。

2）使用dx命令将class文件转化成dex文件。

3）将转化之后的dex文件塞到jar文件中，然后导入设备的指定目录。

4）设置可执行jar文件到类环境变量中。

5）执行app_process命令，运行jar文件。

补充一些说明：上面看到了可以成功使用app_process命令来运行一个jar文件，关于app_process的相关知识网上有很多资料，它是Android系统中启动Java代码的关键。这里也可以直接运行dex文件，使用命令dalvikvm。

首先将上面执行dx命令后的classes.dex文件adb放到/data目录下，如下所示：

然后执行dalvikvm命令，如下所示：

这里同样看到权限拒绝，降级切换到system用户，如下所示：

了解了如何使用app_process来运行jar文件，便体验到了Android中一些API的访问限制该如何处理，主要是用su来进行降权处理，而su能够降权是因为它本身提供了可以修改uid的功能，即调用setuid和setgid函数即可，所以在手机root之后发现还是不能调用系统的一些API时，可以尝试使用su来降权进行调用。

10.6　本章小结

本章知识点有点多，一环扣着一环，分析run-as命令的用法时去分析源码，发现错误的根源之后，引出Linux中的setuid和setgid问题，解决问题之后，总结了setuid和setgid的使用场景，然后继续分析了run-as命令的作用。最后也解决一个问题，即如何调用系统中那些有uid限制的API，通过一个例子来解析问题，引出了Android中如何执行jar文件。

第11章　Android中的allowBackup属性

很多开发者对Android关于AndroidManifest.xml中的allowBackup属性不太了解，而开发者在使用IDE工具开发的时候自动生成代码，而这个属性的值会被设置成true，会直接导致隐私数据的丢失。本章就介绍这个属性对应用安全的影响到底有多大以及注意事项。

11.1　allowBackup属性介绍

Android API Level 8及以上Android系统提供了为应用程序数据的备份和恢复功能，此功能的开关由应用程序中AndroidManifest.xml文件中的allowBackup属性值决定，其属性值默认是true。当allowBackup标志为true时，用户即可通过adb backup和adb restore命令对应用数据进行备份和恢复，这可能会带来一定的安全风险。

这个属性的安全风险源于adb backup命令容许任何一个能够打开USB调试开关的人从Android手机中复制应用数据到外设，一旦应用数据被备份之后，所有应用数据都可被用户读取；adb restore容许用户指定一个恢复的数据来源（即备份的应用数据）来恢复应用程序数据的创建。因此，当一个应用数据被备份之后，用户即可在其他Android手机或模拟器上安装同一个应用，并通过恢复该备份的应用数据到该设备上，在该设备上打开该应用即可恢复到被备份的应用程序的状态。

尤其是通讯录应用，一旦应用程序支持备份和恢复功能，攻击者即可通过adb backup和adb restore命令进行恢复新安装的同一个应用来查看聊天记录等信息；对于支付金融类应用，攻击者可通过此方法来进行恶意支付、盗取存款等。因此为了安全起见，开发者务必将allowBackup标志值设置为false来关闭应用程序的备份和恢复功能，以免造成信息泄露和财产损失。

allowBackup安全风险详情：

·allowBackup风险位置：AndroidMannifest.xml文件android：allowBackup属性。

·allowBackup风险触发前提条件：未将AndroidMannifest.xml文件中的android：allowBackup属性值设为false。

·allowBackup风险原理：当allowBackup标志值为true时，即可通过adb backup和adb restore命令来备份和恢复应用程序数据。

11.2　如何获取应用隐私数据

前面了解了allowBackup属性的相关知识点，可以看到allowBackup属性的风险非常大。如果这个属性是true的话，怎么在没有root的设备去获取数据呢？

首先看这样一个场景，现在市场上有一些应用锁类的App，它们就是给每个应用打开的时候添加一个密码，打开App的时候需要输入密码才能进入应用，一般会把自己的银行App、通讯类的App加密。现在市场上这些应用锁的原理也很简单，一般是三种方式实现：

·最古老的方式是后台启动一个service，然后隔一段时间去轮询，获取当前的topActivity，然后进行操作。

·因为Android 5.0以后，获取当前的topActivity需要授权，所以这里还需要做一个操作就是引导用户去开一些权限功能。

·通过辅助功能可以监听当前Window的变化，这种方式比上面的轮询方式高效多了。

其实前两种方式差不多，唯一的区别就在于获取topActivity的方式，其实Google可能意识到了，获取topActivity是很危险的一件事：比如现在有一个恶意的软件，可以通过这个功能，当用户启动设备中的一些通讯、银行App的时候，可以监听到然后就启动他们自己定义的钓鱼登录页面，模拟这些App的登录，最终可以获取到用户名和密码了，完成盗取。

下面来看一个市场上比较火的一款应用锁App，如图11-1所示。

图11-1　市场中的某个应用锁App

下载这个apk之后使用aapt查看它的AndroidManifest.xml内容：

在拿到一个apk的时候，如果想知道apk中的一些资源和配置信息，aapt命令是个不错的选择，同时，它能够删除apk中的一些资源，添加一些资源到apk中都是可以的，查看结果如图11-2所示。

因为AndroidManifest.xml内容有点多，就重定向到一个txt文件中，看到allowBackup属性为true，因为非0的都是true。说明applock.apk这个应用是可以使用adb backup命令进行隐私数据的备份的。接下来就看一下如何进行备份。

第一步：进行数据的备份

代码如下：

·[-system|-nosystem]　这个指令是告诉adb在备份时是否要连同系统一起备份。若没有打的话默认是-system表示会一起备份系统。若连系统一起备份，在还原的时候会复盖系统档案对于已经升级后的手机是非常不好的。我不知道在没有root的情况下adb是否有权限去还原系统档案，但就算如此，还是不建议这样做。因此-nosystem是建议一定要打上的指令。

图11-2　aapt命令查看应用的AndroidManifest.xml

·-all　这个指令除非只是要备份单一App不然是一定要打上去的。这个是问你是否要备份全部的App，若有加上-nosystem的指令。那么就只会备份你目前已经安装上去的App而不会连系统App一起备份。

·[-apk|-noapk]　默认是-noapk，这个参数的意思是，是否连安装的apk一起备份。若为-noapk，则只会备份apk的资料档（像是游戏存盘、设定之类的）。

·[-shared|-noshared]　默认是-noshared，这个会问你是否连手机储存空间或是SD卡的档案一起备份。关于这个应用的包名，可以直接从上面dump出来的AndroidManifest.xml中获取即可，如图11-3所示。

图11-3　查看AndroidManifest.xml文件

获取包名之后，就可以执行命令，把导出来的文件保存到当前目录下：applock.ab，如下所示，注意这里的格式是ab，这个也是Android中备份文件的格式要求：

当执行这个命令的时候，设备端会出现一个提示页面，如图11-4所示。

这时候会提示输入备份密码，为了简单可以不输入，直接点击备份即可，备份完之后的文件如图11-5所示。

图11-4　备份截图

图11-5　备份之后的文件

第二步：abe工具来解析ab文件

还需要借助一个工具来解析这个ab文件。这个工具是开源的，用Java语言写的。源码位于：https://github.com/nelenkov/android-backup-extractor

这里没有直接下载源码，然后编译了，直接在网上下载了一个编译好的工具，工具下载地址如下。

提示：工具下载地址：

http://download.csdn.net/detail/jiangwei0910410003/9523470

用法也很简单，看到使用unpack参数来将ab文件转化成tar文件：

再用解压缩软件查看内容，如图11-6所示。

图11-6　压缩软件打开备份文件

现在如果想知道它的密码，可以直接查看SharePreferences

文件即可，因为一般都会把密码放到这里保存，当然这是一个非常危险的操作，如果再没有对密码进行加密的话，那更是危险了，如图11-7所示，是应用的沙盒中XML数据。

查看SmartLockConfig.xml内容，如图11-8所示。

被猜中了，它竟然把手势密码以明文的方式保存在这里。这里它用的是九宫格手势方式加密，03678代表的应该是一个L，可以对照图11-9九宫格图查看。

图11-7　SharedPreference文件

图11-8　存储密码文件

到这里就成功破解了应用锁应用的加密App的功能，而且是在设备没有root的情况下。下面来总结一下：

图11-9　九宫格图

1）这个App现在在市场的下载量还是蛮高的，但是看到它一不留神把allowBackup属性设置成true，就导致了数据可能被外泄，更为有趣的是，在使用几个IDE工具，默认新建的工程之后，这个属性的值也是true，如下所示（Eclipse和AndroidStudio开发工具）。

上面就是现在比较常用的两个IDE工具：Eclipse和AndroidStudio。可以看到当新建一个工程时，默认把这个属性设置成true，那么当不设置这个属性时，也是可以进行备份的，因为默认值是true。也许Google的想法是为了防止数据的丢失，就留了这个功能，便于用户备份数据。但是这里会隐含一些安全问题，在开发时如果不去注意这个属性的话，就会产生很大风险。以后在开发的过程中一定要记得把这个属性设置成false，特别是非常重要的需要账号登录的App。

2）上面分析了应用锁的App，并非只是一个案例，其实现在很多App都有这个问题，看看某市场App，如下所示：

它也是没有手动改变这个属性为false，所以它的数据也是可以导出来的。再看看某视频App，如下所示：

也是一样，不过像这些App，它们的账号登录密码的信息肯定不是放在本地的，而是放到服务端进行验证的，不像应用锁这样的，它不需要联网也是可以进行操作，所以密码必须放到本地。但是只要这个属性allowBackup为true的话，就会被查看到沙盒数据，就是不安全的。最后再来看看某社交App的5.0版本也有这个问题，不过它在6.0之后已经把allowBackup设置成false了，如下所示：

这里看到它没有设置allowBackup属性，但是刚刚说了，默认值就是true，所以还是可以导出来的，下面可以使用上面的导出步骤进行操作，如图11-10所示。

图11-10　社交数据库信息

其实社交App把通讯录和聊天记录都是保存到数据库中，但是这个数据库是加密的。数据库加密，其实是需要反编译App才知道，然后动态调试跟踪代码破解，因为通讯录和聊天内容是没有网络就可以查看的，那么加解密肯定是放在本地做的，只要是放在本地做的就可以进行破解。

不过可以看到这个App在后续的版本已经修复了这个问题，6.0版本之后就不能导出来数据了，那么5.0的用户其实还是会受影响的。

3）还是回到这个应用锁App中来，其实在这里还是想说一番的，作为下载量蛮高的App，竟然把密码用明文保存到XML中，这种做法其实是对用户的不负责，首先这里完全可以使用MD5来做密码验证就可以了，为何要暴露明文；其次是为什么把这么重要的数据保存到XML中，稍微加点破解难度考虑放到so中也是可以的。

4）开发的过程中，最后一步发包的时候一定要检查一下这个属性是否为false，因为它牵涉到应用的太多隐私信息了。

11.3　如何恢复应用数据

可以使用adb backup命令来做一下操作，就是在开发过程中，如果遇到手机没有root的，但是又想查看沙盒数据，那么这个也是一种方式，当然可以使用run-as命令来操作。

与上面的adb backup命令相对应的还有一个就是adb restore命令，它是用来恢复数据的，具体用法：

这时候也会在客户端出现确认页面，如图11-11所示。

点击恢复数据即可。现在如果想改了这个应用锁的密码，直接修改XML中密码数值，如下所示：

然后再保存成ab文件进行还原，密码就被修改了。比如有些应用把一些隐私的链接URL这样的信息保存到XML中，就可以导出来数据，然后修改URL为自己的URL再还原，而这些URL如果是一些上报、或者是登录的URL，就可以在这里做手脚，达到想要的目的了。

图11-11　数据还原

11.4　本章小结

从本章内容可以看到，对一个属性不留意会带来多大的风险，这个属性也是很容易忽视的，它默认值是true，必须手动设置成false才可以。这个属性导致现在市场上很多App都存在这样的风险。本章主要分析了一个应用锁应用的信息，发现它犯了一个最大的错误就是把密码用明文的方式保存在XML中，破解难度为0。所以开发者在开发一个App的时候，不仅考虑是用户体验，而且要对用户的隐私负责。用户在下载和安装一些App的时候也需要多留点心。

第12章　Android中的签名机制

Android开发者对于签名校验机制应该不陌生，就是为了安全，不让别人修改apk，防止恶意破解者在反编译apk之后再重新安装。如果重新安装系统会提示安装失败，因为apk的内容发生改变，签名结果不一样，Android系统中是不允许安装同一个包名但是签名不一样的apk程序的。

12.1　基本概念

在讲Android签名之前，需要了解几个知识点：

·数据摘要（数据指纹）、签名文件、证书文件。

·jarsign工具和signapk工具。

·keystore文件和pk8文件、x509.pem文件的关系。

·如何手动签名apk。

1.数据摘要

数据摘要其实也是一种算法，就是对一个数据源进行一个算法操作之后得到一个摘要，也叫作数据指纹，不同的数据源，数据指纹肯定不一样，就和人一样。

消息摘要算法（Message Digest Algorithm）是一种能产生特殊输出格式的算法，其原理是根据一定的运算规则对原始数据进行某种形式的信息提取，提取出的信息就称为原始数据的数据摘要。

著名的数据摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的变体。

数据摘要的主要特点有：

·无论输入的消息有多长，计算出来的数据摘要的长度总是固定的。例如应用MD5算法摘要的消息有128位，用SHA-1算法摘要的消息最终有160位。

·一般来说（不考虑碰撞的情况下），只要输入的原始数据不同，对其进行摘要以后产生的数据摘要也必不相同，即使原始数据稍有改变，输出的数据摘要便完全不同。但是，相同的输入必会产生相同的输出。

·具有不可逆性，即只能进行正向的数据摘要，而无法从摘要中恢复出任何的原始消息。

2.签名文件和证书文件

签名文件和证书文件是成对出现的，二者不可分离，而且后面通过源码可以看到，这两个文件的名字也是一样的，只是后缀名不一样。

数字签名要确保可靠通信，必须要解决两个问题：首先，要确定消息的来源确实是其申明的那个人；其次，要保证信息在传递的过程中不被第三方篡改，即使被篡改了，也可以发觉出来。

所谓数字签名，就是为了解决这两个问题而产生的，它是对前面提到的非对称加密技术与数字摘要技术的一个具体的应用。

对于消息的发送者来说，先要生成一对公私钥对，将公钥给消息的接收者。

如果消息的发送者有一天想给消息接收者发消息，在发送的信息中，除了要包含原始的消息外，还要加上另外一段消息。这段消息通过如下两步生成：

1）对要发送的原始消息提取数据摘要。

2）对提取的数据摘要用自己的私钥加密。

通过这两步得出的消息，就是所谓的原始信息的数字签名。

而对于信息的接收者来说，他所收到的信息，将包含两个部分，一是原始的消息内容，二是附加的那段数字签名。他将通过以下三步来验证消息的真伪：

1）对原始消息部分提取数据摘要，注意这里使用的数据摘要算法要和发送方使用的一致。

2）对附加上的那段数字签名，使用预先得到的公钥解密。

3）比较前两步所得到的两段消息是否一致。如果一致，则表明消息确实是期望的发送者发的，且内容没有被篡改过；相反，如果不一致，则表明传送的过程中一定出了问题，消息不可信。

这种数字签名技术确实可以有效解决可靠通信的问题。如果原始消息在传送的过程中被篡改了，那么在消息接收者那里，对被篡改的消息提取的摘要肯定和原始的不一样。并且，由于篡改者没有消息发送方的私钥，即使他可以重新算出被篡改消息的摘要，也不能伪造出数字签名。

综上所述，数字签名其实就是只有信息的发送者才能产生的别人无法伪造的一段数字串，这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。

不知道大家有没有注意，前面讲的这种数字签名方法，有一个前提，就是消息的接收者必须要事先得到正确的公钥。如果一开始公钥就被别人篡改了，那坏人就会被你当成好人，而真正的消息发送者给你发的消息会被你视作无效的。而且，很多时候根本就不具备事先沟通公钥的信息通道。那么如何保证公钥的安全可信呢？这就要靠数字证书来解决了。

数字证书一般包含以下一些内容：

·证书的发布机构（Issuer）

·证书的有效期（Validity）

·消息发送方的公钥

·证书所有者（Subject）

·数字签名所使用的算法

·数字签名

可以看出，数字证书其实也用到了数字签名技术。只不过要签名的内容是消息发送方的公钥，以及一些其他信息。但与普通数字签名不同的是，数字证书中签名者不是随随便便一个普通的机构，而是要有一定公信力的机构。这就好像你的大学毕业证书上签名的一般都是德高望重的校长一样。一般来说，这些有公信力机构的根证书已经在设备出厂前预先安装到了你的设备上了。所以，数字证书可以保证数字证书里的公钥确实是这个证书的所有者的，或者证书可以用来确认对方的身份。数字证书主要是用来解决公钥的安全发放问题。

综上所述，总结一下，数字签名和签名验证的大体流程如图12-1所示。

图12-1　签名过程

3.jarsign和signapk工具

了解到完了签名中的三个文件的知识点之后，下面继续来看Android中签名的两个工具：jarsign和signapk

开始使用这两个工具很容易混淆，它们到底有什么区别吗？其实这两个工具很好理解，jarsign是jdk本身自带的一个工具，它可以对jar进行签名。而signapk是后面专门为了Android应用程序apk进行签名的工具，它们的签名算法没什么区别，主要是签名时使用的文件不一样。

4.keystore文件和pk8、x509.pem文件的区别

上面了解到了jarsign和signapk两个工具都可以进行Android中的签名，那么它们的区别在于签名时使用的文件不一样：

·jarsign工具签名时使用的是keystore文件。

·signapk工具签名时使用的是pk8、x509.pem文件。

在使用Eclipse工具编写程序出debug包的时候，默认用的是jarsign工具进行签名的，而且Eclipse中有一个默认签名文件，如图12-2所示。

图12-2　Eclipse中默认签名文件

有默认签名的keystore文件，当然也可以选择指定的keystore文件。看到上面有MD5和SHA1的摘要，这个就是keystore文件中私钥的数据摘要，这个信息也是在申请很多开发平台账号的时候需要填入的信息，比如申请百度地图、微信SDK等，会需要填写应用的MD5或者SHA1信息。

5.手动签名apk包

（1）使用keytool和jarsigner来进行签名

当然，在正式签名处release包的时候，需要创建一个自己的keystore文件，如图12-3和图12-4所示。

图12-3　导出签名apk

图12-4　导出签名apk

这里可以对keystore文件起自己的名字，而且后缀名也是无关紧要的。创建完文件之后，也会生成MD5和SHA1值，这个值可以不用记录的，可以通过命令查看keystore文件的MD5和SHA1的值：keytool-list-keystore debug.keystore，如下所示：

这里看到用Eclipse自动签名和生成一个keystore文件，也可以使用keytool工具生成一个keystore文件。这个方法网上有，就不做太多的介绍了。然后可以使用jarsign来对apk包进行签名了。可以手动生成一个keystore文件：

这个命令有点长，有几个重要的参数需要说明：

·-alias是定义别名，这里是debug。

·-keyalg是规定签名算法，这里是DSA，这里的算法直接关系到后面apk中签名文件的后缀名，到后面会详细说明。

再用jarsigner工具进行签名，如下所示：

这样就成功地对apk进行签名了。

签名的过程中遇到的问题：

1）证书链找不到的问题，如下所示：

这是因为最后一个参数alias是keystore的别名输错了。

2）生成keystore文件的时候提示密码错误，如下所示：

这个原因是因为在当前目录已经有debug.ketystore了，再生成一个debug.keystore的话，就会报错。

3）找不到别名的问题，如下所示：

这个问题的原因是使用keytool生成keystore的时候，起了debug的别名，这个问题困扰了我很久，最后做了很多例子才发现的，只要keystore文件的别名是debug的话，就会报这样的错误。这个应该和系统默认的签名debug.keystore中的别名是debug有关系。

注意：Android中是允许使用多个keystore对apk进行签名的，这里就不再粘贴命令了，创建了几个keystore对apk进行签名，如图12-5所示。

图12-5　签名文件目录

这里把签名之后的apk进行解压之后，发现有三个签名文件和证书（.SF/.DSA）。也可以注意到，签名时用的是DSA算法，这里的文件后缀名就是DSA，而且文件名是keystore的别名，这里算是理清了如何使用keytool产生keystore，以及用jarsigner来进行签名。

（2）用signapk来进行签名

下面再来看看signapk工具进行签名：

这里需要两个文件.pk8和.x509.pem：

·pk8是私钥文件。

·x509.pem是含有公钥的文件。

需要注意的是：signapk签名之后的apk，META-INF文件夹中的三个文件的名字，如图12-6所示。

图12-6　签名文件

因为signapk在前面的时候不像jarsigner会自动使用别名来命名文件，就是写死了是CERT的名字，不过文件名不影响的，后面分析Android中的apk校验过程中会说道，只会通过后缀名来查找文件。

（3）两种的签名方式有什么区别

jarsigner签名时用的是keystore文件，signapk签名时用的是pk8和x509.pem文件，而且都是给apk进行签名的，那么keystore文件和pk8、x509.pem之间是不是有什么联系呢？答案是肯定的，网上搜了一下，果然它们之间是可以转化的，这里就不再分析如何进行转化的，网上的例子很多，有专门的工具可以进行转化。

12.2　Android中签名流程

下面开始从源码中去分析Android中的签名机制和原理流程。因为网上没有找到jarsigner的源码，但是找到了signapk的源码，下面就来看看signapk的源码：源码位置是com/android/signapk/sign.java。

通过上面的签名可以看到，Android签名apk之后，会有一个META-INF目录，这里有三个文件：

·MANIFEST.MF

·CERT.SF

·CERT.RSA

下面来看看这三个文件到底是干啥的。

1.MANIFEST.MF文件

这个文件主要包括apk文件中所有文件的数据摘要内容，如下所示：

下面来看看源码：

在main函数中，需要输入四个参数，然后开始写MANIFEST.MF文件内容：

进入方法manifest.write（outputJar）：

代码逻辑很简单，主要看那个循环的意思：除了三个文件（MANIFEST.MF、CERT.RSA、CERT.SF）之外都会对其他文件内容做一次SHA1算法，就是计算出文件的摘要信息，然后用Base64进行编码即可，下面用工具来做个案例看看是不是这样：首先安装工具：HashTab，这个工具网上有，可以自行搜索下载。然后网上搜索在线计算Base64。

下面就开始验证工作吧。验证一下AndroidManifest.xml文件，首先在MANIFEST.MF文件中找到这个条目，记录SHA1的值，如下所示：

安装HashTab之后，找到AndroidManifest.xml文件，右击，选择Hashtab，如图12-7所示。

复制SHA-1的值：9C64812DE7373B201C294101473636A3697FD73C，到上面的那个Base64转化网站，转化一下，如图12-8所示。

图12-7　查看文件的SHA-1值

图12-8　SHA-1值转化

nGSBLec3OyAcKUEBRzY2o2l/1zw=和MANIFEST.MF中的条目内容一模一样。

那么从上面的分析就知道了，其实MANIFEST.MF中存储的是这样的内容：逐一遍历里面所有条目，如果是目录就跳过，如果是一个文件，就用SHA1（或者SHA256）消息摘要算法提取出该文件的摘要然后进行BASE64编码后，作为“SHA1-Digest”属性的值写入到MANIFEST.MF文件中的一个块中。该块还有一个“Name”属性，其值就是该文件在apk包中的路径。

2.CERT.SF文件

这个文件主要是前面MANIFEST.MF文件的每个块内容的数据摘要信息，如下所示：

这里的内容感觉和MANIFEST.MF的内容差不多，来看看代码吧：

进入到writeSignatureFile方法中：

首先可以看到，需要对之前的MANIFEST.MF文件整个内容做一个SHA1放到SHA1-Digest-Manifest字段中。

看看manifest变量就是刚刚写入了MANIFEST.MF文件的，这个可以验证一下，如图12-9所示。

然后转化一下，如图12-10所示。

图12-9　查看文件的SHA-1值

图12-10　SHA-1值转化成Base64

看到了吧，和文件中的值是一样的，如下所示：

下面继续看代码，有一个循环：

这里还是用到了刚刚传入的mainfest变量，遍历它的条目内容，然后进行SHA算法计算，再用Base64计算一下。其实就是对MANIFEST.MF文件中的每个条目内容做一次SHA，再保存一下即可。

做个例子验证一下：用AndroidManifest.xml为例，把MANIFEST.MF文件中的条目拷贝保存到txt文档中，如图12-11所示。

图12-11　签名文件格式

这里需要注意的是，保存之后，需要添加两个换行，可以在代码中看到逻辑如下所示：

然后计算txt文档的SHA值，如图12-12和图12-13所示。

图12-12　查看文件签名

图12-13　Base64转码

看到了吧，这里计算的值是一样的，如下所示：

到这里就知道CERT.SF文件做了什么，如下所示：

1）计算MANIFEST.MF文件的整体SHA1值，再经过BASE64编码后，记录在CERT.SF主属性块（在文件头上）的“SHA1-Digest-Manifest”属性值值下。

2）逐条计算MANIFEST.MF文件中每一个块的SHA1，并经过BASE64编码后，记录在CERT.SF中的同名块中，属性的名字是“SHA1-Digest”。

3.CERT.RSA文件

这个文件就是对前面CERT.SF文件做签名操作之后的结果，也就是前面提到的签名文件，如下所示：

看到都是二进制文件，因为RSA文件加密了，所以需要用openssl命令才能查看其内容，如下所示：

关于这些信息，可以参考图12-14的解释。

来看一下代码：

图12-14　rsa文件格式说明

这里会把之前生成的CERT.SF文件用私钥计算出签名，然后将签名以及包含公钥信息的数字证书一同写入CERT.RSA保存。CERT.RSA是一个满足PKCS7格式的文件。

12.3　Android中为何采用这种签名机制

本节总结一下Android中为何要用这种方式进行加密签名，如果apk文件被篡改后会发生什么。

首先，如果改变了apk包中的任何文件，那么在apk安装校验时，改变后的文件摘要信息与MANIFEST.MF的检验信息不同，于是验证失败，程序就不能成功安装。其次，如果对更改过的文件算出新的摘要值，然后更改MANIFEST.MF文件里面对应的属性值，那么必定与CERT.SF文件中算出的摘要值不一样，照样验证失败。最后，如果你还不死心，继续计算MANIFEST.MF的摘要值，相应地更改CERT.SF里面的值，那么数字签名值必定与CERT.RSA文件中记录的不一样，还是失败。

那么能不能继续伪造数字签名呢？不可能，因为没有数字证书对应的私钥。所以，如果要重新打包后的应用程序能在Android设备上安装，必须对其进行重签名。

从上面的分析可以得出，只要修改了apk中的任何内容，就必须重新签名，不然会提示安装失败。

在分析了签名技术之后，无意中发现一个问题，就是CERT.SF和MANIFEST.MF这两个文件中内容的name字段都是apk中的资源名，那么就有一个问题了，如果资源名很长，而且apk中的资源很多，那么这两个文件就会很大，这里是不是可以优化呢？确实是可以的。这里不多详细解析，感兴趣的同学可以去看一下开源框架AndResGuard。

12.4　本章小结

本章主要介绍了Android中的apk签名机制知识点，有了这套签名机制，可以做到应用防止被重新打包二次签名的风险。

第13章　Android应用加固原理

本章介绍Android中对apk进行加固的原理。现阶段Android中的反编译工作越来越容易操作，开发者辛苦地开发出一个apk，结果被人反编译了，那心情真心不舒服。虽然加入混淆，做到native层，但是这都是治标不治本的办法。反编译的技术在更新，那么保护apk的技术就不能停止。现在网上有很多apk加固的第三方平台，例如爱加密和梆梆等。有些人认为加固技术很高深，其实不然，加固技术就是对源apk进行加密，然后再套上一层壳即可。当然还有一些细节需要处理，这就是本章需要介绍的内容。

13.1　加固原理解析

下面就来看一下Android中加固的原理，如图13-1所示。

图13-1　加固结构图

在加固的过程中需要三个对象：

·需要加密的apk（源apk）

·自己的壳程序apk（负责解密apk工作）

·加密工具（将源apk进行加密和壳dex合并成新的dex）

加固的主要步骤：拿到需要加密的apk（源apk）和自己的壳程序apk，然后用加密算法对源apk进行加密，再将壳apk进行合并得到新的dex文件，最后替换壳程序中的dex文件即可，得到新的apk，那么这个新的apk也叫作脱壳程序apk。它已经不是一个完整意义上的apk程序了，它的主要工作是：负责解密源apk，然后加载apk，让其正常运行起来。

在这个过程中可能需要了解的一个知识是：如何将源apk和壳apk进行合并成新的dex。这就需要了解dex文件的格式了。在第7章中已经详细介绍了dex文件格式了，下面就来简单回顾一下dex文件的格式。

主要来看一下dex文件的头部信息，其实dex文件和class文件的格式分析原理都是一样的，它们都有固定的格式，现在反编译的一些工具如下所示：

·jd-gui：可以查看jar中的类，其实它就是解析class文件，只要了解class文件的格式就可以。

·dex2jar：将dex文件转化成jar，原理也是一样的，只要知道dex文件的格式，能够解析出dex文件中的类信息就可以了。

当然在分析这个文件的时候，最重要的还是头部信息，它是一个文件的开始部分，也是索引部分，内部信息很重要，如图13-2所示。

图13-2　dex文件头部结构图

只要关注图中标记的三个部分：

1）checksum：文件校验码，使用alder32算法校验文件除去maigc、checksum外余下的所有文件区域，用于检查文件错误。

2）signature：使用SHA-1算法hash除去magic、checksum和signature外余下的所有文件区域，用于唯一识别本文件。

3）file_size：dex文件的大小。

为什么说只需要关注这三个字段呢？因为需要将一个文件（加密之后的源apk）写入到dex文件中，那么肯定需要修改文件校验码（checksum）。因为它检查文件是否有错误。signature也是一样，也是唯一识别文件的算法。还需要修改dex文件的大小。

不过这里还需要一个操作，就是标注一下加密的apk的大小，因为在脱壳的时候，需要知道apk的大小，才能正确得到apk。那么这个值放到哪呢？直接放到文件的末尾就可以了。

总结一下需要做的步骤：修改dex的三个文件头，将源apk的大小追加到新的dex末尾就可以了。修改之后得到新的dex文件样式如图13-3所示。

图13-3　apk加固原理图

知道原理后就来看代码实现，这里有三个项目：

·源程序项目（需要加密的apk）

·加壳项目（对源apk进行加密，和脱壳项目的dex进行合并）

·脱壳项目（解密源apk和加载apk）

13.2　案例分析

根据上节分析得知，加固后新的dex文件中有三个项目，本节来依次看一下这三个项目的代码实现。

1.需要加密的源程序apk项目：ForceApkObj

需要一个Application类，如图13-4所示。

图13-4　加固apk项目结构图

下面来分析一下MyApplication源码：

就是打印一下onCreate方法。继续来分析一下MainActivity源码：

也是简单地打印一下内容。

2.加壳程序项目：DexShellTools

加壳程序其实就是一个Java项目，它的工作就是加密源apk，然后将其写入脱壳dex文件中，修改文件头，得到一个新的dex文件即可，如图13-5所示。

图13-5　加壳项目说明

下面来分析一下具体代码：

下面来详细分析一下具体代码逻辑：

实框部分其实就是最核心的工作。

1）加密源程序apk文件：

加密算法如下所示：

对每个字节进行异或一下即可。

提示：这里是为了简单起见，就用了很简单的加密算法。其实为了增加破解难度，应该使用更高效的加密算法，同时最好将加密操作放到native层去做。

2）合并文件：将加密之后的apk和原脱壳dex进行合并：

3）在文件的末尾追加源程序apk的长度：

4）修改新dex文件的文件头信息：file_size；sha1；check_sum：

具体修改可以参照之前说的文件头格式，修改指定位置的字节值即可。

这里还需要两个输入文件：

·源apk文件：ForceApkObj.apk

·脱壳程序的dex文件：ForceApkObj.dex

第一个文件都知道，就是上面的源程序编译之后的apk文件，第二个文件怎么得到呢？这个就是要讲到的第三个项目：脱壳程序项目。

3.脱壳项目：ReforceApk

脱壳项目是一个Android项目，在编译之后，能够得到它的classes.dex文件，然后修改一下名称就可，如图13-6所示。

图13-6　脱壳项目结构图

在讲解这个项目之前，先来了解一下这个脱壳项目的工作。

1）通过反射置换android.app.ActivityThread中的mClassLoader是加载解密出apk的DexClassLoader，该DexClassLoader一方面加载了源程序，另一方面以原mClassLoader为父节点，这就保证了既加载了源程序又没有放弃原先加载的资源与系统代码。

关于这部分内容，不了解的可以看一下ActivityThread.java的源码：

2）找到源程序的Application，通过反射建立并运行。

这里需要注意的是，现在是加载一个完整的apk，让它运行起来，一个apk程序运行的时候都是有一个Application对象的，这个也是一个程序运行之后的全局类。所以必须找到解密之后的源apk的Application类，运行它的onCreate方法，这样源apk才开始它的运行生命周期。这里如何得到源apk的Application的类呢？这个后面会说到。使用meta标签进行设置。

下面来看一下整体的流程图，如图13-7所示。

图13-7　加载流程

首先来看一下具体步骤的代码实现逻辑：

1）得到脱壳apk中的dex文件，然后从这个文件中得到源程序apk，进行解密，最后进行加载，代码如下：

这里需要注意的一个问题是，需要找到一个时机在脱壳程序还没有运行起来的时候来加载源程序的apk，执行它的onCreate方法，那么这个时机不能太晚，不然的话，就是运行脱壳程序，而不是源程序了。查看源码知道Application中有一个方法即attachBaseContext方法，它在Application的onCreate方法执行前就会执行了，那么所有的工作就需要在这里进行。

2）从脱壳程序apk中找到源程序apk，并且进行解密操作：

脱壳解密操作一定要和之前的加壳以及加密操作对应，不然就会出现dex加载错误问题。

从apk中获取到dex文件：

其实就是解压apk文件，直接得到dex文件即可。

从脱壳Dex中得到源apk文件：

解密源程序apk：

解密算法和加密算法是一致的。

3）加载解密之后的源程序apk：

4）找到源程序的Application程序，让其运行：

直接在脱壳的Application中的onCreate方法中进行就可以了。这里还可以看到是通过AndroidManifest.xml中的meta标签获取源程序apk中的Application对象的。

下面来看一下AndoridManifest.xml文件中的内容，如下所示：

在这里定义了源程序apk的Application类名。

提示：项目下载为http://download.csdn.net/detail/jiangwei0910410003/9102741

13.3　运行项目

上节介绍了这三个项目，下面就来看看如何运行吧。

第一步：得到源程序apk文件和脱壳程序的dex文件

运行源程序项目和脱壳程序项目，得到源程序apk文件和脱壳程序的dex文件，如图13-8所示。

图13-8　项目结构图

之后得到这两个文件（记得将classes.dex文件改名ForceApkObj.dex），然后使用加壳程序进行加壳，如图13-9所示。

这里的ForceApkObj.apk文件和ForceApkObj.dex文件是输入文件，输出的是classes.dex文件。

第二步：替换脱壳程序中的classes.dex文件

在第一步中得到加壳之后的classes.dex文件之后，并且在第一步运行脱壳项目的时候得到一个ReforceApk.apk文件，这时候使用解压缩软件进行替换，如图13-10所示。

图13-9　加壳程序

图13-10　压缩软件查看apk内容

第三步：得到替换之后的ReforceApk.apk文件

这个文件因为被修改了，所以需要重新对它签名，不然运行也是报错的。这里可以使用jarsigner工具对apk进行重签名：

这里最主要的命令就是中间的一条签名的命令，关于命令的参数说明如下：

例如：

那么通过上面的三个步骤之后得到一个签名之后的最终文件：ReforceApk_des.apk。安装这个apk，然后运行，效果如图13-11所示。

图13-11　运行效果图

这个时候再去反编译一下源程序apk，如下所示（这个文件是脱壳出来的payload.apk，看ReforeceApk中的代码就知道它的位置了）：

发现dex文件格式是不正确的。说明加固是成功的。

总结遇到的问题如下：

·研究的过程中遇到签名不正确的地方，开始的时候替换dex文件，就直接运行了apk，但是总是提示签名不正确。

·运行的过程中说找不到源程序中的Activity，需要在脱壳程序中的AndroidManifest.xml中查看一下源程序中的Activity，如下所示：

通过上面的过程可以看到，关于apk加固的工作还是挺复杂的，涉及的东西也挺多，下面就来总结一下。

加壳程序：

·任务：对源程序apk进行加密，合并脱壳程序的dex文件，然后输入一个加壳之后的dex文件。

·语言：任何语言都可以，不限于Java语言。

·技术点：对dex文件格式的解析。

脱壳程序：

·任务：获取源程序apk，进行解密，然后动态加载进来，运行程序。

·语言：Android项目（Java）。

·技术点：如何从apk中获取dex文件，动态加载apk，使用反射运行Application。

13.4　本章小结

Android中的apk反编译可能是每个开发者都会经历的事，但是源程序的开发者对apk加固应运而生，即使是加固，也还是做不到那么安全，现在网上也有很多文章在解析某品牌加固的原理了，而且有人破解成功了，那么加固还不是怎么安全。最后一句话：逆向和加固是一个永不停息的战争。

第14章　Android中的so加固原理

前一章已经介绍了Android中dex加固原理，虽然dex加固是一种防护策略，但是它仍然存在被破解的风险，所以本章就来介绍另外一种加固方法，对so文件的加固防护，这种加固方式和dex加固方式来比较的话，安全性会更高，因为native层的代码会比较难以破解和阅读。

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）修改回内存区域的读写权限。

这里是对代码段的数据进行解密，需要写权限。如果对数据段的数据解密，是不需要更改权限而直接操作的。

14.2　基于对so中的函数加密实现so加固

上一节介绍了对so中指定的section进行加密来实现对so加固方案。延续之前的内容来介绍一下如何对函数进行加密来实现加固。

14.2.1　技术原理

本节内容和上一节有很多类似的地方，这里就不做太多的解释了。和上一节内容唯一的不同点就是如何找到指定的函数的偏移地址和大小。

那么先来了解一下so中函数的表现形式：在so文件中，每个函数的结构描述是存放在.dynsym段中的。每个函数的名称保存在.dynstr段中的，类似于之前说过的每个section的名称都保存在.shstrtab段中，所以在上一节中找到指定section的时候，就是通过每个section的sh_name字段到.shstrtab中寻找名字即可，而且知道.shstrtab这个section在头文件中是有一个index的，就是在所有段列表中的索引值，所以很好定位.shstrtab。

但是在本节内容中可能遇到一个问题，就是不能按照这种方式去查找指定函数名了，如下所示：

可能有的人意识到一个方法，就是可以通过section的type来获取.dynsym和.dynstr。看到上图中.dynsym类型是：DYNSYM，.dynstr类型是STRTAB，但是这种方法是不行的，因为这个type不是唯一的，也就说不同的section，type可能相同，没办法区分，比如.shstrtab和.dynstr的type都是STRTAB.其实从这里就知道这两个段的区别了：.shstrtab值存储段的名称，.dynstr是存储so中的所有符号名称。

那么该怎么办呢？这时候再去看一下so文件格式说明（参见第4章）。看到有一个.hash段，在上图中也可以看到的：由Elf32_Word对象组成的哈希表支持符号表访问。下面的例子有助于解释哈希表组织，如图14-8所示。

图14-8　符号表索引

bucket数组包含nbucket个项目，chain数组包含nchain个项目，下标都是从0开始。bucket和chain中都保存符号表索引。chain表项和符号表存在对应。符号表项的数目应该和nchain相等，所以符号表的索引也可用来选取chain表项。哈希函数能够接受符号名并且返回一个可以用来计算bucket的索引。因此，如果哈希函数针对某个名字返回了数值X，则bucket[X%nbucket]给出了一个索引y，该索引可用于符号表，也可用于chain表。如果符号表项不是所需要的，那么chain[y]则给出了具有相同哈希值的下一个符号表项。可以沿着chain链一直搜索，直到所选中的符号表项包含了所需要的符号，或者chain项中包含值STN_UNDEF。

上面的描述感觉有点复杂，其实说的简单点就是：用目标函数名在用hash函数得到一个hash值，然后再做一些计算就可以得到这个函数在.dynsym段中这个函数对应的条目了。关于这个hash函数是公用的，在Android中的bonic/linker.c源码中也是可以找到的：

那么只要得到.hash段即可，但是怎么获取到这个section中呢？so中并没有对这个section进行数据结构的描述，有人可能想到了在上图中看到.hash段的type是HASH，那么再通过这个type来获取？但是之前说了，这个type不是唯一的，通过它来获取section是不靠谱的？那么该怎么办呢？这时候就要看一下程序头信息了，如下所示：

程序头信息是最后so被加载到内存中的映像描述，这里看到有一个.dynamic段。再看看so文件的装载视图和链接视图，如图14-9所示。

在之前也说过，so被加载到内存之后，就没有section了，对应的是segment，也就是程序头中描述的结构，而且一个segment可以包含多个section，相同的section可以被包含到不同的segment中。.dynamic段一般用于动态链接，所以.dynsym和.dynstr，.hash肯定包含在这里。可以解析了程序头信息之后，通过type获取到.dynamic程序头信息，然后获取到segment的偏移地址和大小，再进行解析成elf32_dyn结构。下面代码就是程序头的type类型和dyn结构描述，可以在elf.h中找到：

图14-9　so文件的装载视图和链接视图

这里的三个字段含义如下：

·d_tag：标示，标示这个dyn是什么类型，是.dynsym还是.dynstr等。

·d_val：section的大小。

·d_ptr：section的偏移地址。

细心的读者可能会发现一个问题，就是在这里寻找.dynamic也是通过类型的，然后再找到对应的section。这种方式和之前说的通过type来寻找section有两个不同之处：

·在程序头信息中，type标示.dynamic段是唯一的，所以可以通过type来进行寻找。

·看到上面的链接视图和装载视图发现，这种通过程序头中的信息来查找.dysym等section靠谱点，因为当so被加载到内存中，就不存在了section了，只有segment了。

14.2.2　实现方案

本节用一个案例来展示so加固方法，即编写native程序直接返回字符串给Java层进行展示。需要做的是对Java_com_example_shelldemo2_MainActivity_getString函数进行加密。加密和解密都是基于装载视图实现。注意，被加密函数如果用static声明，那么函数是不会出现在.dynsym中，是无法在装载视图中通过函数名找到进行解密的。当然，也可以采用取巧方式，类似上节，把地址和长度信息写入so头中实现。Java_com_example_shelldemo2_MainActivity_getString需要被调用，那么一定是能在.dynsym找到的。

1.加密流程

1）读取文件头，获取e_phoff、e_phentsize和e_phnum信息。

2）通过Elf32_Phdr中的p_type字段，找到DYNAMIC。从下图可以看出，其实DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度。

3）遍历.dynamic，找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小。在我的测试环境下，fedora 14和windows7 Cygwin x64中elf.h定义.hash的d_tag标示是：DT_GNU_HASH；而安卓源码中的是：DT_HASH。

4）根据函数名称，计算hash值。

5）根据hash值，找到下标hash%nbuckets的bucket；根据bucket中的值，读取.dynsym中对应索引的Elf32_Sym符号；从符号的st_name索引找到在.dynstr中对应的字符串与函数名进行比较。若不等，则根据chain[hash%nbuckets]找下一个Elf32_Sym符号，直到找到或者chain终止为止。这里叙述得有些复杂，直接上代码表示：

6）找到函数对应的Elf32_Sym符号后，即可根据st_value和st_size字段找到函数的位置和大小。

7）后面的步骤就和上节相同了，这里就不赘述。

2.解密流程

解密流程为加密逆过程，大体相同，只有一些细微的区别，具体如下：

·找到so文件在内存中的起始地址。

·通过so文件头找到Phdr；从Phdr找到PT_DYNAMIC后，需取p_vaddr和p_filesz字段，并非p_offset，这里需要注意。

·对内存区域数据的解密，也需要注意读写权限问题。

14.2.3　代码实现

1.native程序

代码如下：

代码逻辑和前一节的加密section中的代码类似，只是在寻找函数的地方有点不同，在加密的代码中说明一下。

2.加密程序

代码如下：

解密程序需要说明一下。

1）定位到.dynamic的segment，解析成elf32_dyn结构信息：

这里有一个解析elf32_dyn结构：

需要注意的是，elf32_dyn中用到了联合体union结构，Java中是不存在这个类型的，所以需要了解这个联合体的含义，虽然是三个字段，但是大小是8个字节，而不是12字节。dyn.d_val和dyn.d_val是在一个联合体中的。

2）计算目标函数的hash值，得到函数的偏移值和大小：

逻辑有点绕，了解原理即可。其中nbucket和nchain、bucket[i]和chain[i]都是4个字节，它们的值就是目标函数在.dynsym中的位置。

上面对so中的函数加密成功了，那么下面来验证加密，直接使用IDA进行查看，如图14-10所示。

可以看到，加密的函数内容已经面目全非了，看不到信息了。比较加密前的文件，如图14-11所示。

提示：案例下载地址为http://download.csdn.net/detail/jiangwei0910410003/9289009

3.测试Android项目

用加密之后的so文件来测试一下：

图14-10　IDA查看so文件

图14-11　加密前so的内容

运行结果如图14-12所示。

图14-12　运行结果

14.3　本章小结

本章主要介绍了通过加密so中的section和函数来实现加固功能，有了这个so加固方案之后，可以让App开发更加安全，破解难度加大。但是也不能说是绝对安全了，比如现在破解者在init_getString函数下断点，然后动态调试一下，就可以很轻易地破解了，而且通过dump出内存中运行的dex也是可以做到的。所以，没有绝对的安全，只有相对的攻防。

工具篇

第15章　Android逆向分析基础

第16章　反编译神器apktool和Jadx

第17章　Hook神器Xposed

第18章　脱壳神器ZjDroid

第19章　Native层Hook神器Cydia Substrate

第15章　Android逆向分析基础

Android逆向技术不是陌生的话题，网上已经有很多资料，本章主要介绍一下关于逆向分析apk的时候需要做些什么。在逆向之前必须有好工具，其次是还需要掌握逆向的基本知识：smali语法和arm指令，能够大致读懂程序。

15.1　逆向工具

这里介绍几个工具，而且这几个工具在后面的章节中都会用到。

1.反编译利器apktool

这个工具用Java语言写的，而且也是开源的，同时它是逆向的金钥匙，所以非常重要，而且现在很多apk为了防止被这个工具反编译，就针对这个工具的漏洞做了一层防护策略，如果要成功反编译各个apk的话，那么就必须看懂它的源码，然后针对具体反编译异常具体分析，修复异常即可。

2.反编译姐妹花dex2jar+jd-gui

这两个工具其实是辅助使用的，一般是直接解压apk得到classes.dex文件，使用dex2jar转化成jar文件，使用jd-gui来直接查看Java源码。

3.反编译友好工具JEB和Jadx

这两个工具其实是整合了上面两个工具的功能，让反编译实现了可视化操作，而且高效快捷，适合在快速分析apk内部信息的场景，也是不可或缺的工具。

4.Hook神器Xposed框架

这个工具主要用于编写游戏外挂，获取应用关键数据此工具也可以算得上神器，有了它，逆向App就是手到擒来的事，用它可以把每个App的关键逻辑分析得非常清楚。

5.Native Hook神器Cydia Substrate

这个工具和上面的Xposed框架非常类似，但是这个工具除了可以Hook Java层功能，还可以很方便地Hook Native层功能。对于一些游戏和应用有so文件，可以利用这个框架进行hook so中的指定函数功能。

6.脱壳神器ZjDroid工具

现在很多应用为了安全，做了加固策略，那么逆向中有一个技术是脱壳就是针对加固策略的。所谓脱壳就是要把原本加固的应用进行解密，那么ZjDroid工具就是一款脱壳神器。这个工具其实是基于前面的Xposed框架编写的。

7.堪称神器的工具IDA

这个工具不用多解释了，如果说搞逆向不知道这个工具，那就等于还没有进入逆向的大门，比较可惜的是它是收费的。这个工具最厉害的地方就是它能够分析so文件，同时可以动态调试so，而且可以打开apk文件，dex文件也是不在话下的。

15.2　逆向基本知识

要学习逆向，除了有利的工具，还需要了解一些基本知识，比如smali语法、arm指令、Android中的NDK开发等，因为这些都是在逆向过程中经常会用到的知识。

smali语法是逆向的基础核心知识，有的人觉得这个是否是一门新的语言？其实不然，它的语法非常简单，熟悉起来非常方便。比如后面章节会利用smali语法手动插入代码，而并不是需要手动编写smali代码，可以先用Java编写好功能，再反编译成smali代码，再手动插入即可。但是简单的smali语法还是需要看懂，比如赋值、方法调用等操作，具体smali语法知识，在后面章节会详细介绍。

arm指令就不多说了，在大学期间就有这门课，不过很多人可能在项目开发中并没有用到，但是arm指令知识对于逆向来说也是至关重要，也是最为复杂的，现在很多应用为了更加安全就把代码都放到了so中，这样在逆向so文件的时候就必须要了解arm指令，才能大致看懂逻辑代码，才能继续后面工作。arm指令也并非那么复杂，多看多读就会慢慢熟练了。相关命令在后面章节会详细介绍。

15.3　打开系统调试总开关

本节将介绍如何在不需要反编译的情况下添加android：debuggable属性，就可以进行调试。现在已经有很多工具可以做这件事了，先来说说具体的原理吧。

其实Android中有一些常用的配置信息都是存放在一个文件中，如下所示，比如设备的系统、版本号、CPU型号等信息，这个文件位置在：/system/build.prop。

查看文件的内容，可以看到很多设备的信息，而且这些ro开头的文件表示这些属性值是只读的，不能进行修改的。

同时Android中提供了两个命令来操作这些信息：getprop和setprop命令，如下所示：

查看系统的sdk版本号如下所示：

设置系统的sdk版本号为22，可是这里并没有修改成功，原因是因为ro开头的属性是不允许后期修改的。如果修改，需要重新编译系统镜像文件boot.img，但是这里并不是本节介绍的重点。

既然Android中的一些系统属性值存放在一个文件中，而且这些值是只读的，当然不仅可以通过getprop命令读取，有一个API也是可以直接读取的，就是：

其实这个方法是native层实现的，具体就不分析了。那么这个文件是存储这些属性值的，是谁来进行解析加载到内存中，能够给每个App都能访问到呢？

这个工作就是init.rc进程操作的，系统启动时第一步就是解析init.rc文件，这个文件是在系统的根目录下，这里会做很多初始化操作，同时会做属性文件的解析工作，所以Android属性系统通过系统服务提供系统配置和状态的管理。为了让运行中的所有进程共享系统运行时所需要的各种设置值，系统会开辟一个属性存储区域，并提供访问该内存区域的API。所有进程都可以访问属性值，但是只有init进程可以修改属性值，其他进程若想修改属性值，需要向init进程发出请求，最终由init进程负责修改属性值。

上面说到的是system/build.prop文件。里面主要是系统的配置信息，其实还有一个重要文件在根目录下面：default.prop，如下所示：

这里有一个重要属性ro.debuggable，关系到系统中每个应用是否能够被调试的关键。在Android系统中一个应用能否被调试是这么判断的：当Dalvik虚拟机从Android应用框架中启动时，系统属性ro.debuggable为1，如果该值被置1，系统中所有的程序都是可以调试的。如果系统中的ro.debuggable为0，则会判断程序的AndroidManifest.xml中application标签中的android：debuggable元素是否为true，如果为true则开启调试支持。

这相当于Android系统中有一个开关，即根目录中default.prop文件中的ro.debuggable属性值，可用于调试所有设备中的应用。如果把这个属性值设置成1，设备中所有应用都可以被调试，即使在AndroidManifest.xml中没有android：debuggable=true，还是可以调试的。而这些系统属性的文件system/build.prop和default.prop，都是init进程来进行解析的，系统启动的时候就会去解析init.rc文件，这个文件中有配置关于系统属性的解析工作信息。然后会把这些系统属性信息解析到内存中，提供给所有App进行访问，这块信息也是内存共享的。但是这些ro开头的属性信息只能init进程进行修改。下面来分析一下修改这个属性值的三种方式。

第一种方式：直接修改default.prop文件中的值，然后重启设备。那么现在如果按照上面的目的：就是不需要反编译apk，添加android：debuggable属性的话，直接修改default.prop文件，把ro.debuggable属性改成1即可，但是通过上面的分析，修改完成之后肯定需要重启设备的，因为需要让init进程重新解析属性文件，把属性信息加载内存中方可起作用的。但是并没有那么顺利，在实践的过程中，修改了这个属性，结果会发现设备死机了，其实想想也是正常的，如果属性能够通过这些文件来修改的话，那就感觉系统会出现各种问题了，系统是不会允许修改这些文件的。

第二种方式：改写系统文件，重新编译系统镜像文件，然后刷入到设备中。前面已经提到过，这些属性文件是在系统镜像文件boot.img系统启动的时候，释放到具体目录中的，也就是说如果能够直接修改boot.img中的这个属性即可，那么这个操作是可以进行的。理论上是可以的，但是我没成功操作过。而且这种方式如果成功了，那么这个设备就是永远可以进行各种应用的调试了。

第三种方式：注入init进程，修改内存中的属性值。上面分析了，init进程会解析这个属性文件，然后把这些属性信息解析到内存中，给所有App进行访问使用，所以在init进程的内存块中是存在这些属性值的，那么这时候就好办了，有一个技术可以做到，就是进程注入技术。可以使用ptrace注入到init进程，然后修改内存中的这些属性值，只要init进程不重启的话，那么这些属性值就会起效。好了，这个方法可以尝试，但是这个方法有一个弊端，就是如果init进程挂了重启的话，那么设置就没有任何效果了，必须重新操作了，所以有效期不是很长，但是一般情况下只要保证设备不重启的话，init进程会一直存在的，而且如果发生了init进程挂掉的情况，那么设备肯定会重启的。到时候再重新操作一下即可。

上面的三种方式设置系统中的调试属性总开关，最后一种方式是最靠谱的。而且思路也很简单，但是不用重新去写这个代码逻辑的，因为网上已经有这个工具了，这个工具叫做mprop，是一个执行文件。用法很简单，首先把可执行文件mprop拷贝到设备中的目录下，然后运行命令，如下所示：

这个工具可以修改内存中所有的属性值，包括机型信息。修改完成之后，使用getprop命令再查看值，发现修改成功了，但是需要注意的是，修改的是内存的值，而不是文件中的值。所以default.prop文件中的内容是没有发生变化：

这时候，可以使用Eclipse的DDMS来查看可以调试的应用列表，如图15-1所示。

图15-1　Eclipse中DDMS查看应用列表

当然也可以使用adb jdwp命令来查看可以调试的进程id，如下所示：

但是可惜的是，发现还是没有展示设备中所有的应用，其实这里是有一个细节问题，虽然修改了内存值，但是有一个进程需要重启一下，哪个进程呢？就是adbd这个进程，这个进程是adb的守护进程，就是设备连接信息传输后台进程，所以想看到可以调试的进程信息的话，那么需要重启这个进程，这样连接信息才会更新。

重启这个进程很简单，直接使用命令即可，如下所示：

其实这是两个命令，用分号隔开，首先是干掉进程，然后重启。运行完命令之后，再去看DDMS窗口信息，如图15-2所示。

图15-2　 Eclipse 中DDMS界面展示可调式应用列表

这时候所有的应用进程都是可以调试的了，再使用dumpsys package命令查看一个应用的包信息，如下所示：

可以看到，这个应用的flags标志中并没有debuggable属性值，但是这个应用是可以调试的。

15.4　本章小结

本章主要简单介绍了，在做逆向工作之前，要先准备一些基础知识，主要从三个方面介绍了基本知识点：首先是逆向工具，包括apktool、Jadx、Xposed、ZjDroid等相关信息，其次是逆向的基本语法知识，主要包括smali和arm语法，最后是逆向的辅助功能，通过修改系统调试开关，便于后续逆向应用。

第16章　反编译神器apktool和Jadx

apktool工具是开源的，也是用Java语言开发的，代码相对简单，本章就来分析一下它的大体逻辑。分析这个工具的原因只有一个，就是在之前的反编译过程中可以发现，总是有一些apk应用不那么轻易地被反编译，它们利用apktool的漏洞对apk做了一定的混淆工作，所以需要通过分析源码来解决这些异常，从而对每个apk的反编译都能如鱼得水。本章还介绍一个分析工具Jadx，它能更高效地分析apk。

16.1　逆向操作惯例

当对一个apk进行破解之前，会做如下两件事：

·用压缩软件解压apk，得到classes.dex，然后使用dex2jar+jd-gui工具查看代码逻辑。但是这里会发现资源文件如AndroidManifest.xml和res下面的XML文件都是乱码的，因为它们遵循Android中的arsc文件格式，关于这个文件格式在之前几章已经详细介绍过了。其实不管是什么文件格式，都有文件格式的说明文档，只要按照说明文档去做解析即可。

·使用apktool工具反编译apk，得到smali源码和资源文件。smali语法是Android虚拟机识别执行的指令代码，和dex文件可以相互转化，使用baksmali.jar和smali.jar这两个工具即可。当然这里还可以得到所有的资源文件，即arsc格式解析之后的内容，而且这个工具可以实现回编译，这个功能也是很强大的，不过后面分析源码就知道了，回编译其实是借助aapt这个强大的系统命令来完成的。可以看到如果想要完全分析一个apk，apktool工具是不可或缺的，它是开启破解大门的钥匙，这个工具也是逆向领域的敲门砖，而且现在很多可视化的破解工具的核心都是使用apktool+dex2jar+jd-gui这三个工具组成的，只是后期做了一定的界面优化。

16.2　反编译常见的问题

apktool工具在反编译的过程中总会遇到一些问题，下面介绍最常见的两类问题。

1.第一类问题

错误提示：

主要是因为利用了apktool的一个漏洞，做了属性id的混淆，如下所示。

2.第二类问题

错误提示：

这是在使用apktool工具时报错最多的错误，主要是利用apktool的漏洞，修改了resource.arsc的头部信息，如下所示：

网上很多解决方案都是说apktool工具的版本太旧了，用最新版本，但是这里可以看一下apktool.jar的版本，如下所示：

这个版本是最新的了。反编译失败的原因是应用开发公司知道了apktool工具反编译的厉害，所以去看apktool的源码，分析得到漏洞，然后进行apk的一些混淆，防止反编译。因此，防护和破解真的是无休止的战争。幸好apktool的代码也更新得比较快，所以会解决这些漏洞，但是在破解的时候遇到这些问题，不能一味地等待apktool的更新，既然是开源的，那么就直接分析源码，发现报错的地方修复即可。

16.3　分析apktool的源码

上面说了为什么要分析apktool的源码，下面就真正开始分析。当然第一步是得到apktool的源码，地址为https://code.google.com/p/android-apktool/。看到有Google的域名是不是很郁闷？的确，国内程序员一般打开时始终处于loading状态，直至报错，所以只能去万能的GitHub上搜索了，找到了地址https://github.com/iBotPeaches/Apktool。可以看到有很多人关注，而且代码是有人维护和更新的，所以下载到本地。

但是下载后发现是一个Gradle项目，所以就给Eclipse装一个Gradle插件，然后导入项目即可。但是又遇到一个问题，还是国内网络的问题，Gradle下载失败，因为这里引用了一些第三方的jar，如下所示：

那么，只能无奈地手动去一个一个找这些jar包，这个过程还是比较辛苦的，有些jar很难找，不过最后还是都找到了，不再有报错，项目结构如图16-1所示。

图16-1　apktool项目结构

Apktools这个项目是入口，也是主要功能项目类，Baksmali和Smali、SmaliUtil是操作smali的工具类，BrutCommon和BrutDir、BrutUtil是一些辅助的工具类，代码简单，不做太多的解释。除了Apktools之外，其他项目都是功能库，它们之间的引用关系如下：

·Baksmali依赖于SmaliUtil。

·BrutDir依赖于BrutCommon、BrutUtil。

·BrutUtil依赖于BrutCommon。

·Smali依赖于SmaliUtil。

·Apktools依赖于Baksmali、BrutCommon、BrutDir、BrutUtil、Smali。

直接来看看主要功能Apktools项目。首先Java项目的入口方法肯定是main方法，搜一下找到这个Main类：

在方法中得到参数，然后进行参数的分析和组装。继续往下看执行代码：

这里看到了经常用的一些命令参数，它们的含义都了解了，ApkDecoder类是反编译的核心类：

最终也是调用它的decode方法，如下所示：

可以看到使用了Androlib这个核心类来做了一些操作，首先判断是否需要解析arsc格式的资源文件，下面仔细解析resource.arsc和AndroidManifest.xml这两个文件：

这里解析了dex文件，得到smali源码，而且区分了多个dex的情况。可以发现，apktool在整个反编译的过程中有三个核心点：解析resource.arsc文件、AndroidManifest.xml文件、dex文件。

关于这三个文件，在本书的前几章已经讲解过具体格式和解析方法，所以这里就不详细介绍了。

继续分析Androidlib这个核心解析类，就几个方法，下面来一一讲解。

1.解析原生文件

这个方法主要解析原生的文件，就是Android在编译apk的过程中不参与编译的文件目录，一般是assets和libs：

2.解析配置文件

这个方法主要是解析AndroidManifest.xml文件格式的：

Android在安装一个apk的时候，肯定也需要解析AndroidManifest.xml文件，而且Android中解析XML文件采用的是Pull解析法，所以这里直接把Android中的一些方法拷贝过来了，如图16-2所示。

然后再找一个xmlPull的解析jar包即可，如图16-3所示。

图16-2　apktool解析AndroidManifest功能类

图16-3　apktool项目引入的jar包

3.解析资源文件

这个方法是用来解析resource.arsc文件的，这个文件主要包含了所有资源文件的一种格式。Android中资源文件都有相应的类型，以及唯一的一个整型id值，那么这个文件就包含这些内容，如下所示：

这个方法用到的解析类和AndroidManifest.xml的解析类是一样的，因为它们都属于arsc格式，而且资源文件也是XML格式。这里值得注意的是，会产生一个反编译中最关键的文件public.xml，这个文件位于反编译之后的res\values\public.xml目录下：

可以看到，每个id字段都有对应的类型、名称和id值，而这里的id值是一个整型值，8个字节，由PackageId+TypeId+EntryId：三部分组成，如下所示：

·PackageId：包的id值，Android中如果是第三方应用的话，这个值默认是0x7F，系统应用的话是0x01，具体可以看后面的aapt源码得知，占用两个字节。

·TypeId：资源的类型id值，一般Android中有以下几个类型：attr、drawable、layout、dimen、string、style等，这些类型的值是从1开始逐渐递增的，顺序不能改变，attr=0x01，drawable=0x02……占用两个字节。

·EntryId：具体的类型下资源实体的id值，从0开始，依次递增，占用四个字节。

4.解析dex文件

这个方法主要是将dex文件解析成smali源码：

这里使用了SmaliDecoder的decode方法，如下所示：

这里需要借助一个工具包dexlib，它用来处理dex文件，处理完dex文件之后，再交给baksmali工具类生成smali文件即可。

源码分析完了，下面开始测试一下。这里为了运行简单，在入口的main方法中手动构造一个参数：

16.4　解决常见问题

反编译也会出现错误，本节分析并解决这类问题。

1.第一类反编译错误

错误信息如图16-4所示。

图16-4　反编译错误（一）

报错信息和开始使用apktool工具的时候是一样的，看看崩溃代码：

可以发现，这里使用一个Map结构存放ResResSpec格式数据，而且key是spec的name值，那么知道资源id的值是唯一的，不可能出现相同的name值，是做了混淆机制，这种混淆机制只对apktool工具有效，对Android系统解析apk运行是不影响的。知道了崩溃的原因，修复也就简单了，直接加一个判断，判断这个key是否在map中，存在的话就直接返回，如图16-5所示。

图16-5　修复问题

再次运行，如图16-6所示。

过滤了重复的资源值，反编译成功了。这里因为反编译过程中会用到系统的资源id，需要系统资源包framework.apk参与解析工作，因为程序包比较大，反编译时间会长点。可以看看反编译之后的目录，如图16-7所示。

图16-6　再次运行

图16-7　回编译文件apktool.xml

它的AndroidManifest.xml内容如下：

解析成功，可以正常查看了。这里就解决了反编译的错误。

2.第二类反编译错误

错误信息如图16-8所示。

这个错误和开始看到的错误是一样的，下面看看崩溃的地方是什么原因导致的：

图16-8　反编译错误（二）

这里是读取一个字符串常量池Chunk头部信息报错的，StringChunk的头部信息包括如下内容：

·header：标准的Chunk头部信息结构。

·stringCount：字符串的个数。

·styleCount：字符串样式的个数。

·flags：字符串的属性，取值包括0x000（UTF-16）、0x001（字符串经过排序）、0x100（UTF-8）及其组合值。

·stringStart：字符串内容块相对于其头部的距离。

·stylesStart：字符串样式块相对于其头部的距离。

其中header是一个标准的Chunk头部信息：

·type：当前Chunk的类型（两个字节）。

·headerSize：当前Chunk的头部大小（两个字节）。

·size：当前Chunk的大小（四个字节）。

共八个字节。继续分析错误代码：

这里会检查已给Chunk结构的完整性，输入的StringPool值是：

下面是字符串常量池Chunk的头部信息，而且值是固定的：0x001C0001，再进入看看代码：

如果发现格式不正确就抛出一个异常，即格式不是0x001C0001的话就不正确。那么问题差不多清楚了，崩溃的原因很可能是应用的resource.arsc的StringPool这个Chunk的头部信息被混淆了。通过上面的分析知道，StringPool这个Chunk的头部标准格式是0x001C0001，我们来看看应用的resource.arsc文件的二进制数据，如图16-9所示。

图16-9　分析resource.arsc头部信息（一）

可以发现有这个值，那么为何还报错呢？到这里或许不知道该怎么办了。再去看一个能够反编译的apk的resource.arsc文件，如图16-10所示。

图16-10　分析resource.arsc头部信息（二）

发现果然不一样，头部信息多了8个字节0x000001000，那么再看上面检查Chunk头部类型数据的代码：

可以看到，apktool其实已经做了头部信息的检查，但是这里只是检查0x001C0001这个正确信息之前的值只有四个字节，而且是0的情况，读取int整型值，四个字节，发现值等于传递进来的possible的话，就继续执行这个方法，但是这里的possible值是-1了，也就是这里只会检查四个字节，但是分析了应用的resource.arsc文件，发现它是八个字节，而且还不全是0，前四个字节是0，后四个字节是1：

所以这里检测也是失败的，抛出异常了。

分析完资源文件混淆的机制，下面修改就简单了，首先把上面的8个字节全部改成0：

然后替换之前的resource.arsc文件，直接用压缩软件替换即可，然后再修改上面的检测代码，如下所示：

修改代码，一直做检测，直到遇到正确的值为止，修复完成之后，运行：

不报错了，看看反编译之后的目录：

反编译也成功！

通过上面的分析可见，使用apktool反编译的问题主要有两个：

·Exception in thread“ main” brut.androlib.AndrolibException：Multiple res specs：attr/name

异常原因：通过分析源码知道，这个错误主要是因为apk做了混淆操作，导致在反编译的过程中存入了重复的id值。

错误代码：ResTypeSpec.java的addResSpec方法78行。

修复：在这个方法存入map数据之前做一个判断操作即可。

·Exception in thread“ main” brut.androlib.AndrolibException：Could not decode arsc file

异常原因：通过分析源码知道，这个错误主要是因为apk做了resource.arsc头部信息的修改，导致在分析头部数据结构的时候出错。

错误代码：ExtDataInput.java的skipCheckChunkTypeInt方法73行。

修复：修复resource.arsc头部数据，修改skipCheckChunkTypeInt检测方法逻辑。

16.5　apktool的回编译源码分析

前面分析了apktool的反编译功能源码，解决了两个应用反编译的失败问题，下面继续分析一下apktool的回编译功能。关于回编译功能，首先要了解aapt命令功能，如图16-11所示。

图16-11　apk编译流程图

使用aapt命令编译资源文件：

命令参数有点多，只说明如下几个：

·-J后面跟着的是gen目录，也就是编译之后产生的R类，存放资源id。

·-S后面跟着的是res目录，也就是需要编译的资源目录。

·-I后面跟着的是系统的库，因为我们在项目资源中会用到系统的一些资源文件，所以这里需要链接一下。

·-M后面跟着的是项目的清单文件，需要从这个文件中得到应用的包名，然后产生对应的R文件和包名。

这个命令不仅可以进行编译，也可以反编译，如上面提到的解析AndroidManifest.xml和resource.arsc的时候，使用它可以反编译，解析dex文件可以使用dumpdex命令。这些命令都在androidsdk的build-tools目录下。

知道了编译过程，回编译就是按照这个步骤来的，而这里重要的就是使用aapt命令：

把命令放到了项目的framework目录下，如图16-12所示。

图16-12　framework目录

然后开始构造命令参数，主要需要用到系统的jar包android.jar：

修改一下入口代码，添加回编译运行参数：

运行程序如下：

回编译成功，得到apk文件，如图16-13所示。

图16-13　回编译之后的apk文件

分析完了apktool工具所有的源码，总结一下其功能，如下所示：

·反编译过程中主要是解析AndroidManifest.xml、resource.arsc、dex文件。

·回编译的时候借助aapt命令完成编译操作。

16.6　Jadx源码分析

下面介绍反编译的另外一个神器Jadx，这个工具也是开源的，可以直接去GitHub上搜索：https://github.com/skylot/jadx。这个工具其实和apktool反编译的功能差不多，但是有一个特色，就是可视化功能，能够高效地分析apk的结构。下面来看一个例子，如图16-14所示。

图16-14　Jadx工具效果图

可以看到结构很清晰，分析起来会比较方便，感觉它是集成了apktool+jd-gui的功能，但是和apktool相比的话，它还是有点缺陷的。首先反编译会比较耗时，其次它不能修改代码，进行回编译，这里为什么分析它呢？因为它是开源的，又借助了asm工具来生成class文件，实现Java代码的可视化。下面就来说说asm这个工具类的用途。这里写了一个demo来看看效果：

运行结果如图16-15所示。

图16-15　asm案例运行结果

这里没有打印“Hello world！的代码”，但是结果却打印了，这就是asm功能，即能够手动构造一个class文件，如图16-16所示。

图16-16　asm运行产生的class文件

大家是否联想到了动态代理模式，会产生一个动态代理类，而且还会生成一个Proxy.class文件？JavaWeb的Spring框架中Cglib也是采用这个功能来实现AOP编程的，它可以通过输入一个字符串来定义类，给这个类添加方法、字段等信息，然后生成类的字节码数组。可以保存成class文件，同时也可以使用ClassLoader来加载字节码数据，然后再反射调用指定的方法。Jadx的可视化功能就是借助这个功能，同时著名的dex2jar工具也是这样的，可以去dex2jar工具的lib目录看看，如图16-17所示。

图16-17　asm工具包

Jadx的反编译步骤是这样的：解析dex文件→smali源码→解析smali指令→借助asm生成class文件→解析class文件得到Java源码。

提示：Apktool+Jadx源码下载地址为http://pan.baidu.com/s/1cHU30M。

16.7　本章小结

本章主要介绍了反编译神器apktool和Jada工具，其中apktoot是逆向工作中的金钥匙，所以本章着重分析了这个工具的实现原理，以便读者在反编译应用遇到问题的时候能够快速定位到问题，让反编译更加顺利。使用apktool是逆向反编译的第一步也是最重要的一步，所以了解它的工作原理是至关重要的。

第17章　Hook神器Xposed

相信大家对Xposed框架应该不陌生，它是Android中Hook技术的一个著名框架，还有一个框架是CydiaSubstrate，但是这个框架是收费的，而Xposed框架是免费的且是开源的，网上已有很多文章介绍Xposed框架的原理实现，本章主要介绍如何使用这个框架。

17.1　安装教程

在安装这个工具之前，要先解决如下几个问题：

·首先知道这个框架的核心点是系统进程注入技术，那么如果要注入系统进程，就必须要root权限，所以如果想用这个框架的话就必须得有一个root的设备。

·然后就是这个框架的适配问题，不是所有的设备、所有的系统都支持这个框架的使用的，本人在实验的过程中就遇到了小米3+MIUI7操作失败了，结果重新刷了一个原生的Android 4.4系统才成功的。

·最后一个问题就是Xposed框架本身的版本问题，它针对不同系统发布了多个版本，所以得针对于自己的设备系统安装正确的Xposed版本。

解决了这三个问题才能成功安装Xposed框架。而在这个过程中还会发现遇到两个问题。

第一个问题是不兼容问题，如图17-1所示。

图17-1　不兼容问题

第二个问题是提示安装框架问题，如图17-2所示。

图17-2　安装框架问题

本人在操作也是遇到了这两个问题，最后也是没有找到合适的答案，所以就刷了一个原生的Android 4.4系统。

17.2　环境搭建

如果上面的问题都解决了，再打开应用，点击安装框架，如图17-3所示。

这里还是提示未激活，点击进入，如图17-4所示。

图17-3　安装框架界面

图17-4　框架激活状态图

这时候看到了正常了，可以点击安装了，直接点击安装即可，如图17-5所示。

图17-5　安装界面

这里是需要root授权的，点击允许，安装成功之后也会提示重启生效的，因为要注入系统进程，必须重启才有效果。

到这里就成功安装了Xposed框架了，在这个过程中肯定有人会遇到问题，个人觉得解决问题的最根本办法就是刷机了，本章操作的环境是：小米3移动版+Android原生4.4系统+Xposed_v33版本。

17.3　编写模块功能

环境搭建好了，下面就开始操作了。上面安装的那个工具其实是一个模块管理器，如果想做一些hook操作还得自己编写模块，然后把这个模块安装到设备中，这个工具就可以检测出来了，会提示加载这模块然后重启设备，模块功能就有效果了。那么下面来看一下如何编写一个Xposed模块。

第一步：新建模块项目

导入Xposed工具包，如图17-6所示。

图17-6　模块工程

这里一定要注意：不能使用libs文件夹而要使用lib文件夹，如果这里使用了libs文件夹，在安装模块成功之后重启，会发现Hook是失败的，通过打印tag为xposed的日志信息会发现这样的错误：

这个错误主要是因为把接口包含到了插件项目中了，那么可以猜想错误问题也是这个Xposed工具导致的。那么只需要把libs文件夹改成lib文件夹，然后用add buildpath命令即可。

注意：在Eclipse中，如果把工具包放到libs文件中，默认是加入到编译路径中的，同时在编译出来的程序中也是包含了这个工具包中的所有类，而对于其他非libs文件夹，添加工具包之后执行add buildpath只是做到了项目引用工具包的功能，而最终并不会把这个工具包包含到程序中。

第二步：编写模块代码

编写模块代码只要新建一个实现IXposedHookLoadPackage接口类，然后在handle-LoadPackage回调方法中进行拦截操作即可，而具体的拦截操作是借助XposedHelpers.findAndHookMethod方法和XposedBridge.hookMethod方法实现的，这两个方法从参数含义可以看到，主要是需要Hook的类名和方法名，还有一个就是拦截的回调方法，一般是拦截之前做什么的beforeHookedMethod方法，以及拦截之后做什么的afterHookedMethod方法，如图17-7所示。

图17-7　模块代码

对于IXposedHookLoadPackage这个接口和回调方法，可以知道，应该是拦截系统中所有应用的运行信息，传递回来的一个LoadPackageParam参数类型就是包括了Hook应用的具体信息，打印应用的包名就可以看到效果了。

注意：如果想Hook一个类的具体方法，那么就必须要清楚地了解到这个方法的详细信息，比如参数类型和个数，返回类型等。因为在拦截的过程中必须要对这个方法进行分析，比如得到方法参数来进行具体参数修改，返回值信息来进行返回值修改。

看到了获取imei值的方法是一个无参数的返回字符串类型的方法，那么如果要拦截它的返回值，就需要修改它的返回值，使用setResult方法即可。所以从这里可以看到不管是Hook系统的方法，还是以后去Hook第三方应用的具体类方法，第一步都得了解到Hook对象的具体信息。关于系统方法可以通过查看源码来得到信息，而对于第三方应用的话只能借助反编译技术了，比如修改游戏金币功能，必须先反编译游戏知道修改金币的类和具体方法才可行。

这里不仅Hook了系统的imei信息，也简单Hook了系统的地理位置信息，在Android中获取经纬度信息有三种方式，这里为了演示简单，用了GPS定位功能，一般获取经纬度信息的代码主要是两处。

第一处是初始化的时候调用getLastKnowLocation方法获取最后一次系统中的地理位置信息，如下代码：

第二处就是监听地理位置变化的回调接口中的onLocationChanged回调方法，如下代码：

如果想Hook系统的地理位置信息进行拦截，就需要操作这两处代码。而它们有一个区别，第一处是通过返回值得到的，第二处是通过回调方法中的参数得到的。下面来看一下具体的Hook代码。

Hook第一处代码比较简单，直接构造一个假的Location对象，然后设置返回值即可，如下代码所示：

Hook第二处代码有点复杂，需要先找到添加位置监听的方法requestLocationUpdates，然后通过反射得到这个回调对象，找到具体的回调方法，再进行操作，因为回调方法是通过参数把Location对象传递回来的，所以这里需要修改参数值。如下代码所示：

到这里就编写好了Hook系统的imei值和地理位置信息的模块了。

第三步：添加模块入口

这一步是非常重要的，也是最容易忘记的，就是要告诉Xposed框架一个模块中Hook的入口，可以看到模块的入口是Main类，所以需要在模块的assets中添加一个xposed_init文件，如图17-8所示。

图17-8　入口文件xposed_init

这里的内容就是模块入口类的全称名称即可，如图17-9所示。

图17-9　xposed_init文件内容

第四步：添加模块的额外信息

最后一步需要在模块的AndroidManifest.xml文件添加额外信息，具体包括模块的描述信息、版本号等，如下所示：

其中：

·xposedmodule：是Android程序作为Xposed中的一个模块，所以值为true。

·xposeddescription：是对本模块的功能的描述，可以自己简单叙述一下就可以了。

·xposedminversion：是本模块开发时用到的Xposed的jar包最低版本号，这里是30，这里所用的Xposed的jar包版本是54。

经过上面4步之后就完成了模块的定义，最后为了验证Hook的结果，再新建一个Activity类，在内部调用一下系统的获取imei方法以及位置信息方法，并且显示在屏幕中，如下代码所示：

17.4　运行模块

下面就来运行一下模块程序，安装到设备之后，Xposed会提示模块未激活，如图17-10所示。

这个XposedInstaller程序工具应该是通过接收安装广播，然后得到这个应用信息分析它是否包含了Xposed模块的特殊属性来判断的。点击进行激活，如图17-11所示。

图17-10　提示未激活信息

图17-11　激活模块

这时候看到激活成功之后，会提示再次重启设备才能生效。每当有新的模块或者模块代码有更新都需要重启设备模块才生效，如图17-12所示。

这一点还是麻烦的。重启设备之后，再运行模块代码看看效果，如图17-13所示。

从这显示结果看到了，Hook成功了，在没有Hook之前的效果是，如图17-14所示。

图17-12　提示模块更新

图17-13　运行结果

图17-14　没有拦截之前的运行效果

这时候来看一下打印的日志信息，如下所示：

百度地图在获取设备的imei和位置信息，当然这是符合正常情况的，从这里可以看到，还可以利用这个技术来观察设备中有哪些应用在获取设备的一些隐私数据。

提示：项目下载地址为https://github.com/fourbrother/xposedhookdemo

17.5　本章小结

本章主要介绍Xposed的基本使用规则和方法，XposedInstaller.apk其实是一个模块载体和管理器，如果想实现具体的Hook操作，就必须自己编写模块程序，然后激活加载方可生效。在实际过程中，这个框架是非常有用的，可以通过修改系统的一些信息来帮助测试模拟复杂的测试环境，但是这个框架现在用得最广泛的当属破解操作，比如用这个框架可以进行应用的脱壳，编写游戏的外挂等。

第18章　脱壳神器ZjDroid

前面一章介绍了Xposed框架工具的基本使用，本章将继续介绍Xposed框架的另外一个功能即脱壳，主要是利用Xposed的脱壳模块工具ZjDroid实现的，因为它是开源的，所以直接分析源码即可，源码的下载地址：https://github.com/halfkiss/ZjDroid。不过可惜的是现在只公开了Java层的代码，而native层的代码并没有公开，分析源码之后会发现最重要的功能就在native层。

18.1　ZjDroid原理分析

下面就来详细分析一下ZjDroid工具的源码，由Eclipse工程导入，基于之前的Xposed模块编写的经验，找到入口代码，在assets目录下有一个xposed_init文件中就记录了模块的入口类，如图18-1、图18-2所示。

图18-1　xposed_init文件

图18-2　xposed_init入口类

直接进入到这个类即可，代码如下：

遵循统一规则，实现了IXposedHookLoadPackage接口，再实现handleLoadPackage回调方法即可，下面继续分析入口方法ModuleContext，代码如下：

这里开始拦截Application的onCreate方法，而这个方法一般是每个应用程序的启动方法，在这里做拦截操作也是合情合理的，再看拦截之后做了什么，也就是ApplicationOnCreateHook类的实现，代码如下：

在这里开始真正的拦截操作，主要是添加了一个广播，每个应用在启动的时候都会去注册这个广播，而如果后续使用该工具发送这样对应Action广播的话，每个应用程序都会收到。所以这里可以看到，核心工作就在这个广播的接收之后做了，接下来继续去看这个广播的定义，代码如下：

果然在这里，可以看到首先会通过发送广播的intent中携带一些数据过来，主要是两个数据：

·进程id：作用主要是为了过滤其他应用，只处理本应用的逻辑，因为这个广播发送之后所有的应用都能接收到，但是脱壳时肯定只是针对于某一个应用，那么只需要在这个应用的广播接收中做处理即可。

·命令字符串：是为了发送广播可以支持多种功能，后面分析也可以看到的确有很多功能。

得到命令之后就开始构造一个执行器类，这里用到了设计模式中的命令模式。下面继续看看有哪几种命令执行器类，代码如下：

18.2　工具命令分析

下面就来逐一分析这个工具支持哪几种命令。

1.dump_dexinfo

获取应用运行时内存中dex的信息DumpDexInfoCommandHandler，代码如下：

进入方法详细查看一下：

可以看到，这里的实现逻辑是，全部通过反射机制获取每个应用的dex文件对应的DexFile类型对象，这里的工作和Android中插件化开发逻辑有点类似，通过应用的默认类加载PathClassLoader类得到DexPathList类，然后再得到具体的DexFile对象即可。

这里要说的就是这个dex文件对应的cookie值，这个值非常重要，是后续命令操作的基本信息，它代表的含义就是底层中每个应用的dex文件对应的唯一id值，系统会维护一个map结构来保存这些数据，然后通过这个cookie值找到对应的dex文件信息。

命令用法：

这里使用的是命令方式发送一个广播，通过--ei携带目标进程id是一个int类型，通过--es携带命令字符串。

2.dump_dexfile

这个命令也是后续脱壳的重要命令，就是dump出应用内存中的dex文件DumpDex-FileCommandHandler，代码如下：

这里可以看到dump出应用的内存数据，首先需要传入源应用的dex数据，也就是apk文件，这一般都是存放在/data/app/xxx.apk目录下的，然后构建了一个dump之后的dex文件路径，通过源码查看是在/data/data/xxx/files/dexdump.odex中。接下继续查看dump的核心代码：

看到这里有一个核心的方法，但是可惜的是这个方法是native的，而这个工具并没有把native层的代码公开，但是通过这里传递的参数可以了解到，底层应该是采用了libdvm.so或者是libart.so库得到具体的函数，然后通过dex对应的cookie值获取信息。

命令用法：

注意这里的dexpath参数代表需要脱壳的dex文件，也就是应用程序文件。

3.backsmali

这个命令与上面的命令功能差不多，只是这个命令多了一层操作，就是把dex文件转化成smali文件，所以这里不再详细说明了，可以先得到dex文件，然后通过工具得到smali文件。

命令用法：

注意这里的dexpath参数代表需要脱壳的dex文件，也就是应用程序文件。而最终生成的smali文件是放在/data/data/xxx/smali下面的。

4.dump_mem

这个命令是用来dump出应用程序运行时内存中指定开始位置和长度的内存块数据的DumpMemCommandHandler，代码如下所示：

可惜这个方法也是native层的，但是可以知道每个应用运行时的内存地址都在/proc/[pid]/maps文件中，如下所示：

那么查找内存地址，然后使用memcpy进行内存数据拷贝也是非常简单的。

命令用法：

注意这里的start和length都是十进制的，而不是十六进制的数据格式。

5.dump_heap

这个命令可以dump出虚拟机的堆内存信息，文件可以使用Java heap工具进行分析，而对于这个命令想一下应该也知道实现逻辑，也是在native层的，而且这个代码逻辑应该和上面的那个命令差不多。

命令用法：

6.dump_class

这个命令主要用于dump出dex文件中的类信息，因为在DexFile对象中有一个隐藏的方法可以把dex文件中的所有类名获取到getClassNameList，如下代码：

可以看到这个方法的传入参数为一个dex文件对应的cookie值。

命令用法：

这里的dexpath是需要得到所有类信息的dex文件路径，也就是应用的apk文件路径。

7.invoke

这个命令用于运行时动态调用Lua脚本，该功能可以通过Lua脚本动态调用Java代码。使用场景：可以动态调用解密函数，完成解密；可以动态触发特定逻辑。代码就不进行分析了，因为觉得这个命令很少使用。

命令用法：

这里的filepath是Lua脚本文件的存放路径。

18.3　工具日志信息

ZjDroid工具除了操作命令，还有两个非常重要的打印日志的tag：

·adb logcat-s zjdroid-shell-{package name}。这个tag可以查看上面每个命令执行的结果，便于查看命令执行的状态。

·adb logcat-s zjdroid-apimonitor-{package name}。这个tag可以监听对应包名应用调用的API信息，这个作用有点类似于运行时权限请求。可以直接通过Xposed提供的方法对一些敏感API进行拦截，然后添加监控代码即可。

18.4　工具用法总结

完全分析完ZjDroid工具的功能，下面就来总结一下：

1）获取apk当前加载dex文件信息：

2）获取指定dex文件包含可加载类名：

3）根据Dalvik相关内存指针动态反编译指定dex，并以文件形式保存：

4）dump指定dex内存中的数据并保存到文件（数据为odex格式，可在PC上反编译）：

5）dump指定内存空间区域数据到文件：

6）dump Dalvik堆栈信息到文件，文件可以通过Java heap分析工具分析处理：

7）运行时动态调用Lua脚本。该功能可以通过Lua脚本动态调用Java代码。使用场景：可以动态调用解密函数，完成解密。可以动态触发特定逻辑：

8）相关命令执行结果查看。命令执行结果：

敏感API调用监控输出结果：

18.5　工具使用案例

下面就要用一个案例来看看这个工具到底如何使用，有哪些功效。案例是捕鱼达人v1.0.1版本，具体的apk文件可以自行去网上搜索。安装游戏之后，然后顺便把上面的ZjDroid模块工具也安装上，之后进行重启生效。

最好是单独开一个CMD窗口用来查看打印结果，但是从上面的命令可以看到，应该需要这游戏的包名和进程id，那么这两个数据怎么获取呢？其实在前几章已经介绍很多次，用一个命令即可：adb shell dumpsys activity top。如下所示，但是这时候需要运行起来捕鱼达人游戏：

使用命令就可以获取到游戏的包名org.cocos2d.fishingjoy3和进程id=25304，这两个数据非常重要，这里可以先进行保管，进而后面使用。

下面首先来看一下这个应用用了哪些敏感的API数据，使用上面查看日志的命令即可：

运行结果如下所示：

这里有网络请求信息、网络切换的广播等数据，感觉这个工具的功能还是蛮多的。

下面再来看一下这个应用的dex文件信息，可以使用下面命令即可：

运行之后的结果如下所示：

这时候会发现，运行没有看到实际效果，原因是需要通过日志才能看到数据，因为上面命令运行的结果都是需要通过这个日志才可以看到的：

查看日志信息，结果如下所示：

这里可以看到具体的信息，看到有一个filepath，这就是后续有些命令需要用到的dex路径，所以一定要记下来。

接下来看一个关于dump出游戏中所有的类名的命令：

这里的路径就是上面获取到的apk路径，结果还需要通过上面的日志命令才可以看到，如下所示：

这就导出了游戏包含的所有类名了。

最后来看如何脱壳，这是最关键的，也是本章的重点，当然也是这个工具最实用的一个功能。而本章用的游戏也是经过加固处理的，可以反编译看看这个游戏，如图18-3所示。

图18-3　游戏加固图

会发现没几个类，而且有一个Application类，那么可以认定这个游戏被加固了，这里不介绍是使用哪家的加固平台操作了，也不再使用IDA等工具去动态调试脱壳了，这里直接使用这个工具进行操作即可。为了后续代码阅读方便，可以直接获取它的smali代码，使用这个命令：

这个命令的运行结果通过日志查看，如下所示：

而这个smali文件夹是存放在应用的/data/app/org.cocos2d.fishingjoy3/smali中，可以把它拷贝出来即可，如图18-4所示。

图18-4　dump出游戏之后的smali代码

可以看到成功脱壳了，生成了游戏的所有smali文件代码。这个脱壳操作和后面章节介绍使用IDA工具进行脱壳的原理都差不多，因为应用程序不管怎么加固，最终都会使用一个系统函数将dex文件加载到内存中，而加载之前肯定要进行解密操作，只要在加载之前解密之后进行拦截即可。

18.6　本章小结

本章介绍了基于Xposed框架的脱壳神器ZjDroid的实现原理以及具体用法，从而使读者感受到Xposed框架的强大之处。当然本章只讲了一部分功能，还可以利用这个框架编写游戏外挂等操作。可以慢慢地解读这个框架的源码，能够学习到很多知识点。

第19章　Native层Hook神器Cydia Substrate

前面介绍了Android中一款hook神器Xposed，本章介绍另外一个hook神器Cydia Substrate，当逆向游戏和应用的时候遇到so文件中的一个重要方法，这时候就想进行hook操作，但是Xposed几乎没法用，就需要使用这个框架了。这个框架的优点在于Hook底层函数非常方便，对so中的函数进行hook操作非常便捷。

19.1　环境搭建

先介绍这个框架如何安装使用，操作的环境如下：

·设备系统：小米三+原生CM 4.4系统

·框架版本：0.9.4010

·是否root：必须root

关于这个环境，可能有的同学操作最大的问题就在于设备和系统，用不同设备、不同系统，可能安装会失败，具体问题可能需要自己去解决了。关于框架apk和功能jar包下载地址可以去官网：http://www.cydiasubstrate.com。

19.2　Hook Java层功能

搭建好了环境，下面就直接操作了。首先来看看如何Hook Java层功能。

第一步：导入jar包

在Eclipse中新建一个Android工程，将下载好的框架的jar包拷贝到工程的libs目录下即可，如图19-1所示。

图19-1　导入jar包

第二步：编写Hook入口类

具体API不多介绍了，就那么几个，没必要详细讲。下面代码对系统的imei进行Hook操作了：

下面代码是对系统的颜色值进行了Hook：

第三步：配置XML信息

在AndroidManifest.xml中需要配置两个地方：一个是使用权限，一个是声明hook的入口类。

第四步：安装运行

代码编写完成之后，直接运行安装即可，前提是需要正确安装Cydia Substrate框架程序，安装成功界面如图19-2所示。

安装Hook项目时会出现提示，如图19-3所示。

点击按钮，进入框架界面，点击重启即可。然后查看系统界面颜色以及返回的imei值，如图19-4所示。

图19-2　安装成功

图19-3　提示安装Hook项目

图19-4　框架界面

查看颜色的确变成粉色了，再看看imei值的修改：

imei值也Hook成功了。到这里就用Cydia Substrate框架Hook了Java层功能。之前提到的Xposed框架也是可以做到这些功能的。

19.3　Hook Native层功能

下面继续来看如何Hook Native层的功能，也是本章的重点。

第一步：创建一个Native项目

这里用Eclipse操作，简单便捷，有很多人问为什么不用AS，这要看每个人的使用习惯。虽然AS工具是主流，不过本人还是习惯用Eclipse，如图19-5所示。

第二步：导入Substrate的Native功能包

需要导入一个substrate.h头文件，和两个so功能包。Native层应用都是这么操作的，提供一个头文件告诉你API，具体实现在so包中。

图19-5　创建Native项目

第三步：寻找Hook的函数名

这里网上没有好的hook代码，为了更好的了解这个工具的厉害之处，弄一个比较实际的案例就是hook系统加载dex的函数，这样就可以获取到每个应用的dex文件了，这种方式对于早期加固是一个比较好的脱壳方案。在之前介绍脱壳会使用IDA在指定函数处下个断点，这里如果要hook的话，就需要找到这个加载dex的函数名称，一定要记的是导出的函数名。

下面就用IDA来查找需要Hook的函数名，首先导出设备的libdvm.so文件：system/lib/libdvm.so

然后使用IDA打开，寻找加载dex函数，如图19-6所示。

图19-6　寻找加载dex函数

切换到Exports视图页面，然后搜索dexFileParse函数，点进去：

需要得到的是EXPORT函数，需要Hook的是它，一定要注意函数，不然Hook没效果的。找到函数之后还得获取函数的参数类型和返回类型，这个也好办，因为有Android源码，所以直接在源码中找这个函数参数说明、已经返回值说明即可。因为Native层Hook的其实是函数指针的替换，所以如果想Hook原来的函数，必须新建一个和原来一样的函数功能，然后传递函数指针即可。这个函数的参数和返回值定义如下：

参数含义非常简单，第一个参数表示dex文件的起始地址，第二个参数是dex文件的长度，有这两个参数就可以写入文件了。这里需要获取DexFile类型，这个直接在Android源码目录下找到这个头文件DexFile.h即可。然后导入工程中。这样就找到了需要hook的函数，下面就开始编写hook代码了。

第四步：编写hook代码

在编写hook代码之前，需要考虑这几件事：

·hook之后的dex存在哪？怎么存？这里直接通过当前的pid值获取进程名，然后将其作为dex的文件名，这样每个进程的dex文件名就不会冲突了。这里要理解一点：一个进程对应一个DVM，加载一个dex文件。所以这里hook其实就是注入每个进程，在每个进程中hook每个函数功能。

·需要过滤系统进程，并不是所有的进程都是想要hook的，而且这些进程未必有dex文件，比如鼻祖进程zygote，而这些进程过滤规则，需要自己打印看结果，然后构造。

下面开始写代码了，首先定义想要hook的so文件：

主要是第二个参数，是需要hook的so路径。然后在入口处开始hook代码：

这里找到so中需要hook函数符号，然后直接调用MSHookFunction传入符号，新函数地址，旧函数地址。可以看到在C中指针是多么强大，实现了函数的回调机制，而且非常方便。然后继续来看新定义的hook函数功能：

这里先获取当前进程名称，然后构造dex文件名，保存dex文件，最后一定要记得返回原始的函数功能，不能影响正常的流程。还要记得过滤规则，不要对每个进程都进行操作，并不是每个进程都是有效的。而这些过滤规则是根据自己打印进程名来自行添加即可。

第五步：编写MK文件

上面代码已经编写完成了，下面来编写编译脚本吧。注意编译之后的文件名一定要以cy结尾，不然是hook是失败的，然后就是需要导入substrate的so库文件：

第六步：安装并运行

和之前一样，运行之后，需要重启设备，然后先看看native层的log信息：

然后再去目录中查看保存的dex文件信息，如图19-7所示。

图19-7　dex文件

dex文件都保存成功了，这样会发现如果对于早期的加壳应用，可以采用这种方式进行脱壳操作。也不需要用IDA进行调试dump出dex文件了。

19.4　框架使用事项说明

关于Native层Hook就介绍完了，这里还是需要说明几点：

·Hook之前需要分析so文件获取需要Hook的函数名称，参数返回值定义，这个和Hook Java层一样，必须先找到突破点才能进行下一步。

·Hook可能会有一些错误，因为native层比java层错误信息难发现，所以最好是在某些地方加一些日志观察结果。

如果在使用过程中发现Hook失败，注意检查如下个条件：

·XML中是否配置了权限和入口。

·编译脚本MK中的后缀名是否为cy。

项目下载地址：https://github.com/fourbrother/CydiaSubstrateHook

19.5　本章小结

本章主要从Hook Java层和Native层两个方面介绍了Cydia Substrate框架，关于Hook Java层功能Xposed框架也可以做到，但是对于Native层功能的Hook操作，还是Cydia Substrate框架比较好用，有了这个框架再也不畏惧Hook了，Native层代码也可以一览无余。

操作篇

第20章　静态方式逆向应用

第21章　动态调试smali源码

第22章　IDA工具调试so源码

第23章　逆向加固应用软件技术分析

第24章　逆向应用经典案例分析

第25章　Android中常见漏洞分析

第26章　文件加密病毒Wannacry样本分析

第20章　静态方式逆向应用

本章主要介绍Android中如何使用静态方式逆向apk，学习逆向前，需要了解smali语法和arm指令，本章先介绍这两个知识点，然后介绍如何使用IDA工具来分析so文和工具，最后会通过一个案例来分析静态方式逆向apk的流程和注意事项。

20.1　smali语法

smali、baksmali分别是指安卓系统里的Java虚拟机（Dalvik）所使用的一种，dex格式文件的汇编器、反汇编器，其语法是一种宽松式的Jasmin/dedexer语法，而且实现了.dex格式所有功能（注解、调试信息、线路信息等）。

当对apk文件进行反编译后，便会生成此类的文件，其中在Davlik字节码中，寄存器都是32位的，能够支持任何类型，64位类型（Long/Double）用2个寄存器表示；Dalvik字节码有两种类型：原始类型、引用类型（包括对象和数组）。

1.smali指令

部分smali指令如下：

.field private isFlag：z　定义变量

.method　方法

.parameter　方法参数

.prologue　方法开始

.line 12　此方法位于第12行

invoke-super　调用父函数

const/high16 v0，0x7fo3　把0x7fo3赋值给v0

invoke-direct　调用函数

return-void　函数返回void

.end method　函数结束

new-instance　创建实例

iput-object　对象赋值

iget-object　调用对象

invoke-static　调用静态函数

条件跳转分支指令如下：

"if-eq vA，vB，：cond_**"　如果vA等于vB则跳转到：cond_**

"if-ne vA，vB，：cond_**"　如果vA不等于vB则跳转到：cond_**

"if-lt vA，vB，：cond_**"　如果vA小于vB则跳转到：cond_**

"if-ge vA，vB，：cond_**"　如果vA大于等于vB则跳转到：cond_**

"if-gt vA，vB，：cond_**"　如果vA大于vB则跳转到：cond_**

"if-le vA，vB，：cond_**"　如果vA小于等于vB则跳转到：cond_**

"if-eqz vA，：cond_**"　如果vA等于0则跳转到：cond_**

"if-nez vA，：cond_**"　如果vA不等于0则跳转到：cond_**

"if-ltz vA，：cond_**"　如果vA小于0则跳转到：cond_**

"if-gez vA，：cond_**"　如果vA大于等于0则跳转到：cond_**

"if-gtz vA，：cond_**"　如果vA大于0则跳转到：cond_**

"if-lez vA，：cond_**"　如果vA小于等于0则跳转到：cond_**

2.smali语法案例分析

下面是一个简单方法：

上面代码反编译之后对应的smali语法如下：

如果符合if分支则程序往下走，最终return；而如果条件不符合则会走到：cond_0分支，最终执行goto：goto_0走回到：goto_0返回。

20.2　手动注入smali语句

有时候破解应用时，需要通过全局搜索一些关键的字符串来找突破点，但是这招有时候不好使，所以这时需要加一些代码来观察信息了，这里有一个通用的方法就是加入自己的log代码，来追踪代码的执行逻辑，因为这里讲的是静态分析技术，所以就用代码注入技术来跟踪执行逻辑，后面介绍了动态分析技术之后，那就简单了，可以随意打断点来进行调试。这里的添加代码，就是修改smali代码，添加日志信息即可，下面会用例子来进行讲解，这也是最常用的一种技术。

20.3　ARM指令

逆向者必须能看懂汇编代码，就类似于在调试Java层代码的时候一样，必须会smali语法。庆幸的是，这两种语法都不是很复杂，知道一些大体的语法和指令就可以了，下面来看看ARM指令中的寻址方式、寄存器、常用指令，了解这三个知识点，就会对ARM指令有一个大体的了解。

1.ARM指令中的寻址方式

·立即数寻址

立即数寻址也叫立即寻址，是一种特殊的寻址方式。操作数本身包含在指令中，只要取出指令也就取到了操作数。这个操作数叫做立即数，对应的寻址方式叫作立即寻址。例如：

·寄存器寻址

寄存器寻址是利用寄存器中的数值作为操作数，也称为寄存器直接寻址。例如：

·寄存器间接寻址

寄存器间接寻址就是把寄存器中的值作为地址，再通过这个地址去取得操作数，操作数本身存放在存储器中。例如：

·寄存器偏移寻址

这是ARM指令集特有的寻址方式，它是在寄存器寻址得到操作数后再进行移位操作，得到最终的操作数。例如：

·寄存器基址变址寻址

寄存器基址变址寻址又称为基址变址寻址，是在寄存器间接寻址的基础上扩展来的。它将寄存器（该寄存器一般称作基址寄存器）中的值与指令中给出的地址偏移量相加，从而得到一个地址，通过这个地址取得操作数。例如：

·多寄存器寻址

这种寻址方式可以一次完成多个寄存器值的传送。例如：

·堆栈寻址

堆栈是一种数据结构，按先进后出（First In Last Out，FILO）的方式工作，使用堆栈指针（Stack Pointer，SP）指示当前的操作位置，堆栈指针总是指向栈顶。例如：

2.ARM中的寄存器

R0-R3：用于函数参数及返回值的传递。

R4-R6，R8，R10-R11：没有特殊规定，就是普通的通用寄存器。

R7：栈帧指针（Frame Pointer），指向前一个保存的栈帧（stack frame）和链接寄存器（link register，lr）在栈上的地址。

R9：操作系统保留。

R12：又叫IP（intra-procedure scratch）。

R13：又叫SP（stack pointer），是栈顶指针。

R14：又叫LR（link register），存放函数的返回地址。

R15：又叫PC（program counter），指向当前指令地址。

3.ARM中的常用指令含义

ADD：加指令。

SUB：减指令。

STR：把寄存器内容存到栈上去。

LDR：把栈上内容载入一寄存器中。

.W：是一个可选的指令宽度说明符。它不会影响为此指令的行为，它只是确保生成32位指令。Infocenter.arm.com的详细信息。

BL：执行函数调用，并把使lr指向调用者（caller）的下一条指令，即函数的返回地址。

BLX：同上，但是在ARM和thumb指令集间切换。

CMP：指令进行比较两个操作数的大小。

4.ARM指令简单代码段分析

C代码：

对应的ARM指令：

20.4　用IDA静态分析so文件

IDA工具太强大了，它可以查看so中的代码逻辑。如图20-1所示。

图20-1　IDA打开so文件内容

可以看到左边栏中有so中的函数，找到指定函数的定义的地方进行查看即可。IDA也是可以直接查看apk文件的，如图20-2所示。

可以查看apk文件中的所有文件。可以选择classes.dex文件，如图20-3所示。

这里可能会遇到一个问题，就是如果应用程序太大，打开过程中会很慢，有可能IDA停止工作，所以要慢慢等，如图20-4所示。

图20-2　IDA查看apk文件

图20-3　查看classes.dex文件

图20-4　查看classes.dex文件

打开之后，可以看到类和方法名，这里还可以支持搜索类名和方法名Ctrl+F，也可以查看字符串内容（Shirt+F12），如图20-5所示。

图20-5　查看字符串内容

发现IDA也是一个分析Java代码的好手，所以说这个工具太强大了。

20.5　案例分析

本节通过一个例子来看看如何使用静态分析技术进行破解。

20.5.1　静态分析smali代码

首先拿到需要破解的apk，使用apktool.jar工具来反编译：

这个apk很容易就被反编译了，看来并没有进行任何的加固，那就好办了。这里改一下它的AndroidManifest.xml中的信息，改成可调式模式，这是后面进行动态调试的基础，当然也可以不用修改，在之前章节介绍了如何修改系统的调试总开关，如果把系统的调试总开关打开，这里就不用修改了。一个正式的apk在AndroidManifest.xml中这个值是false的。看看它的AndroidManifest.xml文件，如下所示：

把这个值改成true，再回编译，这时候就可以动态调试apk了。所以在这点上可以看到，静态分析是动态分析的前提，这个值不修改的话，是没有办法进行后续的动态调试的。修改成功之后，进行回编译：

这里是为了简化内容，写了一个批处理，首先进入到目录，然后使用命令进行回编译：

sq是之前反编译的目录，debug.apk是回编译之后的文件，这时，debug.apk是不能安装运行的，因为没有签名，Android中是不允许安装一个没有签名的apk。

下面还要继续签名，用系统自带的签名文件即可签名：

后面就直接安装这个apk，然后运行apk。这个过程中只需要知道应用的包名和入口Activity名称即可，这个信息在AndroidManifest.xml中也是可以获取到的，当然可以使用如下命令得到：

回编译之后，运行程序，发现有问题，即点击程序的icon没反应，运行不起来。查看log中的异常信息，发现也没有抛出任何异常，那么这时就可以判断，它内部肯定做了什么校验工作。校验方式一般有两种：

·对dex做校验，防止修改dex。

·对apk的签名做校验，防止重新打包。

这需要重新看看它的代码，看是否做了校验。在分析代码的时候，要先看看它有没有自己定义Application，如果有定义的话，就需要看它自己的Application类，这里看到它定了自己的Application：com.shuqi.application.ShuqiApplication。

解压apk，得到dex，然后用dex2jar进行转化，得到jar，再用jd-gui查看这个类，如图20-6所示。

图20-6　jd-gui查看源码

这里看到它的代码做混淆了，但是一些系统回调方法肯定不能混淆的，比如onCreate方法。这里一般找问题的方法是：

1）首先看这个类有没有静态方法和静态代码块，因为这类的代码会在对象初始化之前运行，可能在这里加载so文件，或者是加密校验等操作。

2）再看看这个类的构造方法。

3）最后再看生命周期方法。

看到它的核心代码在onCreate中，调用了很多类的方法，猜想这里的某个方法做工作了。这时可注入代码来跟踪是哪个方法出现问题了。

下面来看看怎么添加日志信息，就是添加日志，需要修改smali文件，再去查看smali源码：

这里可以很清晰地看到调用了这些方法，那么就在每个方法中加上日志信息，这里加日志有两种方式。一种方式就是直接在这里调用系统的log方法，但是有两个问题：

·需要导入包，在smali中修改。

·需要定义两个参数：tag、msg，才能正常打印log。

明显这个方法有点麻烦。另外一种方式就自己定义一个MyLog类，然后反编译，得到MyLog的smali文件，添加到这个ShuqiApplication.smali的root目录下，然后在代码中直接调用即可。放到root目录下的目的是在代码中调用就不需要导入包了，比如SuqiApplication.smali中的一些静态方法调用，如图20-7所示。

图20-7　静态方法调用

编写日志类MyLog，这里就不粘贴代码了，新建一个项目之后，反编译得到MyLog.smail文件，放到目录中：

得到这个文件的时候，一定要注意，把MyLog.smali的包名信息删除，因为放到root目录下的，意味着这个MyLog类是没有任何包名的，这需要注意，不然最后加也是报错的。

在ShuqiApplication的onCreate方法中插入的日志方法，如下所示：

在加代码的时候需要注意，要找对地方加，就是在上个方法调用完之后添加，比如：invoke-virtual，invoke-static等，而且这些指令后面不能有move-result-object，因为这个指令是获取方法的返回值，所以一般是这么加代码的：

·在invoke-static/invoke-virtual指令返回类型是V之后可以加入。

·在invoke-static/invoke-virtual指令返回类型不是V，那么在move-result-object命令之后可以加入。

加好了日志代码之后就回编译执行，在这个过程中可能会遇到samli语法错误，针对指定的文件修改就可以了，得到回编译的apk之后，可以再反编译一下，看看它的Java代码，如图20-8所示。

图20-8　添加日志之后的代码

可以看到添加的代码，在每个方法之后打印信息。下面运行程序，同时开启log的tag：

看到打印的日志了，发现打印了三个log，这里需要注意的是，虽然打印了三个log，但是都是在不同的进程中，一个进程中的log只打印了一个，所以判断，问题出现在vr.h这个方法，如下所示：

查看这个方法源码如下所示：

果然，这个方法做了签名验证，校验不正确的话，直接退出程序。那么现在要想正常运行程序的话，直接注释用“#”，注释smali对应的这行代码：vr.h（this）。

然后回编译，再运行，果然不报错了，这里就不再演示了：

上面是通过注入代码来跟踪问题，这个方法很常用，也很实在。

20.5.2　静态分析native代码

下面介绍如何使用IDA来静态分析native代码。在反编译之后，看到它的onCreate方法中有一个加载so的代码，如下所示：

看看这个代码，如下所示：

获取密码的方法是native的，就来看看getDbPassword方法。用IDA打开libpsProcess.so文件，如图20-9所示。

图20-9　IDA打开so文件内容

看看这个函数的实现，一般直接看BL/BLX等信息，跳转逻辑，还有就是返回值。在函数的最后部分发现一个重点，就是BL__android_log_print。这是在native层调用log的函数。再往上看，发现tag是System.out.c。

运行程序看起log，此时也可以在Java层添加日志的：全局搜索这个方法，发现在yi这个类中调用，如图20-10所示。

图20-10　获取数据库密码代码

修改yi.smali代码：

可以看到，这个应用使用了sqlcipher框架进行了数据库的加密。其实现在很多应用加密数据库都是使用这个框架的，微信也是使用这个框架对本地通讯信息数据库进行加密的，这个加密工具对应的有一个Window程序可以打开它，不过需要密码，如图20-11所示。

图20-11　打开加密的数据文件

如果想破解加密的数据库内容，只要得到加密数据库密码即可，而这个密码一般是保存在本地的，只要在本地就有办法获取的。

回编译，再运行程序，开启log，如图20-12所示。

图20-12　打印日志信息

发现，返回的密码Java层和native层是一样的。说明静态分析native还是有效的。

提示：案例下载地址为http://download.csdn.net/detail/jiangwei0910410003/9308217。

案例中有个说明文件，运行前请阅读。

本节通过案例介绍了如何使用静态方式去破解一个apk，在破解一个apk的时候，其实就是改点代码，然后能够运行起来，达到想要的功能，一般步骤如下：

1）注释特定功能，比如广告展示等。

2）得到方法的返回值，比如获取用户的密码。

3）添加指定的代码，比如加入自己的监测代码和广告等。

在静态分析代码的时候，需要遵循的大体路线如下：首先能够反编译，得到AndroidManifest.xml文件，找到程序入口代码。找到想要的代码逻辑，一般会结合界面分析，比如要想登录成功，肯定想要得到用户登录界面Activity，这时可以用adb shell dumpsys activity top命令得到Activity名称，然后用Eclipse自带的程序当前视图分析工具得到控件名称，或者在代码中获取layout布局文件，一般是setContentView方法的调用地方，然后用布局文件结合代码得到用户登录的逻辑，进行修改。

·在关键的地方通过代码注入技术来跟踪代码执行逻辑。

·注意方法的返回值，条件判断等比较显眼的代码。

有些apk中的源码可能有自己的加密算法，这时候需要获取到这个加密方法，如果加密方法比较复杂，就需要大批的测试数据来获取这个加密方法的逻辑，一般是输入和输出作为一个测试用例。

对于System.loadLibrary加载so文件的代码，只需要找到这个so文件，然后用IDA打开进行静态分析，因为有些apk中把加密算法放到了so中了，这时候也可以通过测试数据来获取加密算法。

通过上面的例子，可以得出一个经验，就是现在很多apk会做一些校验工作，因为一般在代码中包含“signature”字符串信息，所以可以全局搜索一下，也许能获取一些重要信息，如图20-13所示。

图20-13　签名校验关键字搜索结果

20.6　本章小结

本章主要介绍了如何通过静态分析方式进行破解，介绍了一些工具的使用，以及破解流程、破解技巧。最常用的就是代码注入技术和全局搜索关键字符串等方式。可以看到，现在市面上的很多apk，仅通过静态分析是无法满足破解需求了，所以动态分析方式就来了，而且动态方式破解难度会很大，需要掌握的东西也很多，后面篇章会一一介绍动态破解的技巧和常见的问题。而静态方式破解也很重要，是动态分析的前提，所以这两种技术都必须很好地掌握。

第21章　动态调试smali源码

本章开始介绍另外一种破解apk方式：动态方式。动态方式相对于静态方式来说难度大一点，但是它比静态方式高效，能够针对更多的破解范围。破解一般的apk没有任何问题，不过不能代表能够破解所有的apk，因为没有绝对的安全，也是没有绝对的破解，双方都在进步，只能具体问题具体分析。

首先需要解释一下，为什么调试smali源码，而不是Java源码，因为进行过反编译的人知道，使用apktool反编译apk之后，会有一个smali文件夹，这里存放了apk对应的smali源码。

21.1　动态调试步骤

第一步：反编译apk

通过apktool工具进行apk的反编译，得到smali源码和AndroidManifest.xml，然后修改AndroidManifest.xml中的debug属性为true，同时在入口处加上waitForDebug代码，进行debug等待。一般入口都是先找到入口Activity的onCreate方法中的第一行处，这里需要注意的是，apktool工具一定要加上-d参数，这样反编译得到的文件是Java文件，这样才能够被Eclipse识别，进行调试。

第二步：回编译apk

修改完成AndroidManifest.xml和添加waitForDebug之后，需要使用apktool进行回编译，回编译之后得到的是一个没有签名的apk，还需要使用signapk.jar来进行签名，签名文件直接使用测试程序的签名文件就可以，最后进行安装。

第三步：将反编译smali工程导入Eclipse

将反编译之后的smali源码导入到Eclipse工程中，找到关键点，进行下断点。这里的关键点，一般是先大致了解程序运行的结构，然后找到需要破解的地方，使用View分析工具，或者使用jd-gui工具直接查看apk源码（使用dex2jar将dex文件转化成jar文件，然后用jd-gui进行查看），找到代码的大体位置，然后下断点。这里可以借助Eclipse的DDMS自带的View分析工具找到对应控件的resid，然后全局搜索这个控件的resid，或者直接在values/public.xml中查找，最终定位到这个控件位置，再查看它的点击事件即可。

第四步：设置远程调试

设置远程调试工程，首先运行需要调试程序，然后在DDMS中找到对应的调试服务端的端口号，然后在Debug Configurations中设置远程调试项目，设置对应的调试端口和ip地址（一般都是本机pc，那就是localhost），然后红色小蜘蛛变成绿色的，表示远程调试项目连接关联上了调试程序。这里需要注意的是，一定需要关联正确，不然是没有任何效果的，关联成功之后，就可以进行操作。

第五步：调试apk程序

操作的过程中，会进入到关键的断点处，通过F6单步，F5单步进入，F7单步跳出，进行调试。找到关键方法，然后通过分析smali语法了解逻辑，如果逻辑复杂的，可以通过查看具体的环境变量的值来观察，这里也是最重要的，也是最复杂的，同时这里也是没有规章可寻的，这与每个人的逻辑思维以及破解能力有关系。分析关键的加密方法是需要功底的，当然这里还需要注意一个信息，就是Log日志，这有时候也是很重要的一个信息。

第六步：编写代码实现核心逻辑

当知道了核心方法的逻辑，要想得到正确的密码，还是需要自己用语言去实现逻辑的，需要手动编写代码才能得到正确的密码。

21.2　案例分析

因为逆向是动手操作的过程，所以这里用一个例子来介绍如何操作。本节用阿里2014年安全挑战赛的第一题：AliCrack_one.apk，如图21-1所示。

图21-1　破解样本

这个题目输入密码就可以破解了，下面就来看看如何获取这个密码。

第一步：使用apktool来破解apk

命令如下：

命令不做解释了，但是有一个参数必须带上，那就是：-d。因为这个参数代表反编译得到的smali是Java文件，这里指文件后缀名是Java，如果不带这个参数，后缀名是smali的，但是Eclipse中是不会识别smali文件的，而能识别Java文件，所以这里一定要记得加上这个参数。

反编译成功之后，我们得到了一个out目录，如图21-2所示。

图21-2　反编译之后的目录

源码都放在smali文件夹中，如图21-3所示，进入查看一下文件。

图21-3　smali代码目录

这里全是Java文件，其实只是后缀名为Java了，内容还是smali文件：

第二步：修改AndroidManifest.xml中的debug属性

上面反编译成功了，下面为了后续的调试工作，所以还是需要做两件事：

1）修改AndroidManifest.xml中的android：debuggable=“true”。

这个属性在前面介绍run-as命令的时候也提到了，它标识这个应用是否是debug版本，这将会影响到这个应用是否可以被调试，所以这里必须设置成true。

2）在入口处添加waitForDebugger代码进行调试等待。

这里说的入口处就是程序启动的地方，就是一般的入口Activity。查找这个Activity的方法太多了，比如这里直接从上面得到的AndroidManifest.xml中找到，因为入口Activity的action和category是固定的：

当然还有其他方式，比如用aapt查看apk的内容方式，或者安装apk之后用adb dumpsys activity top命令查看。找到入口Activity之后，直接在它的onCreate方法的第一行加上waitForDebugger代码即可，找到对应的MainActivity的smali源码，然后添加一行代码：

这个是smali语法的，其实对应的Java代码就是：

注意：其实还有一种更方便的方式，就是使用am命令，以debug模式启动一个应用：

后面再进行调试的时候都会采用这种方式了。

第三步：回编译apk并且进行签名安装

代码如下：

还是使用apktool进行回编译：

编译完成之后，将得到debug.apk文件，但是这个apk是没有签名的，所以是不能安装的，那么下面需要进行签名。这里使用Android中的测试程序的签名文件和sign.jar工具进行签名，如图21-4所示。

图21-4　签名工具signapk.jar

命令如下：

签名之后，就可以进行安装了。

第四步：将smali源码导入到Eclipse中

这里新建一个Java项目，记住不是Android项目，因为最后的调试工作其实是借助于Java的调试器，然后勾选“Use default location”选项，选择smali源码目录，也就是上面反编译之后的out目录，点击完成，如图21-5所示。

图21-5　Eclipse中导入smali项目

导入源码之后的项目工程结构如图21-6所示。

主要看MainActivity类，如图21-7所示。

第五步：找到关键点，然后打断点

这一步要具体问题具体分析。比如这个例子中，当输入密码之后，肯定要点击按钮，然后触发密码的校验过程，那么要找到这个button的定义的地方，然后进入它的点击事件中就可以了。这里分为三步走：

1）使用Eclipse自带的View分析工具找到Button的ResId，如图21-8所示。点击之后，需要等待一会，分析View之后的结果，如图21-9所示。

这里能够看到整个当前页面的全部布局，以及每个控件的属性值，需要找到button的resource-id，这里看到定义是@+id/button这个值。

2）得到这个ResId之后，能否在smali项目中全局搜索这个值，就可以定位到这个button的定义的地方呢？如图21-10所示。然后看看搜到的结果，如图21-11所示。

是在资源文件中搜到了这个id的定义，这个id值对应的是0x7F05003E。当然除了这种方式，还有一种方式能快速找到这个id对应的整型值，那就是在反编译之后的values/public.xml文件中，如图21-12所示。

这个文件很有用，它是整个apk中所有资源文件定义的映射内容，比如drawable/string/anim/attr/id等这些资源文件定义的值、名字和整型值对应的地方，如图21-13所示。

图21-6　导入成功效果图

图21-7　入口类代码

图21-8　view分析工具按钮

图21-9　分析工具效果图

图21-10　全局搜索关键字

图21-11　全局搜索结果

图21-12　public.xml文件

图21-13　public.xml内容

这个文件很重要，是寻找突破口的关键点，比如有时候需要通过字符串内容来定位关键点，这里就可以通过string的定义来找到对应的整型值即可。

当找到了button对应的id值了之后，就可以用这个id值再一次全局搜索一下，因为Android中编译之后的apk，在代码中用到的ResId都是用一个整型值代替的，这个整型值就是在R文件中做了定义，将资源的id和一个值对应起来，然后代码里面一般使用R.id.button这样的值，再编译出apk的时候，这个值就会被替换成对应的整型值，所以再全局搜索0x7F05003E，如图21-14所示。搜索的结果如图21-15所示。

这里就定位到了代码中用到的这个button。进入代码看看，如图21-16所示。在这里看到使用了findViewById的方式定义Button，再简单分析一下smali语法，下面是给button添加一个按钮事件，这里用的是内部类MainActivity$1，到这个类看看，它肯定实现了OnClickListener接口，那么直接搜onClick方法，如图21-17所示。

图21-14　全局搜索id值

图21-15　全局搜索id结果值

图21-16　定位代码位置

图21-17　点击事件方法

在这里就可以下个断点了，这里就是触发密码校验过程的地方。

第六步：运行程序，设置远程调试项目

在第五步中，找到了关键点，然后打上断点，下面就来运行程序，然后在Eclipse中设置远程调试的项目，运行程序。因为加入了waitForDebug的代码，所以启动的时候会出现一个Wait debug的对话框。不过，测试的时候，手机没有出现这个对话框，而是一个白屏，不过这不影响。程序运行起来之后，看看如何在Eclipse中设置远程调试项目，首先找到需要调试的程序对应远程调试服务端的端口，如图21-18所示。

图21-18　调试端口图

这里要注意以下几点：第一点，在程序等待远程调试服务器的时候，前面会出现一个红色的小蜘蛛。第二点，在调试服务端我们会看到两个端口号8600/8700，这里需要解释一下，为什么会有两个端口号呢？首先这里的端口号代表的是，远程调试服务器端的端口，下面简单来看一下Java中的调试系统，如图21-19所示。

图21-19　Java中的调试系统

这里有三个角色：

1）被调试的客户端。可以认为需要破解的程序就是客户端，如果一个程序可以被调试，当启动的时候，会有一个JDWP线程用来和远程调试服务端进行通信，如下所示：

需要破解的程序启动了JDWP线程，注意这个线程也只有当程序是debug模式下才有的，也就是AndroidManifest.xml中的debug属性值必须是true的时候，这也是一开始为什么要修改这个值的原因。

2）JDWP协议（用于传输调试信息，比如调试的行号、当前的局部变量的信息等），这可以说明，为什么在一开始的时候，反编译成Java文件，因为为了Eclipse导入能够识别的Java文件，然后为什么能够调试呢？因为smali文件中有代码的行号和局部变量等信息，所以可以进行调试。

3）远程调试的服务端，一般是有JVM端，就是开启一个JVM程序来监听调试端，这里就可以认为是本地的PC机。当然必须有端口用来监听，那么上面的8600端口就是这个作用，而且端口是从8600开始，后续的程序端口号都是依次加1的，比如其他调试程序端口如图21-20所示。

图21-20　调试端口

那么有了8600端口，为什么还有一个8700端口呢？它是干什么的？其实它的作用就是远程调试端备用的基本端口。不过，在实际过程中，还是建议使用程序独有的端口号8600，可以查看8600和8700端口在远程调试端（本地pc机）的占用情况，如下所示。

8600端口和8700端口号都是对应的javaw程序，javaw程序就是启动一个JVM来进行监听的。到这里就弄清楚了Java中的调试系统以及远程调试的端口号。

注意，其实可以使用adb jdwp命令查看当前设备中可以被调试的程序的进程号信息，如下所示：

下面继续，知道了远程调试服务端的端口8600以及ip地址，这里就是本地ip：localhost/127.0.0.1。可以在Eclipse中新建一个远程调试项目，将smali源码工程和设备中需要调试的程序关联起来。右击被调试的项目→选择Debug Configurations，如图21-21所示。

图21-21　设置调试项目

然后开始设置调试项目，如图21-22所示。

图21-22　设置调试项目

选择Romote Java Application，在Project中选择被调试的smali项目，在Connection Type中选择SocketAttach方式。其实还有一种方式是Listener的，如图21-23所示。这两种方式的区别如下所示：

·Listner方式：调试客户端启动就准备好一个端口，当调试服务端准备好了，就连接这个端口进行调试。

·Attach方式：调试服务端开始就启动一个端口，等待调试端来连接这个端口。

图21-23　调试的两种方式

一般都是选择Attach方式来进行操作的。设置完远程调试的工程之后，开始运行，发现设备上的程序还是白屏，这是为什么呢？看看DDMS中调试程序的状态，如下所示：

关联到了这个进程，上面使用的是8700端口号，这时选中了这个进程，就把smali调试项目关联到了这个进程，破解的进程没响应了，立马改一下，用8600端口，如下所示：

这下成功了，看到红色的小蜘蛛变成绿色的了，说明调试端已经连接上远程调试服务端了。

注意：在设置远程调试项目的时候，一定要注意端口号的设置，不然没有将调试项目源码和调试程序关联起来，是没有任何效果的。

第七步：开始运行调试程序，进入调试

在程序的文本框中输入：gggg内容，点击开始，如图21-24所示。

图21-24　进入断点

到这里看到期待已久的调试界面出来了，到了开始的时候加的断点处，就可以开始调试了，使用F6单步调试，F5单步跳入，F7单步跳出进行操作。这里使用v3变量保存了输入的内容，如图21-25和图21-26所示。

图21-25　查看变量值

图21-26　查看变量值

这里有一个关键的地方，就是调用MainActivity的getTableFromPic方法，获取一个String字符串，从变量的值来看，貌似不是规则的字符串内容，这里先不用管了，继续往下走，如下所示。

这里又遇到一个重要的方法getPwdFromPic，如图21-27所示，从字面意义上看，应该是获取正确的密码，用于后面的密码字符串比对。

图21-27　查看密码内容

查看一下密码的内容，貌似也是一个不规则的字符串，但是可以看到和上面获取的table字符串内容格式很像，接着往下走。这里还有一个信息就是调用了系统的Log打印，log的tag就是v6保存的值lil，如下所示：

这时候看到v3是保存的输入密码，这里使用utf-8获取它的字节数组，然后传递给access$0方法：

使用F5进入这个方法：

在这个方法中，还有一个bytesToAliSmsCode方法，使用F5进入：

这个方法其实看上去还是很简单的，就是把传递进来的字节数组循环遍历，取出字节值，然后转化成int类型，再调用上面获取到的table字符串的chatAt来获取指定的字符，使用StringBuilder进行拼接，然后返回即可，如图21-28所示。

图21-28　加密之后的内容

按F7跳出，查看，返回来加密的内容是“日日日日”，如图21-29所示，也就是说gggg=>日日日日。

最后再往下走，可以看到进行代码比对的工作了。

图21-29　代码比对方法

上面就分析完了所有的代码逻辑，还不算复杂，来梳理一下流程：

1）调用MainActivity中的getTableFromPic方法，获取一个table字符串。

可以进入看看这个方法的实现：

这里可以大体了解了，它是读取asset目录下的一个logo.png图片，然后获取图片的字节码再进行操作，得到一个字符串，那么从上面的分析可以知道，其实这里的table字符串类似于一个密钥库。

2）通过MainActivity中的getPwdFromPic方法获取正确的密码内容。

3）获取输入内容的utf-8的字节码，然后调用access$0方法，获取加密之后的内容。

4）access$0方法中调用bytesToAliSmsCode方法，获取加密之后的内容。

这个方法是最核心的，通过分析知道，通过传递进来的字节数组，循环遍历数组，拿到字节转化成int类型，然后再调用密钥库字符串table的charAt得到字符，使用StringBuilder进行拼接。通过上面的分析之后，知道获取加密之后的输入内容和正确的密码内容做比较，那么现在有的资源是：密钥库字符串和正确的加密之后的密码，以及加密的逻辑。

那么破解思路就有了，相当于，知道了密钥库字符串，也知道了加密之后的字符组成的字符串，那么可以通过遍历加密之后的字符串，循环遍历，获取字符，然后再去密钥库找到指定的index，然后再转成byte，保存到字节数组，然后用utf-8获取一个字符串，那么这个字符串就是想要的密码。

下面就用代码来实现这个功能：

代码中的函数相当于上面加密函数的bytesToAliSmsCode的反向实现，运行结果如图21-30所示。

图21-30　破解结果

得到了正确的密码，下面来验证一下，如图21-31所示。

图21-31　验证结果

破解成功。

补充：刚刚在断点调试的时候，看到了代码中用了Log来打印日志，tag是lil，那么可以打印这个log看看结果，如下所示：

这里table是密钥库，pw是正确的加密之后的密码，enPassword是输入之后加密的密码。通过这个例子可以知道，在破解apk的时候，日志也是一个非常重要的信息。

提示：资料下载地址为http://download.csdn.net/detail/jiangwei0910410003/9526113。

21.3　本章小结

本章介绍了如何使用Eclipse动态调试反编译之后的smali源码，这种方式比静态方式高效很多，但是现在市场上的大部分应用没有这么简单就破解了，比如核心的加密算法放到了native层去做，那么这时候就需要去动态调试so文件，这是下一章的内容。

第22章　IDA工具调试so源码

本章继续介绍逆向apk的相关知识，主要介绍如何使用IDA来调试Android中的native源码，因为现在有一些App，为了安全或者效率问题，会把一些重要的功能放到native层，那么用Eclipse调试smali源码就显得很无力了，因为Android中native层使用的是so库文件，所以本章介绍如何调试so文件的内容，从而提高破解成功率。

22.1　IDA中的常用快捷键

前一章中使用IDA工具静态分析so文件，通过分析ARM指令，来获取破解信息，比如用打印的log信息来破解apk。那时候就已经介绍了如何使用IDA工具，如图22-1所示。

图22-1　IDA工具的窗口视图

IDA工具有多个窗口，也有多个视图，用到最多的如下所示：

·Function Window对应so函数区域：在这里可以使用ctrl+f进行函数的搜索。

·IDA View对应so中代码指令视图：在这里可以查看具体函数对应的ARM指令代码。

·Hex View对应so的十六进制数据视图：在这里可以查看ARM指令对应的数据等。

下面先介绍IDA中一些常用的快捷键。

1.ARM指令转化成C语言快捷键

强大的F5快捷键可以将ARM指令转化成可读的C语言，能帮助分析，如下所示：

首先选中需要翻译成C语言的函数，然后按下F5，如下所示：

下面还需要做一步，就是还原JNI函数方法名。一般JNI函数方法名是一个指针加上一个数字，比如v3+676。然后将这个地址作为一个方法指针进行方法调用，并且第一个参数就是指针自己，比如（v3+676）（v3…）。这实际上就是在JNI里经常用到的JNIEnv方法。因为IDA并不会自动对这些方法进行识别，所以当对so文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么，因为这个函数实在是太抽象了。解决方法非常简单，只需要对JNIEnv指针做一个类型转换即可。比如说上面提到a1和v4指针，如下所示：

可以选中a1变量，然后按一下y键，如图22-2所示。

图22-2　选中变量

然后将类型声明为JNIEnv*，如图22-3所示。

图22-3　设置JNIEnv*指针

确定之后再来看，如下所示：

修改之后是不是瞬间清晰了很多。另外还总结了所有JNIEnv方法对应的数字、地址以及方法声明，如下所示：

2.打开字符串内容窗口快捷键

用Shirt+F12快捷键快速打开so中所有的字符串内容窗口，如图22-4所示。

图22-4　so中字符串内容窗口

有时候，字符串是一个非常重要的信息，特别是对于破解的时候，可能就是密码，或者是密码库信息。

3.定位so中段地址快捷键

Ctrl+S快捷键有两个用途，在正常打开so文件的IDA View视图的时候，可以查看so对应的段信息，如图22-5所示。

图22-5　打开so中的段信息

可以快速得到一个段的开始位置和结束位置，不过这个位置是相对位置，不是so映射到内存之后的位置，关于so中的段信息，已经在前面章节中详细介绍。

当在调试页面的时候，用Ctrl+S键可以快速定位到想要调试的so文件映射到内存的地址，如图22-6所示。

图22-6　定位调试的so文件的内存地址

一般一个程序肯定会包含多个so文件，比如系统的so文件就有好多，一般都是在/system/lib下面，当然也有自己的so，这里可看到开始位置和结束位置就是这个so文件映射到内存中，如下所示：

这里可以使用cat命令查看一个进程的内存映射信息：

看到映射信息中有多个so文件，其实这不是多个so文件，而是so文件中对应的不同Segement信息被映射到内存中的，一般是代码段、数据段等，因为需要调试代码，所以只关心代码段。代码段有一个特点就是具有执行权限x，只需要找到权限中有x的那段数据即可。

4.跳转到指定地址快捷键

在IDA调试页面的时候，可以使用G键快速跳转到指定的内存位置，如图22-7所示。

图22-7　跳转到指定内存位置

这里的跳转地址是可以算出来的。比如现在想跳转到A函数，然后下断点，那么可以使用上面说到的Ctrl+S查找到so文件的内存开始的基地址，然后再用IDA View中查看A函数对应的相对地址，相加就是绝对地址，然后跳转到即可，比如这里的：Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals函数的IDA View中的相对地址是（也就是so文件的地址）E9C，如下所示：

上面看到so文件映射到内存的基地址为74FE4000，如下所示：

那么跳转地址就是74FE4000+E9C=74FE4E9C。

注意，只要程序没有退出，一般这里的基地址在运行中的值就不会变，因为程序的数据已经加载到内存中了。同时相对地址是永远不会变的，只有在修改so文件的时候，文件的大小改变了，可能相对地址会改变，其他情况下不会改变，相对地址就是数据在整个so文件中的位置。这里可以看到函数映射到内存中的绝对地址，如下所示：

有时候发现跳转到指定位置之后，看到的全是DCB数据，这时候选择函数地址，点击P键就可以看到ARM指令源码了，如下所示：

5.调试快捷键

调试快捷键包括：F8单步调试，F7单步进入调试，如下所示：

上面找到函数地址之后，可以下断点了，下断点很简单，点击签名的绿色圈点，变成红色条目即可，然后可以点击F9快捷键，或者点击运行按钮（绿色右箭头），即可运行程序，如图22-8所示。

图22-8　F9快捷键

其中还有暂停和结束按钮。运行之后再点击so的native函数，触发断点逻辑，如下所示：

这时候，看到进入调试界面，点击F8可以单步调试，看到有一个PC指示器。其实在ARM中PC是一个特殊的寄存器，用来存储当前指令的地址。

总结一下IDA在调试so文件的时候需要用到的快捷键：

·Shift+F12　快速查看so文件中的字符串信息。

·F5　可以将ARM指令转化成可读的C代码，同时可以使用Y键，修改JNIEnv的函数方法名。

·Ctrl+S　有两个用途，在IDA View页面中可以查看so文件的所有段信息，在调试页面可以查看程序所有so文件映射到内存的基地址。

·G　可以在调试界面快速跳转到指定的绝对地址，进行下断点调试。如果跳转到目的地址之后，发现是DCB数据的话，可以使用P键进行转化。

·F7　可以单步进入调试，F8键可以单步调试。

22.2　构造so案例

了解IDA工具之后，就可以开始动手操作了。为了方便开始，先自己写一个简单的Android native层代码，然后用IDA进行分析即可。这里可以使用Android Studio新建一个简单工程，然后创建JNI。如何在Android Studio中开发NDK，前面章节中已经详细介绍了。下面直接在Android Studio中新建了一个Native项目，这里关于native项目的代码不想解释太多，就是Java层传递了用户输入的密码，然后native做了校验过程，把校验结果返回到Java层。如下所示：

具体的校验过程这里不再解释了。运行项目之后得到apk文件，那么下面就开始破解旅程了。

22.3　逆向so文件

下面开始破解编译之后的apk文件。

22.3.1　获取应用的so文件

按照国际惯例，直接使用解压软件，获取应用apk中的so文件，如图22-9所示。

图22-9　解压so文件

得到libencrypt.so文件之后，使用IDA打开它，如图22-10所示。

一般so中的函数方法名都是：Java_类名_方法名。那么这里直接搜Java关键字即可，或者使用jd-gui工具找到指定的native方法，如图22-11所示。

图22-10　IDA打开so文件

图22-11　native函数名

双击即可在右边的IDA View页面中看到Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals函数的指令代码，如图22-12所示。

可以简单分析一下这段指令代码：

·PUSH{r3-r7，lr}是保存r3，r4，r5，r6，r7，lr的值到内存的栈中，那么最后当执行完某操作后，想返回到lr指向的地方执行，当然要给pc了，因为pc保留下一条CPU即将执行的指令，只有给了pc，下一条指令才会执行到lr指向的地方。

·pc：程序寄存器，保留下一条CPU即将执行的指令。

·lr：连接返回寄存器，保留函数返回后，下一条应执行的指令。

图22-12　函数对应的ARM指令代码

这个和函数最后面的POP{r3-r7，pc}是相对应的。

然后是调用了strlen、malloc、strcpy等系统函数，在每次使用BLX和BL指令调用这些函数的时候，都发现了一个规律：在调用它们之前一般都是用MOV指令来传递参数值的，比如这里的R5里面存储的就是strlen函数的参数，R0就是is_number函数的参数，如下代码所示。这样分析之后，在后面的动态调试的过程中可以得到函数的入口参数值，这样就能得到一些重要信息。

在每次调用有返回值的函数之后的命令，一般都是比较指令，比如CMP、CBZ，或者是strcmp等，如上面代码最后一行。这里是破解的突破点，因为一般加密再怎么厉害，最后比较的参数肯定是正确的密码（或者是正确的加密之后的密码）和输入的密码（或者是加密之后的输入密码），在这里就可以得到正确密码，或者是加密之后的密码。

到这里就分析完了native层的密码比较函数：Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals。如果觉得上面的ARM指令看的吃力，可以使用F5键，查看它的C语言代码，如下所示：

这里可以看到，有两个函数是核心点。

·is_number函数，这个函数看名字就能猜到是判断是不是数字，可以使用F5键，查看它对应的C语言代码，如下所示：

这里简单一看，主要是看return语句和if判断语句，看到这里有一个循环，然后获取_BYTE*这里地址的值，并且自增加一，然后存到v2中，如果v3为'\0'的话，就结束循环，然后做一次判断，就是v2-48是否大于9，那么这里知道48对应的是ASCII中的数字0，所以这里可以确定：用一个循环遍历_BYTE*这里存的字符串是否为数字串。

·get_encrypt_str函数，这个函数看到名字就可以猜测，它是获取输入的密码加密之后的值，再次使用F5快捷键查看对应的C语言代码，如下所示：

首先是一个if语句，用来判断传递的参数是否为NULL。如果是的话，直接返回；不是的话，使用strlen函数获取字符串的长度保存到v2中，然后使用malloc申请一块堆内存。首指针保存到result，大小是v2+1，也就是传递进来的字符串长度+1，然后就开始进入循环。首指针result，赋值给i指针，开始循环，v3是通过v1-1获取到的，就是函数传递进来字符串的地址，那么v6就是获取传递进来字符串的字符值，然后减去48，赋值给v7。

可以猜到了，这里想做字符转化，把char转化成int类型。继续往下看，如果v6==48的话，v7=1，也就是说这里如果遇到字符'0'，就赋值1。看到上面得到的v7值，被用来取key_src数组中的值，那么这里双击key_src变量，就跳转到了它的值地方。果不其然，这里保存了一个字符数组，看到它的长度正好是18，如下代码所示，那么应该明白了，这里通过传递进来的字符串，循环遍历字符串，获取字符，然后转化成数字，再倒序获取key_src中的字符，保存到result中。然后返回。

这两个重要函数的功能为，一个是判断输入的内容是否为数字字符串，一个是通过输入的内容获取密码内容，然后和正确的加密密码ssBCqpBssP进行比较。

22.3.2　用IDA进行调试设置

本节就用动态调试方法来跟踪传入的字符串值，以及加密之后的值。看到打印log的函数，很难知道具体的参数和寄存器的值，所以这里需要开始调试，得知每个函数执行之后的寄存器的值，在用IDA进行调试so的时候，需要以下准备步骤。

1.获取android_server文件

在IDA安装目录\dbgsrv\android_server，如图22-13所示。

图22-13　android_server文件

这个文件是干嘛的呢？它是怎么运行的呢？下面来介绍一下。在前一章中介绍了关于Android中的调试原理，其实是使用gdb和gdbserver来做到的，gdb和gdbserver在调试的时候，必须注入到被调试的程序进程中。但是非root设备的话，注入别的进程中只能借助于run-as这个命令。也就是说如果要调试一个应用进程的话，必须要注入它内部，那么用IDA调试so文件也是这个原理，需要注入进程才能进行调试。IDA有类似于gdbserver这样的工具，那就是android_server，需要运行在设备中，保证和PC端的IDA进行通信，比如获取设备的进程信息、具体进程的so内存地址、调试信息等。

把android_server保存到设备的/data目录下，修改一下它的运行权限，然后必须在root环境下运行，因为要做注入进程操作，所以必须要root：

注意，这里把它放在了/data目录下，然后是./android_server命令，这里提示了IDA Android 32-bit，所以后面在打开IDA的时候一定要是32位的IDA，不是64位的。IDA在安装之后都是有两个可执行的程序，一个是32位，一个是64位的，如果没打开正确会报这样的错误，如图22-14所示。

图22-14　报错信息

还有一类问题：error：only position independent executables（PIE）are supported这主要是Android 5.0以上的编译选项默认开启了pie，在5.0以下编译的原生应用不能运行，有两种解决办法，一种是用Android 5.0以下的手机进行操作，还有一种就是用IDA6.6+版本即可。

然后开始监听了设备的23946端口，如果想让IDA和这个android_server进行通信，必须让PC端的IDA也连上这个端口，这时候就需要借助于adb的一个命令了：

这里就可以把android_server端口转发出去：

这时候，只要在PC端使用IDA连接上23946这个端口就可以了。到这里会有人好奇，为什么远程端的端口号也是23946呢？因为后面在使用IDA进行连接的时候，发现IDA把这个端口设置死了，就是23946，可使用如下命令修改：android-server-p1234。

可以使用netstat命令查看端口23946的使用情况，看到是ida在使用这个端口

2.IDA获取进程信息

上面准备好了android_server，运行成功，下面就用IDA进行尝试连接，获取信息，进行进程附加注入。这时候需要再打开一个IDA，之前打开一个IDA是用来分析so文件的，一般用于静态分析，要调试so的话，需要再打开一个IDA。这里一般都是需要打开两个IDA，也叫作双开IDA操作，采用动静结合策略，如图22-15所示。

图22-15　双开IDA

这里记得选择Go这个选项，就是不需要打开so文件了，进入一个空白页，如图22-16所示。

在Debugger选项卡中选择Attach，看到有很多debugger，可见IDA工具真的很强大，做到很多debugger的兼容，可以调试很多平台下的程序。这里选择Android debugger，出现页面如图22-17所示。

图22-16　选择Debugger选项

图22-17　设置端口

这里可看到端口是23946，所以上面用adb forward进行端口转发的时候是23946。这里PC本地机就是调试端，所以host就是本机的ip地址：127.0.0.1，点击OK，出现界面如图22-18所示。

图22-18　进程信息列表

可以看到列出了设备中所有的进程信息，其实都是android_server干的事，获取设备进程信息传递给IDA进行展示。

注意，如果当初没有用root身份去运行android_server：

IDA就不会列举出设备的进程信息，如图22-19所示。

图22-19　无进程列表信息

还有一个注意的地方，就是IDA和android_server版本一定要保持一致。

可以用Ctrl+F快捷键搜索需要调试的进程，这里必须运行要调试的进程，不然也是找不到这个进程的，如图22-20所示。

图22-20　搜索进程

双击进程，即可进入调试页面，如图22-21所示。

图22-21　进入调试页面

为什么会断在libc.so中呢？Android系统中libc是c层中最基本的函数库，libc中封装了io、文件、socket等基本系统调用。所有上层的调用都需要经过libc封装层。所以libc.so是最基本的，所以会断在这里。而且一些常用的系统so，比如linker，如图22-22所示。

图22-22　linker文件

这个linker是用于加载so文件的模块，所以后面再分析如何在.init_array处下断点，还有一个就是libdvm.so文件，它包含了dvm中所有的底层加载dex的一些方法，如图22-23所示。

图22-23　libdvm.so文件

在后面动态调试需要dump出加密之后的dex文件，就需要调试这个so文件了。

3.找到函数地址下断点

使用Ctrl+S快捷键找到需要调试so的基地址：74FE4000，如图22-24所示。

图22-24　查找基地址

然后通过另外一个IDA打开so文件，查看函数的相对地址：E9C，如下所示：

那么得到了函数的绝对地址就是：74FE4E9C，使用G键快速跳转到这个绝对地址，如图22-25所示。

图22-25　跳转到绝对地址

跳转这个地址之后，开始下断点，点击最左边的绿色圆点即可下断点，如下所示：

然后点击左上角的绿色三角按钮运行，也可以使用F9键运行程序。

点击程序中的按钮，如图22-26所示。

图22-26　点击程序中的按钮

触发native函数的运行，如下所示：

进入调试阶段了，这时候可以使用F8快捷键进行单步调试，用F7进行单步调试，如下所示：

点击F8进行单步调试，到达is_number函数调用处，看到R0是出入的参数值，可以查看R0寄存器的内容，然后看到是123456，这个就是Java层传入的密码字符串。接着往下走，如下所示：

这里把is_number函数返回值保存到R0寄存中，然后调用CBZ指令，判断是否为0，如果为0就跳转到locret_74FE4EEC处，查看R0寄存器的值不是0，继续往下走，如图22-27所示。

图22-27　查看寄存器值

看到了get_encrypt_str函数的调用，函数的返回值保存在R1寄存器中，查看内容为：zytyrTRA*B，那么看到，上层传递的：123456→zytyrTRA*B，前面静态分析了get_encrypt_str函数的逻辑，继续往下看，如下所示：

这里把上面得到的字符串和ssBCqpBssP作比较，那么ssBCqpBssP就是正确的加密密码了。那么现在的资源是：正确的加密密码为ssBCqpBssP，加密密钥库为zytyrTRA*BniqCPpVs，加密逻辑为get_encrypt_str。

可以写一个逆向的加密方法，去解析正确的加密密码得到值即可。为了给大家一个破解的机会，这里就不公布正确答案了。

加密apk下载地址：

http://download.csdn.net/detail/jiangwei0910410003/9531638

22.3.3　IDA调试的流程总结

到这里，就分析了如何破解apk的流程，下面来总结一下：

1）通过解压apk文件得到对应的so文件，然后使用IDA工具打开so文件，找到指定的native层函数。

2）通过IDA中的一些快捷键：F5、Ctrl+S、Y等，静态分析函数的ARM指令，大致了解函数的执行流程。

3）再次打开一个IDA来进行调试so文件。

4）将IDA目录中的android_server拷贝到设备的指定目录下，修改android_server的运行权限，用root身份运行android_server。

5）使用adb forward进行端口转发，让远程调试端IDA可以连接到被调试端。

6）使用IDA连接上转发的端口，查看设备的所有进程，找到需要调试的进程。

7）通过打开so文件，找到需要调试函数的相对地址，然后在调试页面使用Ctrl+S找到so文件的基地址，相加之后得到绝对地址，使用G键，跳转到函数的地址处，下好断点。点击运行或者点击F9键。

8）触发native层的函数，使用F8和F7快捷键进行单步调试，查看关键的寄存器中的值，比如函数的参数、函数的返回值等信息。

总结：在调试so文件的时候，需要双开IDA，采用动静结合的方式进行分析。

22.4　用IDA解决反调试问题

到这里就结束了本章的破解旅程了？答案是否定的，因为上面的例子其实是自己构建的，先写了一个apk，目的就是为了给大家演示。如何使用IDA来进行动态调试so文件，下面用一个操作动手的案例来讲解，这个案例解决了反调试问题。这是2014年阿里安全挑战赛的第二题：AliCrackme_2。

如图22-28所示，下面来看看破解流程。首先使用aapt命令查看其AndroidManifest.xml文件，得到入口的Activity类，如下所示：

图22-28　破解程序

然后使用dex2jar和jd-gui查看它的源码类com.yaotong.crackme.MainActivity，如下所示：

看到它的判断，securityCheck方法是native层的，所以这时候去解压apk文件，获取它的so文件，使用IDA打开查看native函数的相对地址11A8，如图22-29所示。

图22-29　查看函数的相对地址

这里的ARM指令代码不分析了，大家自行查看即可，直接进入调试即可，再打开一个IDA进行关联调试，如图22-30所示。

图22-30　附加进程

选择对应的调试进程，然后点击OK，如图22-31所示。

图22-31　查找需要调试的so文件

使用Ctrl+S键找到对应so文件的基地址74EA9000，和上面得到的相对地址相加得到绝对地址74EA9000+11A8=74EAA1A8。使用G键直接跳到这个地址，如下所示：

下个断点，然后点击F9运行程序，如图22-32所示。

图22-32　运行程序

这时候会发现IDA退出调试页面了，再次进入调试页面，运行，还是退出调试页面了。似乎没法调试了。

其实这里是阿里做了反调试侦查，如果发现自己的程序被调试，就直接退出程序。那么怎么知道有人调试呢？这里限于篇幅，只是简单介绍一下原理。

前面说到，IDA是使用android_server在root环境下注入到被调试的进程中，那么这里用到一个技术就是Linux中的ptrace，关于ptrace这里也不解释了，大家可以自行的去搜一下ptrace的相关知识。那么Android中如果一个进程被另外一个进程ptrace了之后，在其status文件中有一个字段TracerPid可以标识是被哪个进程trace了，可以使用命令查看到被调试的进行信息status文件在：/proc/[pid]/status，如下所示：

这里的进程被27445进程trace了，在用ps命令看看27445是哪个进程：

果不其然是android_server进程。知道原理了，也大致猜到了阿里在底层做了一个循环检测这个字段如果不为0，那么代表自己进程有人trace，那么就直接停止退出程序，这个反调试技术用在很多安全防护的地方。

那么下面就来看看如何应对这个反调试？刚刚看到，只要一运行程序，就退出了调试界面，说明，这个循环检测程序执行的时机非常早，那么现在知道的最早的两个时机是：一个是.init_array，一个是JNI_OnLoad。

.init_array是一个so最先加载的一个段信息，时机最早，现在一般so解密操作都是在这里做的。JNI_OnLoad是so被System.loadLibrary调用的时候执行，它的时机要早于哪些native方法执行，但是没有.init_array时机早。知道了这两个时机，下面先来看看是不是在JNI_OnLoad函数中做的策略，所以需要先动态调试JNI_OnLoad函数。

既然知道了JNI_OnLoad函数的时机，如果把检测函数放在这里的话，不能用之前的方式去调试了，因为之前的那种方式时机太晚了，只要运行就已经执行了JNI_OnLoad函数，所以就会退出调试页面，幸好这里IDA提供了在so文件load的时机，只需要在Debug Option中设置一下就可以了，在调试页面的Debugger选择Debugger Option选项，如图22-33所示。

然后勾选Suspend on library load/unload即可，如图22-34所示。

图22-33　设置Debugger选项

图22-34　勾选Suspend on library load/unload

这样设置之后，还是不行，因为程序已经开始运行，就在static代码块中加载so文件了，如图22-35所示。static的时机非常早，所以这时候，需要让程序停在加载so文件之前即可。

图22-35　static代码块中加载so文件

那么想到的就是添加代码waitForDebugger，这个方法就是等待debug，还记得在之前的调试smali代码的时候，就是用这种方式让程序停在了启动初，然后等待去用jdb进行attach操作。

这一次可以在System.loadLibrary方法之前加入waitForDebugger代码即可，但是这里不这么干了。还有一种更简单的方式就是用am命令，am命令本身可以启动一个程序，当然可以用debug方式启动：

这里一个重要参数就是-D，用debug方式启动：

运行完之后，设备是出于一个等待Debugger的状态，如图22-36所示。

图22-36　 程序处于等待调试状态

这时候，再次使用IDA进行进程的附加，然后进入调试页面，同时设置一下Debugger Option选项，然后定位到JNI_OnLoad函数的绝对地址，如图22-37所示。

但是发现，这里没有RX权限的so文件，说明so文件没有加载到内存中，想一想还是对的，因为现在的程序是wait Debugger，也就是还没有走System.loadLibrary方法，so文件当然没有加载到内存中，所以需要让程序跑起来，这时候可以使用jdb命令去attach等待的程序，命令如下：

图22-37　定位到JNI_OnLoad绝对地址

其实这条命令的功能类似于前一章中说到用Eclipse调试smali源码的时候，在Eclipse中设置远程调试工程，选择Attach方式，调试机的ip地址和端口，还记得8700端口是默认的端口，但是运行这个命令之后，出现了一个错误：

无法连接到目标的VM，那么这种问题大部分都出现在被调试程序不可调试。可以查看apk的android：debuggable属性：

果不其然，这里没有debug属性，所以这个apk是不可以调试的。需要添加这个属性，然后再回编译即可：

回编译：

签名apk：

然后再次安装，使用am命令启动。

1）运行命令：

出现Debugger的等待状态。

2）启动IDA进行目标进程的Attach操作。

3）运行命令：

4）设置Debugger Option选项。

5）点击IDA运行按钮，或者F9快捷键，运行。

看到了，这次jdb成功的attach住了，debug消失，正常运行了，但是同时弹出了一个选择提示，如图22-38所示。

图22-38　so文件选择框

这时候不用管它，全部选择Cancel按钮，然后就运行到了linker模块了，如下所示：

说明so已经加载进来了，再去获取JNI_OnLoad函数的绝对地址，如图22-39所示。

图22-39　获取JNI_OnLoad函数的绝对地址

用Ctrl+S查找到了基地址7515A000，用静态方式IDA打开so查看相对地址1B9C，如下所示：

绝对地址加上相对地址为7515A000+1B9C=7515BB9C，然后点击G键跳转，如图22-40所示。

图22-40　跳转到运行的内存地址

跳转到指定的函数位置，如下所示：

这时候再次点击运行，进入了JNI_OnLoad处的断点，如下所示：

下面就开始单步调试了，但是每次到达BLX R7这条指令执行完之后，JNI_OnLoad就退出了，如下所示：

经过好几次尝试都是一样的结果，发现这个地方有问题，可能就是反调试的地方了，再次进入调试，看见BLX跳转的地方R7寄存器中是pthread_create函数，这个是Linux中新建一个线程的方法。

阿里的反调试就在这里开启一个线程进行轮询操作，如图22-41所示，去读取/proc/[pid]/status文件中的TrackerPid字段值，如果发现不为0，就表示有人在调试本应用，在JNI_OnLoad中直接退出。关于反调试代码在前面已经详细介绍了。

图22-41　反调试代码

问题找到了，现在怎么操作呢？只要把BLX R7这段指令干掉即可，如果是smali代码的话，可以直接删除这行代码即可。但是so文件不一样，它是汇编指令，如果直接删除这条指令的话，文件会发生错乱，因为本身so文件就有固定的格式，比如很多段的内容，每个段的偏移值也是保存的，如果这样去删除会影响这些偏移值，会破坏so文件格式，导致so加载出错的，所以这里不能手动去删除这条指令。还有另外一种方法，就是把这条指令变成空指令，在汇编语言中，nop指令就是一个空指令，它什么都不干，所以这里直接改一下指令即可，arm中对应的nop指令是：00 00 00 00；那么看到BLX R7对应的指令位置为：1C58，如下所示：

查看它的Hex内容是37 FF 2F E1，如下所示：

可以使用一些二进制文件软件进行内容的修改，这里使用010Editor工具进行修改，如下所示：

直接修改成00 00 00 00，如下所示：

保存修改之后的so文件，再次使用IDA进行打开查看，如下所示：

指令被修改成了ANDEQ R0，R0，R0。

修改了之后，替换原来的so文件，再次重新回编译，签名安装。再次按照之前的逻辑给主要的加密函数下断点，这里不需要在给JNI_OnLoad函数下断点了，因为已经修改了反调试功能了，所以这里只需要按照如下简单几步操作即可：

1）启动程序。

2）使用IDA进行进程的attach。

3）找到Java_com_yaotong_crackme_MainActivity_securityCheck函数的绝对地址。

4）打上断点，点击运行，进行单步调试，如下所示：

这里可以单步调试进来了，说明修改反调试指令成功了。

下面就继续用F8单步调试，如下所示：

调试到这里，发现一个问题，就是CMP指令之后，BNE指令就开始跳转到loc_74FAF2D0处，那么就可以猜到了，CMP指令比较的应该就是输入的密码和正确的密码。再次重新调试，看看R3和R1寄存器的值，如下所示：

这里的R3寄存器的值就是用寄存器寻址方式赋值字符串的，这里R2寄存器就是存放字符串的地址，看到的内容是aiyou...但是这里肯定不是全部字符串，因为没看到字符串的结束符：'\0'，点击R2寄存器，进入查看完整内容，如下所示：

这里是全部内容aiyou，bucuoo。继续查看R1寄存器的内容，如下所示：

这里同样用寄存器寻址，R0寄存器存储的是R1中字符串的地址，看到这里的字符串内容是jiangwei，这个就是输入的内容，那么到这里就豁然开朗了，密码是上面的aiyou，bucuoo；再次输入这个密码，就可见到破解成功。如图22-42所示。

图22-42　破解成功效果图

提示：项目下载地址为

http://download.csdn.net/detail/jiangwei0910410003/9531957

注意：时刻需要注意BL/BLX等跳转指令，在它们执行完之后，肯定会有一些CMP/CBZ等比较指令，这时候就可以查看重要的寄存器内容来获取重要信息。

22.5　本章小结

本章主要介绍用IDA调试so文件，这也是一种全新的方式去破解native层的代码。现在有些程序依然把关键代码放在了Java层，那么可以使用Eclipse调试samli文件去破解；如果程序把关键代码放到native层，那么这时候，可以使用IDA来调试so文件去破解。当然破解和加密总是相生相克的。

第23章　逆向加固应用

本章主要介绍如何应对Android中一些apk加固的安全防护，在之前的两章中，可以看到一个是针对于Java层的逆向，一个是针对于native层的逆向，还没有涉及apk的加固，那么本章就要来介绍一下如何应对现在市场中一些加固apk的逆向方法，现在市场中加固apk的方式有两种：一种是对源apk整体做一个加固，放到指定位置，运行的时候再解密动态加载；还有一种是对so进行加固，在so加载内存的时候进行解密释放。本章主要介绍第一种加固方式，就是对apk整体进行加固。

23.1　逆向加固应用的思路

按照逆向惯例，用一个案例来分析讲解，这次依然采用的是阿里的CTF比赛的第三题，如图23-1所示。

图23-1　破解应用界面

题目是：要求输入一个网页的URL，然后跳转到这个页面，但是必须要求弹出指定内容的Toast提示，这个内容是：祥龙！

了解到题目，就来简单分析一下。这里大致的逻辑应该是，输入的URL会传递给一个WebView控件，进行展示网页。如果按照题目的逻辑的话，应该是网页中的JavaScript会调用本地的一个Java方法，然后弹出相应的提示。那么下面就开始操作。

按照之前的破解步骤：

第一步：先用解压软件解压出它的classes.dex文件，然后使用dex2jar+jd–gui查看Java代码，如图23-2所示。

这里只有一个Application类，这个apk可能被加固了，为什么这么说呢？因为一个apk加固，外面肯定得套一个壳，这个壳必须是自定义的Application类，它需要做一些初始化操作，那么一般现在加固的apk壳的Application类都喜欢叫StubApplication。这里可以看到，除了一个Application类，没有其他任何类了，包括入口Activity类都没有了，那么这时候会发现，无处下手了。

图23-2　应用的classes.dex文件

第二步：会使用Apktool工具进行apk的反编译，得到apk的AndroidManifest.xml和资源内容，如下所示：

反编译之后，看到程序会有一个入口的Activity，就是MainActivity类，记住一点，不管最后的apk如何加固，即使看不到代码中的四大组件的定义，也肯定会在AndroidManifest.xml中声明，因为如果不声明的话，运行是会报错的。这里还是没发现入口Activity类，而且知道它肯定是放在本地的一个地方，因为需要解密动态加载，所以不可能是放在网上的，肯定是本地。

这里的技巧如下：当发现apk中主要的类都没有了，肯定是apk被加固了，加固的源程序肯定是在本地，一般会有这么几个地方需要注意的：

·应用程序的assets目录，知道这个目录是不参与apk的资源编译过程的，所以很多加固的应用喜欢把加密之后的源apk放到这里。

·把源apk加密放到壳的dex文件的尾部，这个方式肯定不是本文的案例，但是也有这样的加固方式，遇到这种加固方式会导致用dex2jar工具解析dex失败，这时候就知道了，肯定对dex做了手脚。

·把源apk加密放到so文件中，这就比较难了，一般都是把源apk进行拆分，存到so文件中，分析难度会加大的。

一般都是这三个地方。记住一点，不管源apk被拆分，被加密了，被放到哪了，只要是在本地，都有办法得到它。

按照这上面的三个思路来分析一下，这个apk中加固的源apk放在哪了？通过刚刚的dex文件分析，发现第二种方式肯定不可能，那么会放在assets目录中吗？查看assets目录，如图23-3所示。

图23-3　应用的assets目录

assets目录中的确有两个jar文件，而且第一反应是使用jd-gui来查看jar，可惜的是打开失败，所以猜想这个jar是经过处理了，应该是加密，这里很有可能是存放源apk的地方。但是上面也说了还有第三种方式，去看看libs目录中的so文件，如图23-4所示。

图23-4　应用的libs目录

这里有三个so文件，而上面的Application中加载的只有一个so文件：libmobisec.so，那么其他的两个so文件很有可能是拆分apk文件的藏身之处。

通过上面的分析之后，大致知道了两个地方很有可能是源apk的藏身之处，一个是assets目录，一个是libs目录。那么分析完了之后发现现在面临两个问题：

·assets目录中的jar文件被处理了，打不开，也不知道处理逻辑。

·libs目录中的三个so文件，唯一加载了libmobisec.so文件了。

这里现在的唯一入口就是这个libmobisec.so文件，因为上层的代码没有，没法分析，下面来看一下so文件，如图23-5所示。

图23-5　so中的函数列表

这里没有特殊的方法，比如Java_开头的方法，所以猜测这里应该是自己注册了native方法，混淆了native方法名称。那么到这里，会发现遇到的问题用现阶段的技术是没法解决了。

23.2　获取解密之后的dex文件

分析完上面的破解流程之后，发现首要的任务是先得到源apk程序，通过分析知道，处理源apk程序很难找到，所以就要引出本章的内容了：使用动态调试，给libdvm.so中的函数dvmDexFileOpenPartial下断点，然后得到dex文件在内存中的起始地址和大小，然后dump出dex数据即可。

这里会有人提出几个问题。

第一个问题：为何要在dvmDexFileOpenPartial函数下断点？

不管之前的源程序如何加固，放到哪了，最终都是需要被加载到内存中然后运行，而且是没有加密的内容，那么只要找到dex的内存位置，把这部分数据弄出来就可以了，管它之前是如何加固的，那么问题就变成了，如何获取加载到内存中的dex的地址和大小。这要用到函数dvmDexFileOpenPartial，因为这个函数是最终分析dex文件，加载到内存中的函数：

第一个参数是dex内存起始地址，第二个参数是dex大小。

第二个问题：如何使用IDA给这个函数下断点

在前一章中介绍了动态调试so，下断点的时候必须知道一个函数在内存中的绝对地址，而函数的绝对地址是：这个函数在so文件中的相对地址+so文件映射到内存中的基地址。这个函数肯定是存在libdvm.so文件中的，因为一般涉及dvm有关的函数功能都是存在这个so文件中，那么可以从这个so文件中找到这个函数的相对地址，运行程序之后，再找到libdvm.so的基地址，相加即可。如何获取到libdvm.so文件呢？这个文件是存放在设备的/system/lib目录下的：

只需要使用adb pull命令把这个so文件弄出来就可以了。

解决了这两个问题，下面就开始操作了。

第一步：运行设备中的android_server命令，使用adb forward进行端口转发：

这里的android_server工具可以去ida安装目录中dbgsrv文件夹中找到：

第二步：使用命令以debug模式启动apk：

因为需要给libdvm.so下断点，这个库是系统库，所以加载时间很早。需要像之前给JNI_OnLoad函数下断点一样，采用debugger模式运行程序，这里通过上面的AndroidManifest.xml中得到应用的包名和入口Activity：

而且这里的android：debuggable=true可以进行debug调试的。

第三步：双开IDA，一个用于静态分析libdvm.so，一个用于动态调试libdvm.so。

通过IDA的Debugger菜单，如图23-6所示。进行进程附加操作，如图23-7所示。

图23-6　设置调试端口

图23-7　搜索附加进程

第四步：使用jdb命令启动连接attach调试器：

但是这里可能会出现这样的错误：

这是因为8700端口没有指定，这时候可以通过Eclipse的DDMS进行端口查看，如图23-8所示。

图23-8　查看进程调试端口

这里是8600端口，但是基本端口8700不在。有两种处理方式，一种方式是把上面的命令的端口改成8600，还有一种方式是选中这个应用，使其具有8700端口，如图23-9所示。

图23-9　设置8700端口

点击这个条目即可，这时候再运行上面的jdb命令：

处于等待状态。

第五步：给dvmDexFileOpenPartial函数下断点。

使用一个IDA静态分析得到这个函数的相对地址43308，如下所示：

在动态调试的IDA中进行解密，使用Ctrl+S键找到libdvm.so在内存中的基地址41579000，如图23-10所示。

图23-10　函数的基地址

然后将两者相加得到绝对地址43308+41579000=415BC308，使用G键，跳转，如图23-11所示。

图23-11　跳转到绝对地址

跳转到dvmDexFileOpenPartial函数处，下断点，如下所示：

第六步：点击运行按钮或者F9键运行程序。

之前的jdb命令就连接上了：

IDA出现一些提示界面，不要理会，一路点击Cancel按钮即可，如图23-12所示。

图23-12　加载so文件对话框

运行到了dvmDexFileOpenPartial函数处，如下所示：

使用F8键进行单步调试。这里需要注意的是，只要运行过了PUSH命令就可以了，记得不要越过下面的BL命令，因为没必要走到那里。当执行了PUSH命令之后，就是使用脚本来dump出内存中的dex数据了。这里有一个知识点：R0～R4寄存器一般是用来存放一个函数的参数值的，而dvmDexFileOpenPartial函数的第一个参数就是dex内存起始地址，第二个参数就是dex大小，如下所示：

这里就可以使用这样的脚本进行dump操作：

脚本不解释了，非常简单，而且这是固定的格式，以后dump内存中的dex都是用这段代码，将dump出来的dex保存到F盘中。

然后使用Shirt+F2键调出IDA的脚本，如图23-13所示。

图23-13　dump出dex文件

点击Run，这里可能需要等一会。运行成功之后，去F盘得到dump.dex文件。到这里，IDA使命就完成了，因为得到了内存的dex文件了，下面只要分析dex文件即可。

23.3　分析解密之后的dex文件内容

拿到dump.dex文件之后，使用dex2jar工具进行反编译，如下所示：

可惜的是，报错了，反编译失败，主要是因为有一个类导致的。开始我以为是dump出来的dex文件有问题，最后用baksmali工具得到smali文件是可以的，所以不是dump出来的问题，最后用baksmali工具将dex转化成smali源码：

得到的smali源码目录classout在C盘中，如图23-14所示。

图23-14　反编译之后的smali源码

得到了指定的smali源码了。

那么下面就可以使用静态方式分析smali。首先找到入口的MainActivity源码：

这里不解释了，肯定是找按钮的点击事件代码处，这里是一个btn_listener变量，看这个变量的定义：

是MainActivity$1内部类定义，查看这个类的smali源码，直接查看它的onClick方法：

这里可以看到，把EditText中的内容用Intent传递给WebViewActivity中，但是这里的intent数据的key是加密的。下面继续看WebViewActivity这个类：

直接查找onCreate方法即可，这里是初始化WebView，然后进行一些设置。这里看到一个@JavascriptInterface。

这个注解在使用WebView的时候都知道，它是用于JavaScript中能够访问而设置的，没有这个注解的方法JavaScript是访问不了的。

注意：这个注解是在SDK 17加上的，也就是Android 4.2版本中，那么在之前的版本中没有这个注解，任何public的方法都可以在JavaScript代码中访问，而Java对象继承关系会导致很多public的方法都可以在JavaScript中访问，其中一个重要的方法就是getClass（）。然后JavaScript可以通过反射来访问其他一些内容。那么这里就有这个问题了，比如下面的一段JavaScript代码：

这段JavaScript代码很危险，使用getClass方法得到这个对象（Java中的每个对象都有这个方法的），用这个方法可以得到一个Java对象，然后就可以调用这个对象中的方法了。这个也算是WebView的一个漏洞。所以通过引入@JavascriptInterface注解，则在JavaScript中只能访问@JavascriptInterface注解的函数。这样就可以增强安全性。

回归到正题，上面分析了smali源码，看到了WebView的一些设置信息，可以继续往下面看：

这里看到了一些重要的方法，一个是addJavascriptInterface，一个是loadUrl方法。知道addjavaascriptInterface方法一般的用法：

第一个参数是本地的Java对象，第二个参数是给JavaScript中使用的对象的名称。然后JavaScript得到这个对象的名称就可以调用本地的Java对象中的方法了。这里用如下代码：

将JavaScript中的名称进行混淆加密了，这也是为了防止恶意的网站来拦截后，然后调用本地的Java中的方法。

注意：这里又存在一个关于WebView的安全问题，就是JavaScript访问的对象的名称问题，比如现在程序中有一个JavaScript交互的类，类中有一个获取设备重要信息的方法，获取设备的imei，如果程序没有做这样名称的混淆的话，破解者得到这个JavaScript名称和方法名，然后就伪造一个恶意URL，来调用程序中的这个方法，比如这样一个例子：

然后再设置JavaScript名称：

就可以伪造一个恶意的URL页面来访问这个方法，比如这个恶意的页面代码如下：

运行程序如图23-15所示。

图23-15　调用JavaScript方法

恶意页面就成功地调用了程序中的一个重要方法。可以看到，对JavaScript交互中的对象名称做混淆是必要的，特别是本地一些重要的方法。

分析完了WebView的一些初始化和设置代码，而且知道如果要被JavaScript访问的方法，必须要有@JavascriptInterface注解，因为在Java中注解也是一个类，所以去注解类的源码看看那个被JavaScript调用的方法：

这里看到有一个showToast方法，展示的内容为\u7965\u9f99\uff01，在线转化一下，如图23-16所示。

图23-16　在线转化字符串

这里就是题目要求展示的内容。

到这里就分析完了apk的逻辑了，下面来整理一下：

1）在MainActivity中输入一个页面的URL，跳转到WebViewActivity进行展示。

2）WebViewActivity有Java Script交互，需要调用本地Java对象中的showToast方法展示消息。

问题：因为这里的Java Script对象名称进行了加密，所以这里自己编写一个网页，但是不知道这个Java Script对象名称，无法完成showToast方法的调用。

23.4　逆向方法

下面就来分析一下如何解决上面的问题。其实解决这个问题现有的方法太多了。

第一种方法：修改smali源码。

把上面的那个Java Script对象名称改成自己想要的，比如jiangwei，然后在自己编写的页面中直接调用jiangwei.showToast方法即可。不过这里需要修改smali源码，使用smali工具回编译成dex文件，再返回到apk中运行。方法是可行的，但是感觉太复杂，这里不采用。

第二种方法：利用WebView的漏洞。

直接使用如下Java Script代码即可。

这里根本不需要任何JavaScript对象的名称，只需要方法名就可以完成调用。可以看到这个漏洞还是很危险的。

第三种方法：调用加密方法。

看到了那个加密方法，自己写一个程序，来调用这个方法，既然上面已经得到正确的JavaScript对象名称，这里就采用这种方式。因为这个方式有一个新的技能，所以这里就讲解一下了。

如果用第三种方法的话，就需要再去分析那个加密方法逻辑了：

android.support.v4.widget.ListViewAutoScrollHelpern在这个类中，再去查找这个smali源码：

这个类加载了libtranslate.so库，而且加密方法是native层的，那么用IDA查看libtranslate.so库，如图23-17所示。

图23-17　native函数

搜一下Java开头的函数，发现并没有和decrypt_native方法对应的native函数，说明这里做了native方法的注册混淆，直接看JNI_OnLoad函数，如下所示：

这里果然是自己注册了native函数，但是分析到这里，就不往下分析了，为什么呢？因为其实没必要搞清楚native层的函数功能，知道了Java层的native方法定义，那么可以自己定义一个native方法来调用libtranslate.so中的加密函数功能，如图23-18所示。

图23-18　Demo工程

新建一个Demo工程，仿造一个ListViewAutoScrollHelpern类，内部再定义一个native方法。

然后在MainActivity中加载libtranslate.so：

然后调用那个native方法，打印结果如下：

这里的方法参数可以查看smali源码中的那个方法参数：

点击运行，发现有崩溃的，查看log信息，如下所示：

是由于libtranslate.so中有一个PagerTitleStripIcsn类找不到，这个类应该也有一个native方法，再构造这个类：

再次运行，还是报错，原因差不多，还需要再构造一个类TaskStackBuilderJellybeann：

好了，再次点击运行：

成功了，从这个log信息可以看出来，解密之后的Java Script对象名称是：SmokeyBear，那么下面就简单了，再构造一个URL页面，直接调用SmokeyBear.showToast即可。

注意：这里如果知道了Java层的native方法的定义，那么就可以调用这个native方法来获取native层的函数功能，这还是很不安全的。如何防止自己的so被别人调用呢？在前面章节中已经讲解了安全防护的知识，可以在so中的native函数做一个应用的签名校验，只有属于自己的签名应用才能调用，否则直接退出。

23.5　逆向测试

上面已经知道了Java Script的对象名称，下面就来构造这个页面了：

这里又有一个问题了，这个页面构造好了？放哪里呢？有的人可能有服务器，放到服务器上，然后输入URL地址就可以了，的确这个方法是可以的，但是有的人没有服务器怎么办呢？也有方法，知道WebView的loadUrl方法是可以加载本地页面的，所以可以把这个页面保存到本地，但是需要注意的是，这里不能存到SD卡中，因为这个应用没有读取SD的权限，可以查看它的AndroidManifest.xml文件：

在不重新打包的情况下，是没办法做到的，那么放哪里呢？其实很简单，可以放在这个应用的/data/data/com.ali.tg.testapp/目录下，因为除了SD卡位置，这个位置是最好的了，那么知道WebView的loadUrl方法在加载本地的页面的格式是：file：///data/data/com.ali.tg.testapp/crack.html；那么直接输入即可。

这里再介绍一个小技巧：在一个文本框中输入这么多内容，手动输入很烦琐，可以借助命令来实现输入，就是使用如下命令：

具体用法是，打开需要输入内容的EditText，点击调出系统的输入法界面，然后执行上面的命令即可：

不过这里有一个小问题，就是它不识别分号，如图23-19所示。

图23-19　文本框输入内容

直接修改成分号点击“进入”，运行成功。可看到toast的展示。

提示：项目下载地址为http://download.csdn.net/detail/jiangwei0910410003/9543445。

23.6　逆向加固应用的方法总结

破解的步骤比较多，下面来整理一下破解步骤。

1.破解的常规套路

按照破解惯例，首先解压出classses.dex文件，使用dex2jar工具查看Java代码，发现只有一个Application类，所以猜测apk被加壳（加固）了，然后用apktool来反编译apk，得到它的资源文件和AndroidManifest.xml内容，找到了包名和入口的Activity类。

2.加固apk的源程序一般存放的位置

知道加固apk后，那么就分析这个加固的apk放在哪里，肯定是存放在本地的一个地方，一般是三个地方：

·应用的asset目录中。

·应用的libs中的so文件中。

·应用的dex文件的末尾。

分析了之后，发现asset目录中的确有两个jar文件，但是打不开，猜测是经过处理了，所以得分析处理逻辑，但是这时候也没有代码，怎么分析呢？这时候就需要借助于dump内存dex技术了：不管最后的源apk放在哪里，最后都是需要经历解密动态加载到内存中的，所以分析底层加载dex源码，知道有一个函数dvmDexFileOpenPartial，这个函数有两个重要参数，一个是dex的地址，一个是dex的大小，而且知道这个函数是在libdvm.so中的。所以可以使用IDA进行动态调试获取信息。

3.双开IDA开始获取内存中的dex内容

双开IDA，用之前的动态破解so方式，来给dvmDexFileOpenPartial函数下断点，获取两个参数的值，然后使用一段脚本，将内存中的dex数据保存到本地磁盘中。

4.分析获取到的dex内容

得到了内存中的dex之后，使用dex2jar工具去查看源码，但是发现无法保存，以为是dump出来的dex格式有问题，但是最后使用baksmali工具进行处理，得到smali源码是可以的，然后就开始分析smali源码。

5.分析源码了解破解思路

通过分析源码得知，在WebViewActivity页面中会加载一个页面，然后那个页面中的JavaScript会调用本地的Java对象中的一个方法来展示toast信息。但是这里遇到了个问题：JavaScript的Java对象名称被混淆加密了，需要去分析那个加密函数，但是这个加密函数是native的，然后就是用IDA去静态分析了这个native函数，但是没有分析完成，因为不需要，其实很简单，只需要结果，不需要过程。现在解密的内容知道了，native方法的定义也知道了，那么就去写一个简单的demo去调用这个so的native方法即可，结果成功了，得到了正确的JavaScript对象名称。

6.了解WebView的安全性

WebView的早期版本的一个漏洞信息，在Android 4.2之前的版本WebView有一个漏洞，就是可以执行Java对象中所有的public方法，那么在JavaScript中就可以这么处理了，先获取getClass方法获取这个对象，然后调用这个对象中的一些特定方法即可，因为Java中所有的对象都有一个getClass方法，而这个方法是public的，同时能够返回当前对象。所以在Android 4.2之后有了一个注解@JavascriptInterface，只有这个注解标识的方法才能在JavaScript中调用。

7.获取输入的新技能

验证结果的过程中发现了一个技巧，就是在输入很长的文本的时候比较烦琐，可以借助adb shell input text命令来实现。

提示：

·通过dump出内存中的dex数据，可以佛挡杀佛了，不管apk如何加固，最终都是需要加载到内存中的。

·了解WebView的安全性相关知识，比如在WebView中JavaScript对象名称做一次混淆还是有必要的，防止被恶意网站调用我们的本地隐私方法。

·可以尝试调用so中的native方法，在知道了这个方法的定义之后。

·用adb shell input text命令来辅助输入。

23.7　本章小结

本章介绍了在Android中如何dump出那些加固的apk程序，核心方法就一个：不管上层怎么加固，最终加载到内存的dex文件肯定不是加固的，所以这个dex就是想要的。这里使用了IDA来动态调试libdvm.so中的dvmDexFileOpenPartial函数来获取内存中的dex文件，同时还可以使用gdb+gdbserver来获取，这个方法请自行搜索吧。前两章的内容，加上本章内容就是Android中大体的破解方式，当然这三种方式不是万能的，因为加固和破解是军备竞赛，不断升级，没有哪一方有绝对的优势。当然还有很多其他的破解方式，后面如果遇到的话，会再详细说明。

现在市场中的应用为了安全考虑采用了第三方平台进行应用的加固，加固的思想也是大同小异。在逆向加固应用的时候，其实只要遵循一个目标就是找到关键点函数，下好断点，dump出内存中的dex文件。问题就在于这个关键点函数了，这个关键点函数会很多，所以在逆向的时候需要多次进行尝试，而这些关键点函数也是随着逆向经验的增加而收集得更多。

第24章　逆向应用经典案例分析

本章介绍如何逆向市场中的一些加壳方案，现在市场中有很多加壳平台，这些加壳平台会做一些反调试操作，对so文件进行混淆加密等，让脱壳变得比较困难。本章将介绍加壳原理以及脱壳方法，并总结了脱壳经验。

24.1　加壳原理分析

先用一个案例来看看加壳原理。首先自己弄一个demo程序，然后去某加壳网站上加固一下，得到加固之后的apk，然后开始破解。

1.反编译apk

解压apk，看看大体的目录，得到classes.dex文件，然后用dex2jar+jd-gui得到Java源码，如图24-1所示。

图24-1　反编译之后的源码

看到这里只有Application的壳，而且这个是加固之后的特点，都是这两个Application的。

使用apktool来反编译apk，获取资源文件信息：

2.分析加密流程

加固程序把源程序进行加密操作然后隐藏到了一个地方，在之前一章中也说过了，隐藏的地方就那么几个：assets目录、libs目录、自己的dex文件。这里直接看assets目录，如图24-2所示。

图24-2　assets目录

多了这个文件，猜想这个可能就是处理之后的源apk了。在AndroidManifest.xml中看到了入口的SuperApplication类，下面来分析一下这个类：

这里一般都是在attachBaseContext方法中进行操作的，这里的时机比较早，首先会调用loadLibs方法进行加载libs：

这里区分不同的平台，然后拷贝不同的so文件，继续看copyLib方法：

可以看到，从assets目录下把增加的两个so文件libexec.so和libexecmain.so拷贝到应用程序的files目录下，可以去看看assets/ijm_lib目录下的so文件，如图24-3所示。

图24-3　so文件目录

到这里loadLibs方法就执行完了，下面就开始调用NativeApplication的load方法进行加载数据，继续看NativeApplication类：

开始从应用程序的files目录中加载这两个so文件，load方法也是一个native方法，继续看看这两个so文件内容。

首先用IDA打开libexecmain.so文件，如图24-4所示，但是发现，它里面并没有什么重要信息，连JNI_OnLoad函数都没有内容。

图24-4　IDA打开libexecmainso文件

继续查看libexec.so文件，如图24-5所示。

图24-5　IDA打开libexec.so文件

可惜的是，打开提示so文件格式错误，到这里就猜到了，这个so可能被加密处理了，ELF格式改了，点击Yes继续强制打开之后，再使用Ctrl+S查看so的各个段信息，如图24-6所示。

现在确定，没办法分析so文件了，分析到这里，也知道了大体加密流程：

1）按照惯例把源apk进行加密处理存放在一个地方，通过分析猜想是assets目录下的ijiami.dat文件。

图24-6　查看so中的段信息

2）添加壳Application：SuperApplication类在这个壳的attachContext方法中主要做了两件事：

·把assets/ijm_lib目录下的两个so文件copy到程序的files目录中。

·调用NativeApplication的load方法，在这个类中同时也把上面的两个so文件加载到内存中。

3）对apk的加密都是放在底层的两个so文件中操作的，通过IDA去分析这两个so文件之后，发现核心功能的so文件被加密了，IDA打开是看不到具体信息了。

到这里知道加固之后的特点是：在程序的assets目录下多了一个ijiami.dat文件和两个so文件，同时这两个so文件被加密处理了，增加破解难度。

24.2　脱壳过程

上面简单分析了加密的原理和流程，但是没有继续往下面分析了，因为这不是本章讲解的重点，本章的重点是如何脱掉加密的壳。脱壳的核心就一个：给dvmDexFileOpenPartial函数下断点，dump出内存的dex文件即可，那么下面就用IDA开始脱壳操作了。

第一步：启动设备中的android_server

进行端口转发：

第二步：用debug模式启动程序

代码如下：

这里的包名和入口Activity都可以在上面反编译之后的AndroidManifest.xml文件中找到：

第三步：双开IDA

一个用于静态分析libdvm.so，一个用于动态调试，如图24-7所示。

图24-7　获取函数相对地址

记录dvmDexFileOpenPartial函数的相对地址4777C，再次打开一个IDA，进行attach调试进程，如图24-8所示。

图24-8　附加进程

第四步：使用jdb命令attach上调试器

代码如下：

第五步：对dvmDexFileOpenPartial函数下断点

进入调试页面之后，用Ctrl+S键查找libdvm.so的内存基地址415BB000，如图24-9所示。

图24-9　获取so的基地址

将第三步中获取到的相对地址加上基地址4777C+415BB000=4160277C，得到了dvmDex-FileOpenPartial在内存中的绝对地址。

注意，这里还有一个更方便的办法，就是直接打开Modules View，如图24-10所示。

图24-10　Modules视图

在这里查找libdvm.so文件，如图24-11所示。

然后双击libdvm.so文件，如图24-12所示。

图24-11　查找libdvm.so文件

图24-12　获取函数的绝对地址

查找需要下断点的函数名称，看到这里的绝对地址也是4160277C。这里有两种方式可以得到一个函数在内存中的绝对地址。然后使用G键，直接跳转到函数处，下断点，如下所示：

第六步：设置Debugger Options选项

此操作能够让程序断在dvmDexFileOpenPartial函数处，如图24-13所示。

图24-13　设置Debugger Options选项

注意：上面的第四步、第五步、第六步没有顺序，只要在运行之前设置就可以了。

第七步：运行程序

出现这个对话框不要在意，一路点击Cancel即可，如图24-14所示。

图24-14　提示对话框

jdb也attach上了调试程序：

一直点击运行按钮，直到运行到dvmDexFileOpenPartial处的断点，但是可惜的是，这里遇到了错误，如图24-15所示。

图24-15　运行报错

点击OK之后，出现了下面对话框，如图24-16所示。

图24-16　报错对话框

再次点击任何一个按钮，都会退出调试页面，如图24-17所示。

图24-17　退出调试页面

再重新尝试一次上面的流程，开始调试，但是错误是一样的，到这里就立马想到了，之前第8章说的IDA调试so遇到的那个问题：反调试检测。其实这是现在加固平台必要选择的一种方式，反调试原理很简单，就是在程序运行最早的时机，比如so加载的时候即JNI_OnLoad方法中，读取本进程的status文件，查看TracerPid字段是否为0，如果不为0，那么就表示自己的进程被别人跟踪了，也就是attach了，这时候立马退出程序，下面使用IDA在attach进程成功之后，查看本进程的status信息：

看到这里的TracerPid为24336，不为0，表示被24336进程attach了，那么可以查看一下这个进程是谁：

这个进程就是在设备中安插的android_server，它用于和IDA进行通信。

到这里，可以看到应用做了反调试检测，按照上一章的内容，可以给JNI_OnLoad函数下断点，然后找到检测代码，把对应的ARM指令改成空指令，检测失效了，但是这里两个so文件被处理了，IDA没法分析了，那么该怎么办呢？

如何应对反调试呢？可以借助IDA修改寄存器和内存数据的特性来做到。首先上面分析了反调试的原理，一般在native代码去做检测的话，都是用fopen系统函数打开status文件，然后用fgets函数读取一行的内容，一般操作文件都是用fopen函数。

那么思路就有了，既然反调试肯定用到了fopen和fgets这两个函数，那么直接像给dvmDexFileOpenPartial下断点的方式一样，给这两个函数下断点，然后运行到fgets断点处的时候，发现如果是读取TracerPid这行内容的时候，就开始修改内存内容，把TracerPid字段的值改成0，或者修改R0寄存器的内容，跳过反调试检测。

这两个函数是在libc.so文件中的，可以把设备的/system/lib/libc.so使用adb pull到本地即可，然后用IDA得到相对地址，在调试页面得到基地址，然后相加得到绝对地址，跳转即可。但是这里不用这种复杂的方式，有两种方式可以进行跳转。

第一种方式：在Modules界面找到libc.so，然后再找到这两个函数，就可以得到它们的绝对地址了，如图24-18所示。

图24-18　查看fopen函数的绝对地址

然后使用G键，跳转下断点即可，如下所示：

第二种方式：也是最简单的方式，就是用G键，本身就有可以直接输入函数名进行跳转的功能，如图24-19所示。

图24-19　输入函数名跳转

下断点，如下所示：

到这里就给这两个函数下好了断点，当然这里还需要给dvmDexFileOpenPartial函数下断点，一切弄好了之后，这时候再次运行，如下所示：

停在了fopen断点处，使用F8键单步调试，看到R7寄存器中的内容是/proc/...，直接点击R7查看全部内容，如下所示：

内容有点长，大致的内容是/proc/self/cmdline.debug.atrace.app_cmdlines，这个是干什么的？看看这个目录内容：

发现没有这个文件内容，只有cmdline文件，但是可以先不管它了，知道这个肯定不是读取status文件的，那直接略过这个断点，点击F9键运行到下一个断点，中间过程先忽略，一路用F9键，直到运行到了fopen这个断点，如下所示：

果然，这里使用了fopen来读取status文件了，点击R7寄存器查看全部内容，如下所示：

这个16396就是本进程的id：

到这里，下一个断点肯定是fgets，所以点击F9键进入到fgets断点处，如下所示：

这里还看不到什么信息，继续点击F8键单步调试，如下所示：

途中，会看到有memchr和memcpy这两个重要函数，这个也是操作字符串的核心点，继续往下走，如下所示：

到了fgets函数结束的地方，看到了R0寄存器的内容是Name，点击R0查看全部内容，如下所示：

全部内容是Name：der.crackme0201，这就是status文件的第一行内容：

开始读取status文件的每行内容了，但是到TracerPid那行还要继续执行5次fgets函数，所以还会进入5次断点。为了节省时间，这里点击5次F9键，直接运行到读取TracerPid那行的内容的fgets断点处，如下所示：

看到关键的内容TracerPid字段了，这时候打开Hex View查看16进制的内存数据，如下所示：

但是看到，这并没有和调试页面View位置相对应，查看数据很费劲，所以可以这么操作，在寄存器窗口查看到R0寄存器的内容，如下所示：

这里就是TracerPid字段在内存的地址，记录一下，然后在Hex View页面中使用G键，进行跳转，一定要注意是在HexView，而不是调试页面，调试页面使用G键跳转到的是指令地址了，如图24-20所示。

图24-20　Hex View页面

这里看到TracerPid的内存内容了，就开始修改吧，选择要修改的内容：是11340那里，如图24-21所示。

图24-21　修改寄存器值

选择内容开始处，右击，选择Edit，进入修改状态，如下所示：

改了之后的内容是橘黄色的，修改完成之后，再点击右键，选择Apply changes，如图24-22所示。

完成修改，颜色变成灰土色了，如下所示：

注意：这里其实还可以直接修改寄存器R0的值，如图24-23所示。

图24-22　保存设置

图24-23　直接修改寄存器值

这时候就修改成功了，继续使用F8键单步调试下去，如下所示：

到这里就开始把TracerPid字段的值和0进行比较了，点击R0寄存器查看全部内容，如下所示：

这里的值已经被改成了0，所以就骗过去了。继续运行会发现，又进入了fopen函数的断点处，而且查看还是读取status文件。这个也不好奇，因为反调试检测肯定是一个轮询机制，所以肯定会反复地读取这个文件，fopen走多次也是正常的，但是这个反调试肯定是在子线程的，所以只要到了主线程中解密dex文件肯定到了dvmDexFileOpenPartial，这里会多次重复上面的操作，修改多次TracerPid的值。这里就不演示了。在操作的过程中修改了三次，当没有走fopen函数的时候，遇到了这个错误，这里不用关心，直接点击OK就可以了，如图24-24所示。

图24-24　警告对话框

再次点击运行，如下所示：

这里说明已经开始解密dex文件了，应该离成功不远了，继续运行，如下所示：

终于到了想要的地方了，到这里就好办了，直接点击Shirt+F2键，打开脚本运行窗口，运行下面脚本，如图24-25所示。

图24-25　运行脚本

把内存中的dex保存到F：\dump.dex中。这里R0寄存器就是dex在内存中的起始地址，R1寄存器就是dex文件的大小，如图24-26所示。

图24-26　查看dex文件的起始地址和文件大小

使用G键，可以在HexView页面中查看R0寄存器中的地址内容，如下所示：

这里就是dex的头文件格式。

得到了内存中的dex数据之后，可以使用baksmali工具转化成smali源码，查看代码逻辑即可，这里不再演示了。最后还有一步：还原apk。下一节介绍。

24.3　如何还原应用

首先修改反编译之后的AndroidManifest.xml中的内容：

把这段内容删除，如果有自己的Application的话，就改成自己的Application即可，同时删除assets目录下面的文件。然后使用apktool进行回编译，这时候，先不要着急签名apk，而是替换classes.dex，把上面得到的dump.dex改成classes.dex然后直接用压缩软件，替换未签名的apk中的dex文件即可，最后再进行签名操作，完成还原apk工作。

24.4　脱壳经验总结

到这里算是脱壳成功了，下面总结脱壳流程。

在脱壳的过程中，就一个目标：dump出内存中的dex文件。但是在上面分析了加固流程之后，发现它做了这些事：把源程序apk加密处理放到了assets目录下的ijiami.dat，也同时在assets\ijm_lib目录下添加两个so文件libexec.so和libexecmain.so，这里两个so文件用来处理整个apk解密，动态加载等逻辑，但是用IDA查看得知，这两个so文件被加密处理了。

首先，添加自己的壳Application即SuperApplication类，这个类中的attachContext方法中，首先把assets目录中的两个so文件copy到应用的files目录下，然后使用System.load方法，加载这个files目录中的两个so文件。

然后，在给dvmDexFileOpenPartial下断点进行调试的时候，发现有反调试检测，因为无法给JNI_OnLoad下断点来去除反调试功能的arm指令，所以只能去修改内存数据，把TracerPid的值变成0，骗过检测了。这里的思路就是给fopen和fgets这两个函数下断点，因为反调试的原理就是读取本进程的status文件，然后获取TracerPid那行内容即可，所以这里肯定用到了fopen和fgets函数，在使用fgets这个函数的时候，会读取每行内容，那么我们只要发现在读取到TracerPid那行内容的时候，去修改内存值，把TracerPid字段的值改成0即可。

有了上面的反调试思路之后，就开始进行操作了，但是在操作的过程中发现多次执行了fopen和fgets函数，而且需要修改多次TracerPid的值，原因很简单，因为是反调试检测，肯定是在子线程中轮询去检测这个值，会执行多次很正常，所以要修改多次TracerPid的值，骗过检测，直到当在主线程中，代码运行到了解密dex文件的时候，即到了dvmDexFileOpenPartial函数处的断点处为止。

最后修改多次TracerPid值，骗过检测，到了dvmDexFileOpenPartial这里，这时候，再执行dump脚本，把内存中的dex数据dump到本地即可。

通过上面的调试和破解流程其实不难发现，加密的流程是这样的：

1）fopen——/proc/self/cmdline.debug.atrace.app_cmdlines。

2）fgets——com.droider.crackme0201。

3）dvmLoadNativeCode——加载libexec.so。

4）dvmLoadNativeCode——加载libexecmain.so。

5）建立反调试线程（通过检查是否存在调试进程）。

6）调用fopen——打开/proc/pid/status。

7）调用fgets——读取调试进程pid。

这里除了dvmDexFileOpenPartial函数，还有一个重要的函数dvmLoadNativeCode，它是加载和初始化so的函数，感兴趣的读者可以去给这个函数下断点看看运行逻辑。

所以只需记住目的只有一个：到达dvmDexFileOpenPartial函数处，dump出内存中的dex文件，就算是完成脱壳工作了。

24.5　本章小结

本章用案例介绍如何脱掉某平台加密的壳，其实现在各个加固平台的原理都差不多，最后看到的就是各家的加固算法了，所以在脱壳的过程中目标也很明确，就是dump出内存中的dex文件即可。不管它上层用什么样的复杂的加密拆分操作，到了内存肯定是完整的dex文件，所以现在加固平台的应对方法也是一个思想，就是不让你dump出来，就是让你给dvmDexFileOpenPartial函数下断点失败，调试失败。

第25章　Android中常见漏洞分析

前几章介绍了Android开发中需要注意的安全问题以及如何做逆向工作，但是有时候光有这些知识还不够做到百分百安全，因为一个系统必定会有一些漏洞，而这些漏洞如果被恶意利用，也是非常危险的。本章介绍开发中比较常见的两个漏洞：文件解压漏洞和录屏功能漏洞。

25.1　解压文件漏洞分析

25.1.1　漏洞场景

Android开发过程中会有很多场景中使用到解压缩文件，比如动态加载，可能需要下载apk/zip文件，然后在本地做解压工作。还有一些资源在本地占用apk包的大小，就也打包成zip放到服务端，使用的时候再去服务端下载，本地做解压工作。在Android中解压zip文件，使用的是ZipInputStream和ZipEntry类，代码如下所示：

代码看上去很简单，但是这段代码中存在一个问题，就是zipEntry.getName方法，这个方法返回的是zip文件中的子文件名称，按照正常逻辑，解压这个子文件到本地都是直接用相同的名字即可，但是这里解压zip文件存在一个漏洞：zip文件中的子文件名格式没有格式要求，也就是可以包含特殊字符。但是在PC系统中文件名是有限制的，比如Windows中，如图25-1所示。在Linux中，如图25-2所示。

图25-1　Windows中文件命名限制

图25-2　Linux中文件命名限制

这些系统中是不允许文件名包含一些特殊字符的，而在ZipInputStream/ZipOutput-Stream类却是可以的。

也就是说使用ZipOutputStream类进行压缩文件，可以对文件名不做任何限制，压缩代码也简单：

只用ZipEntry类做单个文件压缩：

这里的ZipEntry可以指定随意的名称，而这个名称就是文件在zip文件中的文件名。那么如果zip包被人恶意的拦截，然后进行修改，这里可以使用ZipInputStream/ZipOutputStream类，写一个简单的小程序，就可以把恶意文件写入到zip中。

25.1.2　漏洞原因分析

因为文件名没有限制，所以攻击者可以把恶意文件名称命名为：../../../../data/data/xxx.xxx.x/hacker.dex，因为Android是基于Linux系统的，在Linux系统中../符号代表是回到上层目录，那么这里可以多写几个这个符号，这样就会回到Android系统的根目录，然后进入当前应用的沙盒目录下写一个文件了。当然这里只能写入本应用中，而不能写入其他应用。

存在的风险：比如现在知道了一个应用的沙盒数据的详细信息，一些隐私数据存放在SharedPreferences.xml中，或者有动态加载机制，需要加载的dex文件存放在一个目录中，这时可以利用这个漏洞，把几个恶意文件，命名改成../../../../data/data/xxx.xxx.xxx/shared_pref/info.xml，或者是../../../../data/data/xxx.xxx.xxx/dexfile/dynamic.dex。

这样在使用ZipEntry进行解压文件的时候，因为直接使用了ZipEntry.getName方法或者文件名，然后直接释放解压到本地了，所以就相当于替换了本应用的沙盒数据，这也是利用了App本身的权限来写入沙盒数据。

25.1.3　漏洞案例分析

上面分析完了漏洞，下面就用一个简单的例子来看看问题吧，从上面的分析知道，在本地系统中命名这样特殊格式的文件，在进行压缩成zip文件是不行的，因为系统不支持这种命名格式，所以这里需要写一个小程序，把这个特殊的文件名的文件压缩成zip，便于测试，然后再进行解压，如下所示：

先进行压缩文件，方便测试，产生一个demo.zip文件，然后再进行解压验证漏洞问题，这里验证问题的重点是：

这里把zip中需要解压的文件名前加上前缀：../../../data/data/cn.wjdiankong.androidzipleakdemo/这样解压之后的目录就在本应用的沙盒中了，注意../这样的符号不要太多，只要能回到根目录就可以了。

这里为了方便，直接使用echo命令，写入aaa内容到demo.txt文件中，然后点击压缩，再次查看，多了demo.zip文件：

这里就把zip中的恶意文件demo.txt释放到应用的沙盒中了，如果这个文件是dex或者其他文件，替换原来应用的一些重要文件，那么后果会很严重。

25.1.4　漏洞修复

上面了解到了漏洞产生的主要原因，由于ZipEntry在进行文件压缩时名称没有做任何限制，而在Android系统中../这种特殊符号代表的是回到上层目录，又因为这个解压工作在本应用中，可以借助App的自身权限，把恶意文件名改成：../../../data/data/...，即可在解压的时候把文件解压到了应用的沙盒中。

其实问题在于最后一步解压，因为不会去修改解压之后的文件名，默认都是直接解压即可。那么修复这个漏洞问题很简单了，有很多种方式，核心就是不要让有特殊字符，比如：../的文件成功解压，或者解压到本地文件名称不能包含这种特殊字符，代码如下：

上面分析了现在应用很多都有从服务端下载zip文件，然后在本地进行解压的功能，在解压的过程中默认不会去进行文件名的操作，所以就存在一些特殊字符的文件名被释放到了本地，从而产生安全问题。这里有一个前提，就是需要下载的zip文件被攻击者拦截到了，并且替换了。那么如果在下载的过程中就做一层安全其实也是可以的，比如使用HTTPS协议，然后再结合文件的MD5比对功能，就可以防止zip包被人拦截替换了。本地也就不会存在这样的风险了。

现在Android中动态加载技术和插件化开发已经很普遍了，但是在下载这个阶段一定要做好防护工作。如果你的项目现在正好有解压zip的功能，请尽快去审查一下项目中解压有没有做防护，没有的话就尽快加上吧！

25.1.5　漏洞总结

本章前面介绍了一个不起眼的漏洞，但是隐藏着很严重的问题，这个漏洞主要是因为如下几点综合产生的：

·ZipEntry对压缩文件名没有特殊要求。

·在Android系统中../特殊字符代表着回到上级目录。

·借助本应用的权限，把数据写入沙盒中。

关于这个漏洞的修复，Google一直没有做，只是在API中做了提醒：

可以看到Google对该方法给出了一个安全提示，提示开发者如果该方法的返回值中包含有“../”跳转符，需要特别注意不要将文件写到了目标文件夹之外。如果不对“../”跳转符做过滤，就有可能遍历目录，在解压zip文件时以本App的权限覆盖任意文件。

项目下载：http://download.csdn.net/detail/jiangwei0910410003/9594958

25.2　录屏授权漏洞分析

25.2.1　漏洞场景

在Android 5.0中新增了一个API来进行录制屏幕视频。在当今直播和游戏那么火的时代，录制也是非常不错的功能，现在很多应用就是利用这个API来做到游戏的录制效果的。在使用这个API的时候，系统会给一个授权提示，如图25-3所示。

图25-3　屏幕录制授权提示

这个对话框是系统弹出来的提示消息，主要由两部分组成，应用的名称+提示文案，看似是一个很平常的授权对话框，但是这背后却有一个很大的界面漏洞，危险性很大。当应用被授权了，那么就代表这个应用可以监听用户设备的屏幕信息。

Android系统中在弹出对话框的时候，如果内容过多，会采用滚动样式，来展示全部消息，而不是使用字符省略的方式。那么就存在一个问题，如果把应用的名称故意弄的很长，导致后面一段提示文案“将开始截取您的屏幕上显示的所有内容”给抵到底部，只有滑动的时候才能看到，这样来骗取用户点击“立即开始”。同时再把应用的名称改成一些银行或者是社交账号，支付应用的提示文案，比如：“××宝新增了特性...点击立即开始，即可体验！”，用户如果相信就开始点击了。当然这里还需要一个功能，就是监听系统的TopActivity，当监听到用户打开了银行App或者是社交App的时候，就去申请权限，弹出对话框！

下面通过MediaProjectionManager源码来分析一下，授权提示流程：在使用录制屏幕功能的时候，去授权页面调用的是createScreenCaptureIntent方法，获取授权Intent：

这里调用的是MediaProjectionPermissionActivity来进行授权：

这里直接用系统对话框展示了。

下面就来演示一个例子，比如现在恶意软件在后台监听到了用户启动了某某应用，然后就启动授权界面，如图25-4所示。

图25-4　恶意应用的授权界面

恶意App的名称为：

这就看到了提示信息，与打开银行App时给的提示信息很类似，用户会感觉很正常，如图25-5所示。一般弹出的提示对话框，用户不会去滑动看到底部，而关键提示信息就在底部。

图25-5　恶意App授权界面

如果用户点击“立即开始”就相当于授权了，那么恶意App就在后台偷偷录制你的屏幕，当你输入账号和密码的时候也会被记录，再把录制App发到服务端进行分析，账号就会被盗取！

25.2.2　漏洞原因分析

这个漏洞其实就是一个界面漏洞，在提示信息展示的时候没有做省略处理，从而把最重要的提示文案给隐藏了，导致用户被骗，同意授权。

该漏洞实际上是由于Google没有制定合理的Android应用名称规范导致，综合表现为如下两点：

·没有规范应用名称长度，使得应用名称可为任意长度。

·没有规范应用名称字符集，如应用名称可包含换行符和制表符。

25.2.3　漏洞修复

Google在后续的6.0系统中修复了这个问题，修复之后的效果如图25-6所示。

图25-6　修复之后的授权界面

采用了省略号代替了，不会把重要的提示信息给隐藏了。那么对于5.0系统的用户该怎么办呢？这个漏洞依旧存在，这就要求开发者做一些工作了，在Android中涉及用户隐私的Acitivity中（例如登录、支付等其他输入敏感信息的界面中）增加属性：WindowManager.LayoutParams.FLAG_SECURE

看一下属性源码说明：

该属性能防止屏幕被截图和录制。这个属性就是可以防止当前Activity不会被录制，可以做一个案例：

使用MediaProjection进行截图功能，如果加上了这个属性，再次截图，效果如图25-7所示。

图25-7　应用截图失败

看到下面的截图是一片漆黑，截图失败，而且这时候使用adb shell screencap命令去截图也是失败，如图25-8所示。

图25-8　应用截图失败提示

从这里可以看出，adb shell screencap和screenrecord命令底层的实现和MediaProjection是一样的。

在5.0的系统中，或者对于银行App、社交App、支付App等，开发者应该把当前登录的Activity添加这个属性！如果没有添加的话，就加上吧，给自己的App加上一份保险！

25.2.4　漏洞总结

下面来总结一下这个漏洞：

·漏洞产生的原因：因为Google在处理对话框提示的时候，没有做字符限制，导致一些重要的提示信息被遮挡。

·漏洞的危险：恶意App利用这个漏洞，把自己的App名称弄得很长，而且在后台监听用户打开了一些支付App、银行App之后，就弹出这个授权对话框，但是因为名称太长了，重要的提示文案被盖住了，用户很难发现问题，授权之后就被录制了重要信息。

·漏洞修复：Google在6.0以后进行了修复，使用字符限制功能，但是对于没有升级到6.0的用户，开发者就需要在自己的项目中给Activity添加安全属性，特别是登录页面，这样就可以防止当前页面被录制或者截屏了。

25.3　本章小结

本章主要分析了两个在开发中常见的漏洞：解压漏洞和录屏授权漏洞。开发者需要在开发应用中专门针对这两个漏洞进行处理。

第26章　文件加密病毒Wannacry样本分析

之前有一个Wannacry病毒样本在PC端肆意了很久，它是用RSA算法加密文件，勒索钱财，不给钱就删除内容。现在移动设备如此之多，就有一些不法分子想把这个病毒扩散到移动设备。本章就来分析一下这个病毒样本程序，并给出防范方法。

26.1　病毒样本分析

这类病毒一般是用特殊的App名称吸引用户下载，例如有一个叫做“魅影WIFI”的App，下载安装之后界面如图26-1所示。

图26-1　引诱APP

点击“免费激活”，出现授权界面，如图26-2所示。

图26-2　授权界面

如果点击“锁定屏幕”，就被锁屏了，解锁屏幕的界面如图26-3所示。

图26-3　解锁屏幕

病毒作者做了一个浮窗锁机，让用户通过QQ给钱。这时我们不得不看代码来找到这个密码。这个浮窗类型的锁机其实很容易解决，主要是借助WindowManager，所以连接adb使用命令直接杀掉这个进程即可：am force-stop pkgname。可以用dumpsys命令获取其包名：

然后再强制停止App：

停止之后，发现解锁屏幕了，但密码已经被篡改了。如果设备root了，直接删除/data/system/password.key文件即可。当然需要用户本身就是个开发者，“小白”用户肯定没办法。下面就来分析App，获取两个锁机密码。

26.2　获取密码

1.获取锁屏密码

因为我们知道现在大多数锁机软件都是利用设备管理器来修改设备密码的，所以想查看他的密码可直接用Jadx打开软件，然后全局搜索类DeviceAdminReceiver，如图26-4所示。

图26-4　用Jadx打开软件

看看这个类里面修改代码部分逻辑：

可以看到，这里直接将手机的锁机密码修改成9815了，这就是我们后面需要解锁的密码。

2.获取浮窗锁机密码

获取这个密码有点麻烦，因为是自定义的浮窗锁机，所以直接看到锁机界面文字，去Jadx全局搜索即可，这里全局搜索字符串“输入密码”，如图26-5所示。

图26-5　全局搜索字符串“输入密码”

进入下面这个类即可：

这里定义了一个Service，然后用WindowManager实现，再把权限设置为最高，用户就无法进行任何操作了。因为最终的密码输入都是在EditText，取密码作比较也要用到这个文本框，看这里的this.ed在哪里取值：

这里直接比对密码，如果密码正确了，就直接去掉服务，浮窗锁机就没有了。所以这里最重要的是decrypt方法，它是从SP中拿到key为passw的密文进行解密比对。其实这里我们可以借助Xposed工具直接hook这个decrypt方法，就能很轻易获取密码了：

然后直接运行就可以获取对应的密码了：

不过这里还想继续分析这个DES的加密逻辑，因为目的就是要学习的，所以我们继续手动分析这个加密算法，我们看看des变量定义：

这里会看到有一个du和du2变量，不要在意，可能是代码混淆原因，其实就是一个值。看到初始化传入一个字符串值，可能是DES加密的key值，然后立即对一个密文进行解密，之后的内容再作为新的DES的密钥值，相当于这里二次获取密钥了。看一下DU代码实现：

传入的字符串就是作为key进行加解密操作的，那么下面我们就需要手动写一个简单的DES加解密算法：

第一次初始化密钥是字符串"flower"，然后直接解密内容"c29fe56fa59ab0db"：

获取第二次要用到的密钥"xxx"，然后再初始化以下key：

我们需要去程序的XML中找到加密内容，再拷贝出来进行解密：

把这个串拷贝出来进行解密：

这就是解锁密码了，我们回过头看看是怎么把这个加密串存到XML中的：

这里生成密码竟然采用的是随机值，然后再加上123456，最后进行DES加密保存到XML中，同时也把随机数保存到XML中。从上面可以看到值是：

这个值加上123456就是密码了：92944926+123456=93068382，这也是上面我们解密之后的密码。如果“小白”用户加他QQ索要密码，其实他是需要让小白用户做点东西，因为这个密码是随机的，不是固定值，所以“小白”用户需要把程序的XML文件给他，不然他也不知道解锁密码。

26.3　文件解密

上面的锁机还不是重点，这个病毒的重点其实是文件加密。在本章开始的图26-1中，如果点击“注册使用”，会出现如图26-6所示的勒索界面。

图26-6　勒索界面

点击“注册软件”，会等一会儿，其实这里他在做一些坏事，然后出现提示界面，如图26-7所示。

这就是Wannacry病毒界面，需要付钱才能对文件解密。而这时手机设备SD卡中的文件已经被全部加密了，如图26-8所示。

最糟糕的是，可能SD卡都打不开了，这是因为病毒制作者无限制地新建一些文件和文件夹到SD中。可以发现，每个文件都是0字节，然后文件对应一个空文件夹。

图26-7　提示界面

图26-8　文件加密界面

下面来分析病毒代码：

进入这个类看看，在onCreate方法中看到几个方法：

继续追踪：

有一个可怕的“爆炸”方法：

在这里会创造出很多空文件和空文件夹，这样你的SD卡到最后几乎打不开了，手机也就废了。

文件加密在程序启动的时候就做了，在MainActivity中：

然后跟踪这个方法：

这里会过滤他自己创建出来的空文件和空文件夹不进行加密，继续看代码：

这里通过方法的最后一个参数来判断是加密文件还是解密文件。加解密方法参数都是类似的，第一个参数是AES加解密的密钥。到这里我们大致清楚了这个病毒是用AES对文件进行加密的，而密钥又用AES加密。这时加密的key和内容是：

分析到这里，我们就可以开始手动解密文件了。当然有很多种方法：

第一种方法：借助Xposed工具直接hook他的FormetFileSize方法，因为从上面的分析知道，这个方法的最后一个参数标志着是解密文件还是加密文件，拦截这个方法之后，修改这个参数状态为false表示是解密文件：

第二种方法：把他的解密功能代码拷贝出来，自己写一个解密程序，这比较适合给中招的“小白”使用：

因为他的代码这几个类都比较独立，所以直接拷贝出来不会有太多的错误，而错误就是变量定义重复，自己手动改一下即可：

下面就开始运行这个程序，这里为了操作不浪费时间，把SD卡清空了，然后写入三个简单的文件，让他加密：

这时会发现，他加密之后的文件名就是原始文件名凭借一个串而已，请忽略乱码。因为只有这样，他在解密之后才能获取原始文件，从他的代码中也可以看到这点：

然后运行我们的解密程序，看到日志：

到这里，我们就成功进行解密了。

26.4　病毒分析报告

这个病毒作者锁机采用了两套机制，而且浮窗锁机还用随机密码坑人。最可恨的是他竟然在SD中创建了那么多的空文件和空文件夹，让设备无法使用。最后他用的是AES算法对文件进行加解密操作。这里没有采用RSA加密的原因是RSA加密算法非常耗时且“吃内存”，手机会扛不住的。所以只好用AES了，但是对于解密来说只要知道密钥即可。这里解密用两种方式：一种是用Xposed框架进行hook，一种是自己把他的界面代码拷贝出来写一个解密程序。当然第二种是最优的，因为那些中招的“小白”用户是没有root的，也没有安装Xposed框架的，只有写一个解密程序给他安装进行解密就好了。不过有个很大的问题，就是因为病毒还创建了很多空文件和空文件夹，导致操作的时候需要区别对待。要做一次过滤，这些病毒自己生成的文件和文件夹就不要做解密了，不然会无限制死机。

从这个病毒中我们可以了解到关于锁机策略的机制，对于设备管理器的锁机直接找到DeviceAdminReceiver这个类就好了，或者直接看XML中的定义：

快速找到修改密码的地方，找到锁机密码即可。而对于浮窗锁机，因为都是采用Service和WindowManager来实现逻辑的，所以直接使用am命令强制停止程序运行即可。

26.5　本章小结

面对这个病毒，即使你付费了，病毒制作者帮你解密了，也是很麻烦的，因为病毒制作者创建了那么多文件，删除操作需要时间，解密还消耗机器性能。所以遇到这种病毒千万不要给钱，可以自己解密，或者刷机。最重要的一点就是千万不要去下载来历不明的App，去正规的应用市场下载应用是最保险的。