本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/WmJyiA3fDNriw5qXuoA9MA

作者:lilycai

本文主要讲述了代码混淆和资源混淆的原理,Studio默认的混淆方案,混淆的参数,以及如何对Apk进行代码混淆(自定义混淆文件)和资源混淆(结合微信混淆和美团混淆两种方案),避免Apk被逆向。

为什么要混淆

我们的apk在打包发布之前,都要进行混淆处理来避免源代码和资源文件被小白用户通过反编译拿到。未混淆代码的反编译操作非常简单,网上有很多教程, 也可以通过使用Android Studio自带的apk分析工具(Build---Analyze APK)直接看到未混淆Apk的源代码和原始的资源文件。对比图如下,从图中可以看到未混淆apk所有的代码都一目了然,随便改改资源和代码,就能变成一个新的apk。为了避免我们的劳动成果被窃取,也避免出现安全漏洞和隐患,此篇文章从混淆的原理到代码和资源文件的混淆实践做一下阐述。

混淆前:

混淆后:

混淆的原理

Java 是一种跨平台、解释型语言,Java 源代码编译成的class文件中有大量包含语义的变量名、方法名的信息,很容易被反编译为Java 源代码。为了防止这种现象,我们可以对Java字节码进行混淆。混淆不仅能将代码中的类名、字段、方法名变为无意义的名称,保护代码,也由于移除无用的类、方法,并使用简短名称对类、字段、方法进行重命名缩小了程序的size。

ProGuard由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要哪些步骤都可以在脚本中配置。 参见ProGuard官方介绍

  • 压缩(Shrink): 侦测并移除代码中无用的类、字段、方法、和特性(Attribute)。
  • 优化(Optimize): 分析和优化字节码。
  • 混淆(Obfuscate): 使用a、b、c、d这样简短而无意义的名称,对类、字段和方法进行重命名。

上面三个步骤使代码size更小,更高效,也更难被逆向工程。

  • 预检(Preveirfy): 在java平台上对处理后的代码进行预检。

混淆流程图如下:



Proguard读入input jars(or wars,zips or directories),经过四个步骤生成处理之后的jars(or wars,ears,zips or directories),Optimization步骤可选择多次进行。

为了确定哪些代码应该被保留,哪些代码应该被移除或混淆,需要确定一个或多个Entry Point。Entry Point经常是带有main methods,applets,midlets的classes,它们在混淆过程中会被保留。我们来看一下Proguard的几个步骤如何处理Entry Points。

  1. 在压缩阶段,Proguard从上述Entry Points开始遍历搜索哪些类和类成员被使用。其他没有被使用的类和类成员会移除。
  2. 在优化阶段,Proguard进一步设置非Entry Point的类和方法为private、static和final来进行优化,不使用的参数会被移除,某些方法会被标记被内联。
  3. 在混淆阶段,Proguard重命名非Entry Points的类和类成员。
  4. 预检阶段是唯一没有触及Entry Points的阶段。

Android Studio 默认的混淆方案及字段解读

开启混淆

参见google官方文档压缩代码和资源

要通过Proguard启动代码压缩,在build.gradle文件内相应的构建类型中添加minifyEnabled true。

android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}

除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:

proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'

google的官方文档介绍:

getDefaultProguardFile('proguard-android.txt') 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁),内容为空。

通过试验,gradle 2.2之后,defaultProguardFile没有使用sdk目录下的proguard-android.txt,而是使用了gradle自带的proguard-android.txt,不同的gradle版本带有不同的默认混淆文件,在项目根目录的build/intermediates/proguard-files/proguard-android.txt-2.3.3(笔者用的gradle版本)即为gradle自带的混淆文件。在proguard-android.txt-2.3.3文件中也写有说明,gradle 2.2之后自带混淆文件:

 Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
will be ignored by new version of the Android plugin for Gradle.

构建输出

构建时Proguard都会输出下列文件:

(1)dump.txt --- 说明APK中所有类文件的内部结构

(2)mapping.txt --- 提供原始与混淆过的类、方法和字段名称之间的转换

(3)seeds.txt --- 列出未进行混淆的类和成员

(4)usage.txt --- 列出从APK移除的代码

这些文件保存在/build/outputs/mapping/release目录下。

解码混淆过的堆叠追踪

使用混淆后,一定要保存好mapping文件,程序csh时通过脚本进行解码。

retrace工具位于/tools/proguard/目录中,解码命令为:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如mac平台下:

retrace.sh -verbose mapping.txt obfuscated_trace.txt

