虽然现在有App加固的方式,但是商业加固非常贵。免费加固的网上依旧有破解方式。混淆依旧是非常有用的。
另外,混淆下还包括一些优化,删除没有使用的代码和资源文件,以节省包的大小。
反编译的工具
之所以先讲反编译工具,是想方便你实践混淆的功能。因为jadx这个工具太好用了。把APK拖进去就直接帮你反编译好了。不像以前还要自己手动做这些事。
下载地址:
JADX download | SourceForge.net
需要预装java11或以上。
jadx 支持的格式: apk、dex、jar、zip、class、aar 文件。它都可以进行反编译。可见支持的格式挺多的。把文件拖拽进软件就可以了。
看看裸奔的代码反编译后是多么可怕的样子:
jadx还有些强大的功能:
1. 搜索(Navigation -> Text Search),有四种搜索方式:class method,field,code
2. Find Usgae(右键),和android studio使用一样。
3. deobfuscation( Tools -> deobfusation), 重构一个有意义的名字。
4. 一键导出 Gradle 工程( File -> Save as gradle project。工程是可以由Android Studio 打开的。但是大多数情况下编译不起来。但是这样的功能,主要是为了借助 AS 强大的 IDE 功能,例如方法跳转、引用搜索等等,让我们阅读起来更方便。
5. inconsistent code(File->Preferences)时常会有一些无法编译的代码段,显示的是绿色的字节码。这个功能可以解决这个问题。处理后显示的是比较友好的伪代码。
jadx有时会有卡顿问题,尤其当apk大小大于50M。
可以同过以下几种方式解决:
1、减少处理的线程数。可以在 Preferences 中,通过配置 Processing threads count 来配置线程数。
2、修改 jadx 脚本,增大内存
3、使用命令行命令
查看混淆后的代码堆栈
这个问题太重要,就拿到开头来说。初期很多人由于这个原因放弃了混淆。
针对这个问题可以直接用 /android-sdk/tools/proguard/bin/proguardgui.bat:
我们先造一个crash:
h2.m$a.a是混淆的。我们无法知道到底哪个类crash了。
现在我们使用proguardgui.bat
填入mapping.txt文件,这个文件记录了类名方法名等是如何替换的。
把crash日志考入Obfuscated stack trace。
点击右下ReTrace!
接下来就显出原形了!
打开混淆
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
将release
下 minifyEnabled
的值改为true
,打开混淆;
加上shrinkResources true
,打开资源压缩。
在Android Studio建一个默认的Andriod工程。打包后是4.8M,当开启混淆后就变成1.8M了。所以不仅能混淆代码,还能删除无用的代码和资源。
混淆规则
混淆参数的设置:
-optimizationpasses 5 # 代码混淆的压缩比例,值介于0-7,默认5
-verbose # 混淆时记录日志
-dontoptimize # 不优化输入的类文件
-dontshrink # 关闭压缩
-dontpreverify # 关闭预校验(作用于Java平台,Android不需要,去掉可加快混淆)
-dontoptimize # 关闭代码优化
-dontobfuscate # 关闭混淆
-ignorewarnings # 忽略警告
-dontwarn com.squareup.okhttp.** # 指定类不输出警告信息
-dontusemixedcaseclassnames # 混淆后类型都为小写
-dontskipnonpubliclibraryclasses # 不跳过非公共的库的类
-printmapping mapping.txt # 生成原类名与混淆后类名的映射文件mapping.txt
-useuniqueclassmembernames # 把混淆类中的方法名也混淆
-allowaccessmodification # 优化时允许访问并修改有修饰符的类及类的成员
-renamesourcefileattribute SourceFile # 将源码中有意义的类名转换成SourceFile,用于混淆具体崩溃代码
-keepattributes SourceFile,LineNumberTable # 保留行号
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod # 避免混淆注解、内部类、泛型、匿名类
-optimizations !code/simplification/cast,!field/ ,!class/merging/ # 指定混淆时采用的算法
不被混淆的参数设置:
语法结构:
[保持命令] [类] {
[成员]
}
保持命令:
-keep # 防止类和类成员被移除或被混淆;
-keepnames # 防止类和类成员被混淆;
-keepclassmembers # 防止类成员被移除或被混淆;
-keepclassmembernames # 防止类成员被混淆;
-keepclasseswithmembers # 防止拥有该成员的类和类成员被移除或被混淆;
-keepclasseswithmembernames # 防止拥有该成员的类和类成员被混淆;
类:
- 具体的类
- 访问修饰符 → public、private、protected
- 通配符(*) → 匹配任意长度字符,但不包含包名分隔符(.)
- 通配符(**) → 匹配任意长度字符,且包含包名分隔符(.)
- extends → 匹配实现了某个父类的子类
- implements → 匹配实现了某接口的类
- $ → 内部类
成员:
- 匹配所有构造器 → <init>
- 匹配所有域 → <field>
- 匹配所有方法 → <methods>
- 访问修饰符 → public、private、protected
- 除了 * 和 ** 通配符外,还支持 *** 通配符,匹配任意参数类型
- ... → 匹配任意长度的任意类型参数,如void test(...)可以匹配不同参数个数的test方法
常用自定义混淆规则范例:
# 不混淆某个类的类名,及类中的内容
-keep class com.nero.studyproguard.JsonConvertor { *; }
# 不混淆指定包名下的类名,不包括子包下的类名
-keep class com.nero.studyproguard*
# 不混淆指定包名下的类名,及类里的内容
-keep class com.nero.studyproguard* {*;}
# 不混淆指定包名下的类名,包括子包下的类名
-keep class com.nero.studyproguard**
# 不混淆某个类的子类
-keep public class * extends com.nero.studyproguard.base.BaseFragment
# 不混淆实现了某个接口的类
-keep class * implements com.nero.studyproguard.dao.DaoImp
# 不混淆类名中包含了"entity"的类,及类中内容
-keep class **.*entity*.** {*;}
# 不混淆内部类中的所有public内容
-keep class com.nero.studyproguard.widget.CustomView$OnClickInterface {
public *;
}
# 不混淆指定类的所有方法
-keep com.nero.studyproguard.example.Test {
public <methods>;
}
# 不混淆指定类的所有字段
-keep com.nero.studyproguard.example.Test {
public <fields>;
}
# 不混淆指定类的所有构造方法
-keep com.nero.studyproguard.example.Test {
public <init>;
}
# 不混淆指定参数作为形参的方法
-keep com.nero.studyproguard.example.Test {
public <methods>(java.lang.String);
}
# 不混淆类的特定方法
-keep com.nero.studyproguard.example.Test {
public test(java.lang.String);
}
# 不混淆native方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 不混淆枚举类
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#不混淆资源类
-keepclassmembers class **.R$* {
public static <fields>;
}
# 不混淆自定义控件
-keep public class * entends android.view.View {
*** get*();
void set*(***);
public <init>;
}
# 不混淆实现了Serializable接口的类成员,此处只是演示,也可以直接 *;
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 不混淆实现了parcelable接口的类成员
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
通用混淆配置:
#指定压缩级别
-optimizationpasses 5
#不跳过非公共的库的类成员
-dontskipnonpubliclibraryclassmembers
#混淆时采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#把混淆类中的方法名也混淆了
-useuniqueclassmembernames
#优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification
#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile
#保留行号
-keepattributes SourceFile,LineNumberTable
#保持所有实现 Serializable 接口的类成员
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
# 保持测试相关的代码
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
混淆注意事项
- jni方法不可混淆,方法名需与native方法保持一致;
- 反射用到的类不混淆,否则反射可能出问题;
- 四大组件、Application子类、Framework层下的类、自定义的View默认不会被混淆,无需另外配置;
- WebView的JS调用接口方法不可混淆;
- 注解相关的类不混淆;
- GSON、Fastjson等解析的Bean数据类不可混淆;
- 枚举enum类中的values和valuesof这两个方法不可混淆(反射调用);
- 继承Parceable和Serializable等可序列化的类不可混淆;
- 第三方库或SDK,请参考第三方提供的混淆规则,没提供的话,建议第三方包全部不混淆;