什么叫获取系统属性?
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[]; char nodename[]; char release[]; char version[]; char machine[]; #ifdef _GNU_SOURCE char domainname[]; #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");
Pointer nodeName = sysName.share(SYS_NMLN); nodeName.setString(0, "localhost");
Pointer release = nodeName.share(SYS_NMLN); release.setString(0, "1.0.0-unidbg");
Pointer version = release.share(SYS_NMLN); version.setString(0, "#1 SMP PREEMPT Thu Apr 19 14:36:58 CST 2018");
Pointer machine = version.share(SYS_NMLN); machine.setString(0, "armv8l");
Pointer domainName = machine.share(SYS_NMLN); domainName.setString(0, "localdomain");
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
| 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);
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();
int popenAddress = (int) moduleLibc.findSymbolByName("popen").getAddress();
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,即做完打印程序继续跑,不用真断下来。
第一个问题解决, 第二个问题,怎么给它合适的返回值呢?
其实下面奇怪的报错就是popen导致的,popen返回的是文件描述符 。
NR = 190,190是什么系统调用?Unidbg尚未实现
查一下表 https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm-32_bit_EABI
而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(); 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
|
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();
|
直接跑出了结果,但我们的任务其实还没有完成= =,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的全局变量来完成这一点
对应的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 = " "; 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
getValue取不到结果,原因就是getenv没有返回值,那么该怎么办呢? 这里给env返回正确的值有几种办法呢?
方法一
Unidbg提供了对环境变量的初始化,它在 src/main/java/com/github/unidbg/linux/AndroidElfLoader.java中。
我们填上这一个就行,为了辨别不同方法是否生效,我们这里返回1
1 2 3 4 5 6 7
| this.environ = initializeTLS(new String[] { "ANDROID_DATA=/data", "ANDROID_ROOT=/system", "NO_ADDR_COMPAT_LAYOUT_FIXUP=1", "PATH=1", });
|
方法二
libc 提供了setenv方法,可以设置环境变量。
在调用函数前先调用该方法
1 2 3 4 5
| 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寄存器跳过这条指令
方法五
仿照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); }
}
|