WebView執行在系統程序出現的問題 WebView is not allowed in privileged processes
WebView在Android4.4之前使用的Webkit核心,在Android4.4以後切換到了Chromium核心。本文的內容主要不是講解Chromium核心上WebView的特性。關鍵是要講解webview切換到Chromium核心後我遇到的一個坑(實際上可能叫做坑不合適,因為這是安卓為了安全著想才這麼做的),並提出解決這個坑的一個方法。
這個問題的情形是這樣的,當時需要在一個系統應用中使用webview,系統應用因為需要使用一些特殊的系統許可權,所有配置了android:sharedUserId="android.uid.system" ,那麼當你執行webview的時候,就會發現程式crash並丟擲以下異常:
Caused by: java.lang.UnsupportedOperationException: For security reasons, WebView is not allowed in privileged processes
at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:155)
at android.webkit.CookieManager.getInstance(CookieManager.java:42)
接下來我們來跟蹤原始碼看下這個異常是怎麼丟擲來的根據crash資訊,我們來看WebViewFactory中的getProvider方法,在這裡提一下,在切換WebView的核心之前,Google就已經修改了WebView的程式碼架構,使用了工廠模式來決定WebView的具體實現,目的就是為了日後可以方便的切換WebView核心。在這裡以Android-22的原始碼為例,每個Android版本的WebViewFactory原始碼都有所出入,不過本文的關鍵是描述解決這個問題的思路。
static WebViewFactoryProvider getProvider() {
synchronized (sProviderLock) {
// For now the main purpose of this function (and the factory abstraction) is to keep
// us honest and minimize usage of WebView internals when binding the proxy.
if (sProviderInstance != null) return sProviderInstance;
final int uid = android.os.Process.myUid();
if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
loadNativeLibrary();
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
Class<WebViewFactoryProvider> providerClass;
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getFactoryClass()");
try {
providerClass = getFactoryClass();
} catch (ClassNotFoundException e) {
Log.e(LOGTAG, "error loading provider", e);
throw new AndroidRuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
try {
try {
sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
.newInstance(new WebViewDelegate());
} catch (Exception e) {
sProviderInstance = providerClass.newInstance();
}
if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
return sProviderInstance;
} catch (Exception e) {
Log.e(LOGTAG, "error instantiating provider", e);
throw new AndroidRuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
StrictMode.setThreadPolicy(oldPolicy);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
}
}
原來這個異常是這句程式碼丟擲的:
final int uid = android.os.Process.myUid();
if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}
那麼問題來了,我們有什麼辦法可以讓它不丟擲這個異常嗎?我們看原始碼,可以發現在走到這一步之前,有這麼一句:
if (sProviderInstance != null) return sProviderInstance;
我們再來看sProviderInstance是什麼:
private static WebViewFactoryProvider sProviderInstance;
原來只要sProviderInstance被建立過一次,那麼以後再進入getProvider()時,就會直接返回以建立的例項,也就是說後面丟擲異常的語句就不會被執行啦!
那麼,我們的目的就明確了,就是要在程式執行getProvider()之前,人為的給sProviderInstance賦值。這就要借鑑Hook的思想,什麼是Hook?請自行百度,這裡不做講解。
要Hook的話,必須要有合適的Hook點,比如在方法內部實時new的物件,非static變數,static方法,這些都是無法Hook的,或者也可以說是就算Hook了也沒有任意意義,而我們看到sProviderInstance是一個static變數,也就是說所有的路徑都是讀取這一個物件,那麼這裡剛好就是一個很好的Hook點,真是天助我也!
我們再看getProvider()的程式碼,檢測完uid之後,後面的程式碼就開始建立sProviderInstance了,那麼我們的工作就是要想辦法來執行它們。這裡我有兩個方案,(1)通過反射呼叫裡面被呼叫的方法,(2)採用欺騙Api的方式。反射呼叫很好理解,欺騙Api又是什麼呢?其實就是在我們的專案工程中,按照需要呼叫的類和方法建立一個class檔案,方法可以什麼都不實現,然後我們打包apk的時候,把這些人為建立的類給幹掉,也就是說只引用,不打包。這樣App最終編譯安裝呼叫的時候,會連線到系統中的這個類裡面去。
但是要注意,無論是反射也要,欺騙Api也好,不是任何類和方法都可以被建立和呼叫的,我們看原始碼的時候會發現有些類和方法會被@SystemApi或者@Hide註解,這兩個東西又是什麼呢?@Hide註解的類,說明這個類是不對外開放的,也就是說Android Sdk中是無法直接呼叫的,但是可以通過反射去呼叫。@SystemApi註解包括了@Hide的隱藏功能,但是區別在於@SystemApi註解的類和方法只有系統App才能去呼叫,非系統App是不允許連線到這些類和方法的,如果非系統App非要去呼叫@SystemApi註解的類,那麼會丟擲NoClassDefFoundError的異常,這個異常的意思是這個類在編譯時是可以找得到的,但是在執行時沒有這個類了。所以這個辦法只能針對系統App來用。
當我們用各種反射,各種欺騙Api,終於仿照getProvider的流程弄出一個WebViewFactoryProvider型別的物件後,我們要做的就是把它賦值給sProviderInstance
以下貼上解決這個問題的程式碼,用的是反射的方法,找到WebViewDelegate的構造,設定訪問級別,然後newinstance就行。WebViewDelegate的構造方法是package級別的,因此用欺騙API的方式是行不通的。
private void hookWebView(){
Class<?> factoryClass = null;
try {
factoryClass = Class.forName("android.webkit.WebViewFactory");
Method getProviderClassMethod = null;
Object sProviderInstance = null;
if (Build.VERSION.SDK_INT == 23) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass");
getProviderClassMethod.setAccessible(true);
Class<?> providerClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate");
Constructor<?> constructor = providerClass.getConstructor(delegateClass);
if (constructor != null) {
constructor.setAccessible(true);
Constructor<?> constructor2 = delegateClass.getDeclaredConstructor();
constructor2.setAccessible(true);
sProviderInstance = constructor.newInstance(constructor2.newInstance());
}
} else if (Build.VERSION.SDK_INT == 22) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
getProviderClassMethod.setAccessible(true);
Class<?> providerClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate");
Constructor<?> constructor = providerClass.getConstructor(delegateClass);
if (constructor != null) {
constructor.setAccessible(true);
Constructor<?> constructor2 = delegateClass.getDeclaredConstructor();
constructor2.setAccessible(true);
sProviderInstance = constructor.newInstance(constructor2.newInstance());
}
} else if (Build.VERSION.SDK_INT == 21) {//Android 21無WebView安全限制
getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
getProviderClassMethod.setAccessible(true);
Class<?> providerClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
sProviderInstance = providerClass.newInstance();
}
if (sProviderInstance != null) {
Log.i("cym", sProviderInstance.toString());
Field field = factoryClass.getDeclaredField("sProviderInstance");
field.setAccessible(true);
field.set("sProviderInstance", sProviderInstance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
本文的宗旨是讓大家對Hook的實際用途有個概念,在開發中如果遇到一些問題時,解決問題的武器庫中多一把利刃。當Hook配合動態代理來使用時,就可以做到很多一般情況下做不到的事情!比如360的DroidPlugin主要就是使用了動態代理來Hook住FrameWork層!
最新的寫法相容了了8.0及以上的android版本
// 如果是非系統程序則按正常程式走 if (Process.myUid() != Process.SYSTEM_UID) { return; } int sdkInt = Build.VERSION.SDK_INT; try { Class<?> factoryClass = Class.forName("android.webkit.WebViewFactory"); Field field = factoryClass.getDeclaredField("sProviderInstance"); field.setAccessible(true); Object sProviderInstance = field.get(null); if (sProviderInstance != null) { VLog.e("sProviderInstance isn't null"); return; } Method getProviderClassMethod; if (sdkInt > 22) { getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass"); } else if (sdkInt == 22) { getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass"); } else { VLog.e("Don't need to Hook WebView"); return; } getProviderClassMethod.setAccessible(true); Class<?> factoryProviderClass = (Class<?>) getProviderClassMethod.invoke(factoryClass); Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate"); Constructor<?> delegateConstructor = delegateClass.getDeclaredConstructor(); delegateConstructor.setAccessible(true); if (sdkInt < 26) {//低於Android O版本 Constructor<?> providerConstructor = factoryProviderClass.getConstructor(delegateClass); if (providerConstructor != null) { providerConstructor.setAccessible(true); sProviderInstance = providerConstructor.newInstance(delegateConstructor.newInstance()); } } else { Field chromiumMethodName = factoryClass.getDeclaredField("CHROMIUM_WEBVIEW_FACTORY_METHOD"); chromiumMethodName.setAccessible(true); String chromiumMethodNameStr = (String) chromiumMethodName.get(null); if (chromiumMethodNameStr == null) { chromiumMethodNameStr = "create"; } Method staticFactory = factoryProviderClass.getMethod(chromiumMethodNameStr, delegateClass); if (staticFactory != null) { sProviderInstance = staticFactory.invoke(null, delegateConstructor.newInstance()); } } if (sProviderInstance != null) { field.set("sProviderInstance", sProviderInstance); VLog.e("Hook success!"); } else { VLog.e("Hook failed!"); } } catch (Exception e) { LogUtil.e("", e); }
--------------------- 本文來自 mgdbaby 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/qq_33890656/article/details/69656285?utm_source=copy