由于 Android 应用程序中的大部分代码使用 Java 语言编写,而 Java 语言又比较容易进行逆向,所以 Android 应用程序的自我保护具有一定的意义。本文总结了 Android 中可以使用的一些 APK 自我保护的技术,大部分都经过实际的代码测试。
classes.dex 文件是 Android 系统运行于 Dalvik Virtual Machine 上的可执行文件,也是Android 应用程序的核心所在,所以我们首先来看下 DEX 文件的结构,这样能够更好的理解后续的分析,需要更加详细的信息,可以参考 Google 关于 Dex 的技术文档。从 Java 源文件(当然 Android 也支持 JNI 的调用方式)到生成 Dex 文件的基本映射关系,如图 1 所示,Java 源文件通过 Java 编译器生成 class 文件,再通过 dx 工具转换为 classes.dex文件。Dex 文件从整体上来看是个索引的结构,类名、方法名、字段名等信息都存储在常量池中,这样能够充分减少存储空间,一个 Dex 文件的基本结构如图 2 所示,相关结构声明定义在 DexFile.h 中,在 AOSP 中的路径为/dalvik/libdex/DexFile.h。
header: Dex 文件头,包含 magic 字段、adler32 校验值、SHA-1 哈希值、string_ids 的个数
以及偏移地址等。Dex 文件头结构固定,占用 0x70 个字节,定义如下所示。
DexStringId: 定义了字符串数据的偏移, stringDataOff 指向字符串数据;
DexTypeId: 表示应用程序代码中使用到的具体类型,如整型、字符串等,在 Dalvik 字节 码中表示为 I、Ljava/lang/String;,descriptorIdx 指向 DexStringId 列表的索引;
DexProtoId:表示方法声明的结构体,shortyIdx 是方法声明字符串,格式为返回值类型后紧跟参数列表类型,如方法声明为VI,表示返回值为 V(空,无返回值),参数为 I(整型),所有的引用类型用 L 表示;returnTypeIdx 指向 DexTypeId 列表的索引,表示返回值类型;parametersOff 指向 DexTypeList 的偏移,表示参数列表类型;
DexMethodId: 表示代码中使用的方法, classIdx 表示方法所属的类, protoIdx 指向DexProtoId 列表索引,表示方法原型,nameIdx 表示方法名;
DexClassDef: 该结构相对要复杂一些,定义了代码中的使用的类,以及相关的代码指令。
classIdx 指向 DexTypeId 列表索引,表示该类的类型;accessFlags 是类的访问标志,如public,private,static 等superclassIdx 表示父类的类型;interfacesOff 指向一个 DexTypeList的偏移值,因为 Java 中可以实现多个接口,这里使用列表也就不难理解了;sourceFileIdx指向 DexStringIdx 列表的索引,表示类所在的源文件名称;annotationsOff 指向注解目录结构;classDataOff 指向 DexClassData 结构,表示类的数据部分;staticValuesOff 表示类中的静态数据。
DexClassData 结构体定义在 DexClass.h 文件中,路径为/dalvik/libdex/DexClass.h,声明如下,header 中包含静态字段个数,实例字段个数,直接方法(通过类直接访问的方法)个数,虚方法(通过类实例访问的方法)个数;
DexField 表示字段的类型和访问标志, fieldIdx 指向 DexFieldId;
DexMethod 结构描述了方法的原型、名称、访问标志以及代码指令的偏移地址,methodIdx指向 DexMethodId 索引,需要注意的是在 Google 的 Dex 文件文档中对此的定义:index into the method_ids list for the identity of this method (includes the name and descriptor),represented as a difference from the index of previous element in the list. The index of the first
element in a list is represented directly.
注意红色字体部分,表示的是在 Dex 文件中,methodIdx 是相对于前一个 DexMethod 中的 methodIdx 的增量,例如如果一个类中有两个 directMethods,第一个 directMethod 的methodIdx 值为 0x13,表示指向索引为 0x13 的 methodIdx,那么第二个 directMethod 的methodIdx 的值是相对于前一个值的增量,例如 0x01,表示指向索引为 0x14 的 methodIdx;accessFlags 为方法的访问标志,codeOff 表示指令代码的偏移地址;
DexCode 的结构体声明如下。
需要注意的是,在 DexClass.h 中,所有的 u4 类型,实际上是 uleb128 类型。每个 uleb128类型是 leb128 的无符号类型,每个 leb128 类型的数据包含 1-5 个字节,表示一个 32bit的数值。每个字节只有 7 位有效,最高一位用来表示是否需要使用到下一个字节,比如如果第一个字节最高位为 1,表示还需要使用到第 2 个字节,如果第二个字节的最高位为 1,表示会使用到第 3 个字节,以此类推,最多 5 个字节。对于一个 2 个字节的 leb128类型数据,其结构如图 3 所示。
此部分内容可参考 Playing Hide and Seek with Dalvik Executables。
前文分析了 Dex 文件的结构,根据 Dex 的文件结构,可以实现对 Dex 中特定方法的隐藏,这样在使用 baksamli 或者 apktool 工具对 classes.dex 文件进行反汇编时,无法发现隐藏的方法,不过会有特定的现象发生,其实也是比较容易检测出来的。
在 Dex 文件格式分析中关于 method 的结构体是 DexMethod,如果将 methodIdx 的值指向另一个 method,同时修改相应的代码偏移量 codeOff(accessFlags 一般不需要修改),修改后续相应的 methodIdx,则可以实现特定方法的隐藏。对 Dex 文件修改后需要重新计算 Dex文件的 SHA1 值以及校验值,用来更新 Dex 文件。
隐藏方法的步骤如下:
修改 Dex 文件中需要隐藏方法的 DexMethod 结构体,如图 4 所示,图中隐藏了方法B。具体包括:
[*]将 DexMethod 的 methodIdx 值设为 0x0,相当于将原先的方法指向了前一个方法;
[*]访问标志符 accessFlags 一般不需要修改,在 Dex 文件格式里,directMethods和 virtualMethods 是分开的;
[*]将 codeOffset 设置为前一个方法的代码偏移地址。
[*]更新需隐藏方法的下一个方法的 methodIdx,可以使用公式:
next_method_idx=original_next_method_idx + original_method_idx
[*]重新计算 Dex 的 SHA1 哈希值和 Adler 校验值,并用以更新 DexHeader,可以使用
DexFixer 修复 classes.dex 文件;
[*]重新打包生成 APK 文件:
[*]将 APK 解压缩,提取其中出 META-INF 文件夹之外的所有文件;
[*]压缩成 Zip 格式文件;
[*]使用 jarsigner 或者其他工具对生成的 Zip 文件签名,后缀名修改成.apk。
隐藏的方法仍然需要在程序中进行调用,调用隐藏方法的步骤如下:
[*]使用反射调用 android.content.res.AssetManager.openNonAsset 方法打开当前应用程序 的 classes.dex 文 件 , 将 数 据 保 存 到 内 存 中 ; 还 可 以 通 过 调 用Context.getPackageCodePath()来获得当前应用程序对应的 apk 文件的路径,利用此路 径 构 造 ZipFile 对 象 , 进 而 获 取 classes.dex 的 ZipEntry , 利 用 ZipFile 的getInputStream(ZipEntry)方法获取 classes.dex 的数据流,核心代码如下所示;
[*]修复 Dex 文件,将之前隐藏方法的 DexMethod 结构体恢复;
[*]将修复后的 Dex 数据使用类加载器重新加载;
[*]搜索被隐藏的方法;
[*]调用被隐藏的方法。
需要注意的是,方法在 Dex 文件中是按方法名的字典序排序的,所以需要隐藏的方法如果是该类中所有方法排序第一个的话,那么 methodIdx 值是个绝对值,如果要隐藏的话就不是很方便,所以建议可以写个无用的方法,其方法名排序为第一个,让需要隐藏的方法重新指向该方法。
使用修改 methodIdx 的方法,让其指向另一个 DexMethodId 的结构体,如果使用 baksmali进行反汇编,则会发现在一个类中有两个完全相同的函数。
那有没有更加隐蔽的手段来隐藏一个方法了?考虑到在 DexClassData 结构体中的DexClassDataHeader 头部,其中 directMethodsSize 和 virtualMethodsSize 分别表示直接方法个数和虚方法个数,因此如果希望隐藏某个方法,可以通过将相应的 directMethodsSize 或virtualMethodsSize 减 1,同时将表示该需要隐藏方法的 DexMethod 结构体中的数据全部修改为 0,这样就可以将该方法隐藏起来,使用 baksmali 反汇编时,不会显示出该方法的反汇编代码,具体可以参考 Hashdays 2012 Android Chanllenge。
当然,上述这两种隐藏方法,都没能隐藏掉 DexMethodId 结构体,这个结构体中包含了方法所属的类名、原型声明以及方法名,所以可以通过对比 DexMethodId 的个数和 DexMethod结构体的个数来判断是否存在方法隐藏的问题。
classes.dex 在 Android 系统上基本负责完成所有的逻辑业务,因此很多针对 Android 应用程序的篡改都是针对 classes.dex 文件的。在 APK 的自我保护上,也可以考虑对 classes.dex文件进行完整性校验,简单的可以通过 CRC 校验完成,也可以检查 Hash 值。由于只是检查classes.dex,所以可以将 CRC 值存储在 string 资源文件中,当然也可以放在自己的服务器上,通过运行时从服务器获取校验值。基本步骤如下:
[*]首先在代码中完成校验值比对的逻辑,此部分代码后续不能再改变,否则 CRC 值会发生变化;
[*]从生成的 APK 文件中提取出 classes.dex 文件,计算其 CRC 值,其他 hash 值类似;
[*]将计算出的值放入 strings.xml 文件中。
核心代码如下:
但是上述的保护方式容易被暴力破解, 完整性检查最终还是通过返回 true/false 来控制后续代码逻辑的走向,如果攻击者直接修改代码逻辑,完整性检查始终返回 true,那这种方法就无效了,所以类似文件完整性校验需要配合一些其他方法,或者有其他更为巧妙的方式实现?
虽然 Android 程序的主要逻辑通过 classes.dex 文件执行,但是其他文件也会影响到整个程序的逻辑走向,以上述 Dex 文件校验为例,如果程序依赖 strings.xml 文件中的某些值,则修改这些值就会影响程序的运行,所以进一步可以整个 APK 文件进行完整性校验。但是如果对整个 APK 文件进行完整性校验,由于在开发 Android 应用程序时,无法知道完整 APK 文件的 Hash 值,所以这个 Hash 值的存储无法像 Dex 完整性校验那样放在 strings.xml 文件中,所以可以考虑将值放在服务器端。核心代码如下:
Android 应用程序开发主要使用 Java 语言,Java 中可以使用反射技术来更加灵活地控制程序的运行,为 Java 运行时的行为提供了强大的支持。Java 反射机制允许运行中的 Java 程序对自身进行检查,并能直接操作程序的内部属性或方法,可动态生成类实例、变更属性内容以及调用方法。关于 Java 反射更详细内容可以参考 Java programming dynamics, Part 2:Introducing reflection。
在 Android 中使用反射技术来动态调用方法,可以增加对应用程序进行静态分析的难度。以下代码是使用 Java 反射的一个简单例子,需要使用反射调用的方法存在于 Reflection 类中。
以下代码完成对 Reflection 类中方法的直接调用和反射调用。
当然以上 Java 反射的例子过于简单,使用 dex2jar 反编译后,用 jd-gui 打开,还是能够很容易的识别出需要调用的方法,如图 5 所示。
图 5 使用 dex2jar+jd-gui 反编译结果
所以需要进一步采取措施增加静态分析的难度。反射调用需要获取调用的类名和方法名,而上述代码将需要调用的类名或方法硬编码在代码中,一方面违背了 Java 反射使用的场景,Java 反射主要是为了提供程序的运行时动态行为的控制,另一方面并没有增加了静态分析的难度。
可以根据程序运行过程中的实时状态来调用相应的方法,从而进一步提高静态分析的难度。一个可能的应用场景是:根据当前应用程序的状态,从网络服务器获取需要进行反射调用的方法以及参数信息。例如对于上述例子,类名和方法名都可以从网络获取。这样做的好处是使得仅仅通过静态分析无法获知程序运行过程中实际调用的方法,也会增加自动化分析的难度。也可以使用反射加密的方式,将类名、方法名做加密处理,在实际调用时再进行解密。当然以上两种处理方式可能对性能有较大影响(本身 Java 反射对性能就有一定影响),不应该频繁使用,而且必须申请网络连接的权限(不过现在凡是个 Android 应用程序,不申请个网络连接权限都不好意思说自己是个 Android 应用)同时还得需要接入网络。
动态加载
Android 系统提供了 DexClassLoader 来支持在程序运行过程中动态加载包含 classes.dex的.jar 或者.apk 文件,如果再结合 Java 反射技术,可以实现执行非应用程序部分的代码。利用动态加载技术,可以提供逆向分析的难度,在一定程度上可以保护 APK 自身的业务逻辑防止被破解。
DexClassLoader 的构造函数原型如下:
其中,dexPath 为包含 dex 文件的.apk 或者.jar 路径,optimizedDirectory 是优化后的 dex 文件的路径,libraryPath 表示 Native 库的路径,parent 是父类加载器。通过 DexClassLoader 实例化对象,调用 loadClass 加载需要调用的类,获得 Class 对象后,就可以进一步使用 Java 反射技术来调用相应的方法。如下:
上述代码实现调用 com.example.dexclassloaderslave.DexSlave 类中的 sayHello 方法。
对于需要通过 DexClassLoader 被调用的.apk 或者.jar 文件的分发,可以将其放入 Android项目的 assets 或者 res 目录下,也可以将其放在服务器端,在实际需要调用时通过网络获取文件。为了提高逆向的难度,可以对被调用的.apk 或者.jar 文件采取以下措施进行进一步的保护:
[*]进行完整性校验,防止文件被篡改;
[*]进行加密处理,在调用加载前进行解密;
[*]对需要调用的函数相关信息使用通过网络获取的方式,而不是硬编码在代码中,可
以真正实现动态调用,提高静态分析的难度;
[*]对于使用网络服务器分发的方式,注意对网络服务器地址的保护,不要以字符串硬
编码的方式写在代码中,对下载请求也需要使用 cookie 等辅助识别的技术。
除了使用 DexClassLoader 类实现动态加载外,还可以使用 dalvik.system.DexFile 类实现Dex 文件的加载,但是 DexFile 类提供的构造方法在实例化过程中需要在/data/davik-cache 目录下生成相应的 Dex 文件,而/data/davik-cache 目录对于一般应用程序是没有写权限的,所以在程序中无法实例化 DexFile 对象,也就无法调用 DexFile.loadClass 方法。所以需要通过反射调用 DexFile 类的 openDex 方法,具体可以参考该代码中 invokeHidden 函数。
字符串处理
Android 应用程序开发中难免会使用到字符串,如服务器的地址等一些敏感信息,对于这些字符串如果使用硬编码的方式,容易通过静态分析获取,甚至可以使用自动化分析工具批量提取。例如若在 Java 源代码中定义一个字符串如下:
则在反编译的.smali 代码中对应的代码如下(寄存器可能会有区别):
对于自动化分析工具,只需要扫描到 const-string 关键字就可以提取到字符串值。因此应该尽量避免在源代码中定义字符串常量,比较简单的做法可以使用 StringBuilder 类通过 append方法来构造需要的字符串,或者使用数组的方式来存储字符串。使用 StringBuilder 构造字符串反编译后的代码如下,使用这种方式可以增加自动化分析的难度,如果想要完整提取一个字符串,如果仅仅采用静态分析方法就必须要进行相应的词法语法解析了。
另外也可以对字符串进行加密处理,很多恶意代码就采用了此种方法,例如一些具有
bot 功能的恶意代码会将 C&C 服务器地址以及命令进行加密处理,运行时再进行解密。
代码乱序
为了增加逆向分析的难度,可以将原有代码在 smali 格式上进行乱序处理同时又不会影响程序的正常运行。乱序的基本原理如图 6 所示,将指令重新布局,并给每块指令赋予一个label,在函数开头处使用 goto 跳到原先的第一条指令处,然后第一条指令处理完,再跳到
第二条指令,以此类推。
以两个整数相加为例,Java 代码如下所示:
反编译后的 smali 代码如下所示
我们可以根据上述提到的代码乱序原理,将 test 这个函数乱序成如下代码所示(删除了.line):
最后使用 apktool 重新打包发布。进行代码乱序可以在一定程度上增加逆向分析的难度,例如可以使用 dex2jar+jd-GUI 工具来分析上述乱序前后的代码。乱序前代码如图 7 所示:
乱序后的代码如图 8 所示。从乱序前后的代码可以看出,使用代码乱序技术能够在一定程度上增加逆向分析的难度,当然这是因为 dex2jar 工具在进行代码解析时的问题,如果能够针对性的处理这种代码乱序的情况,那么这种反编译的情况应该会有所好转。关于代码乱序的技术,可以参考 ADAM:An automatic and extensible platform to stress test android anti-virus systems 、DroidChameleon:Evaluating Android Anti-malware against Transformation Attacks。
般在分析 APK 的过程中会借助于 Android 模拟器,比如分析网络行为,动态调试等。因此从 APK 自我保护的角度出发,可以增加对 APK 当前运行环境的检测,判断是否运行在模拟器中,如果运行在模拟器中可以选择退出整个应用程序的执行或者跳到其他分支。模拟器检测的手段有很多,下面逐一分析。
1.属性检测
Android 属性系统类似于 Windows 的注册表机制,所有的进程可以共享系统设置值。关于 Android 属 性 系 统 的 详 细 原 理 , 可 以 参 考 我 对 于 Android 属 性 系 统 的 分 析 文 章http://www.kanxue.com/bbs/showthread.php?t=182901。一些属性值在 Android 模拟器和真机上是不同的,例如对于 Nexus4 和 SDK 为 4.1.2 的模拟器来说,Build.BRAND 和 Build.DEVICE 属
性值分别如图 9 和图 10 所示。根据这些属性值在真实机器和模拟器上的差别可以比较容易的
检测 Android 应用程序是否运行在模拟器中。不过对于这种检测方式,绕过也是比较容易的,我现在想到的有 3 种方式绕过:
[*]在源码中修改相应的属性值,重新编译生成内核等镜像文件,再使用这些重新生成的镜像文件加载模拟器(对于 BRAND 属性值可以修改/build/target/product/generic.mk文件中的 PRODUCT_BRAND 值,重新编译过程没测试,是不是只需要修改这个值就能搞定不一定正确,可以参考下 build.prop 生成过程分析);
[*]修改 boot.img 文件;
[*]使用 Xposed 框架,可以 hook SystemProperties.get 函数,在 before 函数中检查需要 获取的属性,根据情况修改对应的值,然后返回;另外还可以通过检测 IMEI,IMSI 等值来判断是否是模拟器,在模拟器中,这两个值默认分别是 000000000000000 和 310260000000000。通过以下代码可以获取 IMSI 值:
TELEPHONY_SERVICE 需要申请 android.permission.READ_PHONE_STATE 权限。同样我们可以有相应的绕过方式,一个相对简单的方法是直接修改 Android SDK 下/tools/emulator-arm.exe 文件(Windows 版本)。使用 010Editor 打开 emulator-arm.exe 文件,搜索 CIMI,如图 11,”CIMI.”后面的 15 位数字值是 IMSI,”CGSN.”后面的 15 位数字为 IMEI,修改这两个值(确保没有运行模拟器),然后保存。
修改后再运行模拟器,此时查看 IMSI 值如所示,IMEI 值如所示,可见可以成功修改这两个值。
还存在其他的修改方式,可以参考 Hide the Emulator以及 Android emulator patch for configurable IMEI, IMSI and SIM card serial number。当然还可以检查一些其他的值,如电池的电池状态、电池电量,Secure.ANDROID_ID,DeviceId,手机号码等。
2.虚拟机文件检测
相 对 于 真 实 设 备 , Android 模 拟 器 中 存 在 一 些 特 殊 的 文 件 或 者 目 录 , 如/system/bin/qemu-props,该可执行文件可以用来在模拟器中设置系统属性。另外还有/system/lib/libc_malloc_debug_qemu.so 文件以及/sys/qemu_trace 目录。我们可以通过检测这些特殊文件或者目录是否存在来判断 Android 应用程序是否运行在模拟器中,关键代码如下:
更完整的代码可以参考 Tim Strazzere 的 Github 中 anti-emulator,该项目中还列举了其他一些
模拟器检测的方法,如检测 socket 文件/dec/socket/qemud。
3.基于 Cache 行为的模拟器检测方法
BlueBox 关于 Android 模拟器检测的方法
http://bluebox.com/corporate-blog/android-emulator-detection/
4.基于代码指令执行的模拟器检测方法
DexLabs 关于 Android 模拟器检测的方法
http://dexlabs.org/blog/btdetect
5.其他方法
其他一些检测方法,可以参考如下文献:
[*]DISSECTING THE ANDROID BOUNCER
[*]逃离安卓动态检测
[*]Guns and Smoke to Defeat Mobile Malware
[*]DEX EDUCATION 201 ANTI-EMULATION
[*]INSECURE MAGZINE 34 – Introduction to Android malware analysis
APK 伪加密
APK 实际上是 Zip 压缩文件,但是 Android 系统在解析 APK 文件时,和传统的解压缩软件在解析 Zip 文件时还是有所差异的,利用这种差异可以实现给 APK 文件加密的功能。Zip文件格式可以参考 MasterKey 漏洞分析的一篇文章。在 Central Directory 部分的 File Header 头文件中,有一个 2 字节长的名为 General purpose bit flags 的字段,这个字段中每一位的作用可以参考 Zip 文件格式规范的 4.4.4 部分,其中如果第 0 位置 1,则表示 Zip 文件的该 CentralDirectory 是加密的,如果使用传统的解压缩软件打开这个 Zip 文件,在解压该部分 CentralDirectory 文件时,是需要输入密码的,如图 14 所示。但是 Android 系统在解析 Zip 文件时并没有使用这一位,也就是说这一位是否置位对 APK 文件在 Android 系统的运行没有任何影响。一般在逆向 APK 文件时,会首先使用 apktool 来完成资源文件的解析,dex 文件的反汇编工作,但如果将 Zip 文件中 Central Directory 的 General purpose bit flags 第 0 位置 1 的话,apktool(version:1.5.2)将无法完成正常的解析工作,如图 15 所示,但是又不会影响到 APK 在Android 系统上的正常运行,如图 16 所示。
对 APK 文件进行伪加密可以使用这个脚本,在 Python 的 zipfile 模块中,ZipInfo 类中记录了 Zip 文件中相应的 Central Directory 的相关信息,包括 General purpose bit flags,在 ZipInfo类中属性为 flag_bits,因此上述脚本中将需加密的 APK 文件的每个 ZipInfo 的 flag_bits 和 1 做或操作,实现在 General purpose bit flags 的第 0 位置 1.而需要去除这些伪加密的标志的话,可以使用这个脚本。相关内容可以参考 BlueBox 之前提出的一个 Android Security Analysis Chanllenge.。
Manifest Cheating
AndroidManifest.xml 是 Android 应用程序的配置文件,包含了包名、应用程序名称、申请的权限信息以及组件信息等。在 Android 应用程序开发,生成 APK 时,aapt 会负责完成资源的打包,打包会将文本格式的 XML 资源文件编译成二进制格式的 XML 资源文件。将文本格式的 XML 文件转换成二进制格式,一方面通过字符串资源池的统一管理,减少文件体积;另一方面二进制格式的 XML 文件解析速度也会更快。在 Android 开发过程中,生成的 R.java文件中包含了相应的资源类型、名称以及对应的 id 值。资源 id 是 32bit 的整型值,格式为:0xPPTTNNNN。其中 PP 表示使用该资源的包,TT 代表该资源的类型,而 NNNN 是该类型中资源的名称。对于应用程序资源,PP 值固定为 7f,而对于被引用的系统资源包,其 PP值为 01。TT 和 NNNN 一般是 aapt 按照资源出现的顺序生成的。更多分析可以参考罗升阳的Android 应用程序资源的编译和打包过程分析。
Manifest Cheating 的基本原理是,在 AndroidManifest 的
[*]将 APK 解压缩,提取其中的 AndroidManifest.xml 文件;
[*]使用 axml 工具,修改二进制的 AndroidManifest.xml 文件,在 application 节点下插入id 未知(如 0x0),名为 name 的属性(值 可以任意,只要不对应到项目中的类文件名即可,如 some.class);
[*]将除 META-INF 文件夹之外的文件压缩成 zip 文件,签名后生成.apk 文件。若是攻击者使用 apktool 重打包,运行重打包后的文件会出现如下运行时错误:
在对 APK 逆向分析时,往往会采取动态调试技术,可以使用 netbeans+apktool 对反汇编生成的 smali 代码进行动态调试。为了防止 APK 被动态调试,可以检测是否有调试器连接。Android 系统在 android.os.Debug 类中提供了 isDebuggerConnected()方法,用于检测是否有调试器连接。可以在 Application 类中调用 isDebuggerConnected()方法,判断是否有调试器连接,如果有,直接退出程序。
除了 isDebuggerConnected 方法,还可以通过在 AndroidManifest 文件的 application 节点中加入 android:debuggable=”false”使得程序不可被调试,这样如果希望调试代码,则需要修改该值为 true,因此可以在代码中检查这个属性的值,判断程序是否被修改过,代码如下:
代码混淆
使用 Java 编写的代码很容易被反编译,因此可以使用代码混淆的方法增加反编译代码阅读的难度。ProGuard 是一款免费的 Java 代码混淆工具,提供了文件压缩、优化、混淆和审核功能。在 Eclipse+ADT 开发环境下,每个 Android 应用程序项目目录下会默认生成project.properties 和 proguard-project.txt 文件。如果需要使用 ProGuard 进行压缩以及混淆,首先需要在 project.properties 文件中去掉对如下语句的注释:
ProGuard 的相关配置信息需要在 proguard-project.txt 文件中声明,在其中可以设置需要混淆和保留的类或方法。由于在某些情况下,ProGuard 会错误地认为某些代码没有被使用,如在只在 AndroidManifest 文件中引用的类,从 JNI 中调用的方法等。对于这些情况,需要在proguard-project.txt 文件中添加-keep 命令,用来保留类或方法。关于 ProGuard 更加详细的配置项可以参考 ProGuard Manual。除了使用 ProGuard 对 Android 代码进行混淆外,还可以使用 DexGuard。DexGuard 是特别针对 Android 的一款代码优化混淆的收费软件,提供代码优化混淆、字符串加密、类加密、Assets 资源加密、隐藏对敏感 API 的调用、篡改检测以及移除 Log 代码。DexGuard 的进一步分析可以参考 JEB 上的相关 blog,可以在这里总结一下:
1.字符串加密
经过 DexGuard 加固过的 APK,对字符串的访问会通过调用一个解密函数来完成加密字符串的解密。如图 18 所示,红框中的字节数组是加密后的字符串,在 onCreate 函数中,调用了解密函数进行解密。字符解密函数如图 19 所示,对其进行处理后如图 20 所示,加密算法也很简单,基本思路是:当前字符由前一个字符加上加密字符数组中的字符,再减去常量8 形成,当字符长度达到给定的长度时,会最终构成字符串并返回。
2.assets 加密
APK 文件的 assets 目录下包含了应用程序需要使用到的资源文件,DexGuard 提供了对assets 资源文件的加密功能。对于一个经过保护的 asset 资源文件,例如 1.png 文件,使用十六进制查看器查看该文件,如图 21 所示。从图中可见,加密后的 png 文件,缺失了相应的文件头。解密则是首先通过反射调用 AssetManager.open 函数,同时对该函数的反射调用
又使用了加密处理,最后通过 Cipher 类完成 png 文件的解密。相关解密处理如所示。
关于 ProGuard 和 DexGuard 还可以参考 ProGuard and DexGuard,其中除了介绍了 ProGuard和 DexGuard,还提供了一些 APK 加固处理的方法。关于代码混淆,还可以参考 Android:Game of Obfuscation。
NDK
Android 软件的开发主要使用 Java 语言,但是 Android 也提供了对本地语言 C、C++的支持。借助 JNI,可以在 Java 类中使用 C 语言库中的特定函数,或在 C 语言程序中使用 Java类库。一般来说,如果代码中对处理速度有较高要求或者为了更好地控制硬件,抑或者为了复用既有的 C/C++代码,都可以考虑通过 JNI 来实现对 Native 代码的调用。由于逆向 Native 程序的汇编代码要比逆向 Java 汇编代码困难,因此可以考虑在关键代码部位使用 Native 代码,如注册验证,加解密操作等。一个可能的借助 Native 代码保护 APK的方法是:将核心业务逻辑代码放入加密的.jar 或者.apk 文件中,在需要调用时使用 Native代码进行解密,同时完成对解密后文件的完整性校验,不过不管是.jar 还是.apk 文件,解密后都会留在物理存储上,为了避免这种情况,可以使用反射技术直接调用dalvik.system.DexFile.openDex()方法,该方法接受 classes.dex 文件字节流返回 DexFile 对象。关于 Native 代码的编写,可以参考 Google 官方文档的 Android NDK。
逆向工具对抗
在逆向分析 Android 应用程序时,一般会使用 apktool,baksmali/smali,dex2jar,androguard,jdGUI 以及 IDA Pro 等。因此可以考虑使得这些工具在反编译 APK 时出错来保护 APK,这些工具大部分都是开源的,可以通过阅读其源代码,分析其在解析 APK、dex 等文件存在的缺陷,在开发 Android 应用程序时加以利用。可以参考 Tim Strazzere 的 Dex Education:Practicing SafeDex,相应的 Demo,看雪上的中文翻译,不过其中的很多技巧已经失效了。DexLabs 的 Dalvik Bytecode Obfuscation on Android 介绍了垃圾字节码插入的技术。
使用 apktool 进行重打包时,对于后缀为 png 的文件,会按照 png 格式的文件进行打包处理,因此如果在项目开发时,有意将一个非 png 格式文件的文件名改为后缀为 png 的文件,则使用 apktool 进行重打包时会出错。可以利用这种方法来对抗重打包。可以试试对这个文件使用 apktool 进行重打包,会报很多错误,但是这种 appt 导致的错误,很多都是由于第一个错误一起的,如图 23 所示。从第一个错误描述中可知,res/drawable-hdpi/station.png 不是
以上 APK 自我保护的技术并不能做到完全的保护作用,只是提高了逆向分析的难度,在实际运用中应该根据情况多种技术结合使用。这些技术其实很多来源于 Android 恶意代码,所以可以关注 Android 恶意代码中使用的一些技术来应用到自己开发的 Android 应用程序中。
注:本帖由看雪论坛志愿者PEstone 重新将pdf整理排版,若和原文有出入,以原作者附件为准
总结的一些关于APK自我保护的方法,当然还有很多其他的技巧,无法一一列举,现在可以使用的一些服务包括:
1.梆梆 :http://www.bangcle.com/
2.爱加密 :https://www.ijiami.cn/
3.APKProtect :http://www.apkprotect.com/
4.Shield4J :http://shield4j.com/
5.DexGuard :http://www.saikoa.com/dexguard
欢迎补充!
转自:https://bbs.pediy.com/thread-183116.htm