默认的混淆方案及字段解读

下面结合默认混淆文件中的内容来解释混淆的参数: 参见Proguard官方字段解读

不使用大小写混写类名
-dontusemixedcaseclassnames

默认情况下混淆的类名可以包含大小写字符的混合。

不忽略公共类库
-dontskipnonpubliclibraryclasses

指定不去忽略非public的library classes。从Proguard 4.5开始,是默认的设置。

-dontoptimize
-dontpreverify

默认optimize和preverify选项是关闭的,因为Android的dex并不像Java虚拟机需要optimize(优化)和previrify(预检)两个步骤。

指定哪个属性不要混淆,可一次指定多个属性
-keepattributes [attribute_filter]

通常Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, and AnnotationDefault属性需要被保留,根据项目具体使用情况保留。

这里需要特别注意的一点是,gradle默认的keepattributes属性不全,只保留了Annotation,Signature,InnerClasses,EnclosingMethod,为了混淆之后定位csh代码方便,我们需要在proguard_rules.pro中手动添加抛出异常时保留代码行号,并且重命名抛出异常时的文件名称,这样能方便定位问题:

抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable 重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile

keep选项非常重要,keep指定了哪些类,哪些方法不被混淆,从而保证了程序的正常运行。官方的keep用法有6种:

Keep From being removed or renamed From being renamed
Classes and class members keep keepnames
Class members only keepclassmembers keepclassmembernames
Classes and class members keepclasseswithmembers keepclasseswithmembernames

左边不带names的选项为From being removed or renames,即不会被移除或重命名,即使类或类成员未被使用。带有names的选项为From being renamed,不会被重命名,如果是无用的类或类成员,会被移除。

(1)-keep(names)选项 指定类和类成员(变量和方法)不被混淆

-keep [,modifier,...] class_specification

eg.

指定类名不被改变
-keep public class com.google.vending.licensing.ILicensingService 指定使用了Keep注解的类和类成员都不被改变
-keep @android.support.annotation.Keep class * {*;}

关于Keep注解的解释参见文末参考链接

(2)-keepclassmembers(names) 指定类成员不被混淆,类名会被混淆

-keepclassmembers [,modifier,...] class_specification

eg.keep setters in views 使得animations仍然能够工作

-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}

(3)-keepclasseswithmembers(names) 指定类和类成员都不被混淆

-keepclasseswithmembers [,modifier,...] class_specification

eg.包含native方法的类名和native方法都不能被混淆,如果native方法未被调用,则被移除。由于native方法与对应so库中的方法名称对应,方法名被混淆会导致调用出现问题,所以native方法不能被混淆

-keepclasseswithmembernames class * {
native <methods>;
}

通用Options:

(1)-verbose 打印混淆详细信息

(2)-dontnote选项:指定不去输出打印该类产生的错误或遗漏

-dontnote com.android.vending.licensing.ILicensingService

-dontnote android.support.**

(3)-dontwarn选项:指定不去warn unresolved references和其他重要的problem

-dontwarn android.support.**

如上面(2)(3)所示,android.support的libraries需要保留

至此,gradle自带的proguard-android.txt文件相关字段已解析完毕。下面将介绍我们自定义的proguard-rules.pro文件需要添加什么参数。

自定义混淆文件

一般而言,我们会定义我们自己的proguard-rules.pro,下面列出自定义的一个proguard-rules.pro供大家参考。在看自定义的混淆文件之前,先讲解一下Filters和assumenosideeffects,以便更好地理解下面的指令。

(1)Filters

?	matches any single character in a name.(匹配一个字符)
* matches any part of a name not containing the directory separator.(匹配一个名字,除了目录分隔符外的任意部分)
** matches any part of a name, possibly containing any number of directory separators.(匹配任意名,可能包含任意路径分隔符)
! exclude
<field> 匹配类中的所有字段
<method> 匹配类中所有的方法
<init> 匹配类中所有的构造函数

eg.

-keep class com.lily.test.** 本包和所包含子包下的类名都保持
-keep class com.lily.test.* 保持该包下的类名
-keep class com.lily.test.** {*;} 保持包和子包的类名和里面的内容均不被混淆

(2)-assumenosideeffects 指令: 下文会用在android log的移除上

assumeosideeffects是Optimization过程中的选项,所以为保证指令的有效,需要开启optimization。这个指令的含义是Proguard会在optimization过程中删除对这些方法的调用,需要注意:Only use this option if you know what you're doing!

