個人部落格
Android應用加固的簡單實現方案
概述
Android應用加固的諸多方案中,其中一種就是基於dex的加固,本文介紹基於dex的加固方案。
原理:在AndroidManifest中指定啟動Application為殼Module的Application,生成APK後,將殼Module的AAR檔案和加密後的APK中的dex檔案合併,然後重新打包簽名。安裝應用執行後,通過殼Module的Application來解密dex檔案,然後再載入dex。
存在的問題:解密過程,會還原出來未加密的原dex檔案,通過一些手段,還是可以獲得未加密的dex。
實現
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…
基於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,可以正常執行。
原始碼
原始碼地址:github.com/milovetingt…