本文參考資料:
《一種繞過Android P對非SDK接口限制的簡單方法》。
《另一種繞過 Android P以上非公開API限制的辦法》。
一、Android P 引入了針對隱藏API的使用限制
眾所周知,Android P 引入了針對非 SDK 接口(俗稱為隱藏API)的使用限制。這是繼 Android N上針對 NDK 中私有庫的鏈接限制之后的又一次重大調整。從今以后,不論是native層的NDK還是 Java層的SDK,我們只能使用Google提供的、公開的標准接口。
舉個例子:
Android SDK的 WifiManager方法對很多的Filed設置了隱藏,舉個例子:
/**
* Broadcast intent action indicating whether Wi-Fi scanning is allowed currently
* @hide
*/
public static final String WIFI_SCAN_AVAILABLE = "wifi_scan_available";
如果直接用反射區訪問:
public static void getWifiReflection(Context context) {
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
try {
Field field = wifiManager.getClass().getDeclaredField("WIFI_SCAN_AVAILABLE");
Log.e("PReflectionUtils", (String) field.get(wifiManager));
} catch (Exception e) {
e.printStackTrace();
}
}
在Android P以上的機型上運行會發現訪問失敗:
W/oid.handwritin: Accessing hidden field Landroid/net/wifi/WifiManager;->WIFI_SCAN_AVAILABLE:Ljava/lang/String; (dark greylist, reflection)
W/System.err: java.lang.NoSuchFieldException: No field WIFI_SCAN_AVAILABLE in class Landroid/net/wifi/WifiManager; (declaration of 'android.net.wifi.WifiManager' appears in /system/framework/framework.jar!classes2.dex)
W/System.err: at java.lang.Class.getDeclaredField(Native Method)
W/System.err: at com.renhui.android.handwriting.common.PReflectionUtils.getWifiReflection(PReflectionUtils.java:18)
W/System.err: at com.renhui.android.handwriting.MyApplication.onCreate(MyApplication.java:17)
W/System.err: at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1162)
W/System.err: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6717)
W/System.err: at android.app.ActivityThread.access$2000(ActivityThread.java:273)
...
但是在低版本上是能訪問的:
E/PReflectionUtils: wifi_scan_available
二、Android系統如何實現對隱藏API的訪問限制
通過反射或者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 調用了嗎?如果它返回false,那么直接返回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的判斷;進而欺騙系統,繞過限制。
三、我們如何實現對隱藏Api的訪問
我們要訪問一個類的成員,除了直接訪問,反射調用/JNI就沒有別的方法了嗎?當然不是。如果你了解ART的實現原理,知道對象布局,那么這個問題就太簡單了。
所有的Java對象在內存中其實就是一個結構體,這份內存在 native 層和Java層是對應的,因此如果我們拿到這份內存的頭指針,直接通過偏移量就能訪問成員。
那么方法如何訪問呢?ART的對象模型采用的類似Java的 klass-oop方式,方法是存儲在 java.lang.Class對象中的,它們是Class對象的成員,因此訪問方法最終就是訪問成員。
下面我們接着上面繼續看,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;
}
...
}
繼續觀察這個方法,接下來 調用了 GetHiddenApiEnforcementPolicy 方法獲取限制策略,如果是 kNoChecks 直接允許;那 GetHiddenApiEnforcementPolicy 這個方法是啥樣呢?在 runtime.h 中,如下:
hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const {
return hidden_api_policy_;
}
也就是說,返回的是 runtime 這個對象的一個成員。如果我們直接修改內存,把這個成員設置為 kNoChecks,那么不就達到目標了嗎?
下面我們來實踐一下,首先要獲取runtime指針:
既然需要修改runtime對象的內存,那么首先得拿到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,上面我們挑出了一些我們能夠使用的參照物,輔助進行內存定位:
- javavm :我們很熟悉的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;
}
到此為止,基本上實現對隱藏API訪問限制的破解了。
但是,大佬不滿足只到這個程度,他發現系統有一個 fn_caller_is_trusted 條件:如果調用者是系統類,那么就允許被調用。
也就是說,如果我們能以系統類的身份去反射,那么就能暢通無阻。問題是,我們如何以「系統的身份去反射」呢?一種最常見的辦法是,我們自己寫一個類,然后通過某種途徑把這個類的 ClassLoader 設置為系統的 ClassLoader,再借助這個類去反射其他類。但是這里的「通過某種途徑」依然要使用一些黑科技才能實現,與修改 flags / inline hook 無本質區別。
以系統類的身份去反射 有兩個意思,1. 直接把我們自己變成系統類;2. 借助系統類去調用反射。我們一個個分析。
1.直接把我們自己變成系統類
這個方式有童鞋可能覺得天方夜譚,APP 的類怎么可能成為系統類?但是,一定不要被自己的固有思維給局限,一切皆有可能!我們知道,對APP來說,所謂的系統類就是被 BootstrapClassLoader 加載的類,這個 ClassLoader 並非普通的 DexClassLoader,因此我們無法通過插入 dex path的方式注入類。但是,Android 的 ART 在 Android O 上引入了 JVMTI,JVMTI 提供了將某一個類轉換為 BootstrapClassLoader 中的類的方法!具體來說,我們寫一個類暴露反射相關的接口,然后通過 JVMTI 提供的 AddToBootstrapClassLoaderSearch將此類加入 BootstrapClassLoader 就實現目的了。不過,JVMTI 要在 release 版本的 APP 上運行依然需要 Hack,所以這種途徑與其他的黑科技無本質區別。
2.借助系統的類去反射
如果系統有一個方法systemMethod,這個systemMethod 去調用反射相反的方法,那么systemMethod毋庸置疑會反射成功。但是,我們從哪去找到這么一個方法給我們用?
- 首先,我們通過反射 API 拿到 getDeclaredMethod 方法。getDeclaredMethod 是 public 的,不存在問題;這個通過反射拿到的方法我們稱之為元反射方法。
- 然后,我們通過剛剛反射拿到元反射方法去反射調用 getDeclardMethod。這里我們就實現了以系統身份去反射的目的——反射相關的 API 都是系統類,因此我們的元反射方法也是被系統類加載的方法;所以我們的元反射方法調用的 getDeclardMethod 會被認為是系統調用的,可以反射任意的方法。
Method metaGetDeclaredMethod =
Class.class.getDeclaredMethod("getDeclardMethod"); // 公開API,無問題
Method hiddenMethod = metaGetDeclaredMethod.invoke(hiddenClass,
"hiddenMethod", "hiddenMethod參數列表"); // 系統類通過反射使用隱藏 API,檢查直接通過。
hiddenMethod.invoke // 正確找到 Method 直接反射調用
到這里,我們已經能通過「元反射」的方式去任意獲取隱藏方法或者隱藏 Field 了。但是,如果我們所有使用的隱藏方法都要這么干,那還有點小麻煩。在 上文中,我們后來發現,隱藏 API 調用還有「豁免」條件,具體代碼如下:
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;
}
// 略
}
只要 IsExempted 方法返回 true,就算這個方法在黑名單中,依然會被放行然后允許被調用。我們再觀察一下IsExempted方法:
bool MemberSignature::IsExempted(const std::vector<std::string>& exemptions) {
for (const std::string& exemption : exemptions) {
if (DoesPrefixMatch(exemption)) {
return true;
}
}
return false;
}
繼續跟蹤傳遞進來的參數 runtime->GetHiddenApiExemptions() 發現這玩意兒也是 runtime 里面的一個參數,既然如此,我們可以一不做二不休,仿照修改 runtime flag 的方式直接修改 hidden_api_exemptions_ 也能繞過去。但如果我們繼續跟蹤下去,會有個有趣的發現:這個API 竟然是暴露到 Java 層的,有一個對應的 VMRuntime.setHiddenApiExemptions Java方法;也就是說,只要我們通過 VMRuntime.setHiddenApiExemptions 設置下豁免條件,我們就能愉快滴使用反射了。
再結合上面這個方法,我們只需要通過 「元反射」來反射調用 VMRuntime.setHiddenApiExemptions 就能將我們自己要使用的隱藏 API 全部都豁免掉了。更進一步,如果我們再觀察下上面的 IsExempted 方法里面調用的 DoesPrefixMatch,發現這玩意兒在對方法簽名進行前綴匹配;童鞋們,我們所有Java方法類的簽名都是以 L開頭啊!如果我們把直接傳個 L進去,所有的隱藏API全部被赦免了!
基於上面的內容weishu 大佬開源了:FreeReflection
同時也提出來了基於修改signature的方式來實現破解的思路。
其實可以看出要實現繞過對非SDK API調用的檢測;實現的方式目的都是一樣的:即通過某種方式修改函數的執行流程;而達到這個目標最直接的方法就是 inline hook!!由於inline hook太強大,你只需要找到一個關鍵的執行流程,hook其中的某個函數,修改他的返回值就OK了;這里我也沒啥好分析的,只能給大家推薦一個 inline hook 庫了,名字叫 HookZz,項目地址:https://github.com/jmpews/Dobby。