安卓整體加殼(一代殼)原理及實踐

Joooook發表於2024-09-15

安卓整體加殼(一代殼)原理及實踐

目錄

  • 1 一代殼簡介
    • 1.1 DEX加密(也稱落地載入)
    • 1.2 相關脫殼方法
  • 2 app啟動流程
    • 2.1 ActivityThread.java
    • 2.2 LoadedApk.java
    • 2.3 Instrumentation.java
    • 2.4 Application.java
    • 2.5 ActivityThread.java
  • 3 基本原理
  • 4 加殼實踐
    • 4.1 源程式
    • 4.2 加殼工具
    • 4.3 脫殼程式
      • 4.3.1 代理Application
      • 4.3.2 讀取自身apk
      • 4.3.3 讀取dex
      • 4.3.4 提取源apk
      • 4.3.5 修正載入器(重點)
      • 4.3.6 載入源apk
      • 4.3.7 載入源Application
      • 4.3.8 載入資源
  • 5 問題
  • 6 引用

寫在前面:寫這篇文章真是嘔心瀝血,網上對一代殼的技術分析很多,但是有實踐操作的文章少。一代殼雖然原理簡單,但是實現細節很多,並且學習一代殼能夠學習到很多二三代殼也用得到的原理和技術,寫這篇文章反反覆覆看了很多其他大佬的文章,但難免還是會漏掉一些要點,比如雙親委派模型就一句話帶過了,還需要讀者們自己去看文章瞭解。由於整體加殼的方式是兩個專案巢狀,想要寫一步除錯一步是有點麻煩的,寫的過程中只能摸著石頭過河,寫完了再一起去debug,還是挺磨性子的。希望這篇文章能給剛入門脫殼的讀者們帶來一些啟發,文中寫的不嚴謹的地方歡迎指正。

1 一代殼簡介

1.1 DEX加密(也稱落地載入)

第一代殼將整個 apk 檔案壓縮加密到殼 dex 檔案的後面,在殼 dex 檔案上寫上解壓程式碼,動態載入執行,由於是加密整個 apk,在大型應用中很耗資源,因此這代殼很早就被放棄了但思路還是不變。其中這種加密還可以具體劃分為幾個方向,如下:

  • Dex 字串加密
  • 靜態 DEX 檔案整體加密解密
  • 資源加密( xml 與 arsc 檔案加密及十六進位制加密)
  • 對抗反編譯(針對反編譯工具,如 apktool。利用反編譯工具本身存在的缺陷,使得反編譯失敗,以此實現對反編譯工具的抵抗)
  • Ptrace 反除錯、TracePid 值校驗反除錯
  • 自定義 DexClassLoader(主要是針對 dex 檔案加固、加殼等情況)
  • 落地載入( dex 可以在 apk 目錄下看到)

1.2 相關脫殼方法

  1. 記憶體 Dump 法
  2. 快取脫殼法
  3. 檔案監視法
  4. Hook 法
  5. 定製系統法
  6. 動態除錯法

2 app啟動流程

ActivityThread.main()是進入App世界的大門,所以從ActivityThread.main()開始,瞭解一下app啟動過程中的一些細節。如果十分了解app的啟動流程,這部分就可以看的快一點。

瞭解啟動流程主要關注呼叫了Application什麼方法,因為一代殼的實現是依賴於app啟動時的一些初始化呼叫來載入或解密dex檔案的。

2.1 ActivityThread.java

原始碼地址:/frameworks/base/core/java/android/app/ActivityThread.java

attach方法如下

這裡的呼叫棧就不深入了,接下來會呼叫到bindApplication方法↓

這裡的HActivityThread的一個內建類

在這個H類中有處理訊息的邏輯

最終呼叫handleBindApplication,進行app例項化。

從例項化開始,就要進行深入了。data.info是一個LoadedApk類。

2.2 LoadedApk.java

原始碼地址:/frameworks/base/core/java/android/app/LoadedApk.java

app的建立進入到了InstrumentationnewApplication

2.3 Instrumentation.java

原始碼地址:/frameworks/base/core/java/android/app/Instrumentation.java

newApplication有兩種實現模式,這裡看引數採用的應當是第一種。

