首先将编写的unidbg代码MainActivity.java复制到同包名目录下,改名为MainJni.java,我们将在此文件中编写响应代码。同时,将上述Android项目编译好的APK解压,取得armeabi-v7a下的so文件,复制到同目录中,并重命名为libjni.so。
接下来简单修改一下源代码,如下所示:
这里仅仅是修改了加载so的名称,模拟执行函数的函数名、函数签名以及传入的参数。
修改完后尝试运行,收到如图3—1所示的报错消息。
图3—1 寻找md52()方法失败报错
这是因为我们修改了类文件的文件名,而上述代码传入了该类实例来当作执行so中函数的java对象,导致unidbg根据传入的类实例处理后的函数名无法找到需要调用的Java_com_dta_lesson2_MainActivity_md52()方法。
可以找到2.2.3节中API的内部实现,下断点进行调试,查看处理后得到的最终的函数名,如图3—2所示。
图3—2 调试查看symbolName
由于我们修改了执行类的类名,导致传入该实例后,拼接后的函数名为Java_com_dta_lesson2_MainJni_md52,这与我们想要调用的函数名是不相符的,自然找不到对应的函数。
所以需要将上述代码修改为如下代码:
DvmObject obj=vm.resolveClass("com/dta/lesson2/MainActivity").newObject(null);
首先使用vm.resolveClass()方法,通过传入一个类名字符串的方式来构建一个class,然后使用newObject()方法来实例化对象,这样unidbg便可以通过该对象来获得正确的函数名。
再次运行,仍然报错,如图3—3所示。
图3—3 未设置setJni()方法报错
由于模拟执行的函数中有JNI操作,而不同的so调用的JNI方法有所不同,unidbg无法为其一一实现,只实现了部分常用的JNI接口,缺失的部分则需要读者自己来实现。因此这里提示我们需要设置vm.setJni()来实现缺失的JNI接口部分。
具体做法是:我们需要调用setJni()方法,让MainJni继承AbstractJni类,并将MainJni类实例当作参数传入setJni()方法中。
当我们进入AbstractJni类时,可以发现unidbg已经实现了常用的JNI操作,如图3—4所示。
图3—4 AbstractJni相关实现
修改代码,如图3—5所示。
图3—5 修改代码
修改后运行,发现还是报错,如图3—6所示。
图3—6 没有MessageDigest.update()实现
虽然我们继承了AbstractJni类,并为VM虚拟机设置了JNI接口,但由于AbstractJni并没有相应MessageDigest.update()的实现,所以报错。MessageDigest.update()是代码中调用的第二个JNI接口,第一个调用JNI接口的方法为MessageDigest.getInstance(),为什么它没有报错呢?
我们尝试在AbstractJni中搜索MessageDigest->getInstance,发现unidbg已经对其做了实现,如图3—7所示。
而unidbg对于update()方法则没有做实现,因此会报java.lang.UnsupportedOperationException错误,如图3—8所示。
我们需要重写报错的callVoidMethodV()方法,为unidbg补全缺失的update()方法的具体实现,使之可以继续执行。这个补齐JNI的过程叫作“补环境”。
在MainJni中编写以下代码:
图3—7 AbstractJni实现MessageDigest->getInstance
图3—8 缺失update()方法报错
public void callVoidMethodV(BaseVM vm,DvmObject<?>dvmObject,String
signature,VaList vaList){
if(signature.equals("java/security/MessageDigest->update([B)V")){
MessageDigest messageDigest=(MessageDigest)dvmObject.getValue();
int intArg=vaList.getIntArg(0);
Object object=vm.getObject(intArg).getValue();
messageDigest.update((byte[])object);
return;
}
super.callVoidMethodV(vm,dvmObject,signature,vaList);
}
当执行到该方法时,参数vm为虚拟机,dvmObject为调用该函数的对象,signature为函数的签名,vaList为函数的参数列表。
在调用callVoidMethodV函数前先判断函数的签名是否与update()方法相匹配,该处填写的签名为报错信息给出的签名,如果匹配则执行补齐的自实现代码。
在执行messageDigest.update()方法时,我们首先要取得MessageDigest对象,如我们已知的≮dvmObject就是调用方法的对象,因此可以使用dvmObject.getValue()方法来获得MessageDigest对象,由于我们已经知道它的类型,所以可以将它强制类型转换为MessageDigest类型。
接下来便是取参数了。通过vaList.getIntArg()方法,传入下标,我们便能取得相应的参数。需要注意的是,由于update()方法的参数类型为byte[],而在JNI中没有对象的概念,因此unidbg为非基本类型维护了一个Map引用,如图3—9所示。通过getIntArg()方法取得的只是Map中的key值,还需要通过vm.getObject()方法从VM虚拟机中取得该对象,再通过.getValue()方法取得实际对象的值。
图3—9 unidbg维护的Map引用
之后通过调用messageDigest.update()方法,完成对JNI环境的修补。
修补完update()方法后继续运行,收到缺失digest()方法的报错提示,如图3—10所示。
图3—10 缺失digest()方法报错
根据报错的调用栈,重写callObjectMethodV()方法,代码如下:
public DvmObject<?>callObjectMethodV(BaseVM vm,DvmObject<?>dvmObject,
String signature,VaList vaList){
if(signature.equals("java/security/MessageDigest->digest()[B")){
MessageDigest messageDigest=(MessageDigest)dvmObject.getValue();
byte[]digest=messageDigest.digest();
DvmObject<?>object=ProxyDvmObject.createObject(vm,digest);
vm.addLocalObject(object);
return object;
}
return super.callObjectMethodV(vm,dvmObject,signature,vaList);
}
步骤与之前大致相同,只是多了一个将运算得到的结果digest先代理创建为DvmObject对象,再添加到VM虚拟机的操作。所有的非基本类型、包装类型都需要添加到VM虚拟机的Map映射中,否则在JNI中无法找到该引用。最后将运算得到的结果当作函数返回值返回。
再次运行,继续报错,提示找不到自编写的byte2Hex()方法,如图3—11所示。继续根据报错信息来补全JNI环境。
图3—11 缺失byte2Hex()方法报错
补全后的byte2Hex()方法代码如图3—12所示,这里只是直接调用了编写好的byte2Hex()方法,并将结果转换为StringObject对象并添加到VM虚拟机中。
图3—12 补全后的byte2Hex()方法代码
再次运行,发现成功得到正确的结果,如图3—13所示。
图3—13 成功模拟执行JNI函数
虽然修补环境的工作较为烦琐,但相较于复杂的so中的算法流程而言,使用unidbg的好处不言而喻。