突破Android P(Preview 1)對呼叫隱藏API限制的方法

移動安仔發表於2018-04-12

一.概要

本文基於對Android P(Preview 1)的原始碼分析,實現了三種繞過對呼叫隱藏API限制的方法,有效性均已得到驗證,能夠成功呼叫系統隱藏API。

二.限制原理

首先拋開Android P的具體實現過程,安卓系統要實現限制使用者程式碼呼叫系統隱藏API,至少要做以下兩個區分:

  1. 必須區分一個Method(或Field)對使用者程式碼是隱藏的還是公開的。只有隱藏的才需要進行限制。
  2. 必須區分呼叫者的身份:是使用者程式碼呼叫的還是系統程式碼(例如Activity類)呼叫的。只有使用者程式碼呼叫時才需要進行限制。

具體到Android P的程式碼實現,它會在所有通過反射方式和JNI方式獲取Method和Field的地方呼叫以下函式判斷是否使用者程式碼呼叫了系統的隱藏API(位於art/runtime/hidden_api.h),如果這個函式返回true,那麼說明使用者程式碼呼叫了系統的隱藏API,Android P(Preview1)會通過log發出警告,使用者程式碼仍然能夠獲取到正確的Method或Field,在後續版本中獲取到的Method或Field極有可能為空。

突破Android P(Preview 1)對呼叫隱藏API限制的方法

那麼它是如何進行上述兩個區分的呢?

  1. 每個Method(或Field)都有一個對應的access_flags_(uint32_t型別),原本這個值通過一些特定位(bit)表明其屬性(public,private,static等),但是還有一些保留的未定義的位,Android P就利用未定義的幾個位,表明這個Method(或Field)是對使用者程式碼隱藏的還是公開的。
  2. 通過回溯呼叫棧找到呼叫者所在的Class,然後判斷這個Class的ClassLoader是否為BootStrapClassLoader,如果是BoootStrapClassLoader那麼就認為呼叫者是系統程式碼,否則就認為呼叫者是使用者程式碼。fn_caller_in_boot就是一個函式指標,它用來判斷呼叫者是否是BootStrapClassLoader,反射呼叫和JNI呼叫時fn_caller_in_boot指向不同的函式,具體細節可檢視原始碼。

下面我們以呼叫android.app.ActivityThread類的currentActivityThread這個隱藏方法為例,講解繞過限制的方法。

三.繞過方法

繞過方法1

通過上面的論述結合原始碼分析,我們發現只有在通過反射方式和JNI方式獲取Method和Field時,系統才有可能攔截對隱藏API的獲取,也就是說直接呼叫是可以的!因此方法一的核心思想就是想方設法直接呼叫系統隱藏API。具體實現時需要用Provided方式提供Module或自定義android.jar。下面以一個例子說明實現過程。
我們新建一個普通的android工程,在其MainActivity中直接呼叫ActivityThread.currentActivityThread();發現IDE提示找不到類ActivityThread,這是因為在sdk的android.jar(位於SDK/platforms/android-XX目錄下)中並沒有這個類的宣告,但是在實際執行時這個類是存在於系統中的。我們的解決方法是以Provided方式提供一個Module,在此Module中提供需要的類(Provided方式是為了編譯通過,這樣的Module的程式碼並不會編譯到最終的apk中)。具體操作如下:
新建一個Module,其型別為Java Library,命名為libfakeandroid,然後在app的build.gradle中以Provided方式依賴libfakeandroid

突破Android P(Preview 1)對呼叫隱藏API限制的方法

之後在libfakeandroid中新建一個類android.app.ActivityThread,並新增需要呼叫的隱藏API,如下
突破Android P(Preview 1)對呼叫隱藏API限制的方法

完成以上操作之後,MainActivity中就能直接呼叫ActivityThread.currentActivityThread();方法了。在Android P(Preview1)系統上執行不會出現警告log,成功!
注意:如果需要呼叫的隱藏API所在的類已經位於android.jar中,Provided方式不再適用,此時需要自定義android.jar,將需要的Method或Field新增到android.jar中。
優點:實現起來非常簡單方便,並且穩定性很好。
缺點:只能呼叫訪問許可權為public和default的Method和Field,不能直接呼叫protected和private的。