兩種方式都是先例項化app,然後呼叫app.attach。於是接下來看attach做了什麼。

2.4 Application.java

原始碼地址:/frameworks/base/core/java/android/app/Application.java

attach中呼叫了attachBaseContext

2.5 ActivityThread.java

原始碼地址:/frameworks/base/core/java/android/app/ActivityThread.java

再回到ActivityThread.javahandleBindApplication中,還會有呼叫ApplicationOnCreate函式。

至此可知App的啟動流程是

3 基本原理

Application啟動流程結束之後才會進入MainActivity中的attachBaseContext函式、onCreate函式。

所以殼要在程式正式執行前,也就是上面的流程中進行動態載入和類載入器的修正,這樣才能對加密的dex進行釋放,而一般的殼往往選擇在Application中的attachBaseContextonCreate函式進行。

簡單點說,就是把源程式給藏起來,然後在外面包一層用於脫殼的程式,這個脫殼的程式會把源程式給釋放出來,並透過反射機制,載入源程式。

4 加殼實踐

加殼之前,需要明確分為哪幾步。

  1. 生成一個源程式(安卓專案),一般來說是將源程式打包為apk之後藏起來,這樣的好處在於源程式的各類資源也都被藏了起來。舉一反三既然可以藏整個apk,那麼也可以分開藏一些東西。
  2. 寫一個加殼工具,這個程式不是一個安卓專案,可以用任意語言(本文使用python)實現功能,就是一個工具。
  3. 脫殼程式,確定了我們如何藏我們的apk檔案之後,使用脫殼程式來釋放源程式,並載入。

構建完成之後我們app的入口應當在脫殼程式裡。

4.1 源程式

簡單新建專案,建立一個空Activity

ActivityOnCreate方法中列印一下。

Log.i("demo", "app:"+getApplicationContext());

然後新增一個MyAppliaction類,並重寫一下OnCreate,輸出一下Log

4.2 加殼工具

將按照下圖的結構構建新dex

這裡為了理解畫了一個流程圖

這裡沒有對apk資料進行處理,如有需要修改process_apk_data即可。

import hashlib
import os.path
import shutil
import sys
import zlib
import zipfile


def process_apk_data(apk_data:bytes):
    """
    用於處理apk資料,比如加密,壓縮等,都可以放在這裡。
    :param apk_data:
    :return:
    """
    return apk_data

# 使用前需要修改的部分
keystore_path='demo1.keystore'
keystore_password='123456'
src_apk_file_path= '/Users/zhou39512/AndroidStudioProjects/MyApplication/app/build/outputs/apk/debug/app-debug.apk'
shell_apk_file_path= '/Users/zhou39512/AndroidStudioProjects/Unshell/app/build/outputs/apk/debug/app-debug.apk'
buildtools_path='~/Library/Android/sdk/build-tools/34.0.0/'

# 承載apk的檔名
carrier_file_name= 'classes.dex'
# 中間資料夾
intermediate_dir= 'intermediates'
intermediate_apk_name='app-debug.apk'
intermediate_aligned_apk_name='app-debug-aligned.apk'
intermediate_apk_path=os.path.join(intermediate_dir,intermediate_apk_name)
intermediate_carrier_path=os.path.join(intermediate_dir, carrier_file_name)
intermediate_aligned_apk_path=os.path.join(intermediate_dir,intermediate_aligned_apk_name)
if os.path.exists(intermediate_dir):
    shutil.rmtree(intermediate_dir)
os.mkdir(intermediate_dir)

# 解壓apk
shell_apk_file=zipfile.ZipFile(shell_apk_file_path)
shell_apk_file.extract(carrier_file_name,intermediate_dir)

# 查詢dex
if not os.path.exists(os.path.join(intermediate_dir, carrier_file_name)):
    raise FileNotFoundError(f'{carrier_file_name} not found')

src_dex_file_path= os.path.join(intermediate_dir, carrier_file_name)

#讀取
src_apk_file=open(src_apk_file_path, 'rb')
src_dex_file=open(src_dex_file_path, 'rb')

src_apk_data=src_apk_file.read()
src_dex_data=src_dex_file.read()

