从案例中学习unidbg补系统调用(二)

一、引言

目标 APK:kuaishou.apk

目标方法:com.kuaishou.dfp.envdetect.jni.Watermelon.jniCommand

目标方法实现:libksse.so

二、任务描述

JADX 反编译目标 APK,找到com.kuaishou.dfp.envdetect.jni.Watermelon类,其中的jniCommand就是目标函数,它位于 libksse.so。

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
package com.kuaishou.dfp.envdetect.jni;

import android.content.Context;
import android.util.Base64;
import com.getkeepsafe.relinker.d;
import com.kwai.robust.PatchProxy;
import com.kwai.robust.PatchProxyResult;
import nz.s;
import qz.o;

/* compiled from: kSourceFile */
/* loaded from: classes14.dex */
public class Watermelon {
public static boolean sLibLoadFail = true;
public static volatile Watermelon singleton;

public native Object jniCommand(int i6, Object obj, Object obj2, Object obj3);

public native byte[] ssec(byte[] bArr, byte[] bArr2, Object obj);

public Watermelon() {
loadSoLib();
}

public static Watermelon getInstance() {
Object apply = PatchProxy.apply(null, null, Watermelon.class, "1");
if (apply != PatchProxyResult.class) {
return (Watermelon) apply;
}
if (singleton == null) {
synchronized (Watermelon.class) {
if (singleton == null) {
singleton = new Watermelon();
}
}
}
return singleton;
}

public final void loadSoLib() {
if (PatchProxy.applyVoid(null, this, Watermelon.class, "2")) {
return;
}
try {
String str = new String(Base64.decode("a3NzZQ==", 0));
Context n7 = s.e().n();
if (n7 != null) {
d.a(n7, str);
} else {
System.loadLibrary(str);
}
sLibLoadFail = false;
o.c("so loaded");
} catch (Throwable unused) {
o.c("so load failed");
sLibLoadFail = true;
}
}
}

它是一个实例方法,我们这里只关注它怎么被调用的以及返回值。jadx中查找该函数用例,发现嗲用代码如下

1
this.mInodes = (String) Watermelon.getInstance().jniCommand(1114128, null, null, null);

使用 Frida hook 其返回值,代码如下

1
2
3
4
5
6
7
function call(){
Java.perform(function() {
let Watermelon = Java.use("com.kuaishou.dfp.envdetect.jni.Watermelon").getInstance();
let ret = Watermelon["jniCommand"](1114128, null, null, null);
console.log("result:"+ret)
})
}

运行结果

1
result:687324639::1394160561|987324656::2294160561|587324657::2394160561|9873246::3294160561|987324656::2294160561

