close

作者:藍師傅鏈接:https://juejin.cn/post/7043399520486424612

一、前言

工信部對於App索權問題越來越重視,先後多個大廠App被下架要求整改

工信部對於App索權問題越來越重視,先後多個大廠App被下架要求整改:

106個app被下架.png其中最關鍵的問題是用戶同意隱私協議之前,不能有收集用戶隱私信息的行為,例如獲取deviceId、androidId等信息,除此之外,對於頻繁申請權限、超範圍申請權限也是需要注意的。

除了開迭代針對性整改,從技術角度思考,有沒有一勞永逸的辦法,杜絕隱私調用不合規問題呢?

這就是這篇文章要介紹的方案, 前期通過運行時hook技術高效檢測隱私方法調用,

後期通過Gradle Plugin+Transform+ASM 來hook並替換隱私方法調用,管控App和第三方SDK的隱私行為,徹底解決隱私不合規問題。

二、運行時hook技術

在隱私整改前期,通過上傳apk到史賓格平台,然後平台會安裝apk並運行,就能動態監測隱私方法調用,如下圖:

史賓格檢測.png

完成整個流程,打包-上傳-檢測,少說也要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差不多,如下圖所示。

epic原理.png

詳細原理可以看原文:我為Dexposed續一秒——論ART上運行時 Method AOP實現

2.3.2 基於epic 實現一個可配置的運行時hook框架

讀取配置:

1valinputStream=context.resources.assets.open("privacy_methods.json") 2valreader=BufferedReader(InputStreamReader(inputStream)) 3valresult=StringBuilder() 4varline:String?="" 5while(reader.readLine().also{line=it}!=null){ 6result.append(line) 7} 8 9valconfigEntity=Gson().fromJson(result.toString(),PrivacyMethod::class.java)10configEntity.methods.forEach{11hookPrivacyMethod(it)12}1314複製代碼

json配置如下,放在assets目錄:

1{ 2"methods":[ 3{ 4"name_regex":"android.app.ActivityManager.getRunningAppProcesses", 5"message":"讀取當前運行應用進程" 6}, 7{ 8"name_regex":"android.telephony.TelephonyManager.listen", 9"message":"監聽呼入電話信息"10},11...12]13}14複製代碼

根據讀取的配置,進行hook

1privatefunhookPrivacyMethod(entity:PrivacyMethodData){ 2if(entity.name_regex.isNotEmpty()){ 3valmethodName=entity.name_regex.substring(entity.name_regex.lastIndexOf(".")+1) 4valclassName=entity.name_regex.substring(0,entity.name_regex.lastIndexOf(".")) 5try{ 6vallintClass=Class.forName(className) 7DexposedBridge.hookAllMethods(lintClass,methodName,object:XC_MethodHook(){ 8overridefunbeforeHookedMethod(param:XC_MethodHook.MethodHookParam?){ 9super.beforeHookedMethod(param)1011Log.i(TAG,"beforeHookedMethod$className.$methodName")12Log.d(TAG,"stack="+Log.getStackTraceString(Throwable()))13}14})15}catch(e:Exception){16Log.w(TAG,"hookPrivacyMethod:$className.$methodName,e=${e.message}")17}18}19}202122複製代碼

運行效果如下:

隱私方法hook效果.png

如圖所示,運行時輸出隱私方法調用堆棧的功能基本實現了,支持通過json配置需要hook的方法。

tip:epic 存在兼容性問題,例如Android 11 只支持64位App,所以建議只在debug環境使用。

三、編譯時hook技術

使用epic只解決了驗證隱私方法調用問題,針對如下問題無能為力:

release環境如何監控隱私方法調用?

如何管控第三方SDK頻繁調用隱私方法問題?

對於這兩個問題,可以使用編譯時hook技術來解決。

說到編譯時hook,首先需要了解編譯流程

3.1 編譯流程

我們使用Android Studio開發,使用Gradle 編譯工具,對於apk編譯流程大家應該都知道,如下圖:

編譯流程.png

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原理圖如下所示

trnsform原理.png

將class文件、jar文件、資源文件作為輸入,經過一系列的Transform處理,

首先是自定義的Transform處理,然後是系統的Transform處理,最後一個Transform是負責生成dex文件。

相關源碼可以看TaskManager的 createPostCompilationTasks方法,編譯流程源碼都在這裡面~

taskmanager.png

截圖只是貼了自定義Transform的源碼,後面還有系統的Transform,例如 appliesCustomClassTransforms,用於Profile插件底層實現。

Transform是跟taskFactory關聯的,可以這樣理解,一個Transform對應Gradle的一個Task。

知道了Transform的大概原理,我們可以通過自定義Plugin,註冊一個自定義的Transform到編譯流程中去,目的是拿到所有.class文件,再結合ASM 工具修改字節碼。

自定義Gradle Plugin,註冊Transform,代碼如下所示

1classPlugin:Plugin<Project>{ 2 3overridefunapply(project:Project){ 4 5if(project.plugins.hasPlugin("com.android.application")){ 6valextension=project.extensions.getByName("android")asAppExtension 7extension.registerTransform(CommonTransform(project)) 8} 9}10}11複製代碼

想要理解為什麼自定義插件要這麼寫,可以看App編譯插件源碼AppPlugin

appPlugin.png

創建AppExtension,name是android,最終是保存到ExtensionsStorage類裡面的一個叫extensions的LinkedHashMap變量裡面,大家感興趣可以去看源碼。

前面的eproject.extensions.getByName,最終就是從LinkedHashMap中讀取的。

拿到.class文件之後,怎麼修改呢?這就涉及到修改字節碼方案選型。

