unidbg补获取系统属性
zsk Lv4

image

什么叫获取系统属性?

Build类

第一种
NDK中最常见的方式是通过JNI调用 , 通过JNI调用JAVA方法获取本机的属性和信息,是最常见的做法,除了Build类,常见的还有 System.getProperty和Systemproperties.get等API。
Unidbg补环境过程中,最好补而且不会遗漏的就是这一类,因为Unidbg会给出清楚的报错,你没法对它置之不理。

1
2
3
4
5
jclass androidBuildClass = env->FindClass("android/os/Build");
jfieldID SERIAL = env->GetStaticFieldID(androidBuildClass, "SERIAL",
"Ljava/lang/String;");
jstring serialNum = (jstring) env->GetStaticObjectField(androidBuildClass,
SERIAL);

system_property_get

第二种常见方式是通过system_property_get 函数获取系统属性也是常见做法

1
2
3
char *key = "ro.build.id";
char value[PROP_VALUE_MAX] = {0};
__system_property_get(key, value);

这类环境缺失容易被大家忽视,因为没有日志提示,即使src/test/resources/log4j.properties中日志全 开,也不会打印相关信息。

通过文件访问

第三个常见方式是通过文件访问,比如读取/proc/pid/maps,此种情况,Unidbg会提供日志输出,但 经常被大家忽视,事实上,不少朋友初学Unidbg时除了JAVA环境的报错,其他日志信息都不去管。

popen()

第四个常见方式是通过popen()管道从shell中获取系统属性,其效果可以理解成在NDK中使用adb shell,popen参数一就是shell命令,返回值是一个fd文件描述符,可以read其内容,其中内容就是adb shell执行该命令应该返回的内容。

1
2
3
4
5
char value[PROP_VALUE_MAX] = {0};
std::string cmd = "getprop ro.build.id";
FILE* file = popen(cmd.c_str(), "r");
fread(value, PROP_VALUE_MAX, 1, file);
pclose(file);

getenv函数

第五个常见方式是通过 getenv函数 获取进程环境变量,首先,Android系统层面存在一些默认的环境变 量,除此之外,样本可以设置自己进程内的环境变量。因此,样本可以在Native层获取系统环境变量或 者自身JAVA层设置的环境变量。
我们可以通过ADB 查看环境变量有哪些,也可以查看环境变量的值。

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
C:\Users\pr0214>adb shell
bullhead:/ $ export
ANDROID_ASSETS
ANDROID_BOOTLOGO
ANDROID_DATA
ANDROID_ROOT
ANDROID_SOCKET_adbd
ANDROID_STORAGE
ASEC_MOUNTPOINT
BOOTCLASSPATH
DOWNLOAD_CACHE
EXTERNAL_STORAGE
HOME
HOSTNAME
LOGNAME
PATH
SHELL
SYSTEMSERVERCLASSPATH
TERM
TMPDIR
USER
bullhead:/ $ echo $HOME
/
bullhead:/ $ echo $ANDROID_DATA
/data
bullhead:/ $ echo $SYSTEMSERVERCLASSPATH
/system/framework/services.jar:/system/framework/ethernetservice.jar:/system/framework/wifiservice.jar:/system/framework/com.android.location.provider.jar
bullhead:/ $ echo $PATH
/sbin:/system/sbin:/system/bin:/system/xbin:/vendor/bin:/vendor/xbin
bullhead:/ $

第六个常见方式是使用系统调用获取相关属性,不管是通过syscall函数还是内联汇编,都属此类。 常见的比如uname系统调用

uname - 获取当前内核的名称和信息
返回的信息是一个结构体

1
2
3
4
5
6
7
8
9
10
struct utsname {
char sysname[]; /* 操作系统名称 (例如 "Linux") */
char nodename[]; /* "一些实现了的网络”内的名称*/
char release[]; /* 操作系统版本 (例如 "2.6.28")*/
char version[]; /* 操作系统发布日期 */
char machine[]; /* 硬件标识符 */
#ifdef _GNU_SOURCE
char domainname[]; /* NIS或YP域名 */
#endif
};