# 處理apk資料
processed_apk_data=process_apk_data(src_apk_data)
processed_apk_size=len(processed_apk_data)

# 構建新dex資料
new_dex_data=src_dex_data+processed_apk_data+int.to_bytes(processed_apk_size,8,'little')

# 更新檔案大小
file_size=len(processed_apk_data)+len(src_dex_data)+8
new_dex_data=new_dex_data[:32]+int.to_bytes(file_size,4,'little')+new_dex_data[36:]

# 更新sha1摘要
signature=hashlib.sha1().digest()
new_dex_data=new_dex_data[:12]+signature+new_dex_data[32:]

# 更新checksum
checksum=zlib.adler32(new_dex_data[12:])
new_dex_data=new_dex_data[:8]+int.to_bytes(checksum,4,'little')+new_dex_data[12:]

# 寫入新dex
intermediate_carrier_file= open(intermediate_carrier_path, 'wb')
intermediate_carrier_file.write(new_dex_data)
intermediate_carrier_file.close()
src_apk_file.close()
src_dex_file.close()

# 新增環境變數,為重打包做準備
os.environ.update({'PATH':os.environ.get('PATH')+f':{buildtools_path}'})
# 重打包
r=os.popen(f"cp {shell_apk_file_path} {intermediate_apk_path}").read()
print(r)
os.chdir(intermediate_dir)
r=os.popen(f'zip {intermediate_apk_name} {carrier_file_name}').read()
os.chdir('../')
print(r)
# 對齊
r=os.popen(f'zipalign 4 {intermediate_apk_path} {intermediate_aligned_apk_path}').read()
print(r)
# 簽名
r=os.popen(f'apksigner sign -ks {keystore_path} --ks-pass pass:{keystore_password} {intermediate_aligned_apk_path}').read()
print(r)
r=os.popen(f'cp {intermediate_aligned_apk_path} ./app-out.apk').read()
print(r)
print('Success')

這裡涉及到重打包的過程,具體可以看安卓打包流程。這裡需要用安卓SDK中的zipalignapksigner進行對齊和重打包。

4.3 脫殼程式

接下來編寫套在源程式外面的脫殼程式。由於我們最終需要執行的是我們的源程式,所以我們必須在啟動流程呼叫ApplicationOnCreate之前釋放出源程式,並替換Application為我們的源程式Application例項(原來是脫殼程式的Application例項)。

我們在基本原理這一節中研究了啟動流程,所以在ApplicationOnCreate之前,有一個attachBaseContext方法,我們可以透過重寫該方法來實現上面的效果。

4.3.1 代理Application

這裡我們要寫一個代理Application,作為appApplication例項。並重寫一下attachBaseContext

public class ProxyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        Log.i("demo","attachBaseContext");
        super.attachBaseContext(base);
    }
}

然後要修改AndroidManifest.xml,將Application的例項改為我們自定義的ProxyApplication

之後執行。在Logcat中看到輸出了log則說明成功。

4.3.2 讀取自身apk

Application中,需要獲取到自身的apk檔案。

private ZipFile getApkZip() throws IOException {
    Log.i("demo", this.getApplicationInfo().sourceDir);
    ZipFile apkZipFile = new ZipFile(this.getApplicationInfo().sourceDir);
    return apkZipFile;
}

我們先測試一下,列印看看this.getApplicationInfo().sourceDir是什麼

發現是一個快取儲存apk的地址,並且就是apk的路徑(而非資料夾路徑)。

4.3.3 讀取dex

private byte[] readDexFileFromApk() throws IOException {
    /* 從本體apk中獲取dex檔案 */
    ZipFile apkZip = this.getApkZip();
    ZipEntry zipEntry = apkZip.getEntry("classes.dex");
    InputStream inputStream = apkZip.getInputStream(zipEntry);
    byte[] buffer = new byte[1024];
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        baos.write(buffer, 0, length);
    }
    return baos.toByteArray();
}

4.3.4 提取源apk

