随着公司上市,我们app在安全方面有更高的要求,目前我们只使用了ProGuard来保护代码的安全,对资源文件的保护力度不大,但是资源文件是存在比较大的安全隐患的。那资源会有哪些安全隐患呢?
通过运行下面命令就能进行反编译;
apktool d -s xxx.apk
通过上图中的目录结构,我们可以看到这个应用的资源文件大概有:anim、drawable、layout、menu、values等等,我们可以通过修改这些文件夹下的资源文件,并通过apktool进行回编译(apktool b 命令)就能创建一个经过修改过的APK应用,例如我们修改下图中红色横线所标示的layout文件,就能往原有APK的支付信息(根据资源名称猜测这个layout的意图)中添加一些我们自己的东西;
网上看,微信和美团在资源保护方面都有自己的实现。对比了这两家资源混淆实现方案,发现微信的方案不会影响编译流程,更加简单。所以决定在我们的app中使用微信的资源混淆。
微信资源混淆:
微信资源混淆有两种方案:https://my.oschina.net/bugly/blog/536064
(1)使用jar包
Java -jar AndResGuard-cli-1.1.16.jar oldapk -config config.xml -out res_proguard -signature release.keystore testres testres testres
--config,指定具体config文件的路径;
--out,指定生成文件具体的输出路径,其中混淆的mapping会在输出文件夹中以resource_mapping_(输入apk的名称).txt命名。
--signature,指定签名信息,若在命令行设置会覆盖config.xml中的签名信息,顺序为签名文件路径、storepass、keypass、storealias。
--mapping, 指定旧的mapping文件,保证同一资源文件在不同版本混淆后的名称保持一致。若在命令行设置会覆盖config.xml中的信息。
--7zip,指定7zip的路径,若已添加到环境变量则不需要设置。此处必须为全路径,例如linux: /shwenzhang/tool/7za,Windows需要加上.exe 结尾。
--zipalign,指定zipalign的路径,若已添加到环境变量则不需要设置。此处必须为全路径,例如linux: /shwenzhang/sdk/tools/zipalign,Windows需要加上.exe结尾。
--repackage,如果想要出渠道包等需求,我们可能希望利用7zip直接重打包安装包。
如何写config配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<resproguard>
<!--defaut property to set -->
<issue id="property">
<!--whether use 7zip to repackage the signed apk, you must install the 7z command line version in window -->
<!--sudo apt-get install p7zip-full in linux -->
<!--and you must write the sign data fist, and i found that if we use linux, we can get a better result -->
<seventzip value="false"/>
<!--the sign data file name in your apk, default must be META-INF-->
<!--generally, you do not need to change it if you dont change the meta file name in your apk-->
<metaname value="META-INF"/>
<!--if keep root, res/drawable will be kept, it won't be changed to such as r/s-->
<keeproot value="false"/>
</issue>
<!--whitelist, some resource id you can not proguard, such as getIdentifier-->
<!--isactive, whether to use whitelist, you can set false to close it simply-->
<issue id="whitelist" isactive="true">
<!--you must write the full package name, such as com.tencent.mm.R -->
<!--for some reason, we should keep our icon better-->
<!--and it support *, ?, such as com.tencent.mm.R.drawable.emoji_*, com.tencent.mm.R.drawable.emoji_?-->
<!--<path value="<your_package_name>.R.drawable.icon"/>-->
<!--<path value="<your_package_name>.R.string.com.crashlytics.*"/>-->
<!--<path value="<your_package_name>.R.string.tb_*"/>-->
<!--<path value="<your_package_name>.R.layout.tb_*"/>-->
<!--<path value="<your_package_name>.R.drawable.tb_*"/>-->
<!--<path value="<your_package_name>.R.color.tb_*"/>-->
</issue>
<!--keepmapping, sometimes if we need to support incremental upgrade, we should keep the old mapping-->
<!--isactive, whether to use keepmapping, you can set false to close it simply-->
<!--if you use -mapping to set keepmapping property in cammand line, these setting will be overlayed-->
<issue id="keepmapping" isactive="false">
<!--the old mapping path, in window use \, in linux use /, and the default path is the running location-->
<path value="{your_mapping_path}"/>
</issue>
<!--compress, if you want to compress the file, the name is relative path, such as resources.arsc, res/drawable-hdpi/welcome.png-->
<!--what can you compress? generally, if your resources.arsc less than 1m, you can compress it. and i think compress .png, .jpg is ok-->
<!--isactive, whether to use compress, you can set false to close it simply-->
<issue id="compress" isactive="true">
<!--you must use / separation, and it support *, ?, such as *.png, *.jpg, res/drawable-hdpi/welcome_?.png-->
<path value="*.png"/>
<path value="*.jpg"/>
<path value="*.jpeg"/>
<path value="*.gif"/>
<path value="resources.arsc"/>
</issue>
<!--sign, if you want to sign the apk, and if you want to use 7zip, you must fill in the following data-->
<!--isactive, whether to use sign, you can set false to close it simply-->
<!--if you use -signature to set sign property in cammand line, these setting will be overlayed-->
<issue id="sign" isactive="true">
<!--the signature file path, in window use \, in linux use /, and the default path is the running location-->
<path value="release.keystore"/>
<!--storepass-->
<storepass value="testres"/>
<!--keypass-->
<keypass value="testres"/>
<!--alias-->
<alias value="release"/>
</issue>
</resproguard>
这种方式更适合用在服务器打包apk,我们平时在开发过程中,不可能每次都是用这种方式把apk包进行混淆之后,再安装到手机上进行测试。所以有第二种方式,在gradle中进行配置。
(2)gradle中配置资源混淆
在工程根目录下的build.gradle的dependencies中进行如下配置
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
//test git2
}
然后在app目录下的build.gradle中进行如下配置
apply plugin: 'com.android.application'
apply plugin: 'AndResGuard'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3" //////////需要加入的配置
defaultConfig {
applicationId "com.example.annotationtestdemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
/////////////需要加入的配置
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
// 当你使用v2签名的时候,7zip压缩是无法生效的。
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key",
"R.drawable.umeng*",
"R.layout.umeng*",
"R.drawable.umeng*",
"R.anim.umeng*",
"R.color.umeng*",
"R.style.*UM*",
"R.style.umeng*",
"R.id.umeng*",
"R.string.UM*"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"resources.arsc"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.8'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA1”
**/
digestalg = "SHA256"
}
加入以上配置以后,sync一下以后,在gradle的task中,可以看到有下面的两个task
双击resguardDebug或是resguardRelease即可生成资源混淆后的apk。。。
生成的apk的路径是在build/outputs/apk/AndResGuard_xxx-debug目录下,
但是生成资源混淆以后的apk不会自动安装,这样的话,不利于我们在开发的过程中进行测试,,为了方便测试,我修改了一下gradle的配置,将resguardDebug/resguardRelease放在assemeble之后,这样点击“Run”按钮之后,编译完成之后会自动安装资源混淆之后的apk,这样在测试过程中,如果发现资源混淆导致的问题,可以随时发现。
public class ProguardRes {
//获取apk的名称
public static void generateResProguardApk(String root) {
String buildOutput = root+"/build/outputs/apk";
def buildDirFile = new File(buildOutput);
buildDirFile.eachFile { file ->
if (file.getName().contains("debug") || file.getName().contains("release")) {
println("jtt: "+file.getName());
proguardRes(root,file.getName())
return
}
}
}
//实用AndResGuard-cli-1.1.16.jar对apk进行资源混淆
public static void proguardRes(String root,String apkName){
String buildOutput = root+"/build/outputs/apk/";
println("java -jar "+root+"/AndResGuard-cli-1.1.16.jar "+buildOutput+apkName+" -config "+root+"/config.xml -out "+buildOutput+"resProguard")
//资源混淆
Process p = Runtime.getRuntime().exec("java -jar "+root+"/AndResGuard-cli-1.1.16.jar "+buildOutput+apkName+" -config "+root+"/config.xml -out "+buildOutput+"resProguard")
try{
p.waitFor()
}catch (Exception e){
}
File oldApk = new File(buildOutput+apkName);
String newApkName = getProguardResFileName(apkName);
File resProguardApk = new File(buildOutput+"resProguard/"+newApkName);
if(resProguardApk.exists()){
println("generation res proguard apk success ")
FileUtils.deleteFile(oldApk)
FileUtils.copyFile(resProguardApk, oldApk);
}
}
private static String getProguardResFileName(String oldApkName){
int indexDot = oldApkName.indexOf(".");
String newName = oldApkName.subSequence(0,indexDot)+"_signed.apk"
return newName;
}
}