日志全开的情况下,系统调用的相关调用会被全部打印, Unidbg的uname系统调用实现是个很好也很简单的检测点,十分规范的表明了自己是Unidbg。

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
protected int uname(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
Pointer buf = context.getPointerArg(0);
if (log.isDebugEnabled()) {
log.debug("uname buf=" + buf);
}

final int SYS_NMLN = 65;

Pointer sysName = buf.share(0);
sysName.setString(0, "Linux"); /* Operating system name (e.g., "Linux") */

Pointer nodeName = sysName.share(SYS_NMLN);
nodeName.setString(0, "localhost"); /* Name within "some implementation-defined network" */

Pointer release = nodeName.share(SYS_NMLN);
release.setString(0, "1.0.0-unidbg"); /* Operating system release (e.g., "2.6.28") */

Pointer version = release.share(SYS_NMLN);
version.setString(0, "#1 SMP PREEMPT Thu Apr 19 14:36:58 CST 2018"); /* Operating system version */

Pointer machine = version.share(SYS_NMLN);
machine.setString(0, "armv8l"); /* Hardware identifier */

Pointer domainName = machine.share(SYS_NMLN);
domainName.setString(0, "localdomain"); /* NIS or YP domain name */

return 0;
}

以上这些是较为常见的获取系统属性的方式,

如何解决?

__system_property_get的处理

一般运行报IO错误,继承IOResolver实现文件重定向接口,打上自己的日志

lilac path:/dev/properties
lilac path:/proc/stat

一般这前两个文件访问,不需要管,这是libc初始化的内部逻辑
文件访问处理好了,接下来用第二种的方式,是__system_property_get 这个函数的处理 ,此Unidbg 在src/main/java/com/github/unidbg/linux/android 目录下有相 关类对它进行了Hook和封装,我们可以直接拿来用

1
2
3
4
5
6
7
8
9
10
11
SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator);
systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {
@Override
public String getProperty(String key) {
System.out.println("lilac Systemkey:"+key);
switch (key){
}
return "";
};
});
memory.addHookListener(systemPropertyHook);

lilac Systemkey:ro.serialno
lilac Systemkey:ro.product.manufacturer
lilac Systemkey:ro.product.brand
lilac Systemkey:ro.product.model

通过adb shell 获取这些信息,一 一填入正确的值,建议使用Unidbg时,对应的测试机Android版本为 6.0,这样或许可以避免潜在的麻烦。

1
2
3
4
5
6
7
8
9
10
11
C:\Users\zsk>adb shell
angler:/ $ su
angler:/ # getprop ro.serialno
84B5T15A04002645
angler:/ # getprop ro.product.manufacturer
Huawei
angler:/ # getprop ro.product.brand
google
angler:/ # getprop ro.product.model
Nexus 6P
angler:/ #
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
// 注册绑定IO重定向
emulator.getSyscallHandler().addIOResolver(this);
SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator);
systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {
@Override
public String getProperty(String key) {
System.out.println("lilac Systemkey:"+key);
switch (key){
case "ro.serialno": {
return "84B5T15A04002645";
}
case "ro.product.manufacturer":
return "Huawei";
case "ro.product.brand": {
return "google";
}
case "ro.product.model": {
return "Nexus 6P";
}
}
return "";
};
});
memory.addHookListener(systemPropertyHook);

// 创建Android虚拟机
vm = emulator.createDalvikVM(new File("xxx.apk"));

popen的处理

接下来是第四种和第五种, 管popen和getenv,它俩都是libc里的函数,所以放一起说。我的想法是Hook这两个函数, 如果产生调用就打印日志 , 基于 Unidbg原生Hook封装的各种Hook。

  • HOOK时机要在什么时候?这个样本的popen调用发生在目标函数中,如果发生在init中呢?
  • 我们通过HOOK得到了其参数,那怎么给它返回正确的值呢?