下面是自定义混淆文件的一个范例,四大组件,native方法,反射用到的类,一些引入的第三方库等都不能进行混淆:

# 代码混淆压缩比,在0~7之间
-optimizationpasses 5 # 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames # 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify -verbose #google推荐算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* # 避免混淆Annotation、内部类、泛型、匿名类
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod # 重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile # 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable # 处理support包
-dontnote android.support.**
-dontwarn android.support.** # 保留四大组件,自定义的Application等这些类不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService # 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
} # 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
} # 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
} #第三方jar包不被混淆
-keep class com.github.test.** {*;} #保留自定义的Test类和类成员不被混淆
-keep class com.lily.Test {*;}
#保留自定义的xlog文件夹下面的类、类成员和方法不被混淆
-keep class com.test.xlog.** {
<fields>;
<methods>;
} #assume no side effects:删除android.util.Log输出的日志
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
} #保留Keep注解的类名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}

资源文件的混淆:

上面讲述了如何进行代码混淆,再来讲讲如何对资源文件进行混淆。对资源文件进行混淆操作本质上是通过修改resources.arsc(参见文末链接详见resources.arsc作用及文件格式)。现针对两种资源混淆方案进行简要说明。第一种是微信的资源混淆方案,第二种是美团的资源混淆方案,两篇文章中都对原理进行了详细的阐述。

(1)微信的资源混淆方案:

微信的资源混淆是自己做了一个安装包解压并且用7z极限压缩打包器,修改的内容也是resources.arsc,优点是可以最大地混淆,不依赖源码与编译过程,无需在编译过程中修改源文件(java、xml、资源文件),无需改变Android打包流程。整体的流程如下:



使用微信的资源混淆方案有两种方法,第一种方式为修改gradle,第二种方式为直接使用命令行。下图为使用命令行最简单的方法生成资源混淆的apk,下载github工程后,进入tool_output文件夹,试验的apk为test.apk

java -jar AndResGuard-cli-1.2.3.jar test.apk

混淆过程中会输出log,混淆后会出现和apk同名的文件夹,里面包含了混淆后mapping的对应文件,新签名打包的apk和混淆后的资源文件目录。如下图所示:



混淆前资源文件:



混淆后资源文件:



可以看到资源文件的路径以及文件名都被混淆了。

(2)美团的资源混淆方案:

采用更改AAPT(Android Asset Packaging Tool)(参见文末链接详细解读AAPT)源码的方式,参考了Proguard Obfuscator,对APK中资源文件名使用简短无意义名称进行替换,如下面代码所示,在AAPT生成resources.arsc和*.ap*时把资源文件的名称进行替换。下面是美团修改后的Resource.cpp文件

static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
ResourceTable* table,
const sp<ResourceTypeSet>& set,
const char* resType)
{
String8 type8(resType);
String16 type16(resType); bool hasErrors = false; ResourceDirIterator it(set, String8(resType));
ssize_t res;
while ((res=it.next()) == NO_ERROR) {
if (bundle->getVerbose()) {
printf(" (new resource id %s from %s)\n",
it.getBaseName().string(), it.getFile()->getPrintableSource().string());
}
String16 baseName(it.getBaseName());
const char16_t* str = baseName.string();
const char16_t* const end = str + baseName.size();
while (str < end) {
if (!((*str >= 'a' && *str <= 'z')
|| (*str >= '0' && *str <= '9')
|| *str == '_' || *str == '.')) {
fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n",
it.getPath().string());
hasErrors = true;
}
str++;
}
String8 resPath = it.getPath();
resPath.convertToResPath(); String8 obfuscationName;
String8 obfuscationPath = getObfuscationName(resPath, obfuscationName); table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
type16,
baseName, // String16(obfuscationName),
String16(obfuscationPath), // resPath
NULL,
&it.getParams());
assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8);
} return hasErrors ? UNKNOWN_ERROR : NO_ERROR;
}

修改的部分在:

String8 obfuscationName;
String8 obfuscationPath = getObfuscationName(resPath, obfuscationName); assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8);

混淆时常见的问题解决

参见官方问题解答

参考文献:

1、Android 项目的代码混淆,Android proguard 使用说明

2、google 混淆官方文档

3、混淆官方网址

4、Android混淆快速配置之@Keep

5、Android resources.arsc文件格式及逆向修改res路径思路

6、Android应用程序资源的编译和打包过程分析(AAPT)


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

