主要摘自于公司内一个同事的分享

混淆是什么

混淆是 “在 Android 项目进行打包时,将程序中的资源、代码以某种规则转换成功能上等价,但是难以阅读和理解的形式” 的行为。

混淆的作用

  1. 可以使得工程难以被逆向,增加反编译的成本,提升安全性
  2. 减小 Apk 的大小

混淆的分类

  1. 代码混淆
  2. 资源混淆

代码混淆

Java 代码混淆

ProGuard

早期Android中一般使用 ProGuard 进行Java代码的混淆:

Java 编译器将源代码编译为 Java 字节码(.class)。ProGuard 可以选择优化此代码,从而生成更小,更快的 Java 字节码。dx 编译器最终可以将此 Java 字节码转换为 Dalvik 字节码(.dex)。 Dalvik 字节码打包在 apk 文件中,最终安装在设备上。

R8

当使用 Android Gradle 插件 3.4.0 或更高版本编译项目时,该插件不再使用 ProGuard 执行编译时代码优化,而是使用 R8 编译器处理任务。

R8 是 D8 的衍生产品,旨在集成 ProGuard 和 D8(dx编译器的替代品) 的功能,一步到位地完成了所有的代码优化和转换成 Dalvik 字节码的过程。R8 和ProGuard 相比,R8 可以更快地缩减代码,同时改善输出大小。R8 支持所有现有 ProGuard 规则文件,因此在更新 Android Gradle 插件以使用 R8 时,不需要更改现有规则。

开启混淆

在 moudule 对应的 build.gradle 文件中找到下面的代码,将 minifyEnabled 修改为 true 即可在 release 包开启混淆。开启混淆开关后就可以在 proguard-rules.pro 文件中配置混淆内容。

1
2
3
4
5
6
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

开启混淆后:

  1. 压缩代码:从应用及其依赖的库中检测并安全地移除未使用的类、字段、方法和属性(这使其成为一个用来规避 64k 引用限制的非常有用的工具)。例如,如果工程里仅使用某个库依赖项的少数几个 API,压缩功能可以识别应用未使用的库代码,从应用中移除这部分代码。
  2. 混淆:缩短类和成员的名称,从而减小 Dex 文件大小。
  3. 优化 :检查并重写代码,以进一步减小应用 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,R8 便会移除 else {} 分支的代码。

混淆配置

上面说了,混淆配置是写在 proguard-rules.pro 文件中的,那么怎么写呢?Google 给了我们示例,在 android-sdk/tools/proguard/proguard-android.txt 中进行展示,有需要的可以去查看下官方建议配置,这里给出常用的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 指定代码的压缩级别
-optimizationpasses 5 // 是否使用大小写混合
-dontusemixedcaseclassnames // 是否混淆第三方jar
-dontskipnonpubliclibraryclasses // 混淆时是否做预校验
-dontpreverify // 混淆时是否记录日志
-verbose
// keep 保留类和类中的成员,防止它们被混淆或移除
-keep public class * extends android.app.Activity
-keep class com.bytedance.catower.** { *; }

// keepclassmembers 只保留类中的成员,防止它们被混淆或移除

// keepclasseswithmembers 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆

// keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除

// keepclassmembernames 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除

// keepclasseswithmembernames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆

做 SDK 的时候,我们往往需要告诉接入方要在 proguard-rules.pro 文件添加哪些配置。但是可以通过在 module 下的 consumer-rules.pro 文件中填写混淆配置,减少接入成本。

解码混淆后的堆栈信息

R8 每次运行时都会创建一个 mapping.txt 文件,其中列出了混淆过的类、方法和字段名称与原始名称的映射关系。此映射文件还包含用于将行号映射回原始源文件行号的信息。R8 将此文件保存在 <module- name>/build/outputs/mapping/<build-type>/ 目录中。

进入到 Android SDK 路径的 /tools/proguard/bin 目录中,运行proguardgui.sh 脚本(Window下为 proguardgui.bat),选择 ReTrace,并添加我们项目中混淆生成的 mapping.txt 文件以及混淆后的崩溃信息即可还原出我们的崩溃日志信息:

工作中也接触到一个需求,在端上实现堆栈(已经实现拦截错误堆栈的功能)反混淆的能力,这个只需要使用 Retrace 库 即可,核心代码如下:

我当时使用库的版本:implementation ‘net.sf.proguard:proguard-retrace:6.2.2’

Native 代码混淆

面仅仅是对 Java 代码的混淆,而 C/C++ 写的 native 代码却没有进行混淆。OLLVM 提供一套开源的针对 LLVM 的代码混淆工具,以增加逆向工程的难度。其提供了三种混淆方式:

利用 OLLVM 进行 Android native 代码混淆

  • 下载并编译ollvm
  • 配置ndk以支持ollvm
  • 使用ollvm进行编译

资源混淆

上面涉及到的仅仅是对代码进行的混淆,并不能对资源文件进行混淆。资源文件的命名可以使得破解者根据该名称揣测这个文件的用途,做到破坏性的修改,所以我们也需要对资源做混淆达到相对的安全。

背景知识

AAPT

AAPT(Android Asset Packaging Tool)。在打包过程中使用AAPT对APK中用到的资源进行打包,可以把资源编译为二进制文件,并生成 resources.arsc。下图是 AAPT 打包的流程:

AAPT这个工具在打包过程中主要做了下列工作:

  1. assetsres/raw 目录下的所有资源进行打包(根据不同的文件后缀选择压缩 or 不压缩)
  2. 编译 AndroidManifest.xml 成二进制的 XML 文件
  3. res/ 目录下的资源进行编译或者其他处理(具体处理方式视文件后缀不同而不同,例如:.xml 会编译成二进制文件,.png文件会进行优化等等)后才进行打包
  4. 对除了 assets 资源之外所有的资源赋予一个资源 ID 常量,并且会生成一个资源索引表 resources.arsc
  5. 把上述步骤中生成结果保存在一个 *.ap_ 文件(zip包),并把各个资源ID常量定义在一个 R.java

Android 查找资源过程

  1. assets 资源可直接使用 AssetManager 通过文件名来加载

  2. AAPT 工具在打包过程中生成的 Resources.arsc 文件是存放在 APK 包中的,其本身是一个资源的索引表,里面维护着资源 ID、Name 等对应关系,Resources 是通过 resources.arscResource 的 ID 转化成资源文件的名称,然后交由 AssetManager 来加载的,找到这个资源对应的文件或者数据

资源混淆核心处理过程

  1. 修改AAPT在处理资源文件相关的源码,对资源文件名字、资源文件路径进行混淆,并将映射关系输出到 mapping 文件中。例如将res/drawable/hello.png 混淆为 r/s/a.png
  2. 生成新的 resources.arsc 文件,并打包进 apk

生成混淆后的资源文件目录:

生成新的 resources.arsc

逆向介绍

工具文档:逆向工具/反编译工具 集合

这里主要想说一个我用过的一个工具:jadx,可以将 Apk 反编译,查看相关代码。(比如查看线上包是否带了不该带的代码)

下载好后,直接点击下图中的 jadx-gui 就会弹出一个页面让你选择 Apk,选好了就可以了。