持续创作,加速生长!这是我参加「日新计划 10 月更文应战」的第6天,点击检查活动概况
前言
汇总了一下众多大佬的功能优化文章,知识点,主要包括:
UI优化/发动优化/崩溃优化/卡顿优化/安全性优化/弱网优化/APP深度优化等等等~
本篇是第四篇:安全性优化[非商业用途,如有侵权,请告知我,我会删去]
着重一下: 功能优化的开发文档跟之前的面试文档相同,需求的跟作者直接要。
安全性优化
1、Android使用加固完成计划
Android使用加固的许多计划中,其间一种便是依据dex的加固,本文介绍依据dex的加固计划。
原理:在AndroidManifest中指定发动Application为壳Module的Application,生成APK后,将壳Module的AAR文件和加密后的APK中的dex文件兼并,然后从头打包签名。装置使用运转后,经过壳Module的Application来解密dex文件,然后再加载dex。
存在的问题:解密进程,会复原出来未加密的原dex文件,经过一些手法,仍是能够取得未加密的dex。
1.1 完成
APK和壳AAR的生成
新建工程,然后新建一个Module,作为壳Module,姓名随意,这儿命名为shell。
在壳Module中新建继承自Application的ShellApplication,重写attachBaseContext办法,在这个办法加载原来的dex
public class ShellApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
//获取使用APK
File apkFile = new File(getApplicationInfo().sourceDir);
//解压目录
File apkUnzipDir = getDir("apk", Context.MODE_PRIVATE);
apkUnzipDir = new File(apkUnzipDir, "unzip");
//如果不存在,则解压
if (!apkUnzipDir.exists()) {
apkUnzipDir.mkdirs();
//解压
ZipUtils.unzipFile(apkFile, apkUnzipDir);
//过滤一切.dex文件
File[] files = apkUnzipDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".dex");
}
});
//解密
File decryptDir = new File(apkUnzipDir, "decrypt");
decryptDir.mkdirs();
ArrayList<File> list = new ArrayList<>();
for (File file : files) {
if (file.getName().endsWith("classes.dex")) {
list.add(file);
} else {
File decryptFile = new File(decryptDir, file.getName());
EncryptUtils.decrypt(file.getAbsolutePath(), decryptFile.getAbsolutePath());
//添加到list中
list.add(decryptFile);
//删去加密的dex文件
file.delete();
}
}
//加载.dex文件
ClassLoaderUtil.loadDex(this, list);
} else {
ArrayList<File> list = new ArrayList<>();
list.add(new File(apkUnzipDir, "classes.dex"));
File decryptDir = new File(apkUnzipDir, "decrypt");
File[] files = decryptDir.listFiles();
for (File file : files) {
list.add(file);
}
//加载.dex文件
ClassLoaderUtil.loadDex(this, list);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
修正app的AndroidManifest中application节点的name为壳Module的Application
<application
android:name="com.wangyz.shell.ShellApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
在Gradle面板,双击app/Tasks/build/目录下的assembleRelease,生成未签名的APK
在app/build/outputs/apk/release/目录下,能够找到生成的apk:app-release-unsigned.apk
在Android Studio中,点击Build-Make Module ‘shell’,生成AAR。
在shell/build/outputs/aar/目录下,能够找到生成的aar:shell-debug.aar
加壳的进程
加壳的完成流程如下:
这儿挑选Eclipse新建Java工程来操作。
项目结构阐明:
- input:寄存需求加壳的apk和aar
- keystore:寄存签名用到的keystore文件
- output:打包后输出目录,signed为签名后的apk
需求装备的环境变量:
-
因为要用到dx来将jar转换成dex,因而需求装备dx的途径。在SDK/build-tools/下,有对应不同版本的build东西,这儿挑选28.0.0,进入28.0.0文件夹,能够看到dx.bat文件。在电脑的环境变量中,修正path,添加dx.bat途径:
-
因为要用到jarsigner来签名apk,因而需求装备jarsigner的环境变量。一般Java开发的话,JDK装备好了后,这个就不需求再装备了。
装备好上面的环境变量后,关掉eclipse,然后从头发动eclipse
Main类中的代码逻辑:
try {
// APK
File apkFile = new File("input/app-debug.apk");
// 壳AAR
File shellFile = new File("input/shell-debug.aar");
// 判别文件是否存在
if (!apkFile.exists() || !shellFile.exists()) {
System.out.println("apkFile or shellFile missing");
return;
}
// *************解压APK*************
System.out.println("解压APK");
// 先删去输出文件夹下的一切文件
File outputDir = new File("output/");
if (outputDir.exists()) {
FileUtils.deleteAllInDir(outputDir);
}
// 创立apk的解压目录
File apkUnzipDir = new File("output/unzip/apk/");
if (!apkUnzipDir.exists()) {
apkUnzipDir.mkdirs();
}
// 解压APK
ZipUtil.unZip(apkFile, apkUnzipDir);
// 删去META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF
File certRSA = new File(apkUnzipDir, "/META-INF/CERT.RSA");
certRSA.delete();
File certSF = new File(apkUnzipDir, "/META-INF/CERT.SF");
certSF.delete();
File manifestMF = new File(apkUnzipDir, "/META-INF/MANIFEST.MF");
manifestMF.delete();
// 获取dex文件
File[] apkFiles = apkUnzipDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
for (int i = apkFiles.length - 1; i >= 0; i--) {
File file = apkFiles[i];
String name = file.getName();
System.out.println("dex:" + name);
String bakName = name.substring(0, name.indexOf(".dex")) + "_bak.dex";
System.out.println("备份dex:" + bakName);
bakName = file.getParent() + File.separator + name.substring(0, name.indexOf(".dex")) + "_bak.dex";
// 加密dex文件
EncryptUtils.encrypt(file.getAbsolutePath(), bakName);
System.out.println("加密dex:" + name);
// 删去原文件
file.delete();
}
// *************解压APK*************
// *************解压壳AAR*************
// 创立壳AAR的解压目录
System.out.println("解压壳AAR");
File shellUnzipDir = new File("output/unzip/shell/");
if (!shellUnzipDir.exists()) {
shellUnzipDir.mkdirs();
}
// 解压AAR
ZipUtil.unZip(shellFile, shellUnzipDir);
// 将jar转成dex
System.out.println("将jar转成dex");
File shellJar = new File(shellUnzipDir, "classes.jar");
File shellDex = new File(apkUnzipDir, "classes.dex");
DexUtils.dxCommand(shellJar, shellDex);
// 打包
System.out.println("打包APK");
File unsignedApk = new File("output/unsigned.apk");
ZipUtil.zip(apkUnzipDir, unsignedApk);
// 删去解压目录
FileUtils.delete("output/unzip/");
System.out.println("签名APK");
File signedApk = new File("output/signed.apk");
SignUtils.signature(unsignedApk, signedApk, "keystore/android.keystore");
System.out.println("Finished!!!");
// *************解压壳AAR*************
} catch (Exception e) {
e.printStackTrace();
}
来看下详细的步骤:
解压APK
File apkUnzipDir = new File(root, "/output/unzip/apk/");
if (!apkUnzipDir.exists()) {
apkUnzipDir.mkdirs();
}
// 解压APK
ZipUtil.unZip(apkFile, apkUnzipDir);
加密解压出来的dex文件、重命名dex文件
// 获取dex文件
File[] apkFiles = apkUnzipDir.listFiles((file, s) -> s.endsWith(".dex"));
for (int i = apkFiles.length - 1; i >= 0; i--) {
File file = apkFiles[i];
String name = file.getName();
System.out.println("dex:" + name);
String bakName = name.substring(0, name.indexOf(".dex")) + "_bak.dex";
System.out.println("备份dex:" + bakName);
bakName = file.getParent() + File.separator + name.substring(0, name.indexOf(".dex")) + "_bak.dex";
// 加密dex文件
EncryptUtils.encrypt(file.getAbsolutePath(), bakName);
System.out.println("加密dex:" + name);
// 删去原文件
file.delete();
}
解压壳AAR
File shellUnzipDir = new File(root, "/output/unzip/shell/");
if (!shellUnzipDir.exists()) {
shellUnzipDir.mkdirs();
}
// 解压AAR
ZipUtil.unZip(shellFile, shellUnzipDir);
将jar转成dex
File shellJar = new File(shellUnzipDir, "classes.jar");
File shellDex = new File(apkUnzipDir, "classes.dex");
DexUtils.dxCommand(shellJar, shellDex);
打包
File unsignedApk = new File(root, "/output/unsigned.apk");
ZipUtil.zip(apkUnzipDir, unsignedApk);
签名
FileUtils.delete(new File(root, "output/unzip/"));
System.out.println("签名APK");
File signedApk = new File(root, "output/signed.apk");
SignUtils.signature(unsignedApk, signedApk, keystore, keyStorePassword, keyPassword, alias);
System.out.println("Finished!!!");
在output目录下,能够看到已经生成signed.apk。将apk装置在手机上,能够正常运转,达到加固的意图。
源码
源码地址:github.com/milovetingt…
1.2依据gradle的主动加固
上面的加固方式,需求在生成APK后,再生成壳Module的AAR文件,然后再经过东西来生成加固的APK。这个进程,手动操作仍是比较麻烦的。能够凭借gradle来生成插件,在生成APK后,主动完成加固。
插件生成
新建工程Plugins,新建module,名为shell,作为加壳的插件。
清空shell模块下的build文件内容修正如下:
apply plugin: 'groovy'
dependencies {
implementation gradleApi()
implementation localGroovy()
}
删去shell模块下的src/main/目录下的一切文件,然后新建目录groovy,在groovy中再新建包:com/wangyz/plugins,详细能够依据实际情况修正。
新建ShellConfig.java,作为自界说装备的bean
public class ShellConfig {
/**
* 壳Module称号
*/
String shellModuleName;
/**
* keystore的位置
*/
String keyStore;
/**
* keystore的密码
*/
String keyStorePassword;
/**
* key的密码
*/
String keyPassword;
/**
* 别名
*/
String alias;
}
新建ShellPlugin.groovy,主要的逻辑都在这儿面
package com.wangyz.plugins
import com.wangyz.plugins.util.ShellUtil
import org.gradle.api.Plugin
import org.gradle.api.Project
class ShellPlugin implements Plugin<Project> {
def printLog(Object msg) {
println("******************************")
println(msg)
println("******************************\n")
}
def createDir(Project project) {
File shellDir = new File("${project.rootDir}/ShellAPK")
if (!shellDir.exists()) {
printLog("create dir")
shellDir.mkdirs()
}
}
def deleteDir(Project project) {
File shellDir = new File("${project.rootDir}/ShellAPK")
if (shellDir.exists()) {
printLog("delete dir")
shellDir.deleteDir()
}
}
@Override
void apply(Project project) {
printLog('ShellPlugin apply')
project.extensions.create("shellConfig", ShellConfig)
project.afterEvaluate {
project.tasks.matching {
it.name == 'assembleRelease'
}.each {
task ->
printLog(task.name)
def shellProject = project.parent.findProject("${project.shellConfig.shellModuleName}")
printLog("shellProject:$shellProject")
File shellDir = new File("${project.rootDir}/ShellAPK")
File apkFile
File aarFile = new File("${shellProject.buildDir}/outputs/aar/shell-release.aar")
project.android.applicationVariants.all {
variant ->
variant.outputs.each {
output ->
def outputFile = output.outputFile
printLog("outputFile:${outputFile.getAbsolutePath()}")
if (outputFile.name.contains("release")) {
apkFile = outputFile
}
}
}
task.doFirst {
//删去原来的文件夹
deleteDir(project)
//生成文件夹
createDir(project)
//生成aar
printLog("begin generate aar")
project.exec {
workingDir("../${project.shellConfig.shellModuleName}/")
commandLine('cmd', '/c', 'gradle', 'assembleRelease')
}
printLog("generate aar complete")
//仿制文件
printLog("begin copy aar")
project.copy {
from aarFile
into shellDir
}
printLog("copy aar complete")
}
task.doLast {
printLog("begin copy apk")
//仿制文件
project.copy {
from apkFile
into shellDir
}
printLog("copy ${apkFile.name} complete")
printLog("begin shell")
ShellUtil.shell(apkFile.getAbsolutePath(), aarFile.getAbsolutePath(), shellDir.getAbsolutePath(), project.shellConfig.keyStore, project.shellConfig.keyStorePassword, project.shellConfig.keyPassword, project.shellConfig.alias)
printLog("end shell")
}
}
}
}
}
ShellPlugin类完成Plugin接口,完成apply办法,当插件被apply时,就会回调这个办法。
首要创立装备,这样引用插件的gradle文件就能够界说shellConfig节点,插件就能够拿到装备节点里的内容
project.extensions.create("shellConfig", ShellConfig)
指定在assembleRelease后履行咱们自己的逻辑
project.afterEvaluate {
project.tasks.matching {
it.name == 'assembleRelease'
}.each {
task ->
printLog(task.name)
}
}
详细的逻辑界说在task的闭包中,在生成apk前,履行task.doFirst里的逻辑,首要生成aar,然后履行生成apk的逻辑,然后在task.doLast中履行加壳的操作。
printLog(task.name)
def shellProject = project.parent.findProject("${project.shellConfig.shellModuleName}")
printLog("shellProject:$shellProject")
File shellDir = new File("${project.rootDir}/ShellAPK")
File apkFile
File aarFile = new File("${shellProject.buildDir}/outputs/aar/shell-release.aar")
project.android.applicationVariants.all {
variant ->
variant.outputs.each {
output ->
def outputFile = output.outputFile
printLog("outputFile:${outputFile.getAbsolutePath()}")
if (outputFile.name.contains("release")) {
apkFile = outputFile
}
}
}
task.doFirst {
//删去原来的文件夹
deleteDir(project)
//生成文件夹
createDir(project)
//生成aar
printLog("begin generate aar")
project.exec {
workingDir("../${project.shellConfig.shellModuleName}/")
commandLine('cmd', '/c', 'gradle', 'assembleRelease')
}
printLog("generate aar complete")
//仿制文件
printLog("begin copy aar")
project.copy {
from aarFile
into shellDir
}
printLog("copy aar complete")
}
task.doLast {
printLog("begin copy apk")
//仿制文件
project.copy {
from apkFile
into shellDir
}
printLog("copy ${apkFile.name} complete")
printLog("begin shell")
ShellUtil.shell(apkFile.getAbsolutePath(), aarFile.getAbsolutePath(), shellDir.getAbsolutePath(), project.shellConfig.keyStore, project.shellConfig.keyStorePassword, project.shellConfig.keyPassword, project.shellConfig.alias)
printLog("end shell")
}
在src/main/目录下新建目录:resources/META-INF/gradle-plugins,再创立com.wangyz.plugins.ShellPlugin.properties的文件,这儿的文件名便是后面插件被引用时的姓名,com.wangyz.plugins.ShellPlugin.properties内容如下:
implementation-class=com.wangyz.plugins.ShellPlugin
key为implementation-class,这个是固定的
value为com.wangyz.plugins.ShellPlugin,便是上面在groovy里创立的类
到这儿,界说好了插件,还需求发布到库房。在shell模块的build.gradle文件中添加以下装备
apply plugin: 'maven-publish'
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'com.wangyz.plugins'
artifactId 'ShellPlugin'
version '1.0.0'
from components.java
}
}
}
publishing {
repositories {
maven {
url uri('E:\Repository')
}
}
}
sync项目后,能够在Gradle面板看到新生成的task
双击publish,会将插件发布到咱们指定的库房
11:22:39: Executing task 'publish'...
Executing tasks: [publish] in project D:\Project\Plugins\shell
Parallel execution with configuration on demand is an incubating feature.
:shell:generatePomFileForMavenJavaPublication
:shell:compileJava NO-SOURCE
:shell:compileGroovy UP-TO-DATE
:shell:processResources UP-TO-DATE
:shell:classes UP-TO-DATE
:shell:jar UP-TO-DATE
Could not find metadata com.wangyz.plugins:ShellPlugin/maven-metadata.xml in remote (file:/E:/Repository)
:shell:publishMavenJavaPublicationToMavenRepository
:shell:publish
BUILD SUCCESSFUL in 0s
5 actionable tasks: 2 executed, 3 up-to-date
11:22:40: Task execution finished 'publish'.
插件使用
在需求加壳的工程的根build.gradle中引进插件:
buildscript {
repositories {
maven {
url uri('E:\Repository')
}
}
dependencies {
classpath 'com.wangyz.plugins:ShellPlugin:1.0.0'
}
}
allprojects {
repositories {
maven {
url uri('E:\Repository')
}
}
}
在app的build.gradle中使用插件:
//引进插件
apply plugin: 'com.wangyz.plugins.ShellPlugin'
//装备插件
shellConfig {
shellModuleName = 'shell'
keyStore = 'E:\Code\Android\android.keystore'
keyStorePassword = 'android'
keyPassword = 'android'
alias = 'android'
}
因为插件中会用到gradle指令,因而需求先装备gradle的途径到环境变量path中。详细装备,能够找下相关资料,这儿不再展开。
双击履行assembleRelease指令,就会在根目录/ShellApk/output/下生成加壳签名后的apk。
装置加壳签名后的apk,能够正常运转。
2、https防抓包机制
2.1 HTTPS的界说
说道HTTPS,不得不提HTTP,HTTP最大的缺陷便是明文传输,数据传输进程中很简单被篡改,所以美国网景公司提出来HTTPS协议,相对HTTP,HTTPS多了一个S,这个S,其实便是SSL/TSL,SSL全称安全套接字层,TSL1.0(传输层安全协议)是SSL3.0的升级版,是用于服务器和客户端加密通讯的,所以能够以为两者是同一种协议,SSL因为自身的不安全性,在Android8.0已经被弃用了,以上能够看出HTTPS=HTTP+SSL/TLS
2.2 作业原理
1.HTTPS建议SSL衔接,链接到服务器的443端口
2.服务器向客户端发送公钥和数字证书
3.客户端经过随机算法生成私钥,然后经过服务器公钥对该私钥加密,生成对称密钥
4.客户端向服务器发送对称密钥
5.服务器经过对称密钥对数据进行加密
6.客户端经过对称密钥来对数据解密
那问题来了,在第二步,如果有好事者截获了服务器对客户端发送的公钥,然后伪造成服务器与客户端通讯,这可怎么是好呢,怎么判别该公钥是合法的呢,数字证书就排上用场了
2.3 数字证书
数字证书是由CA签发,全世界权威的CA总共100多个,数字证书里包括一对非对称密钥,公钥和私钥以及颁布给、颁布者等信息,里面的公钥对服务器端传输的公钥进行加密,生成密文,然后由客户端的数字证书里的私钥进行解密,从而取得服务器的公钥并确认该公钥是合法的
以浏览器的**www.baidu.com**为例,经过点击链接旁的锁标志来翻开数字证书
证书途径展现的是证书链,根证书的权利最大,依次为根证书>A>B>C,如果**www.baidu.com** 信任了根证书,则意味着A、B、C都信任
经过详细信息能够看到公钥、公钥参数、证书战略等证书的详细信息 手机作为拜访网络的客户端,当然也会有内置证书,以小米手机为例,经过设置-更多设置-体系安全-加密与凭证-信任的凭证来翻开内置的CA证书
体系证书是内置的,个人证书是自界说的,比如charles的抓包证书是放在个人证书下面的,那Android是怎么建议HTTPS恳求的呢?
2.4 抓包的原理
往常使用的抓包东西,无论是fidder和charles都能解析客户端和服务器的HTTPS数据,是怎么做到的呢?其实抓包东西就充当了一个中间人署理的角色,参照2.https的东西原理,抓包的作业原理如下:
- 截获客户端向建议的HTTPS恳求,佯装客户端,向真实的服务器建议恳求
- 截获真实服务器的返回,佯装真实服务器,向客户端发送数据
- 获取了用来加密服务器公钥的非对称秘钥和用来加密数据的对称秘钥
2.5 Android 中使用 HTTPS以及怎么避免抓包
Android中怎么拜访HTTPS呢,其实Retrofit、OkHttp均支持HTTPS的拜访 项目中引进网络库,以implementation ‘com.squareup.okhttp3:okhttp:4.2.0’ 为例,
final OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
final Request request = new Request.Builder()
.url("https://www.baidu.com/robots.txt")
.build();
final Response execute = okHttpClient.newCall(request).execute();
final String bodyStr = execute.body().string();
Log.d(TAG, bodyStr);
那如果封闭客户端的CA证书,GlobalSign Root CA-R1,相当于不信任百度服务器的数字证书,会导致报错
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
经过手动将GlobalSign Root CA-R1.cer
放入项目中的assets文件夹,则可避免这一过错,怎么引用项目中集成的证书呢? 经过
SSLContext sslContext;
try {
InputStream inputStream = getAssets().open("");
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{OkhttpU.trustManagerForCertificates(inputStream)}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient okHttpClient = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, OkhttpU.trustManagerForCertificates(inputStream)).build();
final Request request = new Request.Builder()
.url("https://www.baidu.com/robots.txt")
.build();
final Response execute = okHttpClient.newCall(request).execute();
final String bodyStr = execute.body().string();
Log.d(TAG, bodyStr);
} catch (Exception e) {
e.printStackTrace();
}
public class OkhttpU {
public static X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}
private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
就能够正常拜访https了
综上,经过引进自界说证书,然后给OkHttp设置sslSocketFactory能够有用的避免抓包,但是.cer放到assets下很简单被反编译,能够经过jdk下的指令keytool -printcert -rfc -file srca.cer导出字符串,然后经过
OkHttpClientManager.getInstance()
.setCertificates(new Buffer()
.writeUtf8(CER_STRING) //CER_STRING是处处的string常量
.inputStream());