private byte[] splitSrcApkFromDex(byte[] dexFileData) {
    /* 從dex檔案中分離源apk檔案 */
    int length = dexFileData.length;
    ByteBuffer bb = ByteBuffer.wrap(Arrays.copyOfRange(dexFileData, length - 8, length));
    bb.order(java.nio.ByteOrder.LITTLE_ENDIAN); // 設定為小端模式
    long processedSrcApkDataSize = bb.getLong(); // 讀取這8個位元組作為long型別的值
    byte[] processedSrcApkData=Arrays.copyOfRange(dexFileData, (int) (length - 8 - processedSrcApkDataSize), length - 8);
    byte[] srcApkData=reverseProcessApkData(processedSrcApkData);
    return srcApkData;
}

4.3.5 修正載入器(重點)

這裡開始需要了解雙親委派模型,簡單而言就是java中的類載入器有父子關係,當某個載入器需要載入某個類的時候,先會交給其父類,如果載入過了就直接返回,如此往上,如果父載入器都載入不了,再拋回來自己載入。

關於載入源apk,這裡有兩個細節且重要的問題需要思考清楚。從這裡開始希望大家放慢閱讀速度。

  • 如何載入**dex**檔案?
  • 如何讓載入之後的**Application**進入後續的載入流程?

這裡拿一張非常重要的圖

首先解決第一個問題,如何載入**dex**檔案?

引用佬的文章介紹一下BaseDexClassLoader類載入器

Android裡邊的BaseDexClassLoader可以實現在執行的時候載入在編譯時未知的dex檔案,經過此載入器的載入,ART虛擬機器記憶體中會形成相應的資料結構,對應的dex檔案也會由mmap對映到虛擬記憶體當中,透過此載入器的loadClass(String className, boolean resolve)方法就可以得到類的Class物件,從而可以使用該類。

檢視原始碼可以看到PathClassLoader是繼承自BaseDexClassLoader的,而PathClassLoader還有另外兩個兄弟: InMemoryDexClassLoader以及DexClassLoader,而殼程式很多都使用了這兩個類載入器來載入解密後的dex檔案。其中InMemoryDexClassLoader是Android8.0以後新增的類,可以實現所謂的"不落地載入"。

作者:Jerry_Deng
連結:https://juejin.cn/post/6962096676576165918
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

總結一下,InMemoryDexClassLoaderDexClassLoaderPathClassLoader都繼承自BaseDexClassLoader,我們可以用他們來載入dex

第二個問題,如何讓載入之後的**Application**進入後續的載入流程?

後續的載入流程指的就是app元件(比如Activity)的載入,而載入元件時,使用的是載入應用程式的ClassLoader

如若不做任何處理,僅僅在**attachBaseContext方法中使用上面講的某個類載入器對dex載入,後續載入源程式的元件時會出現ClassNotFoundException**的錯誤,為什麼會這樣?

這是因為如果僅僅在attachBaseContext方法中使用類載入器載入dex,之後載入元件時使用的ClassLoader和我們使用的載入器不同,並且,載入元件的ClassLoader透過雙親委派模型發現沒有人能載入元件類(因為元件類在我們的dex中),導致ClassNotFoundException

還記得BaseDexClassLoader嗎,其有一個DexPathList,記錄了已載入的dex檔案路徑。

載入元件時對應的BaseDexClassLoaderDexPathList是沒有源程式的dex路徑的,如果嘗試讓BaseDexClassLoader載入不在這個列表中的類,就會報ClassNotFoundException

因此有兩種方法可以解決這個問題。

  1. 既然使用的載入器不同,那麼改成相同的不就行了。

    透過反射獲取到LoadedApk,修改其mClassLoader為我們載入dex檔案的ClassLoader例項,這樣後續試圖載入元件類的時候,就能找到相應的類。

  2. 透過打破原有雙親委派關係,新增我們的ClassLoader進入關係網。

    原先的mClassLoaderPathClassLoader,其在雙親委派關係中的父親是BootClassLoader,所以只要將我們的ClassLoader新增進他們兩個之間即可。也就是將PathClassLoader的父親設定為我們自己的ClassLoader,再將我們自己的ClassLoader的父親設定為BootClassLoader。如下圖

理解完以上這些,可以開始實踐了。


第一種方法,需要思考如何拿到LoadedApk。在啟動流程的handleBindApplication中,data.info就是我們要拿到的LoadedApk