3.3 字節碼修改框架選擇

目前主流的字節碼修改框架除了ASM,還有Javaassist,兩者對比:

asm和javaassist對比.png

由於項目對性能、包體積方面要求比較高,所以無疑採用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方案.png

核心hook代碼如下所示:

1classNode.methods.forEach{method-> 2method.instructions?.iterator()?.forEach{insnNode-> 3 4if(insnNodeisMethodInsnNode){ 5 6//命中方法,替換 7if(insnNode.desc=="android/app/ActivityManager.getRunningAppProcesses()Ljava/util/List;"&& 8insnNode.name=="getRunningAppProcesses"&& 9insnNode.opcode==Opcodes.INVOKESPECIAL10){11//方法指令替換12insnNode.opcode=Opcodes.INVOKESTATIC13//調用類替換14insnNode.owner="com/lanshifu/asm_plugin_library/privacy/PrivacyUtil"15//方法名替換16insnNode.name="getRunningAppProcesses"17//參數替換18insnNode.desc="com/lanshifu/asm_plugin_library/privacy/PrivacyUtil.getRunningAppProcesses(Landroid/app/ActivityManager;)Ljava/util/List;"1920}21}22}23}24複製代碼

解釋:

通過遍歷每個方法的字節碼指令,判斷是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 自定義註解 1@Target(ElementType.METHOD) 2@Retention(RetentionPolicy.CLASS) 3public@interfaceAsmMethodReplace{ 4ClassoriClass(); 5 6StringoriMethod()default""; 7 8intoriAccess()defaultAsmMethodOpcodes.INVOKESTATIC; 9}10複製代碼

註解是對方法生效,需要知道需要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方法

1publicList<RunningAppProcessInfo>getRunningAppProcesses(){2try{3returngetService().getRunningAppProcesses();4}catch(RemoteExceptione){5throwe.rethrowFromSystemServer();6}7}8複製代碼

由於這個是成員方法,那麼註解的寫法如下:

1@JvmStatic2@AsmMethodReplace(oriClass=ActivityManager::class,oriAccess=AsmMethodOpcodes.INVOKEVIRTUAL)3fungetRunningAppProcesses(manager:ActivityManager):List<RunningAppProcessInfo?>{4//hook處理5}6複製代碼5.2.2 例子2:hook靜態方法

假如要替換掉Settings.System的getString方法

1publicstaticStringgetString(ContentResolverresolver,Stringname){2returngetStringForUser(resolver,name,resolver.getUserId());3}4複製代碼

由於是靜態方法,那麼註解的寫法如下:

1@JvmStatic2@AsmMethodReplace(oriClass=Settings.System::class,oriAccess=AsmMethodOpcodes.INVOKESTATIC)3fungetString(resolver:ContentResolver,name:String):String?{4//處理AndroidId5if(Settings.Secure.ANDROID_ID==name){6}7returnSettings.System.getString(resolver,name)8}9複製代碼

詳細可以參考文末的源碼。

5.3 流程圖進階hook方案.png

最終的流程如上,應該比較清晰了吧~

5.4 注意事項

ASM hook 需要有跡可循,必須明確字節碼修改的地方,可以打印log,可以保存記錄到文件中,如果出現問題可以從hook日誌中排查。

hook記錄日誌.png5.5 小結

進階方案主要做了這幾件事:

用一個註解處理的Transform,編譯期收集自定義註解信息,生成一份hook配;

用另一個Transform,讀取hook配置,hook對應方法;

隱私方法hook之後,增加緩存,解決SDK頻繁讀取隱私信息問題;

在用戶沒有同意隱私協議之前,如果調用隱私方法,可以給toast提示,並打印調用堆棧,如下所示,問題一目了然。

打印堆棧.png六、其它

目前大廠也有一些開源的編譯時插樁的庫,例如餓了麼開源的lancet,原理也是 Gradle Plugin+Transform+ASM。

如果想深入學習字節碼插樁,推薦滴滴開源的dokit,裡面有好多字節碼操作可以學習,例如大圖監控,網絡監控等等。

由於Gradle 版本更新比較快,大家最好是在項目中嘗試自己搭建編譯時hook基礎框架,這樣出問題的話,自己比較好解決,同時也能提升自己字節碼開發的技術。

七、總結

本文從工信部隱私合規要求作為切入點,大概介紹了如下知識點:

運行時hook框架介紹和應用

epic使用和原理

編譯時hook框架

從apk編譯流程介紹Transform的原理和應用

編譯時hook方案對比

最終實現可配置的編譯時方法替換方案,徹底解決隱私方法調用不合規問題

本文難度其實不算非常大,主要是把Gradle插件和字節碼修改的整個流程串起來,涉及到的技術基本都有所提及,最終搭建了一個編譯時方法hook框架,之後可以基於這個hook框架做很多東西,例如慢方法檢測、全埋點、監控線程調用等~

本文源碼 相關參考文章: 一步步治理隱私權限 | 安卓黑魔法 一起玩轉Android項目中的字節碼 去哪兒 Android 客戶端隱私安全處理方案 booster

春節前最後一篇文章,提前祝大家新年快樂!

推薦閱讀

RxJava 堆棧異常信息顯示不全,怎麼搞

『Android自定義View實戰』讓你的輪播指示器「粘」起來

Android 音視頻開發【特效篇】【一】抖音傳送帶特效

快手線上 OOM 監控方案 - KOOM 分析

Flutter自定義之旋轉木馬 - 帶你回到童年時光

愛奇藝 Xcrash 是怎麼捕獲 crash 的

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()