Android應用加固的簡單實現方案


個人博客

http://www.milovetingting.cn

Android應用加固的簡單實現方案

概述

Android應用加固的諸多方案中,其中一種就是基於dex的加固,本文介紹基於dex的加固方案。

原理:在AndroidManifest中指定啟動Application為殼Module的Application,生成APK后,將殼Module的AAR文件和加密后的APK中的dex文件合並,然后重新打包簽名。安裝應用運行后,通過殼Module的Application來解密dex文件,然后再加載dex。

存在的問題:解密過程,會還原出來未加密的原dex文件,通過一些手段,還是可以獲得未加密的dex。

實現

APK和殼AAR的生成

新建工程,然后新建一個Module,作為殼Module,名字隨意,這里命名為shell。

項目結構.png

在殼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

生成apk.png

在app/build/outputs/apk/release/目錄下,可以找到生成的apk:app-release-unsigned.apk

在Android Studio中,點擊Build-Make Module 'shell',生成AAR。

生成aar.png

在shell/build/outputs/aar/目錄下,可以找到生成的aar:shell-debug.aar

加殼的過程

加殼的實現流程如下:

加殼.png

這里選擇Eclipse新建Java工程來操作。

項目結構2.png

項目結構說明:

  1. input:存放需要加殼的apk和aar

  2. keystore:存放簽名用到的keystore文件

  3. output:打包后輸出目錄,signed為簽名后的apk

需要配置的環境變量:

  1. 由於要用到dx來將jar轉換成dex,因此需要配置dx的路徑。在SDK/build-tools/下,有對應不同版本的build工具,這里選擇28.0.0,進入28.0.0文件夾,可以看到dx.bat文件。在電腦的環境變量中,修改path,增加dx.bat路徑:

    dx環境變量配置.png

  2. 由於要用到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安裝在手機上,可以正常運行,達到加固的目的。

源碼

源碼地址:https://github.com/milovetingting/Samples/tree/master/Shell/加固-手動加殼

基於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

創建發布task.png

雙擊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'.

發布插件.png

插件應用

在需要加殼的工程的根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'
}

生成apk.png

由於插件中會用到gradle命令,因此需要先配置gradle的路徑到環境變量path中。具體配置,可以找下相關資料,這里不再展開。

雙擊執行assembleRelease命令,就會在根目錄/ShellApk/output/下生成加殼簽名后的apk。

安裝加殼簽名后的apk,可以正常運行。

源碼

源碼地址:https://github.com/milovetingting/Samples/tree/master/Shell/加固-gradle插件加殼

插件的實現

上面的方案,實際操作起來還是比較麻煩。因此,可以定義一個插件,通過引入這個插件,來實現apk的加固,減少編碼的工作量。

可以參考下一篇文章:Android應用加固的簡單實現方案(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM