一種繞過Android P上非SDK接口限制的簡單方法
衆所周知,Android P 引入了 針對非 SDK 接口(俗稱為隐藏API)的使用限制。這是繼 Android N上 針對 NDK 中私有庫的連結限制 之後的又一次重大調整。從今以後,不論是native層的NDK還是 Java層的SDK,我們隻能使用Google提供的、公開的标準接口。這對開發者以及使用者乃至整個Android生态,當然是一件好事。但這也同時意味着Android上的各種黑科技有可能會逐漸走向消亡。
作為一個有追求的開發者,我們既要尊重并遵守規則,也要有能力在必要的時候突破規則的束縛,帶着鐐铐跳舞。那麼今天就來探讨一下,如何突破Android P上針對非SDK接口調用的限制。
系統是如何實作這個限制的?
知己知彼,百戰不殆。既然我們想要突破這個限制,自然先得弄清楚,系統是如何給我們施加這個限制的。
文檔 中說,通過反射或者JNI通路非公開接口時會觸發警告/異常等,那麼不妨跟蹤一下反射的流程,看看系統到底在哪一步做的限制(以下的源碼分析大可以走馬觀花的看一下,需要的時候自己再仔細看)。我們從 `java.lang.Class.getDeclaredMethod(String)` 看起,這個方法在Java層 最終調用到了 `getDeclaredMethodInternal` 這個native方法,看一下這個方法的源碼:
static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis,
jstring name, jobjectArray args) {
ScopedFastNativeObjectAccess soa(env);
StackHandleScope<1> hs(soa.Self());
DCHECK_EQ(Runtime::Current()->GetClassLinker()->GetImagePointerSize(), kRuntimePointerSize);
DCHECK(!Runtime::Current()->IsActiveTransaction());
Handle<mirror::Method> result = hs.NewHandle(
mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize, false>(
soa.Self(),
DecodeClass(soa, javaThis),
soa.Decode<mirror::String>(name),
soa.Decode<mirror::ObjectArray<mirror::Class>>(args)));
if (result == nullptr || ShouldBlockAccessToMember(result->GetArtMethod(), soa.Self())) {
return nullptr;
}
return soa.AddLocalReference<jobject>(result.Get());
}
注意到那個 **ShouldBlockAccessToMember** 調用了嗎?如果它傳回 true,那麼直接傳回`nullptr`,上層就會抛 `NoSuchMethodXXX` 異常;也就觸發系統的限制了。于是我們繼續跟蹤這個方法,這個方法的實作在 java_lang_Class.cc,源碼如下:
ALWAYS_INLINE static bool ShouldBlockAccessToMember(T* member, Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
hiddenapi::Action action = hiddenapi::GetMemberAction(
member, self, IsCallerTrusted, hiddenapi::kReflection);
if (action != hiddenapi::kAllow) {
hiddenapi::NotifyHiddenApiListener(member);
}
return action == hiddenapi::kDeny;
}
毫無疑問,我們應該繼續看 hidden_api.cc 裡面的 `GetMemberAction`方法 :
template<typename T>
inline Action GetMemberAction(T* member,
Thread* self,
std::function<bool(Thread*)> fn_caller_is_trusted,
AccessMethod access_method)
REQUIRES_SHARED(Locks::mutator_lock_) {
DCHECK(member != nullptr);
// Decode hidden API access flags.
// NB Multiple threads might try to access (and overwrite) these simultaneously,
// causing a race. We only do that if access has not been denied, so the race
// cannot change Java semantics. We should, however, decode the access flags
// once and use it throughout this function, otherwise we may get inconsistent
// results, e.g. print whitelist warnings (b/78327881).
HiddenApiAccessFlags::ApiList api_list = member->GetHiddenApiAccessFlags();
Action action = GetActionFromAccessFlags(member->GetHiddenApiAccessFlags());
if (action == kAllow) {
// Nothing to do.
return action;
}
// Member is hidden. Invoke `fn_caller_in_platform` and find the origin of the access.
// This can be *very* expensive. Save it for last.
if (fn_caller_is_trusted(self)) {
// Caller is trusted. Exit.
return kAllow;
}
// Member is hidden and caller is not in the platform.
return detail::GetMemberActionImpl(member, api_list, action, access_method);
}
可以看到,關鍵來了。此方法有三個return語句,如果我們能幹涉這幾個語句的傳回值,那麼就能影響到系統對隐藏API的判斷;進而欺騙系統,繞過限制。
## 應對之策
在分析這三個條件之前,我們再思考一下,在調用一個方法/擷取一個成員的時候,除了反射(JNI也算)就沒有别的辦法了嗎?看起來系統隻是把反射這條路堵死了,那如果我不走這條路呢?
首先,很顯然,除了反射,我們還能直接調用。打個比方,我們要調用 ActivityThread.currentActivityThread()這個方法,除了使用反射;我們還可以把 Android 源碼中的 ActivityThread 這個類copy到我們的項目中,然後使用 provided 依賴,這樣就能像系統一樣直接調用了。至此,我們得到了第一個資訊:public類的public方法,可以通過直接調用的方式通路;當然,private的就都不行了。
其次,我們要通路一個類的成員,除了直接通路,反射調用/JNI就沒有别的方法了嗎?當然不是。如果你了解ART的實作原理,知道對象布局,那麼這個問題就太簡單了。所有的Java對象在記憶體中其實就是一個結構體,這份記憶體在 native 層和Java層是對應的,是以如果我們拿到這份記憶體的頭指針,**直接通過偏移量就能通路成員**。你問我方法怎麼通路?ART的對象模型采用的類似Java的 klass-oop方式,方法是存儲在 `java.lang.Class`對象中的,它們是**Class對象的成員**,是以通路方法最終就是通路成員。(後續我會專門介紹ART的對象模型,解釋 ArtMethod/java.lang.Method/jmethodId之間的關系)。
思考完畢,我們會到反射調用的流程;仔細分析一下這三個條件。
### 第一個條件
先看第一個return語句,`GetActionFromAccessFlags`,看方法名貌似是根據 Method/Field 的 `access_flag` 來判斷,具體看下代碼:
inline Action GetActionFromAccessFlags(HiddenApiAccessFlags::ApiList api_list) {
if (api_list == HiddenApiAccessFlags::kWhitelist) {
return kAllow;
}
EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();
if (policy == EnforcementPolicy::kNoChecks) {
// Exit early. Nothing to enforce.
return kAllow;
}
// if policy is "just warn", always warn. We returned above for whitelist APIs.
if (policy == EnforcementPolicy::kJustWarn) {
return kAllowButWarn;
}
// 略。。。
}
首先,如果 Method/Field 是白名單,那麼直接允許通路。我們再往前看,發現這個 `api_list` 其實是存儲在 Method/Field 的 `access_flag`中的。
也就是說,所有的Method/Field的access_flag 中存儲了hidden_api 的資訊,如果有辦法把這個flag直接設定為 kAllow,那麼系統就認為它不是隐藏API了。但是,如果要修改 Method/Field 的 `access_flag`這個成員變量,我們首先得拿到這個 Method/Field 的引用,然而 Android P上就是限制了我們拿這個引用的過程,似乎死循環了;前面我們提到可以通過偏移量的方式修改,但實際上這個場景還有别限制(比如壓根拿不到Class對象);是以這個條件看似可以達到,實際上比較麻煩,于是我們暫且放下。
繼續觀察這個方法,接下來 調用了 `GetHiddenApiEnforcementPolicy` 方法擷取限制政策,如果是 `kNoChecks` 直接允許;那 GetHiddenApiEnforcementPolicy 這個方法是啥樣呢?在runtime.h 中,如下:
hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const {
return hidden_api_policy_;
}
也就是說,傳回的是 runtime 這個對象的一個成員。**如果我們直接修改記憶體,把這個成員設定為 kNoChecks**,那麼不就達到目标了嗎?
#### 擷取runtime指針
既然需要修改runtime對象的記憶體,那麼首先得拿到runtime對象的指針。本來這個過程需要去分析 ART runtime的啟動過程,但如果完全寫出來那就又是幾篇文章了;這裡直接給出結論:
在JNI中,我們可以通過 JNIEnv指針拿到 JavaVM指針,這個JavaVM指針實際上是一個 `JavaVMExt`對象,runtime是 JavaVMExt結構體的成員。說起來比較繞,實際上你看看代碼就明白了:
JavaVM *javaVM;
env->GetJavaVM(&javaVM);
JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;
感興趣的可以自己去分析為什麼可以這麼做。
#### 搜尋記憶體
我們已經拿到了 runtime指針,也就是這個對象的起始位置;如果要修改對象的成員,必須要知道偏移量。如何知道這個偏移量呢?直接寫死寫死也是可行的,但是一旦廠商做一點修改,那就完蛋了;你程式的結果就沒法預期。是以,我們采用一種**動态搜尋**的辦法。
runtime是一個很大的結構體,裡面的成員不計其數;如果我們要精準定位裡面的某一個成員,需要找一些參照物;然後通過這些參照物進一步定位。我們先來觀察一下這個結構體:
struct Runtime {
// 64 bit so that we can share the same asm offsets for both 32 and 64 bits.
uint64_t callee_save_methods_[kCalleeSaveSize];
// Pre-allocated exceptions (see Runtime::Init).
GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_throwing_exception_;
GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_throwing_oome_;
GcRoot<mirror::Throwable> pre_allocated_OutOfMemoryError_when_handling_stack_overflow_;
GcRoot<mirror::Throwable> pre_allocated_NoClassDefFoundError_;
// ... (省略大量成員)
std::unique_ptr<JavaVMExt> java_vm_;
// ... (省略大量成員)
// Specifies target SDK version to allow workarounds for certain API levels.
int32_t target_sdk_version_;
// ... (省略大量成員)
bool is_low_memory_mode_;
// Whether or not we use MADV_RANDOM on files that are thought to have random access patterns.
// This is beneficial for low RAM devices since it reduces page cache thrashing.
bool madvise_random_access_;
// Whether the application should run in safe mode, that is, interpreter only.
bool safe_mode_;
// ... (省略大量成員)
}
這個結構體非常大,可以直接去看源碼 runtime.h,上面我們挑出了一些我們能夠使用的參照物,輔助進行記憶體定位:
- java_vm_ :我們很熟悉的JavaVM對象,上面我們已經通過 JNIEnv 擷取了,是個已知值。
- target_sdk_version: 這個是我們APP的 targetSdkVersion,我們可以提前知道。
- safe_mode:safe_mode 是 AndroidManifest 中的配置,已知值。
是以結合這三個條件,我們對runtime指針執行線性搜尋,首先找到 JavaVM指針,然後找到target_sdk_version,最後直達目标;順便用 safe_mode, java_debuggable 等成員驗證正确性。
找到目标 `hidden_api_policy_`之後,直接修改記憶體,就能達到目的。用僞代碼表示就是:
int unseal(JNIEnv *env, jint targetSdkVersion) {
JavaVM *javaVM;
env->GetJavaVM(&javaVM);
JavaVMExt *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;
const int MAX = 1000;
int offsetOfVmExt = findOffset(runtime, 0, MAX, (size_t) javaVMExt);
int targetSdkVersionOffset = findOffset(runtime, offsetOfVmExt, MAX, targetSdkVersion);
PartialRuntime *partialRuntime = (PartialRuntime *) ((char *) runtime + targetSdkVersionOffset);
EnforcementPolicy policy = partialRuntime->hidden_api_policy_;
partialRuntime->hidden_api_policy_ = EnforcementPolicy::kNoChecks;
return 0;
}
代碼我已經放到 github 上了:
tiann/FreeReflectiongithub.com
使用起來非常簡單,添加依賴;一行代碼調用即可。覺得好用别忘了 star 哦~
看起來我們已經達到目标了,但是不要急;還有2個條件呢,我們繼續,說不定有新發現。
### 第二個條件
然後看第二個return語句,`fn_caller_is_trusted`,這裡面的代碼我就不分析了,直接給結論:這個方法通過回溯調用棧,通過調用者的Class來判斷是否是系統代碼的調用(所有系統的代碼都通過BootClassLoader加載,判斷ClassLoader即可),如果是系統代碼,那麼就允許調用(系統自己的API肯定得讓它調)。這裡我們又發現一個判斷條件:`caller.classloader == BootClassLoader`。是以,如果能把這個調用類的ClassLoader修改為 BootClassLoader,那麼問題不就解決了嗎?
那麼問題來了,如何修改Class的classloader?我們看看Class 類的結構:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
/** defining class loader, or null for the "bootstrap" system loader. */
private transient ClassLoader classLoader;
// 略
}
classloader實際上是Class類的第一個成員,而這個`java.lang.Class`我們肯定是能拿到的,是以我們可以通過上面提到的**修改偏移的方式直接修改ClassLoader**,進而繞過限制。
但是需要注意一下這個偏移量。雖然 Class 聲明沒有繼承任何東西,但實際上它繼承自 Object。我們看下 `java.lang.Object`:
public class Object {
private transient Class<?> shadow$_klass_;
private transient int shadow$_monitor_;
}
是以,Class對象在記憶體中實際上是這樣:
struct Class {
Class<?> shadow$_klass_;
int shadow$_monitor_;
ClassLoader classLoader;
}
JVM規範中,一個int占4位元組;在ART實作中,一個Java對象的引用占用4位元組(不論是32位還是64位),是以 **classloader的偏移量為8**;我們拿到調用者的Class對象,在JNI層拿到對象的記憶體表示,直接把偏移量為8處置空(BootClassLoader在為null)即可。當然,如果你不想用JNI,Unsafe 也能滿足這個需求。
看起來我們已經有好幾種辦法達到目的了,别着急;我們繼續看第三個條件。
### 第三個條件
當代碼流程走到這裡,那個action已經不可能是 kAllow了;不要放棄治療,說不定還有救。觀察代碼:
if (shouldWarn || action == kDeny) {
if (member_signature.IsExempted(runtime->GetHiddenApiExemptions())) {
action = kAllow;
// Avoid re-examining the exemption list next time.
// Note this results in no warning for the member, which seems like what one would expect.
// Exemptions effectively adds new members to the whitelist.
MaybeWhitelistMember(runtime, member);
return kAllow;
}
// 略
}
果然有“豁免”條件:GetHiddenApiExemptions()。跟蹤這個方法之後,你會發現解決辦法跟上面兩種是一樣的。要麼去修改 runtime 的記憶體,要麼修改signature;我就不贅述啦。
### 劍走偏鋒
上面我們分析了系統的源代碼,結合各種條件來實作繞過對非SDK API調用的檢測;但實際上所有這些方式我們的目的都是一樣的—— **通過某種方式修改函數的執行流程**;而達到這個目标最直接的方法就是 **inline hook**!!由于inline hook太強大,你隻需要找到一個關鍵的執行流程,hook其中的某個函數,修改他的傳回值就OK了;這裡我也沒啥好分析的,隻能給大家推薦一個 inline hook 庫了,名字叫 https://github.com/jmpews/HookZz,代碼非常優秀,值得一看。