从案例中学习unidbg的使用(四)

一、引言

目标 APK:bilibili.apk

目标方法:com.bilibili.nativelibrary.LibBili.s

目标方法实现:libbili.so

二、任务介绍

JADX反编译 apk,找到com.bilibili.nativelibrary.LibBili类,其中的 s 方法是本篇的目标函数。它是一个静态方法,参数是 Map,返回值是 SignedQuery 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.bilibili.nativelibrary;

...

/* compiled from: BL */
/* loaded from: classes.dex */
public final class LibBili {
...

public static SignedQuery g(Map<String, String> map) {
return s(map == null ? new TreeMap() : new TreeMap(map));//使用了 s 函数
}

// target
static native SignedQuery s(SortedMap<String, String> sortedMap);
...
}

-------------------------------------------------------------------------------------------------------
package com.bilibili.nativelibrary;

...

public final class SignedQuery {
private static final char[] f14891c = "0123456789ABCDEF".toCharArray();
public final String a;
public final String b;

public SignedQuery(String str, String str2) {
this.a = str;
this.b = str2;
}

public String toString() {
String str = this.a;
if (str == null) {
return "";
}
if (this.b == null) {
return str;
}
return this.a + "&sign=" + this.b;
}
}

LibBili类里的 g 函数使用了它,对 g 做 Hook,代码如下

1
2
3
4
5
6
7
8
9
10
Java.perform(function() {
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
let Map = Java.use('java.util.HashMap');
LibBili["g"].implementation = function (map) {
console.log('g is called' + ', ' + 'map: ' + Java.cast(map, Map));
let ret = this.g(map);
console.log('g ret value is ' + ret.toString());//自定义SignedQuery类,但存在toString方法
return ret;
};
})

输出很多,随便选择一条

1
2
3
4
g is called, map: {build=6180500, mobi_app=android, channel=shenma069, appkey=1d8b6e7d45233436, s_locale=zh_CN, c_locale=zh_CN, platform=android, statistics={"appId":1,"platform":3,"version":"6.18.0","abtest"
:""}}
g ret value is appkey=1d8b6e7d45233436&build=6180500&c_locale=zh_CN&channel=shenma069&mobi_app=android&platform=android&s_locale=zh_CN&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226
.18.0%22%2C%22abtest%22%3A%22%22%7D&ts=1665717038&sign=0186ec11e22282128c5d25c82f9813dc

使用frida主动调用 s 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function callS(){
Java.perform(function() {
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
let TreeMap = Java.use("java.util.TreeMap");
var map = TreeMap.$new();

map.put("build", "6180500");
map.put("mobi_app", "android")
map.put("channel", "shenma069")
map.put("appkey", "1d8b6e7d45233436")
map.put("s_locale", "zh_CN")

let result = LibBili.s(map);
console.log("ret:"+result.toString());
})
}

结果如下

1
ret:appkey=1d8b6e7d45233436&build=6180500&channel=shenma069&mobi_app=android&s_locale=zh_CN&ts=1665720140&sign=4de05ca1652dc2dbb7eac6801e391d73

接下来使用unidbg复现 s 函数的调用。

三、初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.bilibili.nativelibrary;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class LibBili extends AbstractJni {

private final AndroidEmulator emulator;

private final VM vm;

private final Memory memory;

private final DvmClass cLibBili;

public LibBili(){

emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("tv.danmaku.bili")
.build();

memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/bilibili/bilibili.apk"));
vm.setJni(this);
vm.setVerbose(true);

DalvikModule dm = vm.loadLibrary("bili", true);
cLibBili = vm.resolveClass("com.bilibili.nativelibrary.LibBili");
dm.callJNI_OnLoad(emulator);

}

public String s() {
TreeMap<String, String> map = new TreeMap<>();
map.put("build", "6180500");
map.put("mobi_app", "android");
map.put("channel", "shenma069");
map.put("appkey", "1d8b6e7d45233436");
map.put("s_locale", "zh_CN");

DvmObject<?> mapObj = ProxyDvmObject.createObject(vm, map);

String result = cLibBili.callStaticJniMethodObject(emulator, "s(Ljava/util/SortedMap;)Lcom/bilibili/nativelibrary/SignedQuery;", mapObj).getValue().toString();

return result;
}

public static void main(String[] args) {
LibBili bili = new LibBili();
System.out.println("call s result: " + bili.s());
}

}

由于 s 函数接收的参数是 SortedMap 接口类,而 TreeMap 是它的实现类,因此需要将参数组装成 TreeMap 对象,同时在传参时,需要将参数转成 Unidbg 中的类型。由于 Map 是 JDK 中的类,unidbg已经帮我们实现好了,我们通过ProxyDvmObject.createObject(vm, obj);构造对象即可。

四、补环境

运行报错

1
2
3
4
5
Find native function Java_com_bilibili_nativelibrary_LibBili_s => RX@0x12001c97[libbili.so]0x1c97
[10:43:35 649] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987312, svcNumber=0x121, PC=unidbg@0xfffe02a4, LR=RX@0x12006697[libbili.so]0x6697, syscall=null
java.lang.UnsupportedOperationException: java/util/Map->isEmpty()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethod(AbstractJni.java:598)
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethod(AbstractJni.java:588)