避免so存在init_proc 函数,或者init_array非空,需要在Loadlibrary加载so文件前面开始Hook,为了实现这个目标,我们提前将libc加载进Unidbg内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DalvikModule dmLibc = vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib/libc.so"), true);
Module moduleLibc = dmLibc.getModule();

// HOOK popen
int popenAddress = (int) moduleLibc.findSymbolByName("popen").getAddress();
// 函数原型:FILE *popen(const char *command, const char *type);
emulator.attach().addBreakPoint(popenAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext registerContext = emulator.getContext();
String command = registerContext.getPointerArg(0).getString(0);
System.out.println("lilac popen command:"+command);
return true;
}
});

addBreakPoint 我们一般用于下断点,添加回调,在命中断点时打印输出popen的参数1(即传给shell的 命令),并设置返回值为true,即做完打印程序继续跑,不用真断下来。

第一个问题解决, 第二个问题,怎么给它合适的返回值呢?

image


其实下面奇怪的报错就是popen导致的,popen返回的是文件描述符 。
NR = 190,190是什么系统调用?Unidbg尚未实现

image

查一下表 https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm-32_bit_EABI

image

而Unidbg提供了一种在底层修复和实现popen函数的法子。
接着是 uname -a

1
2
angler:/ # uname -a
Linux localhost 3.10.73-g33ace82f84b #1 SMP PREEMPT Fri Oct 13 04:41:33 UTC 2017 aarch64

首先实现自己的ARM32SyscallHandler,完整代码如下,你可以把它当成固定讨论,它是针对 popen报错的官方解决方案。

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

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.context.EditableArm32RegisterContext;
import com.github.unidbg.linux.ARM32SyscallHandler;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.linux.file.DumpFileIO;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.sun.jna.Pointer;

import java.util.concurrent.ThreadLocalRandom;

public class qttSyscallHandler extends ARM32SyscallHandler {
public qttSyscallHandler(SvcMemory svcMemory) {
super(svcMemory);
}

@Override
protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {
switch (NR){
case 190:
vfork(emulator);
return true;
case 114:
wait4(emulator);
return true;
}
return super.handleUnknownSyscall(emulator, NR);
}

private void wait4(Emulator<?> emulator) {
EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext();
int pid = context.getR0Int();
UnidbgPointer wstatus = context.getR1Pointer();
int options = context.getR2Int();
Pointer rusage = context.getR3Pointer();
System.out.println("wait4 pid=" + pid + ", wstatus=" + wstatus + ", options=0x" + Integer.toHexString(options) + ", rusage=" + rusage);
}

private void vfork(Emulator<?> emulator) {
EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext();
int childPid = emulator.getPid() + ThreadLocalRandom.current().nextInt(256);
System.out.println("vfork pid=" + childPid);
context.setR0(childPid);
}

protected int pipe2(Emulator<?> emulator) {
EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext();
Pointer pipefd = context.getPointerArg(0);
int flags = context.getIntArg(1);
int write = getMinFd();
this.fdMap.put(write, new DumpFileIO(write));
int read = getMinFd();
// stdout中写入popen command 应该返回的结果
String stdout = "Linux localhost 3.10.73-g33ace82f84b #1 SMP PREEMPT Fri Oct 13 04:41:33 UTC 2017 aarch64
";
this.fdMap.put(read, new ByteArrayFileIO(0, "pipe2_read_side", stdout.getBytes()));
pipefd.setInt(0, read);
pipefd.setInt(4, write);
System.out.println("pipe2 pipefd=" + pipefd + ", flags=0x" + flags + ", read=" + read + ", write=" + write + ", stdout=" + stdout);
context.setR0(0);
return 0;
}
}

解释一下为什么不直接补在ARM32SyscallHandler中?因为Unidbg并没有真正实现wait4和fork这两个 系统调用,只不过对于popen而言,用上述方式可以“凑合用”,既然不是完美的实现,自然不能放到 ARM32SyscallHandler中去,免得出大问题。

