Android混淆研究

191 阅读8分钟

1 Android混淆原理

 

添加混淆的原因

Java代码是非常容易反编译的,作为一种跨平台的、解释型语言,Java 源代码被编译成中间"字节码"存储于class文件中。由于跨平台的需要,这些字节码带有许多的语义信息,很容易被反编译成Java源代码。为了很好地保护Java源代码,开发者往往会对编译好的class文件进行混淆处理。

混淆的原理

混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。ProGuard就是一个混淆代码的开源项目,能够对字节码进行混淆、缩减体积、优化等处理。
Proguard处理流程图如下所示,包含压缩、优化、混淆、预检四个主要环节:

图片.png  图1 Java代码混淆流程

 

Progurad不仅可代码混淆,还提供其他功能。主要有4个功能特性:

 

  1. 压缩:Proguard能通过分析字节码,能够检测并移除没有使用到的类、字段、方法和属性。

 

  1. 优化:优化java字节码,同时移除没有使用到的指令。

 

  1. 混淆:使用无意义的简短字母组合对类名、字段名和方法名进行重命名。

 

  1. 预检验:对上述处理后的代码进行预检验。

2 Android混淆规则

2.1 Android混淆的基本指令

代码混淆压缩比,在 0~7 之间
-optimizationpasses 5 (默认值为5不建议修改)

 

混合时不使用大小写混合,混合后的类名为小写。
-dontusemixedcaseclassnames

 

 指定不忽略非公共库的类和类成员

-dontskipnonpubliclibraryclasses -dontskipnonpubliclibraryclassmembers 

 

混淆时是否记录日志
-verbose

不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度 -dontpreverify

-dontpreverify

保留Annotation不混淆
-keepattributes Annotation,InnerClasses

避免混淆泛型
-keepattributes Signature

抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

指定混淆是采用的算法,后面的参数是一个过滤器
这个过滤器是 Google 推荐的算法,一般不做修改
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/ ,!class/merging/

混淆前后的映射,这个文件在追踪异常的时候是有用的
-printmapping proguard/mapping.txt

2.2 Android混淆的KEEP指令

-keep keep用来指定哪些元素不进行混淆,它有很多变种,比如: -keep 可以保留指定的类名以及成员。

-keepclassmembers  保留指定的类成员不被混淆,但包名类名会被混淆。
该配置只保护类的成员不被压缩和混淆。也就是说,如果类未被使用,则删除类。如果类被使用,保留并重命名该类。类里的成员维持不变,仍然是之前的名字。

-keepclasseswithmembers  可以根据成员找到满足条件的所有类而不用指定类名(这样的类必定拥有所列出的所有成员),可以保留类名和成员名。符合条件的类名不会被重命令。符合条件的方法即使没有调用关系也会被保留。当时其他的
方法如果没有调用关系会被移除。
当未配置-dontshrink(该配置是关闭压缩功能,也就是不会删除未使用的元素,未配置时,也即是开启压缩功能)时,以上3个配置指定的元素即使未使用过,也不会被删除。 以下3个命令与以上3个命令对应,区别是在上述情况中,指定的元素未使用过就会被删除。

-keepnames 也可以写成-keep,allowshrinking 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。该配置压缩类及成员,但不混淆它们。也就是说,未使用的代码将被移除。剩下的代码则维持原状。

-keepclassmembernames 也可以写成-keepclassmembers,allowshrinking
只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。

-keepclasseswithmembernames 也可以写成
-keepclasseswithmembers,allowshrinking
可以根据成员找到满足条件的所有类而不用指定类名(这样的类必定拥有所列出的所有成员),可以保留类名和成员名 当时成员没有引用的时候,方法会被移除。

2.3 Android混淆匹配规则

 

过滤器列表
访问修饰符(public、protected、private) extends,即可以指定类的基类 implement,匹配实现了某接口的类 $,内部类
"成员"代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:  匹配所有构造器  匹配所有域  匹配所有方法 通配符*,匹配任意长度字符,但不含包名分隔符(.) 通配符* ,匹配任意长度字符,并且包含包名分隔符(.) 通配符**,匹配任意参数类型 …,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 这些方法。 访问修饰符(public、protected、private)

-keep class com.chinatsp.test.**
-keep class com.chinatsp.test.*