Android 混淆那些事儿的更多相关文章

  1. Android混淆那些事儿

    博客: 安卓之家 微博: 追风917 CSDN: 蒋朋的家 简书: 追风917 博客园:追风917 # Android混淆 Android混淆是Android开发者经常使用的一种用于代码防止被反编译的 ...

  2. android混淆那些事

    写给Android开发者的混淆使用手册 综述 毫无疑问,混淆是打包过程中最重要的流程之一,在没有特殊原因的情况下,所有 app 都应该开启混淆. 首先,这里说的的混淆其实是包括了代码压缩.代码混淆以及 ...

  3. Android混淆打包配置总结

    Android打包失败出现Proguard returned with error code 1. See console的错误 这个问题是由于代码混淆引起的,找不到引用包. 只需在你的proguar ...

  4. Android混淆、反编译以及反破解的简单回顾

    =========================================================================虽然反编译很简单,也没下面说的那么复杂,不过还是转了过 ...

  5. android -------- 混淆打包报错(warning - InnerClass annotations are missing corresponding EnclosingMember annotations)

    最近做Android混淆打包遇到一些问题,Android Sdutio 3.1 版本打包的 错误如下: Android studio warning - InnerClass annotations ...

  6. Android 混淆代码有关问题总结

    Android 混淆代码问题总结 Android 混淆代码: 最快的方式: 1. 首先更新Android的SDK至最新版本,重新建立1个工程,把源码和资源及其他文件拷到新的工程里面. 2. 工程目录底 ...

  7. Android 混淆[学习笔记]

    Android 混淆 Gradle的简介: http://www.flysnow.org/2015/03/30/manage-your-android-project-with-gradle.html ...

  8. Android混淆代码

    Android代码混淆是必须的,java层代码如果不做混淆等于把源代码送人了.那如何做混淆呢? 之前一般都是提到采用proguard.cfg,但使用新版本ADT后没有这个文件了,取而代之的是progu ...

  9. Android混淆打包

    一.理论知识   ProGuard是一款免费的Java类文件压缩器.优化器和混淆器.它能发现并删除无用类.字段(field).方法和属性值(attribute).它也能优化字节码并删除无用的指令.最后 ...

随机推荐

  1. web文件下载

    web页面实现文件下载的几种方法 今天碰到文件下载的一些问题,本着知其然也要知其所以然的精神,站在巨人的肩膀上深入学习和测试了一下,抛砖引玉,现在总结结论如下: 1)标准URL下载方式可以通过在web ...

  2. Js获取客户端用户Ip地址

    利用搜狐查询接口查询Ip地址: <!DOCTYPE html> <html> <head> <meta charset="utf-8" / ...

  3. 记录一个EF连接查询的异常:the entity or complex type 'x' cannot be constructed in a linq to entities query

    问题解决连接:https://stackoverflow.com/questions/5325797/the-entity-cannot-be-constructed-in-a-linq-to-ent ...

  4. MUI底部导航切换子页面

    1.登陆页面进入之后,进入到main页面,main页面只有一个底部导航,然后引入子页面进行渲染. <nav class="mui-bar mui-bar-tab" id=&q ...

  5. 源码解析Django CBV的本质

    Django CBV模式的源码解析 通常来说,http请求的本质就是基于Socket Django的视图函数,可以基于FBV模式,也可以基于CBV模式. 基于FBV的模式就是在Django的路由映射表 ...

  6. Python判断输入字符类型

    """从键盘上输入 一个字符,判断其字符类型.""" while True: char = input("请输入需要判断的字符:& ...

  7. ios访问web页面<div>点击事件不起效果,以及alert()显示url的解决办法

    ios访问web页面<div>点击不起效果,在其div上添加style=”cursor:pointer:“ jquery web页面动态append()事件调用方法:$(document) ...

  8. Linux中CPU亲和性(affinity)

    0.准备知识 超线程技术(Hyper-Threading):就是利用特殊的硬件指令,把两个逻辑内核(CPU core)模拟成两个物理芯片, 让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和 ...

  9. zabbix企业级监控dell硬件服务状态

    监控dell服务的硬件状态,一般有两种方式 第一,在操作熊上安装OMSA,编写脚本调用omreport命令进行监控 第二.使用IDRAC,只需开启在IARAC上的SNMP,zabbix通过snmp进行 ...

  10. K-means算法性能评估及其优化

    1. SSE误差平方和(Sum of Square due to Error): 聚类情况: 计算公式: 注:SSE参数计算的内容为当前迭代得到的中心位置到各自中心点簇的欧式距离总和,这个值越小表示当 ...