在pipe2中注释下的stdout中传入正确返回值即可,比如uname -a就是,需要注意,结果都i要加换行 符,这是shell结果的返回习惯。

接下来让我们的emulator使用我们自己的syscallHandler,emulator = new AndroidARMEmulator(new File(“target/rootfs”)); 由如下洋洋洒洒十来行取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建模拟器实例,要模拟32位或者64位,在这里区分
// emulator = AndroidEmulatorBuilder.for32Bit().build();
AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(false) {
public AndroidEmulator build() {
return new AndroidARMEmulator(processName, rootDir,backendFactories) {
@Override
protected UnixSyscallHandler<AndroidFileIO>
createSyscallHandler(SvcMemory svcMemory) {
return new qttSyscallHandler(svcMemory);
}
};
}
};
emulator = builder.setRootDir(new File("target/rootfs")).build();

image
直接跑出了结果,但我们的任务其实还没有完成= =,tag中搜索lilac popen,发现一共调用了三次

lilac popen command:uname -a
lilac popen command:cd /system/bin && ls -l
lilac popen command:stat /root

我们上面的代码,似乎只处理了uname -a应该返回的值,后面两次呢?怎么在pipe2中根据 popen输入的command返回合适的输出呢?
我们可以使用emulator的全局变量来完成这一点

image

对应的qttSyscallHandler代码,其中 cd /system/bin && ls -l 和 stat /root 的结果来自adb shell,大家 根据自己的测试机情况填入合适的结果。

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
protected int pipe2(Emulator<?> emulator) {
EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext();
Pointer pipefd = context.getPointerArg(0);
int flags = context.getIntArg(1);
int write = getMinFd();
this.fdMap.put(write, new DumpFileIO(write));
int read = getMinFd();
String stdout = "
";
// stdout中写入popen command 应该返回的结果
String command = emulator.get("command");
switch (command) {
case "uname -a": {
stdout = "Linux localhost 3.10.73-g33ace82f84b #1 SMP PREEMPT Fri Oct 13 04:41:33 UTC 2017 aarch64
";
}
break;
case "cd /system/bin && ls -l": {
stdout = "total 25152
" +
"-rwxr-xr-x 1 root shell 128688 2009-01-01 08:00 abb
" +
"lrwxr-xr-x 1 root shell 6 2009-01-01 08:00 acpi -> toybox
" +
"-rwxr-xr-x 1 root shell 30240 2009-01-01 08:00 adbd
" +
"-rwxr-xr-x 1 root shell 207 2009-01-01 08:00 am
" +
"-rwxr-xr-x 1 root shell 456104 2009-01-01 08:00 apexd
" +
"lrwxr-xr-x 1 root shell 13 2009-01-01 08:00 app_process -> app_process64
" +
"-rwxr-xr-x 1 root shell 25212 2009-01-01 08:00 app_process32
";
}
break;
case "stat /root": {
stdout = "stat: '/root': No such file or directory
";
}
break;
default:
System.out.println("command do not match!");
}
this.fdMap.put(read, new ByteArrayFileIO(0, "pipe2_read_side", stdout.getBytes()));
pipefd.setInt(0, read);
pipefd.setInt(4, write);
System.out.println("pipe2 pipefd=" + pipefd + ", flags=0x" + flags + ", read=" + read + ", write=" + write + ", stdout=" + stdout);
context.setR0(0);
return 0;
}

getenv的处理

getenv的出现频率也挺高, 首先我们看一下当前测试机有哪些环境变量

angler:/system/bin $ export
ANDROID_ASSETS
ANDROID_BOOTLOGO
ANDROID_DATA
ANDROID_ROOT
ANDROID_SOCKET_adbd
ANDROID_STORAGE
ASEC_MOUNTPOINT
BOOTCLASSPATH
DOWNLOAD_CACHE
EXTERNAL_STORAGE
HOME
HOSTNAME
LOGNAME
PATH
SHELL
SYSTEMSERVERCLASSPATH
TERM
TMPDIR
USER

