版權歸作者所有,如有轉發,請注明文章出處:https://cyrus-studio.github.io/blog/
Frida + FART 聯手能帶來什么提升?
-
增強 FART 的脫殼能力:解決對抗 FART 的殼、動態加載的 dex 的 dump 和修復;
-
控制 FART 主動調用的范圍,讓 FART 更精細化,比如按需進行類甚至是函數的修復。
非雙親委派關系下動態加載的 dex 脫殼問題
由于動態加載的 dex 沒有取改變 android 中 ClassLoader 雙親委派關系,所以動態加載的 dex 沒有自動脫殼。
相關文章:
-
深入理解 Android ClassLoader 與雙親委派機制
-
深入剖析 Android 加殼應用運行流程與生命周期劫持方案
在 android studio 中創建一個 plugin module 其中包含一個 FartTest 類源碼如下:
package com.cyrus.example.pluginimport android.util.Logclass FartTest {fun test(): String {Log.d("FartTest", "call FartTest test().")return "String from FartTest."}}
把 plugin-debug.apk push 到 files 目錄下
adb push "D:\Projects\AndroidExample\plugin\build\intermediates\apk\debug\plugin-debug.apk" /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk
ls 一下 files 目錄是否存在 plugin-debug.apk
adb shell ls /sdcard/Android/data/com.cyrus.example/files
在 app 動態加載 files 目錄下的 plugin-debug.apk 并調用 FartTest 的 test 方法
val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apk
val classLoader = DexClassLoader(apkPath,null,this@FartActivity.packageResourcePath,classLoader // parent 設為當前 context 的類加載器
)// classLoader 加載 com.cyrus.example.plugin.FartTest 類并通過反射調用 test 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("test")
method.isAccessible = true
val result = method.invoke(instance) as? Stringlog("動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}")mClassLoader = classLoader
脫殼完成,但是沒有對 plugin-debug.apk 中的目標類 FartTest 發起主動調用
這時候 frida 就派上用場了,因為 frida 本身具有枚舉所有 ClassLoader 的能力。
Frida + FART 脫殼動態加載的 dex
枚舉出所有 ClassLoader 后,再結合 FART 的 api 就可以實現動態加載 dex 的脫殼。
function invokeAllClassloaders() {Java.perform(function () {try {// 獲取 ActivityThread 類var ActivityThread = Java.use("android.app.ActivityThread");Java.enumerateClassLoaders({onMatch: function (loader) {try {// 過濾掉 BootClassLoaderif (loader.toString().includes("BootClassLoader")) {console.log("[-] 跳過 BootClassLoader");return;}// 調用 fartWithClassLoaderconsole.log("[*] 調用 fartwithClassloader -> " + loader);ActivityThread.fartwithClassloader(loader);} catch (e) {console.error("[-] 調用失敗: " + e);}},onComplete: function () {console.log("[*] 枚舉并調用完畢");}});} catch (err) {console.error("[-] 腳本執行異常: " + err);}});
}setImmediate(invokeAllClassloaders)
把 log 導出到 txt
adb logcat -v time > logcat.txt
打開 app 后執行腳本
frida -H 127.0.0.1:1234 -F -l fart_invoke_all_classloaders.js
從輸出日志可以看到已經成功對 FartTest 類中方法發起主動調用
局部變量的 ClassLoader 枚舉不出來
但還有一個問題呢:局部變量的 ClassLoader 枚舉不出來。
因為:
-
enumerateClassLoaders() 只枚舉當前 VM 中可訪問的、被 GC Root 持有的 ClassLoader;
-
如果 DexClassLoader 作為臨時變量創建后,沒有被保存,就會被 GC 回收或無法遍歷到。
比如,下面的 Kotlin 代碼中,當 DexClassLoader 為局部變量時就沒有枚舉出這個 DexClassLoader 。
/*** 局部變量的 ClassLoader*/
fun onLocalClassLoaderClicked(log: (String) -> Unit) {val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apkval classLoader = DexClassLoader(apkPath,null,this@FartActivity.packageResourcePath,classLoader // parent 設為當前 context 的類加載器)// classLoader 加載 com.cyrus.example.plugin.FartTest 類并通過反射調用 test 方法val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.FartTest")val constructor = pluginClass.getDeclaredConstructor()constructor.isAccessible = trueval instance = constructor.newInstance()val method = pluginClass.getDeclaredMethod("test")method.isAccessible = trueval result = method.invoke(instance) as? Stringlog("局部變量的 ClassLoader 動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}\n\n")
}
在構造 ClassLoader 時脫殼
所以,為了解決這種情況,我們 hook DexClassLoader 構造函數去調用 FART 脫殼 就可以解決了。
function fartOnDexclassloader() {Java.perform(function () {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String', // dexPath'java.lang.String', // optimizedDirectory'java.lang.String', // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log(" |- dexPath: " + dexPath);console.log(" |- optimizedDirectory: " + optimizedDirectory);console.log(" |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};});
}setImmediate(fartOnDexclassloader)
啟動 app 并執行腳本
frida -H 127.0.0.1:1234 -l fart_on_dexclassloader.js -f com.cyrus.example
frida 日志如下:
Spawned `com.cyrus.example`. Use %resume to let the main thread start executing!
[Remote::com.cyrus.example]-> %resume
[Remote::com.cyrus.example]-> [+] DexClassLoader created:|- dexPath: /sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk|- optimizedDirectory: null|- libPath: /data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk
[*] Calling fartWithClassLoader...
[+] fartWithClassLoader finished.
可以看到成功 hook 到 局部變量的 DexClassLoader 構造函數
從 logcat 可以看到正在對 ClassLoader 中的類方法發起主動調用
等調用完成,進入 fart 目錄下可以看到脫殼下來的文件
wayne:/sdcard/Android/data/com.cyrus.example/fart # ls
12968_class_list.txt 17104392_ins_7079.bin 400440_class_list_execute.txt 54120_dex_file.dex
12968_class_list_execute.txt 17268924_class_list.txt 400440_dex_file_execute.dex 54120_ins_7079.bin
12968_dex_file.dex 17268924_dex_file.dex 4461704_class_list.txt 66552_class_list_execute.txt
12968_dex_file_execute.dex 17268924_ins_7079.bin 4461704_dex_file.dex 66552_dex_file_execute.dex
12968_ins_7079.bin 20996_class_list_execute.txt 4461704_ins_7079.bin 9085048_class_list_execute.txt
16800_class_list_execute.txt 20996_dex_file_execute.dex 536008_class_list.txt 9085048_dex_file_execute.dex
16800_dex_file_execute.dex 21024_class_list_execute.txt 536008_class_list_execute.txt 9248236_class_list.txt
17104392_class_list.txt 21024_dex_file_execute.dex 536008_dex_file.dex 9248236_class_list_execute.txt
17104392_class_list_execute.txt 33196_class_list.txt 536008_dex_file_execute.dex 9248236_dex_file.dex
17104392_dex_file.dex 33196_dex_file.dex 536008_ins_7079.bin 9248236_dex_file_execute.dex
17104392_dex_file_execute.dex 33196_ins_7079.bin 54120_class_list.txt 9248236_ins_7079.bin
控制 FART 主動調用的范圍
FART 中添加的 api 天生為脫殼而生,比如 fartwithClassLoader,loadClassAndInvoke,dumpArtMethod 等等這些接口都可以由 Frida 進行主動調用來控制脫殼精細度。
1. 過濾某些主動調用
hook loadClassAndInvoke 過濾掉某些 class 的主動調用,加快脫殼進程。
比如:過濾掉 androidx.* 、org.jetbrains.* 、kotlinx.* 、org.intellij.* 相關的主動調用
// 前綴過濾邏輯
function shouldSkipClass(name) {return name.startsWith("androidx.") ||name.startsWith("android.") ||name.startsWith("com.google.android.") ||name.startsWith("org.jetbrains.") ||name.startsWith("kotlinx.") ||name.startsWith("kotlin.") ||name.startsWith("org.intellij.");
}function hookLoadClassAndInvoke() {const ActivityThread = Java.use('android.app.ActivityThread');if (ActivityThread.loadClassAndInvoke) {ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {if (shouldSkipClass(className)) {console.log('[skip] loadClassAndInvoke: ' + className);return; // 不調用原函數}console.log('[load] loadClassAndInvoke: ' + className);return this.loadClassAndInvoke(classloader, className, method); // 正常調用};} else {console.log('[-] ActivityThread.loadClassAndInvoke not found');}
}function fartOnDexclassloader() {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String', // dexPath'java.lang.String', // optimizedDirectory'java.lang.String', // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log(" |- dexPath: " + dexPath);console.log(" |- optimizedDirectory: " + optimizedDirectory);console.log(" |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};
}setImmediate(function () {Java.perform(function () {hookLoadClassAndInvoke()fartOnDexclassloader()})
})
執行腳本并輸出日志到 log.txt
frida -H 127.0.0.1:1234 -l fart_loadClassAndInvoke_filter.js -f com.cyrus.example -o log.txt
輸出日志如下:
2. fart thread 調用
由于每個 app 啟動都會自動調用 fartthread,有點影響手機性能。
先去掉 ActivityThread.java 中 fartthread 調用
路徑:frameworks/base/core/java/android/app/ActivityThread.java
通過 frida 調用 fartthread:
function fartThread() {Java.perform(function () {const ActivityThread = Java.use('android.app.ActivityThread')ActivityThread.fartthread()})
}setImmediate(fartThread)
執行腳本針對當前前臺應用啟動 fart thread 開始脫殼
frida -H 127.0.0.1:1234 -F -l fart_thread.js
執行效果如下:
3. 對某個類發起主動調用
如果我們只想單獨對某個類發起主動調用。
通過反射拿到 dumpMethodCode
function findDumpMethodCodeMethod(){let dumpMethodCodeMethod = null;// 反射獲取 dumpMethodCode 方法try {const DexFile = Java.use("dalvik.system.DexFile");const dexFileClazz = DexFile.class;const declaredMethods = dexFileClazz.getDeclaredMethods();for (let i = 0; i < declaredMethods.length; i++) {const m = declaredMethods[i];if (m.getName().toString() === "dumpMethodCode") {m.setAccessible(true);dumpMethodCodeMethod = m;break;}}if (!dumpMethodCodeMethod) {console.log("[-] dumpMethodCode not found in DexFile");return;}console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());} catch (err) {console.log("[-] Exception: " + err);}return dumpMethodCodeMethod
}
調用 LoadClassAndInvoke 對指定類發起主動調用
function invokeClass(targetClassName, dumpMethodCodeMethod) {let foundLoader = findClassLoader(targetClassName)const ActivityThread = Java.use("android.app.ActivityThread");// 調用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)if (ActivityThread.loadClassAndInvoke) {console.log('[load] loadClassAndInvoke: ' + targetClassName);ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);} else {console.log("[-] ActivityThread.loadClassAndInvoke not found");}
}
完整源碼如下:
function findClassLoader(targetClassName) {let foundLoader = null;try {Java.enumerateClassLoaders({onMatch: function (loader) {try {const clazz = loader.loadClass(targetClassName);if (clazz) {console.log("[+] Found class in loader: " + loader.toString());foundLoader = loader;throw "found"; // 快速退出枚舉}} catch (e) {// Ignore: class not found in this loader}},onComplete: function () {}});} catch (e) {if (e !== "found") {console.log("[-] ClassLoader enumeration error: " + e);}}if (!foundLoader) {console.log("[-] Could not find class: " + targetClassName);}return foundLoader
}function findDumpMethodCodeMethod(){let dumpMethodCodeMethod = null;// 反射獲取 dumpMethodCode 方法try {const DexFile = Java.use("dalvik.system.DexFile");const dexFileClazz = DexFile.class;const declaredMethods = dexFileClazz.getDeclaredMethods();for (let i = 0; i < declaredMethods.length; i++) {const m = declaredMethods[i];if (m.getName().toString() === "dumpMethodCode") {m.setAccessible(true);dumpMethodCodeMethod = m;break;}}if (!dumpMethodCodeMethod) {console.log("[-] dumpMethodCode not found in DexFile");return;}console.log("[+] dumpMethodCode Method: " + dumpMethodCodeMethod.toString());} catch (err) {console.log("[-] Exception: " + err);}return dumpMethodCodeMethod
}function invokeClass(targetClassName, dumpMethodCodeMethod) {let foundLoader = findClassLoader(targetClassName)const ActivityThread = Java.use("android.app.ActivityThread");// 調用 ActivityThread.loadClassAndInvoke(loader, className, dumpMethodCodeMethod)if (ActivityThread.loadClassAndInvoke) {console.log('[load] loadClassAndInvoke: ' + targetClassName);ActivityThread.loadClassAndInvoke(foundLoader, targetClassName, dumpMethodCodeMethod);} else {console.log("[-] ActivityThread.loadClassAndInvoke not found");}
}setImmediate(function () {Java.perform(function () {let dumpMethodCodeMethod = findDumpMethodCodeMethod()// TODO: 替換為你的目標類invokeClass("com.cyrus.example.plugin.FartTest", dumpMethodCodeMethod)})
})
執行腳本,附近到當前前臺應用
frida -H 127.0.0.1:1234 -F -l fart_invoke_class.js
輸入如下:
[+] dumpMethodCode Method: private static native void dalvik.system.DexFile.dumpMethodCode(java.lang.Object)
[+] Found class in loader: dalvik.system.DexClassLoader[DexPathList[[zip file "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-DjrDTvMGrC1TBVLehVPmHQ==/base.apk, /system/lib64, /system/product/lib64]]]
[load] loadClassAndInvoke: com.cyrus.example.plugin.FartTest
在 Logcat 中可以看到只對指定的類進行了主動加載和調用
代碼與功能整合
整合代碼實現如下功能:
-
過濾不需要主動調用的類
-
解決局部變量的 ClassLoader 枚舉不出來問題
-
解決非雙親委派關系下動態加載的 dex 脫殼問題
完整代碼如下:
// 前綴過濾邏輯
function shouldSkipClass(name) {return name.startsWith("androidx.") ||name.startsWith("android.") ||name.startsWith("com.google.android.") ||name.startsWith("org.jetbrains.") ||name.startsWith("kotlinx.") ||name.startsWith("kotlin.") ||name.startsWith("org.intellij.");
}function hookLoadClassAndInvoke() {const ActivityThread = Java.use('android.app.ActivityThread');if (ActivityThread.loadClassAndInvoke) {ActivityThread.loadClassAndInvoke.implementation = function (classloader, className, method) {if (shouldSkipClass(className)) {console.log('[skip] loadClassAndInvoke: ' + className);return; // 不調用原函數}console.log('[load] loadClassAndInvoke: ' + className);return this.loadClassAndInvoke(classloader, className, method); // 正常調用};} else {console.log('[-] ActivityThread.loadClassAndInvoke not found');}
}function fartOnDexclassloader() {var DexClassLoader = Java.use("dalvik.system.DexClassLoader");var ActivityThread = Java.use("android.app.ActivityThread");DexClassLoader.$init.overload('java.lang.String', // dexPath'java.lang.String', // optimizedDirectory'java.lang.String', // librarySearchPath'java.lang.ClassLoader' // parent).implementation = function (dexPath, optimizedDirectory, libPath, parent) {console.log("[+] DexClassLoader created:");console.log(" |- dexPath: " + dexPath);console.log(" |- optimizedDirectory: " + optimizedDirectory);console.log(" |- libPath: " + libPath);var cl = this.$init(dexPath, optimizedDirectory, libPath, parent);// 調用 fart 方法try {console.log("[*] Calling fartWithClassLoader...");ActivityThread.fartwithClassloader(this);console.log("[+] fartWithClassLoader finished.");} catch (e) {console.error("[-] Error calling fartWithClassLoader:", e);}return cl;};
}function invokeAllClassloaders() {try {// 獲取 ActivityThread 類var ActivityThread = Java.use("android.app.ActivityThread");Java.enumerateClassLoaders({onMatch: function (loader) {try {// 過濾掉 BootClassLoaderif (loader.toString().includes("BootClassLoader")) {console.log("[-] 跳過 BootClassLoader");return;}// 調用 fartWithClassLoaderconsole.log("[*] 調用 fartwithClassloader -> " + loader);ActivityThread.fartwithClassloader(loader);} catch (e) {console.error("[-] 調用失敗: " + e);}},onComplete: function () {console.log("[*] 枚舉并調用完畢");}});} catch (err) {console.error("[-] 腳本執行異常: " + err);}
}setImmediate(function () {Java.perform(function () {// 過濾不需要主動調用的類hookLoadClassAndInvoke()// 解決局部變量的 ClassLoader 枚舉不出來問題fartOnDexclassloader()// 解決非雙親委派關系下動態加載的 dex 脫殼問題invokeAllClassloaders()})
})
啟動 app 執行腳本,并輸出日志到 log.txt
frida -H 127.0.0.1:1234 -l fart.js -f com.cyrus.example -o log.txt
或者 hook 當前前臺 app ,并輸出日志到 log.txt
frida -H 127.0.0.1:1234 -F -l fart.js -o log.txt
輸出日志如下:
在 /sdcard/Android/data/com.cyrus.example/fart 下可以找到脫殼文件
FART 脫殼結束得到的文件列表(分 Execute 與 主動調用兩類):
-
Execute 脫殼點得到的 dex (*_dex_file_execute.dex)和 dex 中的所有類列表( txt 文件)
-
主動調用時 dump 得到的 dex (*_dex_file.dex)和此時 dex 中的所有類列表,以及該 dex 中所有函數的 CodeItem( bin 文件)
完整源碼
開源地址:
-
Android 示例代碼:https://github.com/CYRUS-STUDIO/AndroidExample
-
Frida 腳本源碼:https://github.com/CYRUS-STUDIO/frida_fart
-
FART源碼:https://github.com/CYRUS-STUDIO/FART
相關文章:
-
干掉抽取殼!FART 自動化脫殼框架與 Execute 脫殼點解析
-
FART 主動調用組件深度解析:破解 ART 下函數抽取殼的終極武器
-
一步步帶你移植 FART 到 Android 10,實現自動化脫殼
-
FART 自動化脫殼框架優化實戰:Bug 修復與代碼改進記錄