向上找到data.info初始化的地方。

跟進方法

關鍵程式碼

所以我們要從mPackages裡面找LoadedApk

public static void replaceClassLoader1(Context context,DexClassLoader dexClassLoader){
    ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
    try {
        // 1.透過currentActivityThread方法獲取ActivityThread例項
        Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");
        Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");
        Object activityThreadObj = currentActivityThread.invoke(null);
        // 2.拿到mPackagesObj
        Field mPackagesField = ActivityThread.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);
        // 3.拿到LoadedApk
        String packageName = context.getPackageName();
        WeakReference wr = (WeakReference) mPackagesObj.get(packageName);
        Object LoadApkObj = wr.get();
        // 4.拿到mClassLoader
        Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
        Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        Object mClassLoader =mClassLoaderField.get(LoadApkObj);
        Log.i("mClassLoader",mClassLoader.toString());
        // 5.將系統元件ClassLoader給替換
        mClassLoaderField.set(LoadApkObj,dexClassLoader);
    }
    catch (Exception e) {
        Log.i("demo", "error:" + Log.getStackTraceString(e));
        e.printStackTrace();
    }
}

4.3.6 載入源apk

我們使用DexClassLoader載入dex,還需要解決幾個引數。

  • dexPathdex檔案路徑
  • optimizedDirectorydex 最佳化後存放的位置,在 ART 上,會執行 oatdex 進行最佳化,生成機器碼,這裡就是存放最佳化後的 odex 檔案的位置。
  • librarySearchPathnative 依賴的位置
  • parent:雙親委派中的父親,這裡是PathClassLoader

Context.getDir方法是在app的目錄下新建app_的資料夾。比如列印base.getDir("opt_dex",0),結果是 /data/user/0/com.xxx.unshell/app_opt_dex

程式碼如下

@Override
protected void attachBaseContext(Context base) {
    Log.i("demo", "attachBaseContext");
    super.attachBaseContext(base);
    try {
        byte[] dexFileData=this.readDexFileFromApk();
        byte[] srcApkData=this.splitSrcApkFromDex(dexFileData);
        // 建立儲存apk的資料夾,寫入src.apk
        File apkDir=base.getDir("apk_out",MODE_PRIVATE);
        srcApkPath=apkDir.getAbsolutePath()+"/src.apk";
        File srcApkFile = new File(srcApkPath);
        srcApkFile.setWritable(true);
        FileOutputStream fos=new FileOutputStream(srcApkFile);
        Log.i("demo", String.format("%d",srcApkData.length));
        fos.write(srcApkData);
        fos.close();
        srcApkFile.setReadOnly(); // 受安卓安全策略影響,dex必須為只讀
        Log.i("demo","Write src.apk into "+srcApkPath);
        // 新建載入器
        File optDir =base.getDir("opt_dex",MODE_PRIVATE);
        File libDir =base.getDir("lib_dex",MODE_PRIVATE);
        optDirPath =optDir.getAbsolutePath();
        libDirPath =libDir.getAbsolutePath();
        ClassLoader pathClassLoader = ProxyApplication.class.getClassLoader();
        DexClassLoader dexClassLoader=new DexClassLoader(srcApkPath, optDirPath, libDirPath,pathClassLoader);
        Log.i("demo","Successfully initiate DexClassLoader.");
        // 修正載入器
        replaceClassLoader1(base,dexClassLoader);
        Log.i("demo","ClassLoader replaced.");

    } catch (Exception e) {
        Log.i("demo", "error:" + Log.getStackTraceString(e));
        e.printStackTrace();
    }
}

當我們替換掉載入器之後,app載入流程走完,會載入Activity,此時我們為了讓系統載入我們源程式的Activity,我們需要修改xml檔案,將脫殼程式的Activity入口替換為源程式的入口。

之後我們build apk,然後用加殼程式處理,並安裝。

啟動程式,檢視log,發現了我們在源程式中寫的Log,說明啟動源程式的Activity成功。

載入成功!

4.3.7 載入源Application

到了這裡加殼的核心部分已經結束了,接下來都是補充的部分。