三、初始化

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
package com.kuaishou;

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.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class Watermelon extends AbstractJni {

private final AndroidEmulator emulator;
private final Memory memory;
private final VM vm;
private final DvmClass cWatermelon;

public Watermelon(){

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/kuaishou/快手_10.3.40.25268.apk"));
vm.setJni(this);
vm.setVerbose(true);

DalvikModule dm = vm.loadLibrary("ksse", true);
cWatermelon = vm.resolveClass("com.kuaishou.dfp.envdetect.jni.Watermelon");
dm.callJNI_OnLoad(emulator);

}

public String jniCommand(){
return cWatermelon.newObject(null).callJniMethodObject(emulator, "jniCommand(ILjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", 1114128, null, null, null)
.getValue()
.toString();
}

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

}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
[17:54:14 796]  INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1114) - stat64 pathname=/data/system, LR=RX@0x1201990d[libksse.so]0x1990d
[17:54:14 798] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1114) - stat64 pathname=/data/data/, LR=RX@0x1201990d[libksse.so]0x1990d
[17:54:14 798] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1114) - stat64 pathname=/data/data/com.android.shell, LR=RX@0x1201990d[libksse.so]0x1990d
[17:54:14 799] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1114) - stat64 pathname=/data/system/install_sessions, LR=RX@0x1201990d[libksse.so]0x1990d
[17:54:14 799] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1114) - stat64 pathname=/data/data/com.google.android.webview, LR=RX@0x1201990d[libksse.so]0x1990d
JNIEnv->FindClass(java/lang/String) was called from RX@0x12037b87[libksse.so]0x37b87
JNIEnv->GetMethodID(java/lang/String.<init>([BLjava/lang/String;)V) => 0x782c535e was called from RX@0x12037b9d[libksse.so]0x37b9d
JNIEnv->NewByteArray(19) was called from RX@0x12037bb3[libksse.so]0x37bb3
JNIEnv->SetByteArrayRegion([B@0x00000000000000000000000000000000000000, 0, 19, RW@0x12223040) was called from RX@0x12037bc7[libksse.so]0x37bc7
JNIEnv->NewStringUTF("utf-8") was called from RX@0x12037bd5[libksse.so]0x37bd5
JNIEnv->NewObjectV(class java/lang/String, <init>([B@0x6e6e6e7c6e6e6e7c6e6e6e7c6e6e6e7c6e6e6e, "utf-8") => "nnn|nnn|nnn|nnn|nnn") was called from RX@0x12018283[libksse.so]0x18283
jniCommand call result: nnn|nnn|nnn|nnn|nnn

居然跟Frida hook 的结果不一样!返回的结果,同样是通过竖杠分割的五部分,但每部分都是nnn。但是通过 INFO 信息可知,存在对五个文件夹的访问,具体库函数是stat64。它是用于获取文件属性的系统调用

1
int stat(const char *path, struct stat *buf);

stat 结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

这些文件属性可用作信息收集、设备标识等用途。

具体操作是补一个虚拟文件系统的根目录

1
2
3
4
5
6
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("tv.danmaku.bili")
.setRootDir(new File("unidbg-android/src/test/resources/kuaishou/rootfs"))
.build();

然后在rootfs文件夹下补需要访问的五个文件

运行结果如下

1
jniCommand call result: 0::0|0::0|0::0|0::0|0::0

结果中的数值改变了,说明我们补文件夹是有用的。对比一下数值

1
2
687324639::1394160561|987324656::2294160561|587324657::2394160561|9873246::3294160561|987324656::2294160561
0::0|0::0|0::0|0::0|0::0

样本访问了五个文件的属性,函数返回的结果也是五部分,而且格式类似,所以基本可以确定就是从每个文件获取了一个属性。以第一部分为例,将687324639::1394160561逆序反转,就是 1650614931936423786,一个纳秒级的时间戳。(这谁想得到的啊?!)

在 ADB 中查看这几个文件夹的属性,可以发现和每一个文件的 Access 属性对的上,即最后一次访问的时间。

虽然我们补齐了文件,但是unidbg在文件属性这方面的模拟处理上做的不够好。

对于普通文件,Unidbg 的文件属性编码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public int fstat(Emulator<?> emulator, StatStructure stat) {
int st_mode;
if (IO.STDOUT.equals(file.getName())) {
st_mode = IO.S_IFCHR | 0x777;
} else if(Files.isSymbolicLink(file.toPath())) {
st_mode = IO.S_IFLNK;
} else {
st_mode = IO.S_IFREG;
}
stat.st_dev = 1;
stat.st_mode = st_mode;
stat.st_uid = 0;
stat.st_gid = 0;
stat.st_size = file.length();
stat.st_blksize = emulator.getPageAlign();
stat.st_ino = 1;
stat.st_blocks = ((file.length() + emulator.getPageAlign() - 1) / emulator.getPageAlign());
stat.setLastModification(file.lastModified());
stat.pack();
return 0;
}

看起来还不错,对时间做了处理,但 inode、dev 这些没法看,比如 inode 竟然硬编码为 1。

对于文件夹或者说目录的处理,可以说几乎没处理,可见src/main/java/com/github/unidbg/linux/file/DirectoryFileIO.java

1
2
3
4
5
6
7
8
9
10
@Override
public int fstat(Emulator<?> emulator, StatStructure stat) {
stat.st_mode = IO.S_IFDIR;
stat.st_dev = 0;
stat.st_size = 0;
stat.st_blksize = 0;
stat.st_ino = 0;
stat.pack();
return 0;
}

除了文件类型设置为目录外,其余全部置空,这就是返回0::0|0::0|0::0|0::0|0::0的原因。

这里可以做验证,胡乱设置一下 Access 时间。

1
2
3
4
5
6
7
8
9
10
11
public int fstat(Emulator<?> emulator, StatStructure stat) {
stat.st_mode = IO.S_IFDIR;
stat.st_dev = 0;
stat.st_size = 0;
stat.st_blksize = 0;
stat.st_ino = 0;
// test
stat.setSt_atim(12345678, 9999);
stat.pack();
return 0;
}

重新运行

1
ret:999900876::54321|999900876::54321|999900876::54321|999900876::54321|999900876::54321

处理这样的访问文件属性问题,这里可以提供两个思路:

  1. 将对应文件从手机中拷贝到项目目录下,但文件在迁移的过程中,属性等信息都变化和失真了。
  2. 通过 adb shell 或 Hook 等方式从真机获取这些信息,然后做填充;或者不做填充,单纯置空。

四、参考

[补系统调用(四)](https://www.yuque.com/lilac-2hqvv/xdwlsg/ip3v0qlg5b8y620f?# 《补系统调用(四)》)


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