看一下PATH的内容

angler:/system/bin $ echo $PATH
/sbin:/system/sbin:/system/bin:/system/xbin:/vendor/bin:/vendor/xbin

image
getValue取不到结果,原因就是getenv没有返回值,那么该怎么办呢? 这里给env返回正确的值有几种办法呢?

方法一

Unidbg提供了对环境变量的初始化,它在 src/main/java/com/github/unidbg/linux/AndroidElfLoader.java中。
image
我们填上这一个就行,为了辨别不同方法是否生效,我们这里返回1

1
2
3
4
5
6
7
this.environ = initializeTLS(new String[] {
"ANDROID_DATA=/data",
"ANDROID_ROOT=/system",
//"PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin",
"NO_ADDR_COMPAT_LAYOUT_FIXUP=1",
"PATH=1",
});

方法二

libc 提供了setenv方法,可以设置环境变量。
在调用函数前先调用该方法

1
2
3
4
5
// setenv设置环境变量
public void setEnv(){
Symbol setenv = module.findSymbolByName("setenv", true);
setenv.call(emulator, "PATH", "2", 0);
};

方法三

通过HookZz hook函数,替换结果

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
public void hookgetEnvByHookZz(){
HookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("getenv"), new WrapCallback<EditableArm32RegisterContext>() {
String name;

@Override
public void preCall(Emulator<?> emulator, EditableArm32RegisterContext ctx, HookEntryInfo info) {
name = ctx.getPointerArg(0).getString(0);
}

@Override
public void postCall(Emulator<?> emulator, EditableArm32RegisterContext ctx, HookEntryInfo info) {
switch (name){
case "PATH": {
MemoryBlock replaceBlock = emulator.getMemory().malloc(0x100, true);
UnidbgPointer replacePtr = replaceBlock.getPointer();
String pathValue = "3";
replacePtr.write(0, pathValue.getBytes(StandardCharsets.UTF_8), 0, pathValue.length());
ctx.setR0(replacePtr.toIntPeer());
}
}
super.postCall(emulator, ctx, info);
}
});
}

方法四

也可以通过断点的方式hook

1
2
3
4
5
6
7
8
9
10
11
12
13
public void hookgetEnvByBreakPointer() {
emulator.attach().addBreakPoint(module.base + 0x7FE, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
EditableArm32RegisterContext registerContext =
emulator.getContext();
registerContext.getPointerArg(0).setString(0, "4");
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,
(address) + 5);
return true;
}
});
}

直接让R0指针指向正确的值,并操纵PC寄存器跳过这条指令
image

方法五

仿照SystemPropertyHook写一下,代码如下
在 vm.loadLibrary 加载so文件之前

1
2
EnvHook envHook = new EnvHook(emulator);
memory.addHookListener(envHook);

EnvHook.java

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
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.ArmHook;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.HookListener;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
public class EnvHook implements HookListener {
private final Emulator<?> emulator;
public EnvHook(Emulator<?> emulator) {
this.emulator = emulator;
}
@Override
public long hook(SvcMemory svcMemory, String libraryName, String symbolName,
final long old) {
if ("libc.so".equals(libraryName) && "getenv".equals(symbolName)) {
if (emulator.is32Bit()) {
return svcMemory.registerSvc(new ArmHook() {
@Override
protected HookStatus hook(Emulator<?> emulator) {
return getenv(old);
}
}).peer;
}
}
return 0;
}
private HookStatus getenv(long old) {
RegisterContext context = emulator.getContext();
UnidbgPointer pointer = context.getPointerArg(0);
String key = pointer.getString(0);
switch (key){
case "PATH":{
pointer.setString(0, "5");
return HookStatus.LR(emulator, pointer.peer);
}
}
return HookStatus.RET(emulator, old);
}

}
 评论