如果源程式也有自定義的Application,我們就需要重新makeApplication,進入到源程式的Application,保證程式的完整生命週期。

  • 註冊application(用LoadedApk中的makeApplication方法註冊)。

    為了使用makeApplication重新註冊application,需要先把mApplication置空

    並且還需要在在ActivityThread下的連結串列mAllApplications中移除mInitialApplicationmAllApplications存放的是所有的應用,mInitialApplication存放的是初始化的應用(即當前殼應用)。把當前的殼應用,從現有的應用中移除掉,然後在makeApplication方法中會把新構建的加入到裡面去。

    之後,替換ActivityThreadmInitialApplication為剛剛makeApplication建立的app

    總結操作流程就是

    • LoadedApkmApplication置空
    • ActivityThreadmAllApplications中移除mInitialApplication
    • makeApplication()
    • 替換ActivityThreadmInitialApplication
    super.onCreate();
    Log.i(TAG,"進入onCreate方法");
    
    // 提取提前配置的ApplicationName,來引導到源程式的Application入口
    String applicationName="";
    ApplicationInfo ai=null;
    try {
        ai=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        if (ai.metaData!=null){
            applicationName=ai.metaData.getString("ApplicationName");
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    
    // 將當前程序的mApplication設定為null
    Object activityThreadObj=RefinvokeMethod.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
    Object mBoundApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mBoundApplication");
    Object info=RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
    RefinvokeMethod.setField("android.app.LoadedApk","mApplication",info,null);
    
    // 從ActivityThread的mAllApplications中移除mInitialApplication
    Object mInitApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mInitialApplication");
    ArrayList<Application> mAllApplications= (ArrayList<Application>) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mAllApplications");
    mAllApplications.remove(mInitApplication);
    
    // 更新兩處className
    ApplicationInfo mApplicationInfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.LoadedApk",info,"mApplicationInfo");
    ApplicationInfo appinfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"appInfo");
    mApplicationInfo.className=applicationName;
    appinfo.className=applicationName;
    
    // 執行makeApplication(false,null)
    Application app= (Application) RefinvokeMethod.invokeMethod("android.app.LoadedApk","makeApplication",info,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});
    
    // 替換ActivityThread中mInitialApplication
    RefinvokeMethod.setField("android.app.ActivityThread","mInitialApplication",activityThreadObj,app);
    
    // 更新ContentProvider
    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }
    
    // 執行新app的onCreate方法
    app.onCreate();
    

    解釋一下這裡修改className的操作。源程式可能也有自定義的一個Application類,如果有的話我們需要提前配置在xmlmeta-data中提前設定,之後提取出來。

    當然也可以透過解析源程式的xml來實現,感興趣可以研究一下。

  • 更新ContentProvider

    ContentProviderAndroid系統中的一個元件,用於在不同的應用程式之間共享資料。需要修改mProviderMap中所有ContentProvidermContext為新app

    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }
    
  • 執行新app的onCreate方法

    app.onCreate();
    

總體程式碼如下,這裡用到的RefinvokeMethod貼到了文章最下面問題一節中。

