一:混淆的作用
1.1 作用
混淆 并不是让代码无法被反编译,而是将代码中的类、方法、变量等信息进行重命名,把它们改成一些毫无意义的名字。混淆代码可以在不影响程序正常运行的前提下让破解者很头疼,从而大大提升了程序的安全性。
混淆APK build.gradle中minifyEnabled的值是false,这里我们只需要把值改成true,打出来的APK包就会是混淆过的了。
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
其中minifyEnabled用于设置是否启用混淆,proguardFiles用于选定混淆配置文件。注意这里是在release闭包内进行配置的,因此只有打出正式版的APK才会进行混淆,Debug版的APK是不会混淆的。
二:代码混淆
2.1 默认混淆规则
proguard-android.txt文件:
Android SDK/tools/proguard目录下
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Dont warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
-dontusemixedcaseclassnames: 表示混淆时不使用大小写混淆类名。
-dontskipnonpubliclibraryclasses:不跳过library中的非public方法。
-verbose: 打印混淆的详细信息。
-dontoptimize: 不进行优化,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
-dontpreverify: 不进行预校验
-keepattributes Annotation: 对注解参数进行保留。
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService:
表示不混淆上述声明的两个类。
表示不混淆任何包含native方法的类的类名以及native方法名
-keepclasseswithmembernames class * {
native <methods>;
}
表示不混淆任何一个View中的setXxx()和getXxx()方法,因为属性动画需要有相应的setter和getter的方法实现
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
表示不混淆Activity中参数是View的方法
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
表示不混淆枚举中的values()和valueOf()方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
表示不混淆Parcelable实现类中的CREATOR字段
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
表示不混淆R文件中的所有静态字段
-keepclassmembers class **.R$* { public static <fields>;}
proguard中一共有三组六个keep关键字的含义
keep 保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers 只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmember和keep关键字的区别:
如果这个类没有native的方法,那么这个类会被混淆
-keepclasseswithmember class * {
native <methods>;
}
不管这个类有没有native的方法,那么这个类不会被混淆
-keep class * {
native <methods>;
}
proguard-rules.pro:
任何一个Android Studio项目在app模块目录下都有一个proguard-rules.pro文件,这个文件就是用于让我们编写只适用于当前项目的混淆规则的
2.2 mapping文件
三:资源混淆
上面介绍的混淆仅仅针对Java代码进行混淆,而不能对资源文件进行混淆,资源文件是指:anim、drawable、layout、menu、values等等。
3.1 背景知识
3.1.1 Android资源的编译和打包过程简介
这些资源文件是通过Android资源打包工具aapt(Android Asset Package Tool)打包到APK文件里面的。在打包之前,大部分文本格式的XML资源文件还会被编译成二进制格式的XML资源文件。
具体流程可以参照,本文只给出一个大致的流程介绍。
Android应用程序资源的编译和打包过程分析
解析AndroidManifest.xml: 获取包名。
添加被引用资源包: apk中至少会有两个资源包:apk本身的,系统通用资源包有一些系统资源如android:orientation等。
收集资源文件:Package、Type、Config。
Package: 资源所属包名
Type: 资源类型; anim、drawable、layout、menu、values.
Config: 资源配置:hdpi,xhdpi,zh_rCN , land。分辨率和语言等有18个维度.
资源ID 最高字节表示Package ID,次高字节表示Type ID,最低两字节表示Entry ID。将收集到的资源增加到资源表
编译values类资源
给Bag资源分配ID: values的资源除了是string之外,还有其它很多类型的资源,其中有一些比较特殊,如bag、style、plurals和array类的资源。这些资源会给自己定义一些专用的值,这些带有专用值的资源就统称为Bag资源。
编译Xml资源文件
生成资源符号:
收集到的资源项都按照类型来保存在一个资源表中,即保存在一个
ResourceTable对象。因此,Android资源打包工具aapt只要遍历每一个
Package里面的每一个Type,然后取出每一个Entry的名称,并且根据这
个 Entry在自己的Type里面出现的次序来计算得到它的资源ID,那么就可
以 生成一个资源符号了,这个资源符号由名称以及资源ID所组成。 例
如, 对于strings.xml文件中名称为“start_in_process”的Entry来说,它是
一个类 型为string的资源项,假设它出现的次序为第3,那么它的资源符
号就等于 R.string.start_in_process,对应的资源ID就为0x7f050002,其
中,高字 节0x7f表示Package ID,次高字节0x05表示string的Type ID,
而低两字节 0x02就表示“start_in_process”是第三个出现的字符串。
- 生成资源索引表 resources.arsc
- 编译AndroidManifest.xml文件
- 生成R.java文件
-
打包APK文件
3.1.2 AAPT
AAPT是Android Asset Packaging Tool的缩写,它存放在SDK的tools/目录下,AAPT的功能很强大,可以通过它查看查看、创建、更新压缩文件(如 .zip文件,.jar文件, .apk文件), 它也可以把资源编译为二进制文件,并生成resources.arsc, AAPT这个工具在APK打包过程中起到了非常重要作用,在打包过程中使用AAPT对APK中用到的资源进行打包
3.1.3 资源索引表-resouce.arsc文件
Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:
赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.Java文件中。
生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。
-
有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。
3.2 美团资源混淆方案
Hook: Resource.cpp中makeFileResources()。
makeFileResources()的作用:将收集到的资源文件加到资源表(ResourceTable)对res目录下的各个资源子目录进行处理,函数为makeFileResources:makeFileResources会对资源文件名做合法性检查,并将其添加到ResourceTable内。
Hook后的方法:主要增加了getObfuscationName(resPath, obfuscationName)来获取混淆后的名称和路径
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;
//迭代遍历ResourceTypeSet。
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) {
//正则匹配 [a-z0-9_.]
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);
//添加到ResourceTable
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;
}
系统方法
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();
table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
type16,
baseName,
String16(resPath),
NULL,
&it.getParams());
assets->addResource(it.getLeafName(), resPath, it.getFile(), type8);
}
return hasErrors ? UNKNOWN_ERROR : NO_ERROR;
}
3.3 微信资源混淆方案
修改resources.arsc文件
ApkDecoder.decode();
public void decode() throws AndrolibException, IOException, DirectoryException {
if (hasResources()) {
ensureFilePath();
// read the resources.arsc checking for STORED vs DEFLATE compression
// this will determine whether we compress on rebuild or not.
System.out.printf("decoding resources.arsc\n");
RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"));
ResPackage[] pkgs = ARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"), this);
//把没有纪录在resources.arsc的资源文件也拷进dest目录
copyOtherResFiles();
ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
}
}
修改的主要逻辑在ARSCDecoder:
readTable-readPackage-readType-readConfig-readEntry;
资源混淆核心处理过程如下:
- 生成新的资源文件目录,里面对资源文件路径进行混淆(其中涉及如何复用旧的mapping文件),例如将res/drawable/hello.png混淆为r/s/a.png,并将映射关系输出到mapping文件中。
- 对资源id进行混淆(其中涉及如何复用旧的mapping文件),并将映射关系输出到mapping文件中。
- 生成新的resources.arsc文件,里面对资源项值字符串池、资源项key字符串池、进行混淆替换,对资源项entry中引用的资源项字符串池位置进行修正、并更改相应大小,并打包生成新的apk。
四:参考文献
微信资源混淆AndResGuard原理
安装包立减1M--微信Android资源混淆打包工具
美团Android资源混淆保护实践
Android应用程序资源的编译和打包过程分析-luoshengyang
Android应用程序资源的编译和打包过程分析
微信资源混淆AndResGuard原理
AndResGuard