作者:藍師傅
一、前言
工信部對於App索權問題越來越重視,先後多個大廠App被下架要求整改:
其中最關鍵的問題是使用者同意隱私協議之前,不能有收集使用者隱私資訊的行為,例如獲取deviceId、androidId等資訊,除此之外,對於頻繁申請許可權、超範圍申請許可權也是需要注意的。
除了開迭代針對性整改,從技術角度思考,有沒有一勞永逸的辦法,杜絕隱私呼叫不合規問題呢?
這就是這篇文章要介紹的方案, 前期透過執行時hook技術高效檢測隱私方法呼叫,
後期透過Gradle Plugin+Transform+ASM 來hook並替換隱私方法呼叫,管控App和第三方SDK的隱私行為,徹底解決隱私不合規問題。
二、執行時hook技術
在隱私整改前期,透過上傳apk到史賓格平臺,然後平臺會安裝apk並執行,就能動態監測隱私方法呼叫,如下圖:
完成整個流程,打包-上傳-檢測,少說也要50分鐘~
關於隱私行為實時監控,實現原理無非是利用執行時hook技術,記錄方法呼叫資訊。
理論上我們也可以使用執行時hook技術,實現線下快速檢測隱私方法呼叫以及獲取呼叫堆疊的功能。
那麼執行時hook技術有哪些呢?
2.1 Xposed
如果你對Xposed比較熟悉,並且手頭有個root的裝置安裝了Xposed框架,那麼直接開發一個Xposed模組來hook指定方法就可以了。
關於Xposed的原始碼分析感興趣可以參考這一篇文章:抱歉,Xposed真的可以為所欲為——終 · 庖丁解碼,作者有一系列Xposed文章。
由於我的測試裝置是有root許可權的,Xposed方案對我來說難度不大,不過對於普通使用者,有沒有免root的方式呢?
有的~
2.2 VirtualXposed
VirtualXposed 是基於VirtualApp 和 epic 在非ROOT環境下執行Xposed模組的實現(支援5.0~10.0)。
VirtualXposed其實就是一個支援Xposed的虛擬機器,我們把開發好的Xposed模組和對應需要hook的App安裝上去就能實現hook功能。
由於VirtualApp 2017年就閉源轉商業,開源版存在不少問題,而且由於其hook大量系統的函式,所以存在不少相容性問題,有些App安裝之後可能打不開,所以如果手頭的裝置剛好遇到相容性問題,那可以考慮換個手機啦~
2.3 epic
阿里2014年開源了Dexposed 專案,它能夠在Dalvik虛擬機器上無侵入地實現執行時方法攔截,
但是Android 5.0開始使用ART虛擬機器後,不支援ART的Dexposed 就淪為歷史。
之後維術大佬在ART上重新實現了Dexposed,有著與Dexposed完全相同的能力和API,專案地址是epic 。
所以如果不想折騰 Xposed 或者 VirtualXposed,只要在應用內接入epic,就可以實現應用內Xposed hook功能,滿足執行hook需求。
2.3.1 epic 原理:
原理是透過修改ArtMethod的入口函式,把入口函式的前8個位元組修改為一段跳轉指令,跳轉到執行hook操作的函式,原理跟阿里的熱修復框架AndFix差不多,如下圖所示。
詳細原理可以看原文: 我為Dexposed續一秒——論ART上執行時 Method AOP實現
2.3.2 基於epic 實現一個可配置的執行時hook框架
- 讀取配置:
val inputStream = context.resources.assets.open("privacy_methods.json")
val reader = BufferedReader(InputStreamReader(inputStream))
val result = StringBuilder()
var line: String? = ""
while (reader.readLine().also { line = it } != null) {
result.append(line)
}
val configEntity = Gson().fromJson(result.toString(), PrivacyMethod::class.java)
configEntity.methods.forEach {
hookPrivacyMethod(it)
}
- json配置如下,放在assets目錄:
{
"methods": [
{
"name_regex": "android.app.ActivityManager.getRunningAppProcesses",
"message": "讀取當前執行應用程序"
},
{
"name_regex": "android.telephony.TelephonyManager.listen",
"message": "監聽呼入電話資訊"
},
...
]
}
- 根據讀取的配置,進行hook
private fun hookPrivacyMethod(entity: PrivacyMethodData) {
if (entity.name_regex.isNotEmpty()) {
val methodName = entity.name_regex.substring(entity.name_regex.lastIndexOf(".") + 1)
val className = entity.name_regex.substring(0, entity.name_regex.lastIndexOf("."))
try {
val lintClass = Class.forName(className)
DexposedBridge.hookAllMethods(lintClass, methodName, object : XC_MethodHook() {
override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam?) {
super.beforeHookedMethod(param)
Log.i(TAG, "beforeHookedMethod $className.$methodName")
Log.d(TAG, "stack= " + Log.getStackTraceString(Throwable()))
}
})
} catch (e: Exception) {
Log.w(TAG, "hookPrivacyMethod:$className.$methodName,e=${e.message}")
}
}
}
- 執行效果如下:
如圖所示,執行時輸出隱私方法呼叫堆疊的功能基本實現了,支援透過json配置需要hook的方法。
tip:epic 存在相容性問題,例如Android 11 只支援64位App,所以建議只在debug環境使用。
三、編譯時hook技術
使用epic只解決了驗證隱私方法呼叫問題,針對如下問題無能為力:
- release環境如何監控隱私方法呼叫?
- 如何管控第三方SDK頻繁呼叫隱私方法問題?
對於這兩個問題,可以使用編譯時hook技術來解決。
說到編譯時hook,首先需要了解編譯流程
3.1 編譯流程
我們使用Android Studio開發,使用Gradle 編譯工具,對於apk編譯流程大家應該都知道,如下圖:
apk編譯流程無非就是以下這些大的步驟:
1.打包資原始檔,生成R.java檔案
2.將AIDL檔案編譯成java檔案
3.將java檔案透過javac命令編譯成.class檔案
4.將class檔案打包成dex檔案
5.透過apkbuilder工具將dex檔案和資原始檔打包成apk
6.apk簽名
7.apk對齊(可以沒有這一步)
其中第四步(將class檔案打包成dex檔案),中間就涉及到Gradle的一個Transform流程
3.2 瞭解Transform
Transform原理圖如下所示
將class檔案、jar檔案、資原始檔作為輸入,經過一系列的Transform處理,
首先是自定義的Transform處理,然後是系統的Transform處理,最後一個Transform是負責生成dex檔案。
相關原始碼可以看TaskManager的 createPostCompilationTasks方法,編譯流程原始碼都在這裡面~
截圖只是貼了自定義Transform的原始碼,後面還有系統的Transform,例如 appliesCustomClassTransforms,用於Profile外掛底層實現。
Transform是跟taskFactory關聯的,可以這樣理解,一個Transform對應Gradle的一個Task。
知道了Transform的大概原理,我們可以透過自定義Plugin,註冊一個自定義的Transform到編譯流程中去,目的是拿到所有.class檔案,再結合ASM 工具修改位元組碼。
自定義Gradle Plugin,註冊Transform,程式碼如下所示
class Plugin : Plugin<Project> {
override fun apply(project: Project) {
if (project.plugins.hasPlugin("com.android.application")) {
val extension = project.extensions.getByName("android") as AppExtension
extension.registerTransform(CommonTransform(project))
}
}
}
想要理解為什麼自定義外掛要這麼寫,可以看App編譯外掛原始碼AppPlugin
建立AppExtension,name是android,最終是儲存到ExtensionsStorage類裡面的一個叫extensions的LinkedHashMap變數裡面,大家感興趣可以去看原始碼。
前面的eproject.extensions.getByName,最終就是從LinkedHashMap中讀取的。
拿到.class檔案之後,怎麼修改呢?這就涉及到修改位元組碼方案選型。
3.3 位元組碼修改框架選擇
目前主流的位元組碼修改框架除了ASM,還有Javaassist,兩者對比:
由於專案對效能、包體積方面要求比較高,所以無疑採用ASM方案比較合適。
3.4 瞭解ASM框架
我們透過自定義Transform 能拿到.class檔案,之後的位元組碼處理就透過ASM工具,關於ASM的使用就不介紹了,大家可以參考:
Android 中看似高大上的位元組碼修改,這樣學就對了!。
Gradle Plugin + Transform ,這套框架的搭建基本都是模板程式碼,為了節約時間成本和試錯成本,本文直接參考dokit,採用booster api作為外掛的底層實現,booster遮蔽了不同Gradle版本api的差異。
說了那麼多,最重要的還是要看方案設計~
四、初級hook方案
上一步我們透過自定義Transform可以拿到所有.class檔案,後面只要透過ClassVistor和MethodVistor,可以分別拿到每個類和方法的位元組碼,
以 ActivityManager#getRunningAppProcesses 為例,我們要替換成 PrivacyUtil#getRunningAppProcesses,流程圖如下:
核心hook程式碼如下所示:
classNode.methods.forEach { method ->
method.instructions?.iterator()?.forEach { insnNode ->
if (insnNode is MethodInsnNode) {
//命中方法,替換
if (insnNode.desc == "android/app/ActivityManager.getRunningAppProcesses ()Ljava/util/List;" &&
insnNode.name == "getRunningAppProcesses" &&
insnNode.opcode == Opcodes.INVOKESPECIAL
) {
//方法指令替換
insnNode.opcode = Opcodes.INVOKESTATIC
//呼叫類替換
insnNode.owner = "com/lanshifu/asm_plugin_library/privacy/PrivacyUtil"
//方法名替換
insnNode.name = "getRunningAppProcesses"
//引數替換
insnNode.desc = "com/lanshifu/asm_plugin_library/privacy/PrivacyUtil.getRunningAppProcesses (Landroid/app/ActivityManager;)Ljava/util/List;"
}
}
}
}
解釋:
透過遍歷每個方法的位元組碼指令,判斷是ActivityManager.getRunningAppProcesses這個方法呼叫,就替換成PrivacyUtil#getRunningAppProcesses呼叫,涉及到的位元組碼操作是比較基礎的。
tip:為什麼要遍歷每個方法的位元組碼指令?因為需要hook的方法是系統的方法,沒有被打包到apk中, 單純遍歷方法名是找不到的,必須遍歷每個方法裡面呼叫的位元組碼指令。
到此我們初級版本的編譯時隱私方法hook功能就實現了,但是存在幾個問題:
1、硬編碼,不好維護,增加hook方法比較麻煩;
2、對工具類 PrivacyUtil 有依賴,如果後面其它工程使用了這個外掛,但是沒有引入PrivacyUtil,或者後面外掛升級,PrivacyUtil沒升級,就會報Class Not Found Exception;
3、開發需要熟悉 ASM 位元組碼,每次新增一個隱私方法 hook 都需要對比前後位元組碼變化進行修改驗證,麻煩得很;
五、進階方案
想要解決初級方案存在的三個問題,關鍵在於實現”可配置“,
需要在編譯期能夠讀取hook配置,用註解會比較合適。
進階方案思路如下:
- 用第一個Transform來收集註解資訊,生成一份hook配置;
- 用第二個Transform來讀取hook配置,替換隱私方法。
5.1 自定義註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface AsmMethodReplace {
Class oriClass();
String oriMethod() default "";
int oriAccess() default AsmMethodOpcodes.INVOKESTATIC;
}
註解是對方法生效,需要知道需要hook的方法的類名、方法名、方法型別(靜態方法/成員方法)
5.2 註解處理,生成配置
替換一個方法,我們需要的配置如下:
原方法資訊(替換前):oriClass、oriMethod、oriAccess、oriDesc
目標方法資訊(替換後):targetClass、targetMethod、targetAcces、targetDesc
目標方法資訊我們透過ClassNode就能拿到,但是原方法資訊,都放到AsmMethodReplace 註解上就不太合適了,因為oriDesc寫起來比較麻煩, 所以這裡約定好一個註解使用規則,然後oriDesc在程式碼裡讀取就行了。
規則如下:
- 對於hook靜態方法,註解的方法的引數保持跟原方法一致
- 對於hook成員方法,註解的方法的第一個引數是Class物件,之後的引數跟原方法保持一致
然後oriDesc就透過targetDesc減去第一個引數計算得出。
例如:
targetDesc=(Landroid/telephony/TelephonyManager;)Ljava/lang/String;
透過字串擷取後得到:
oriDesc= Ljava/lang/String;
舉個
5.2.1 例子1:hook成員方法
假如要替換掉ActivityManager的getRunningAppProcesses方法
public List<RunningAppProcessInfo> getRunningAppProcesses() {
try {
return getService().getRunningAppProcesses();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
由於這個是成員方法,那麼註解的寫法如下:
@JvmStatic
@AsmMethodReplace(oriClass = ActivityManager::class, oriAccess = AsmMethodOpcodes.INVOKEVIRTUAL)
fun getRunningAppProcesses(manager: ActivityManager): List<RunningAppProcessInfo?> {
//hook 處理
}
5.2.2 例子2:hook靜態方法
假如要替換掉Settings.System的getString方法
public static String getString(ContentResolver resolver, String name) {
return getStringForUser(resolver, name, resolver.getUserId());
}
由於是靜態方法,那麼註解的寫法如下:
@JvmStatic
@AsmMethodReplace(oriClass = Settings.System::class, oriAccess = AsmMethodOpcodes.INVOKESTATIC)
fun getString(resolver: ContentResolver, name: String): String? {
//處理AndroidId
if (Settings.Secure.ANDROID_ID == name) {
}
return Settings.System.getString(resolver, name)
}
詳細可以參考文末的原始碼。
5.3 流程圖
最終的流程如上,應該比較清晰了吧~
5.4 注意事項
ASM hook 需要有跡可循,必須明確位元組碼修改的地方,可以列印log,可以儲存記錄到檔案中,如果出現問題可以從hook日誌中排查。
5.5 小結
進階方案主要做了這幾件事:
- 用一個註解處理的Transform,編譯期收集自定義註解資訊,生成一份hook配;
- 用另一個Transform,讀取hook配置,hook對應方法;
- 隱私方法hook之後,增加快取,解決SDK頻繁讀取隱私資訊問題;
- 在使用者沒有同意隱私協議之前,如果呼叫隱私方法,可以給toast提示,並列印呼叫堆疊,如下所示,問題一目瞭然。
六、其它
目前大廠也有一些開源的編譯時插樁的庫,例如餓了麼開源的lancet,原理也是 Gradle Plugin+Transform+ASM。
如果想深入學習位元組碼插樁,推薦滴滴開源的dokit,裡面有好多位元組碼操作可以學習,例如大圖監控,網路監控等等。
由於Gradle 版本更新比較快,大家最好是在專案中嘗試自己搭建編譯時hook基礎框架,這樣出問題的話,自己比較好解決,同時也能提升自己位元組碼開發的技術。
七、總結
本文從工信部隱私合規要求作為切入點,大概介紹瞭如下知識點:
- 執行時hook框架介紹和應用
- epic使用和原理
- 編譯時hook框架
- 從apk編譯流程介紹Transform的原理和應用
- 編譯時hook方案對比
- 最終實現可配置的編譯時方法替換方案,徹底解決隱私方法呼叫不合規問題
本文難度其實不算非常大,主要是把Gradle外掛和位元組碼修改的整個流程串起來,涉及到的技術基本都有所提及,最終搭建了一個編譯時方法hook框架,之後可以基於這個hook框架做很多東西,例如慢方法檢測、全埋點、監控執行緒呼叫等~
最後
在這裡就還分享一份由大佬親自收錄整理的學習PDF+架構影片+面試文件+原始碼筆記,高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料
這些都是我現在閒暇時還會反覆翻閱的精品資料。裡面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效地幫助大家掌握知識、理解原理,幫助大家在未來取得一份不錯的答卷。
當然,你也可以拿去查漏補缺,提升自身的競爭力。
真心希望可以幫助到大家,Android路漫漫,共勉!
如果你有需要的話,只需私信我【進階】即可獲取