一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了 -keep class com.chinatsp.test.* {*;} 在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extend,implement等这些Java规则。如下例子就避免所有继承Activity的类被混淆 -keep public class * extends android.app.Activity 如果我们要保留一个类中的内部类不被混淆则需要用$符号,

如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。

-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
public *;
}

再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用

; //匹配所有构造器
; //匹配所有域
; //匹配所有方法方法

你还可以在或前面加上private 、public、native等来进一步指定不被混淆的内容,如

-keep class com.chinatsp.test.One {
public ;
}

表示One类下的所有public方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆
-keep class com.chinatsp.test.One {
public (org.json.JSONObject);
}

有时候你是不是还想着,我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers 。

常用到的不混淆

 

1,jni方法不可混淆,因为这个方法需要和native方法保持一致;

-keepclasseswithmembernames class * { # 保持native方法不被混淆
native ;
}

需要特别注意的是JNI方法包括了java调用c/c++方法和c/c++调用
Java的两种情况。其中java调用c/c++方法又包括静态注册和动态注册。对于jni的混淆处理

我们在实际的工作中也是
遇到了该问题。

电话应用使用过程中遇到的crash

-------- beginning of crash 09-25 05:18:33.182  2013  2013 F DEBUG   : ** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ****** 09-25 05:18:33.183  2013  2013 F DEBUG   : Build fingerprint: 'Android/bordrinb31/msm8996_gvmq:8.1.0/YT.CA.17101.01100/128:userdebug/test-keys' 09-25 05:18:33.183  2013  2013 F DEBUG   : Revision: '0' 09-25 05:18:33.183  2013  2013 F DEBUG   : ABI: 'arm' 09-25 05:18:33.183  2013  2013 F DEBUG   : pid: 1111, tid: 1111, name: chinatsp.dialer  >>> com.chinatsp.dialer <<< 09-25 05:18:33.183  2013  2013 F DEBUG   : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr ------- 09-25 05:18:33.191  2013  2013 F DEBUG   : Abort message: 'jni_internal.cc:593] JNI FatalError called: RegisterNatives failed for 'com/chinatsp/proxy/iscmanager/ISCManager'; aborting...'
下面我们来分析出现该问题的原因。

ISCManager由于源码较多 只贴出OpenGrok的地址。
http://10.100.193.141:8080/source/xref/Bordrin_B31/hqx/LINUX/android/vendor/chinatsp/domainServices/TServiceInterface/src/com/chinatsp/proxy/iscmanager/ISCManager.java

ISCManager在反编译代码如下:

图片.png                   图2 ISCManager在启用混淆之后反编译的代码

从crash 日志之中我们发现主要的错误是jni注册本地方法的时候报错。
JNI FatalError called: RegisterNatives failed for 'com/chinatsp/proxy/iscmanager/ISCManager'; aborting...'
我们在OpenGrok上面找到c/c++方法中调用ISCManager 的类是

com_chinatsp_proxy_iscmanager_ISCManager.cpp
opengGork地址如下。
http://10.100.193.141:8080/source/xref/Bordrin_B31/hqx/LINUX/android/vendor/chinatsp/services/ISC/jni/com_chinatsp_proxy_iscmanager_ISCManager.cpp
在该类的jni注册方法如下:
197 * JNI registration.198 /199static const JNINativeMethod gMethods[] =200{201 {"nativeSendMessage", "(I[BI)I", (void )nativeSendMessage},202 {"nativeOpenChannel", "(I)I", ( void )nativeOpenChannel},203 {"nativeCloseChannel", "(I)I", ( void* )nativeCloseChannel},204 {"nativeInit", "()V", ( void* )nativeInit},205 {"nativeRelease", "()V", ( void* )nativeRelease},206 {"nativeRegisterCallback", "()V", ( void* )nativeRegisterCallback},207 {"nativeUnregisterCallback", "()V", ( void* *)nativeUnregisterCallback}208};209

对比混淆之后反编译的源码,发现nativeRegisterCallback,nativeUnregisterCallback

 
这两个方法由于没有外部调用关系在压缩阶段被移除了,导致c/c++注册jni方法过程中报错。