繞過方法2

現在回頭看"限制原理"中論述的兩個區分,其實只要我們能夠混淆任何一個區分點都能夠成功繞過此限制。混淆第一個區分點,會讓系統錯誤地認為原本隱藏的API是公開的;混淆第二個區分點,會讓系統錯誤地將使用者程式碼呼叫識別為系統程式碼呼叫。方法二的核心思想就是混淆第二個區分點
關注第二個區分點,可以發現,其實只要在BootStrapClassLoader載入的類中有任何一個幫助我們進行反射的類就能繞過這個問題,那麼我們能否將我們apk中定義的類的ClassLoader改為BootStrapClassLoader呢?答案是肯定的!檢視art/runtime/mirror/class.h可知SetClassLoader函式可以為一個類指定ClassLoader,用IDA檢視/system/lib/libart.so確認此函式位於匯出符號表中。SetClassLoader的第一個引數型別為ObjPtrmirror::Class,如何將jclass轉化為此型別呢?通過在Android原始碼中查詢,在art/runtime/well_known_classes.h中有一個非常合適的函式ToClass能夠完成此任務,其宣告如下

突破Android P(Preview 1)對呼叫隱藏API限制的方法

檢視libart.so可知,ToClass函式也在其匯出符號表中,因此ToClass函式是一個恰當的函式。方法二的具體實現程式碼見下圖
突破Android P(Preview 1)對呼叫隱藏API限制的方法

其中,my_dlsym與dlsym類似,其功能是根據函式的匯出符號尋找函式在程式中的地址。my_dlsym是我們自定義的一個函式。
makeHiddenApiAccessable呼叫成功之後,使用com.test.hidefix.ReflectionHelper類反射尋找隱藏API,不會再出現log警告,成功!
實際工程中使用時可以將ReflectionHelper類作為一個工具類,程式碼中所有反射尋找Method和Field的地方均使用ReflectionHelper處理。注意:ReflectionHelper類只能呼叫系統類,不能呼叫自己app程式碼中的任何類!否則會因為ClassLoader的全盤委託機制出現問題!
優點:能夠呼叫所有隱藏API;僅需要尋找兩個匯出函式,適配性較好;沒有使用Hook,穩定性好
缺點:JNI方式獲取Method和Field時也需要轉到ReflectionHelper工具類完成

繞過方法3

方法三通過混淆第一個區分點突破限制

突破Android P(Preview 1)對呼叫隱藏API限制的方法
只要修改被隱藏的Method或Field對應的access_flags_,去掉其隱藏屬性即可,下文為了論述方便,只以獲取隱藏的Method為例進行說明,Field同理。
實際上,只要獲取到一個jmethodID,將其強轉為ArtMethod*型別,然後修改其access_flags_即可。但是後續版本中應用程式碼無法獲取隱藏Method的jmethodID,貌似陷入一個死迴圈了。但是檢視原始碼,我們是有方法獲取ArtMethod*的:art/runtime/native/java_lang_Class.cc有以下函式:
突破Android P(Preview 1)對呼叫隱藏API限制的方法

此函式是Class.getDeclaredMethod方法在native的實現,注意到這裡是先獲取的result然後才判斷ShouldBlockAccessToMember,因此我們可以hook獲取result的mirror::Class::GetDeclaredMethodInternal這個函式,將得到的ObjPtrmirror::Method型別的result想辦法轉換為ArtMethod*型別即可。方法三具體實現程式碼如下:
突破Android P(Preview 1)對呼叫隱藏API限制的方法

應用到實際工程中時還需要Hook另外的類似函式,這裡不再一一列舉。
優點:原有程式碼無需修改,適用於原有程式碼量較多的情況。
缺點:需要使用Hook,實現難度較大

四.總結

本文提出並實現了三種在Android P上呼叫隱藏API的方法,分別有不同特點和適用範圍,工程中可以根據實際情況選用不同方法。

相關文章