kuaidui作业sign逆向及unidbg实现
Java层
apk用frida_dump
脱壳后,重新打包,jadx搜索sign=
nativeGetSign
这个名字就值得点进去看看
com.zuoyebang.baseutil.a.b
com.zuoyebang.baseutil.NativeHelper.
nativeGetSign
找到函数了,hook验证一下
android hooking watch class_method com.zuoyebang.baseutil.NativeHelper.nativeGetSign --dump-args --dum-return
输入是一个base64字符串,解码后是请求参数做个拼接。
接下来找找函数在哪个so里面,nativeInitBaseUtil
看这函数名,应该是初始化的,查看用例
com.zuoyebang.baseutil.a.a
看来应该是在libbaseutil.so
so层
ida查看JNI_OnLoad
几个函数都找到了
看看CRYMd5
hook一下CRYMd5
function hook_md5() {
var bptr = Module.findBaseAddress("libbaseutil.so");
Interceptor.attach(bptr.add(0x2ae8), {
onEnter: function(args) {
console.log("md5-arg0: ", args[0].readCString());
},
onLeave: function(retval) {
console.log("md5-ret:", retval.readCString(32));
}
})
}
cyberchef验证是不是标准MD5
多请求几次,发现objSpamServer.random_number
,也就是cdAgblSOFM
不变,说明它是个相对固定的值。
卸载app重装后,发现还是不变,说明和会话无关。
更换设备后,发现值改变了,说明该值和设备相关。
一个比较取巧也比较无奈的办法就是建立设备号和字符串的映射表,计算sign时选用对应的字符串即可。
unidbg实现
由于app只提供了64位的so,所以此次运行的是64位so。依旧是先搭个框架
public class Kuaidui extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.kuaiduizuoye.scan";
public static String apkPath = "unidbg-android/src/test/java/com/kuaidui/kuaidui540.apk";
public static String soPath = "";
public Kuaidui() {
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
new AndroidModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary("baseutil", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
}
}
罕见的没有报错,那就开始调用。
public void call_sign() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm, "everhu")));
Number ret = module.callFunction(emulator, 0x1500, list.toArray());
System.out.println(vm.getObject(ret.intValue()).getValue());
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
test.call_sign();
}
调用出结果了,只是不是正确结果。说明环境不对,可能有些参数没有设置。
native函数里面应该有设置环境的函数,hook看看哪个先被调用了。
objection.exe -g com.kuaiduizuoye.scan explore --startup-command="android hooking watch class com.zuoyebang.baseutil.NativeHelper"
可以看到nativeSetToken
被调用了,再hook看看入参
objection -g com.kuaiduizuoye.scan explore --startup-command="android hooking watch class_method com.zuoyebang.baseutil.NativeHelper.nativeSetToken --dump-args --dump-return"
在unidbg补上。
public void call_token() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
String devid = "F5D53AD5A66144B57783C7C67611F0F7|0";
list.add(vm.addLocalObject(new StringObject(vm, devid)));
String request = "03090b0106000807080e00080201090708060f00040700020d0503070c0606000c01000a080e07050808020d00060b0d070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d0201020f0e02030d0107020d000e0e02090401070505030a0c0605080303040e0803020c020e0d020408010b030e0b090f060302000e0f0902010706040c00080d0e060d000f0805040b0e07000d020b0f07";
list.add(vm.addLocalObject(new StringObject(vm, request)));
String response = "0a040609080d020b0a03090e010306050d0d050c0c060207070105010b0b01090801000a020d0c0b03030209030e0d0a";
list.add(vm.addLocalObject(new StringObject(vm, response)));
Number ret = module.callFunction(emulator, 0x1264, list.toArray());
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
test.call_token();
test.call_sign();
}
需要返回int,用objection + Wallbreaker
查看是64
@Override
public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "android/content/pm/PackageManager->GET_SIGNATURES:I": {
return 64;
}
}
return super.getStaticIntField(vm, dvmClass, signature);
}
然后就出结果了。
完整实现
public class Kuaidui extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.kuaiduizuoye.scan";
public static String apkPath = "unidbg-android/src/test/java/com/kuaidui/kuaidui540.apk";
public static String soPath = "";
public Kuaidui() {
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
new AndroidModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary("baseutil", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
@Override
public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "android/content/pm/PackageManager->GET_SIGNATURES:I": {
return 64;
}
}
return super.getStaticIntField(vm, dvmClass, signature);
}
public void call_sign() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm, "everhu")));
Number ret = module.callFunction(emulator, 0x1500, list.toArray());
System.out.println(vm.getObject(ret.intValue()).getValue());
}
public void call_token() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
String devid = "F5D53AD5A66144B57783C7C67611F0F7|0";
list.add(vm.addLocalObject(new StringObject(vm, devid)));
String request = "03090b0106000807080e00080201090708060f00040700020d0503070c0606000c01000a080e07050808020d00060b0d070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d0201020f0e02030d0107020d000e0e02090401070505030a0c0605080303040e0803020c020e0d020408010b030e0b090f060302000e0f0902010706040c00080d0e060d000f0805040b0e07000d020b0f07";
list.add(vm.addLocalObject(new StringObject(vm, request)));
String response = "0a040609080d020b0a03090e010306050d0d050c0c060207070105010b0b01090801000a020d0c0b03030209030e0d0a";
list.add(vm.addLocalObject(new StringObject(vm, response)));
Number ret = module.callFunction(emulator, 0x1264, list.toArray());
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
test.call_token();
test.call_sign();
}
}
nativeSetToken
nativeSetToken
入参的3个字符串也是会随着设备而改变,分析一下它的来源。
可以看出是从Preference
里取值。
不过,当apk刚安装的时候,shared_prefs
里面是没有这两个值的。
此时是从上图处取值的,可以看出调用了native的nativeInitBaseUtil
函数,之后发了个请求,抓包可以看到
可以看出,post的请求数据就是第二个字符串,post的响应数据就是第三个字符串。
其他
通过com.zuoyebang.baseutil.NativeHelper
这个类看到了另一个app的名字zuoyebang
,很自然认为二者用的是同一个签名方案。jadx打开zuoyebang的apk查看下,发现了同样的接口。
再查看下so文件
稍微不同的是zuoyebang只提供了32位so,而kuaidui作业只提供64位so。
ida查看
可以看到kuaidui作业的函数名全是显式的,能够通过函数名知道函数的作用;而zuoyebang则全都是sub_*
形式,很难直观的了解函数的作用。二者都是用相同的方案,逆向的难度却相差好几倍,看来逆向的时候除了看看旧版本的apk,还可以看看其他使用相同方案的产品。