进一步研究源码发现该C++代码之中还会调用ISCManager类中的transmitMsgFromNative方法,该方法同样由于没有调用关闭在压缩阶段被移除了。
93 clazz = env>FindClass("com/chinatsp/proxy/iscmanager/ISCManager");94 if (clazz == NULL) {95 ALOGE( "ISC get ISCManager class failed");96 return;97 }9899 transmitMsgMethod = env>GetMethodID(clazz, "transmitMsgFromNative", "(I[BZ)V");

当前解决该问题的方法是不对ISCManager进行混淆处理,在proguard.flags
文件之中加入如下代码。
-keep public class com.chinatsp.proxy.iscmanager.ISCManager { *; }
在重新编译之后,电话应用就没有再发生混淆引起的相关异常了。

2,反射用到的类不混淆(否则反射可能出现问题);

 

3,AndroidMainfest中的类不混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆。自定义的View默认也不会被混淆;所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的;

 

4,与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;

 

5,使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;

 

6,有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;

 

7,Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;

-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆
public static final Android.os.Parcelable$Creator *;
}

8,使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。
-keepclassmembers enum * {
public static ** values();
public static ** valueOf(java.lang.String);
}

3 Android源码工程使用混淆

由于网上的资料多是Android Studio工程如何使用混淆。我们公司则多是Android源码环境下开发。下面主要介绍Android源码工程如何使用混淆。
在Android.mk文件中,用LOCAL_PROGUARD_ENABLED来配置混淆的模式;LOCAL_PROGUARD_FLAG_FILES用来指定配置文件。

LOCAL_PROGUARD_ENABLED的取值如下:

 

full:使用编译系统默认的配置:压缩但不混淆和优化,默认的混淆配置文件是build/core/proguard.flags custom:和full一样,但不包括aapt生成的resource相关的混淆配置。

 

nosystem:不使用系统的默认配置,但使用aapt生成的resource相关的混淆配置,其他混淆由模块自己负责。

 

disabled:关闭混淆 obfuscation:和full一样,并且开启混淆 optimization:和full一样,并且开启优化 不设置时,如果是app,默认为full,如果是library,则默认为disabled。 编译userdebug版本时,编译脚本会把app的obfuscation改成full,即不混淆;所以userdebug版本的app是不混淆的。

对于目前我们使用混淆的方式就是在相应应用的Android.mk添加如下两项

LOCAL_PROGUARD_ENABLED := full obfuscation

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

同时在同级目录创建proguard.flags文件在里面添加相应的混淆规则。
在我们前期使用EventBus的时候,曾经遇到过EventBus无法正常的工作的清况。经过研究发现是因为我们在Android.mk文件里面没有配置混淆相关设置。
由于系统默认会开始压缩功能。EventBus接受事件的代码并没有调用关系。在优化过程中被移除。所以功能无法正常使用。

图片.png

图3 编译前的TestActivity源码

图片.png

图4 编译之后经过反编译之后的TestActivity代码

对比编译前的源码和编译后反编译的代码,我们明显的发现编译之后EventBus
的接受事件的方法

onEvent(SyncStateEvent event) { }

被移除了。

对于该问题的处理我们是在userDebug的版本加入
LOCAL_PROGUARD_ENABLED := disabled
来关闭压缩和混淆功能。
在user版本中加入对EventBus框架的混淆处理。在proguard.flags文件中加入
如下处理。

-keepattributes *Annotation*  
-keepclassmembers **class** * {  
    @org.greenrobot.eventbus.Subscribe <methods>;  
}  
-keep **enum** org.greenrobot.eventbus.ThreadMode { *; }  
# Only required if you use AsyncExecutor  
-keepclassmembers **class** * **extends** org.greenrobot.eventbus.util.ThrowableFailureEvent {  
    <init>(java.lang.Throwable);  
}

通常知名的第三方开源框架都会提供相应的混淆代码。用户只需要在github或者其官网上面查找即可。
我们的应用推荐使用如下配置,该配置在userDebug,Eng版本关闭混淆功能方便开发调试。在user版本启动配置,增加代码安全性和保密性。

#Android混淆控制

ifeq ($(TARGET_BUILD_VARIANT), eng)

LOCAL_PROGUARD_ENABLED := disabled

else ifeq ($(TARGET_BUILD_VARIANT), userdebug)

LOCAL_PROGUARD_ENABLED := disabled

else LOCAL_PROGUARD_ENABLED := full obfuscation

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

endif

 

通用的配置文件在如下目录,大多数应用都可以直接使用该配置文件进行混淆。
\tspcdsvr\public\laiwenjie\代码混淆\proguard.flags

