本文作者:燒麥
前言
筆者之前在雲音樂大前端公眾號分享了 Android 隱私合規靜態檢查的一部分實現。
Android隱私合規靜態檢查
上一篇文章通過反編譯 APP 的方式,掃描了 APP 內對隱私方法呼叫的檢查。但存在一些問題:
- 無法檢查到 so 檔案裡是否可能存在隱私方法的呼叫。
- 當我們全量掃描出某個地方存在隱私方法呼叫的時候,我們不知道它實際的呼叫的入口究竟在哪裡。
so 檔案裡的呼叫
有時候我們有一些隱私方法是通過 JNI 反射執行 Java 層程式碼呼叫的,無法通過掃描 Java 層檔案找到。所以需要針對 so 檔案做一個特殊處理。
我們來梳理一下我們的需求:對於 APP 業務方,一般來說只需要知道某些隱私方法有沒有通過 so 呼叫。在哪個 so 裡可能會存在呼叫。剩下的,我們交給 so 的開發者去排查就行了。
需求明確了,那我們怎麼知道 so 檔案裡是否呼叫了某個方法呢?在 Java 中,如果通過反射呼叫方法,類名+方法名的字串肯定是作為字串常量存在 class 檔案的常量池內。那麼 so 裡是否會有類似的儲存方式呢?
答案是肯定的,linux C 程式的字串可能存在於以下 2 個區域:
- .text 程式碼段,通常是指用來存放程式執行程式碼的一塊記憶體區域。這部分割槽域的大小在程式執行前就已經確定,並且記憶體區域通常屬於只讀,某些架構也允許程式碼段為可寫,即允許修改程式。在程式碼段中,也有可能包含一些只讀的常數變數,例如字串常量等。
- .rodata 該段也叫常量區,用於存放常量資料,ro 就是 ReadOnly 的意思。存放 C 中的字串和 #define 定義的常量.
我們可以通過 linux 的 strings
命令,來獲取 so 檔案裡面使用到的字串:
strings xx.so
我們檢查 apk 檔案裡每個 so 檔案的字串,如果能匹配上配置的隱私方法名,那麼就把當前的 so 標記為可疑的呼叫。檢查的流程如下圖:
檢查輸出結果參考下面的 demo 示圖:
方法呼叫鏈分析
很多時候我們不知道是哪裡呼叫了某個 Android API, 一般只能通過執行時去處理一下,例如 hook 這個方法替換它的實現。但是執行時檢查覆蓋不了所有的場景。所以靜態檢查 apk 的方法呼叫鏈是很必要的。至少我們可以看到某個敏感方法的呼叫源頭是哪個類,從而進行溯源和歸因。
筆者在上一篇分享的技術方案基礎之上,進一步分析了方法呼叫鏈。上篇文章我們說到了通過反編譯 apk,我們能轉換生成相關的 smali 檔案,smali 檔案裡會存在相關的方法呼叫資訊。我們可以通過這些方法資訊將整個 app 的方法呼叫關係組織起來。
方法收集
在 smali 檔案的開頭,會標記當前類的相關資訊:
.class public final Lokhttp3/OkHttp;
.super Ljava/lang/Object;
我們會獲取到當前一個類的修飾符和完整的型別描述符。
smali 裡的 .method
指令則描述了當前 class 裡有哪些方法:
.method constructor <init>(Lokhttp3/Call$Factory;Lokhttp3/HttpUrl;Ljava/util/List;Ljava/util/List;Ljava/util/concurrent/Executor;Z)V
.method private validateServiceInterface(Ljava/lang/Class;)V
.method public baseUrl()Lokhttp3/HttpUrl;
這裡以 Retrofit
為例,我們可以看到 Retrofit.smali
裡面的方法描述:
- 構造方法,傳入的引數為 Factory、HttpUrl、List、List、Executor 和 boolean
- 私有方法 validateServiceInterface,引數為 Class,返回 void
- 公開方法 baseUrl,無引數,返回 HttpUrl
通過上述這些資訊,我們可以收集到一個 APP 內,所有的方法。我們需要為每個方法建立自己的可識別性,我們通過下面這些欄位來進行判斷:
- 方法定義所在的類,需要是完整的包名+類名
一個方法簽名內需要的欄位,包括:
- 方法名
- 傳入的引數
在 smali 中,方法的描述符是使用的 jvm 的描述符,我們需要解析描述符裡的資訊,來儲存我們的每個欄位以備輸出顯示。
方法的描述符規則會把符號和型別對應起來,基本型別的關係為:
|符號|型別|
|---|---|
|V|void|
|Z|boolean|
|S|short|
|C|char|
|I|int|
|J|long|
|F|float|
|D|double|
物件則表示為完整的包名和類名,L
開頭,使用檔案描述符間隔,使用分號結尾,例如 Strig:
LJava/lang/String;
方法關係建立
收集到了所有的方法,我們建立呼叫鏈就還需要知道,方法呼叫了誰,以及方法被誰呼叫了。
在 smali 中,我們可以通過 invoke-
指令找到某個方法內呼叫了哪些其他方法:
invoke-
包括
invoke-direct
直接呼叫某個方法invoke-static
呼叫某個 static 方法invoke-virtual
呼叫某個虛方法invoke-super
直接呼叫父類的虛方法invoke-interface
呼叫某個介面的方法
除了 invoke-interface
需要在執行時確認呼叫物件,其他幾個是可以通過 invoke-
後面的描述部分知道當前方法呼叫了哪些方法:
invoke-virtual {v2, p2, v1}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
invoke-
後半段指令描述了具體呼叫的類名和方法,使用 -> 分隔開。解析這部分指令,我們可以獲取到被呼叫方法的完整資訊。
我們可以通過對整個 app 內反編譯出的 smali 檔案的呼叫關係進行一個收集,收集過程中,每個方法都會被儲存下來,每個方法除了自己的方法資訊,還包括被呼叫的列表:
- calleds: 呼叫了自己的方法列表
當某個方法呼叫被掃描到的時候,我們會把這個方法新增到當前呼叫者的 callers 裡面,同時也把呼叫者新增到自己的 calleds 裡面去。最終方法關係就建立成如下圖所示:
我們最終建立了一顆多叉樹的圖結構,這張圖裡,我們可以把我們需要檢查呼叫鏈的隱私方法看做是樹的葉子節點。
當然,我們也可以再新增一個 callers 陣列,來表示每個方法呼叫的方法列表,這樣我們還可以建立一個節點點存在雙向繫結關係的樹結構:
在雙向繫結的樹結構中,我們既可以根據某個方法去分析出這個方法的呼叫鏈。也可以從頂層開始,分析某些入口所有可能存在的呼叫鏈。
例如,當我們懷疑某些頁面存在不合規的呼叫時,我們可以把這些 Activity 的類找到,從上往下去尋找是否呼叫了隱私方法。
呼叫鏈遍歷
方法呼叫的關係建立完畢後,我們需要遍歷出所有的呼叫鏈並輸出給使用方。這裡就比較簡單了,我們可以使用深度優先遍歷來尋找我們的所有可能的路徑:
這裡存在一種特殊情況,在遞迴的時候,有可能會出現 A 被 B 呼叫, B 又被 A 呼叫的情況,反映到當前的資料結構就是圖結構形成了環。所以我們需要針對是否存在環進行判斷。
當我們判斷到當前呼叫鏈上存在重複節點的時候,就可以認定為存在環。這時候可以直接結束這條鏈上的遞迴,實際上也並不會影響我們事後分析這條呼叫鏈的合規性。
這部分邏輯可以用虛擬碼來表示:
fun traversal(method) {
val paths = []
dfs(method, [], paths)
}
fun dfs(method, path, temp) {
if (method.calleds.isNotEmpty) {
for (called in method.calleds) {
if (path.contains(called)) {
temp.add(path)
continue
} else {
newPath = []
newPath.addAll(path)
newPath.add(0, method)
dfs(called.method, newPath, temp)
}
}
} else {
path.add(0, method)
temp.add(path)
}
}
呼叫鏈分析最後的效果如下圖:
總結
到這裡靜態檢查 Android 隱私合規呼叫就分享的差不多了,但是隱私合規相關的工作能做的還有很多。
靜態的檢查也只是輔助我們定位和檢查可能存在的問題。我們仍然可以探索很多執行時的監測方案,兩者互補之後的效果也會更好。
本文釋出自 網易雲音樂大前端團隊,文章未經授權禁止任何形式的轉載。我們常年招收前端、iOS、Android,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!