@Override
public void onCreate() {
    super.onCreate();

    loadResources(apkFileName);
    Log.i(TAG,"進入onCreate方法");

    // 提取提前配置的ApplicationName,來引導到源程式的Application入口
    String applicationName="";
    ApplicationInfo ai=null;
    try {
        ai=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        if (ai.metaData!=null){
            applicationName=ai.metaData.getString("ApplicationName");
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }

    // 將當前程序的mApplication設定為null
    Object activityThreadObj=RefinvokeMethod.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
    Object mBoundApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mBoundApplication");
    Object info=RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
    RefinvokeMethod.setField("android.app.LoadedApk","mApplication",info,null);

    // 從ActivityThread的mAllApplications中移除mInitialApplication
    Object mInitApplication=RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mInitialApplication");
    ArrayList<Application> mAllApplications= (ArrayList<Application>) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mAllApplications");
    mAllApplications.remove(mInitApplication);

    // 更新兩處className
    ApplicationInfo mApplicationInfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.LoadedApk",info,"mApplicationInfo");
    ApplicationInfo appinfo= (ApplicationInfo) RefinvokeMethod.getField("android.app.ActivityThread$AppBindData",mBoundApplication,"appInfo");
    mApplicationInfo.className=applicationName;
    appinfo.className=applicationName;

    // 執行makeApplication(false,null)
    Application app= (Application) RefinvokeMethod.invokeMethod("android.app.LoadedApk","makeApplication",info,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});

    // 替換ActivityThread中mInitialApplication
    RefinvokeMethod.setField("android.app.ActivityThread","mInitialApplication",activityThreadObj,app);

    // 更新ContentProvider
    ArrayMap mProviderMap= (ArrayMap) RefinvokeMethod.getField("android.app.ActivityThread",activityThreadObj,"mProviderMap");
    Iterator iterator=mProviderMap.values().iterator();
    while (iterator.hasNext()){
        Object mProviderClientRecord=iterator.next();
        Object mLocalProvider=RefinvokeMethod.getField("android.app.ActivityThread$ProviderClientRecord",mProviderClientRecord,"mLocalProvider");
        RefinvokeMethod.setField("android.content.ContentProvider","mContext",mLocalProvider,app);
    }

    // 執行新app的onCreate方法
    app.onCreate();
}

這裡透過Log我們可能會看到報錯如下

這是不影響的,因為我們等於是又重新啟動了一次App,只不過這次的Application設定的是源程式的Application

這個報錯的原始碼位於LoadedApk.java


可以看到我們的源程式自定義的Application還是成功載入了

4.3.8 載入資源

我們透過這樣的方式載入源程式,源程式的資源似乎並沒有被載入進來,所以這裡繼續講如何把源程式的資源載入進來。

當然我們也可以直接把源程式的資源複製到殼程式下面,但加殼的目就是為了保護程式碼和資源,所以最好還是動態載入。

這裡資源載入可能還涉及到Resources類的更換,實現起來還有點麻煩,這裡就暫且閣下不表。

5 問題

  • 在過程中出現 Writable dex file '/data/user/0/com.jok.unshell/app_opt_dex/src.apk' is not allowed這樣的報錯,原因是Android14有一個改動: 更安全的動態程式碼載入,簡單來說就是開啟DEXJARAPK等檔案時必須將DEX檔案設定為只讀。
  • RefinvokeMethod.java
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    public class RefinvokeMethod {
        public static Object invokeStaticMethod(String class_name,String method_name,Class[] classes,Object[] objects){
            try {
                Class aClass = Class.forName(class_name);
                Method method = aClass.getMethod(method_name, classes);
                return method.invoke(null,objects);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object invokeMethod(String class_name,String method_name,Object obj,Class[] classes,Object[] objects){
            try {
                Class aClass = Class.forName(class_name);
                Method method = aClass.getMethod(method_name, classes);
                return method.invoke(obj,objects);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object getField(String class_name,Object obj,String field_name){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                return field.get(obj);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static Object getStaticField(String class_name,String field_name){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                return field.get(null);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public static void setField(String class_name,String field_name,Object obj,Object value){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                field.set(obj,value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void setStaticField(String class_name,String field_name,Object value){
            try {
                Class aClass = Class.forName(class_name);
                Field field = aClass.getDeclaredField(field_name);
                field.setAccessible(true);
                field.set(null,value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
  • 如果沒在xml裡面指定源程式的Activity,那就需要在殼程式的attachBaseContext中新增如下程式碼執行MainActivity
    try {
        Object objectMain = dexClassLoader.loadClass("com.example.sourceapk.MainActivity");
        Log.i(TAG,"MainActivity類載入完畢");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    

6 引用

安卓逆向-脫殼學習記錄 - Is Yang's Blog

Android漏洞之戰——整體加殼原理和脫殼技巧詳解

Android中的Apk的加固(加殼)原理解析和實現 - roccheung - 部落格園

android一代殼脫殼方法總結 - 怎麼可以吃突突 - 部落格園

Android脫殼之整體脫殼原理與實踐這裡所說的殼都是指dex加殼,不涉及到so的加殼。 涉及到的程式碼分析基於AOSP - 掘金

【Android 脫殼】DEX殼簡單實現過程分析_mboundapplication-CSDN部落格

相關文章