4 Android混淆日志解码

 

Android SDK默认带着retrace脚本,一般情况下路径为

${Android_HOME}/tools/proguard/bin/proguard.sh
Proguard进行混淆之后,会生成一个映射表,文件名为mapping.txt 我们需要保留每次编译生成的mapping.txt。
对于验证该问题我模拟了未被混淆的类crash。

 

Crash日志如下:

09-25 05:18:37.083 1098 1530 E AndroidRuntime: Caused by: java.lang.ArithmeticException: divide by zero
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at com.chinatsp.dialer.a.o.dv(TelecomUtils.java:281)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at com.chinatsp.dialer.a.k.dp(AsynAreaLoader.java:60)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at com.chinatsp.dialer.a.l.dr(AsynAreaLoader.java:77)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at com.chinatsp.dialer.a.l.doInBackground(AsynAreaLoader.java:74)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at android.os.AsyncTask$2.call(AsyncTask.java:333)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: at java.util.concurrent.FutureTask.run(FutureTask.java:266)
09-25 05:18:37.083 1098 1530 E AndroidRuntime: ... 4 more

对上面的信息处理,去掉时间前缀和E/AndroidRuntime(24006):这些字符串retrace才能正常工作.得到的字符串是

Caused by: java.lang.ArithmeticException: divide by zero
at com.chinatsp.dialer.a.o.dv(TelecomUtils.java:281)
at com.chinatsp.dialer.a.k.dp(AsynAreaLoader.java:60)
at com.chinatsp.dialer.a.l.dr(AsynAreaLoader.java:77)
at com.chinatsp.dialer.a.l.doInBackground(AsynAreaLoader.java:74)
at android.os.AsyncTask$2.call(AsyncTask.java:333)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
... 4 more

将上面的stacktrace保存成一个文本文件,比如名称为crash.txt
使用解码命令生成解码之后的文件out.txt

./retrace.sh mapping.txt crash.txt > out.txt

可以看到解码之后crash堆栈已经没有混淆了。
Caused by: java.lang.ArithmeticException: divide by zero
at com.chinatsp.dialer.utils.TelecomUtils.getAddress(TelecomUtils.java:281)
at com.chinatsp.dialer.utils.AsynAreaLoader.getAreaFromDB(AsynAreaLoader.java:60)
at com.chinatsp.dialer.utils.AsynAreaLoaderAreaAsyncTask.doInBackground(AsynAreaLoader.java:77)atcom.chinatsp.dialer.utils.AsynAreaLoaderAreaAsyncTask.doInBackground(AsynAreaLoader.java:77) at com.chinatsp.dialer.utils.AsynAreaLoaderAreaAsyncTask.doInBackground(AsynAreaLoader.java:74)
at android.os.AsyncTask$2.call(AsyncTask.java:333)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
... 4 more

5 Android反编译的使用

5.1 Apktool工具

 

反编译出apk所需要的资源文件和布局设置文件等。
使用apktool工具,进入apktool所在的目录下,使用如下的命令:     ./apktool d ./xxx.apk(apk所在的路径)
工具路径在
\tspcdsvr\public\laiwenjie\apktools

5.2 dex2jar工具

功能:反编译出jar文件,即apk的源程序文件的字节码。

  1. 因为apk文件其实是使用zip进行打包压缩生成的文件,所以先把xxx.apk文件改名为xxx.zip文件,并对其进行解压。

  2.   进入解压后的目录,其中有一个classes.dex文件,这个文件就是java文件编译再通过dx工具打包而成的,源代码就包含在这个文件中。 3. 把前一步生成的文件classes.dex复制到dex2jar工具的根目录中,并使用如下命令对其进行反编译:     ./dex2jar.sh classes.dex 就会在当前目录下生成一个classes_dex2jar.jar文件

工具路径在
\tspcdsvr\public\laiwenjie\dex-tools-2.1

5.3 jdgui工具

功能:查看反编译出来的jar包的源代码。
由于在ubuntu上面无法正常使用jdgui工具。于是下载了windows版本的
jdgui工具在window 虚拟机里面使用。

图片.png

图5 windows上jdgui工具查看源码截图

工具路径在
\tspcdsvr\public\laiwenjie\dex-tools-2.1\jd-gui-windows-1.6.3