在callBooleanMethod方法中补充Map.isEmpty()方法,取出这个 dvmObject 对应的 map 对象,调用它的 isEmpty 方法。在AbstractJni有类似的情况可以参考,不过还是有区别。

1
2
3
4
5
6
7
8
9
@Override
public boolean callBooleanMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/Map->isEmpty()Z": {
return ((Map)dvmObject.getValue()).isEmpty();
}
}
return super.callBooleanMethod(vm, dvmObject, signature, varArg);
}

继续运行,报错

1
2
3
4
5
6
JNIEnv->NewStringUTF("appkey") was called from RX@0x12003019[libbili.so]0x3019
[10:51:43 331] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987312, svcNumber=0x11e, PC=unidbg@0xfffe0274, LR=RX@0x120064dd[libbili.so]0x64dd, syscall=null
java.lang.UnsupportedOperationException: java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)

同样实现它。不以V结尾的 JNI 函数通过 varArg 取参数,以V结尾的 JNI 函数中通过 varList 取参数。getObjectArg(index)用于获取第indexobject参数,如果是 int 、long 等类型需要使用对应的getintArggetlongArg等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;": {
Map map = (Map)dvmObject.getValue();
//获取参数key
Object key = varArg.getObjectArg(0).getValue();
//获取对应的值
Object value = map.get(key);
return ProxyDvmObject.createObject(vm, value);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}

继续运行,报错

1
2
3
4
5
JNIEnv->NewStringUTF("1732417097") was called from RX@0x12003471[libbili.so]0x3471
[10:58:17 098] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987368, svcNumber=0x11e, PC=unidbg@0xfffe0274, LR=RX@0x1200659d[libbili.so]0x659d, syscall=null
java.lang.UnsupportedOperationException: java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.bilibili.nativelibrary.LibBili.callObjectMethod(LibBili.java:89)

在 callObjectMethod 中的switch-case分支中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
case "java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;": {
Map map = (Map)dvmObject.getValue();
//获取参数key
Object key = varArg.getObjectArg(0).getValue();
//获取参数value
Object value = varArg.getObjectArg(1).getValue();
//调用 Map.put() 方法
Object obj = map.put(key, value);
//转成 dvmobj
return ProxyDvmObject.createObject(vm, obj);
}

继续报错

1
2
3
4
5
6
JNIEnv->CallObjectMethod(java.util.TreeMap@291caca8, put("ts", "1732417308") => null) was called from RX@0x1200659d[libbili.so]0x659d
[11:01:48 382] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987096, svcNumber=0x16e, PC=unidbg@0xfffe0774, LR=RX@0x12003077[libbili.so]0x3077, syscall=null
java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:433)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:422)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethod(DvmMethod.java:54)

要让我们自己实现SignedQuery.r(),这是App里自定义的函数,简单的方法就是把代码及其依赖都copy过来。其中TextUtils.isEmpty 和 string.isEmpty 功能类似,这里直接替换。其中cv.m字段的值为 15,直接硬编码处理一下。然后再重写callStaticObjectMethod方法,实现SignedQuery.r()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;": {
// 获取入参 map
Map map = (Map)varArg.getObjectArg(0).getValue();
//调用 SignedQuery.r()
String str = SignedQuery.r(map);
// 将 String 转成 dvmstr
return new StringObject(vm, str);
}
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}

继续报错

1
2
3
4
5
[11:33:05 939]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=-452987096, svcNumber=0x118, PC=unidbg@0xfffe0214, LR=RX@0x120031cb[libbili.so]0x31cb, syscall=null
java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:733)
at com.github.unidbg.linux.android.dvm.DvmMethod.newObject(DvmMethod.java:224)

须重写newObject函数以完善SignedQuery的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V": {
//获取参数
String str1 = (String) varArg.getObjectArg(0).getValue();
String str2 = (String) varArg.getObjectArg(1).getValue();
//调用构造函数
SignedQuery obj = new SignedQuery(str1, str2);
//转成unidbg所使用的类型
return ProxyDvmObject.createObject(vm, obj);

// 或者这样
//return vm.resolveClass("com/bilibili/nativelibrary/SignedQuery").newObject(obj);
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}

最终需要将SignedQuery对象转成unidbg能接受的obj,有两种方法:

  1. SignedQuery是样本自定义的类库,可以通过resolveClass(className).newObject处理。
  2. 由于我们将SignedQuery类导入到了本地环境中,因此可以使用ProxyDvmObject.createObject(vm, obj);构造出对象。

最终结果如下

1
call s result: appkey=1d8b6e7d45233436&build=6180500&channel=shenma069&mobi_app=android&s_locale=zh_CN&ts=1732419383&sign=828f57ba1aca689e080040a582e44fd5

由于时间戳ts不一样,因此签名也不同。

五、参考

[Unidbg 的基本使用(四)](https://www.yuque.com/lilac-2hqvv/xdwlsg/uo6guvore38kr29y?# 《Unidbg 的基本使用(四)》)


从案例中学习unidbg的使用(四)
http://example.com/2024/12/05/Unidbg模拟执行/从案例中学习unidbg的使用(四)/
作者
gla2xy
发布于
2024年12月5日
许可协议