前言好像还没有人发过编译安卓版ecapture的教程，这里记录下编译要点步骤编译必须是在arm64的系统下进行官方指南建议使用ubuntu aarch64编译，不过编译老是买服务器也耗不起所以我这搞个手机编译，首先参考下面的文章在手机上准备好rootfseBPF on Android之bcc环境准备——eadb简版在rootfs系统安装上下面的库/软件，如果遇到网络问题自行挂代理安装编译工具apt install cmake apt install llvm-12 apt install clang-12安装go，通过下面的方式安装（记得proxychains挂代理）wget "https://golang.org/dl/go1.17.12.linux-arm64.tar.gz" tar -C /usr/local -xvf "go1.17.12.linux-arm64.tar.gz"rootfs好像没有xz命令，安装上apt install xz-utils拉取ecapturegit clone https://github.com/ehids/ecapture.git cd ecapture先向~/.bashrc添加下面的配置，然后source ~/.bashrc注意，如果rootfs退出重新进，这一步需要再次手动执行export GOPATH=$HOME/go export PATH=/usr/local/go/bin:$PATH:$GOPATH/bin然后设置环境变量export CMD_CLANG=clang-12 export CMD_STRIP=llvm-strip-12 export CMD_LLC=llc-12 export GOPROXY=https://proxy.golang.com.cn,direct开始编译ANDROID=1 make nocore然后会提示找不到linux/kconfig.h，详细错误如下clang-12 \ -I ./kern -I ./kern/bpf/arm64 \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/arch/arm64/include \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/arch/arm64/include/uapi \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/arch/arm64/include/generated \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/arch/arm64/include/generated/uapi \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/include \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/include \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/include/uapi \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/include/generated \ -I /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build/include/generated/uapi \ -emit-llvm -O2 -S -xc -g -D__BPF_TRACING__ -D__KERNEL__ -DNOCORE -Wall -Wno-unused-variable -Wno-frame-address -Wno-unused-value -Wno-unknown-warning-option -Wno-pragma-once-outside-header -Wno-pointer-sign -Wno-gnu-variable-sized-type-not-at-end -Wno-deprecated-declarations -Wno-compare-distinct-pointer-types -Wno-address-of-packed-member -fno-stack-protector -fno-jump-tables -fno-unwind-tables -fno-asynchronous-unwind-tables \ \ -c kern/openssl_kern.c \ -o - |llc-12 \ -march=bpf \ -filetype=obj \ -o user/bytecode/openssl_kern.o In file included from kern/openssl_kern.c:1: ./kern/ecapture.h:14:10: fatal error: 'linux/kconfig.h' file not found #include <linux/kconfig.h> ^~~~~~~~~~~~~~~~~ 1 error generated.这是因为找不到内核头文件，这里查找的路径是/lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build然后看看手机上有没有/sys/kernel/kheaders.tar.xz，这个就是内核头文件的压缩包如果没有则需要在编译内核的时候设置CONFIG_IKHEADERS选项还有个方案是参考自己手机内核源码中的kernel_headers.py脚本，自行梳理头文件关系先把/sys/kernel/kheaders.tar.xz复制到rootfs中cp /sys/kernel/kheaders.tar.xz /data/eadb/debian/kheaders.tar.xz在rootfs的终端执行解压命令（不同手机不同内核版本，这个目录可能不同，请根据报错显示的来）mkdir -p /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build tar -xvf kheaders.tar.xz -C /lib/modules/5.10.43-android12-9-00007-g9771767708df-ab8009062/build现在再次执行ANDROID=1 make nocore编译然后就可以使用下面这样的命令抓包了，注意要指定网卡，另外指定uid或者pid./ecapture tls -i wlan0 -u 10224 --libssl /apex/com.android.conscrypt/lib64/libssl.so --pthread /apex/com.android.runtime/lib64/bionic/libc.so编译要点：arm64系统指定CMD_CLANG CMD_STRIP CMD_LLC环境变量设置加速Go下载的环境变量GOPROXY提取内核头文件到特定目录
前言在上一篇系列文章中，以为绿洲为样本演示了dump上下文后模拟执行的整个过程绿洲com.weibo.xvideo.NativeApi的s算法相对简单，涉及的JNI调用较少，整体模拟执行过程还是比较顺利但这似乎让人对dump上下文后模拟执行的优势感知不强，本文将以更加复杂的目标算法为例以提升用户感知~本文的样本是快手 10.6.50.26734 32位目标是模拟执行com.kuaishou.android.security.internal.dispatch.JNICLibrary的doCommandNative方法记录frida hook获取入参和结果首先是通过frida获取传入参数，不熟悉frida的同学可以看看怎么打印[Ljava.lang.Object;包裹的数组和Ljava/lang/Object;包裹的[Bfunction obj_byte_arr_to_hexstr(byte_arr){ let HexDump = Java.use("com.android.internal.util.HexDump"); // HexDump.hexStringToByteArray() // HexDump.toHexString() let buffer = Java.cast(byte_arr, Java.use("[B")); return HexDump.toHexString(Java.array('byte', buffer)); } function hook_ks_doCommandNative(){ Java.perform(function(){ let Arrays = Java.use("java.util.Arrays"); let ArrayClz = Java.use("java.lang.reflect.Array"); let JNICLibrary = Java.use("com.kuaishou.android.security.internal.dispatch.JNICLibrary"); JNICLibrary.doCommandNative.overload('int', '[Ljava.lang.Object;').implementation = function(num, objs){ console.log(`[enter] => ${num} ${objs}`); let bflag = false; if (num == 10400){ bflag = true; } let arr = Arrays.asList(objs); for (let index = 0; index < arr.size(); index++) { let element = arr.get(index); console.log(`[arrarr] ${index} ===>${element}<===`); if (index == 0){ if (bflag){ console.log(`[first arr] ===>${obj_byte_arr_to_hexstr(element)}<===`); } else{ for(let index = 0; index < ArrayClz.getLength(element); index++){ let item = ArrayClz.get(element, index).toString(); console.log(`[first arr] ${index} ===>${item}<===`); } } } } let result = this.doCommandNative(num, objs); if (bflag){ console.log(`[leave] => ${obj_byte_arr_to_hexstr(result)}`); }else{ console.log(`[leave] => ${result}`); } return result; } }) }可以得到这些输出，现在入参和结果都有了[enter] => 10418 [Ljava.lang.String;@a2ac698,d7b7d042-d4f2-4012-be60-d97ff2429c17,-1,false,com.yxcorp.gifshow.App@9ac1cd7,,false, [arrarr] 0 ===>[Ljava.lang.String;@a2ac698<=== [first arr] 0 ===>/rest/n/magicFace/ycnn/models88a33d1370a36dd86aa8af7781ed3bb3<=== [arrarr] 1 ===>d7b7d042-d4f2-4012-be60-d97ff2429c17<=== [arrarr] 2 ===>-1<=== [arrarr] 3 ===>false<=== [arrarr] 4 ===>com.yxcorp.gifshow.App@9ac1cd7<=== [arrarr] 5 ===>null<=== [arrarr] 6 ===>false<=== [arrarr] 7 ===><=== [leave] => e8f989aaaff3b2dc81a0a3a2e2c167d8c96354d8bdb1bfa9 [enter] => 10418 [Ljava.lang.String;@9a6964b,d7b7d042-d4f2-4012-be60-d97ff2429c17,-1,false,com.yxcorp.gifshow.App@9ac1cd7,,false, [arrarr] 0 ===>[Ljava.lang.String;@9a6964b<=== [first arr] 0 ===>/rest/e/load/styleTemplate52729eed3a702a534fe721efea0e9cb0<=== [arrarr] 1 ===>d7b7d042-d4f2-4012-be60-d97ff2429c17<=== [arrarr] 2 ===>-1<=== [arrarr] 3 ===>false<=== [arrarr] 4 ===>com.yxcorp.gifshow.App@9ac1cd7<=== [arrarr] 5 ===>null<=== [arrarr] 6 ===>false<=== [arrarr] 7 ===><=== [leave] => 7b6a1a393c60214f13333031bc11a5225af0c74b2e222c3a [enter] => 10418 [Ljava.lang.String;@7a575ab,d7b7d042-d4f2-4012-be60-d97ff2429c17,-1,false,com.yxcorp.gifshow.App@9ac1cd7,,false, [arrarr] 0 ===>[Ljava.lang.String;@7a575ab<=== [first arr] 0 ===>/rest/n/encode/androidffdb4a81067a6f7e20afbc3ec76652b3<=== [arrarr] 1 ===>d7b7d042-d4f2-4012-be60-d97ff2429c17<=== [arrarr] 2 ===>-1<=== [arrarr] 3 ===>false<=== [arrarr] 4 ===>com.yxcorp.gifshow.App@9ac1cd7<=== [arrarr] 5 ===>null<=== [arrarr] 6 ===>false<=== [arrarr] 7 ===><=== [leave] => 8e9fefccc995d4bae5c6c5c4d00b944fad0532bedbd7d9cf获取doCommandNative在native层的地址直接上hook_RegisterNatives.js即可https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_RegisterNatives.js[RegisterNatives] java_class: com.kuaishou.android.security.internal.dispatch.JNICLibrary name: doCommandNative sig: (I[Ljava/lang/Object;)Ljava/lang/Object; fnPtr: 0xac38d919 fnOffset: 0x4b919 callee: 0xac395547 libkwsgmain.so!0x53547即偏移是0x4b919，用IDA打开看下，直接就是红色...不过暂时可以不关心怎么修复，第一条指令是push那就基本能确定是这了dump上下文ttyd启动ttyd -t fontSize=22 -t 'theme={"background": "#282A36", "foreground": "#F8F8F2", "cursor": "#F8F8F2", "selection": "#44475A"}' bashPC远程访问手机，进入shell，切换到su设置环境变量export SHELL=/data/data/com.termux/files/usr/bin/bash export COLORTERM=truecolor export HISTCONTROL=ignoreboth export PREFIX=/data/data/com.termux/files/usr export TERMUX_IS_DEBUGGABLE_BUILD=1 export TERMUX_MAIN_PACKAGE_FORMAT=debian export PWD=/data/data/com.termux/files/home export TERMUX_VERSION=0.118.0 export EXTERNAL_STORAGE=/sdcard export LD_PRELOAD=/data/data/com.termux/files/usr/lib/libtermux-exec.so export HOME=/data/data/com.termux/files/home export LANG=en_US.UTF-8 export TERMUX_APK_RELEASE=GITHUB export TMPDIR=/data/data/com.termux/files/usr/tmp export TERM=xterm-256color export SHLVL=2 export PATH=/data/data/com.termux/files/usr/bin:$PATH执行lldb命令，附加到目标进程lldb attach 16714处理SIGSEGV信号process handle SIGSEGV -p true -s false下断点breakpoint set -s libkwsgmain.so -a 0x4b919按c继续执行，操作APP等待断点断下来的时候，通过register read r2看看传入参数，这里对应的也就是java层的第一个参数前面frida hook已经知道大部分情况下参数一是10418，也就是图中的0x28b2另外注意如果你有多个断点，也就是图中圈起来的Thread 250，你可以通过thread select xxx来切换线程因为有时候当前断下的可能不是想要的线程，千万不要搞错了加载dump上下文脚本，执行dumpctx开始dump上下文command script import lldb_dumper.py等待dump完成之前提到还需要获取Canary指针地址，但是经过分析32位so的目标函数附近似乎暂时没用到，所以跳过删除断点，结束附加下载dump好的上下文压缩包(这时间怎么不对...看了下原来手机时间确实是错的...)lsz DumpContext_20220808_000250.zip模拟执行先搭一个框架如下，本案例是32位的，所以写寄存器部分略有不同，仅供参考，其他部分和绿洲案例一致默认先只载入了libkwsgmain.so和libc.so有关的内存另外加了emulator.traceCode();看看能模拟执行到什么地方另外这里要把pc + 1作为起始地址，而不是直接传pc，否则unicorn会不认识指令。。。public class JNICLibrary extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; JNICLibrary() { emulator = AndroidEmulatorBuilder.for32Bit() .setProcessName("com.smile.gifmaker") .addBackendFactory(new Unicorn2Factory(true)) .build(); // 创建模拟器实例，要模拟32位或者64位，在这里区分 emulator.getSyscallHandler().setEnableThreadDispatcher(false); final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 vm = emulator.createDalvikVM(); // 创建Android虚拟机 vm.setJni(this); vm.setVerbose(true); // 设置是否打印Jni调用细节 } public static void main(String[] args) throws Exception { JNICLibrary mJNICLibrary = new JNICLibrary(); mJNICLibrary.load_context("unidbg-android/src/test/resources/DumpContext_20220808_000250"); mJNICLibrary.doCommandNative(); } int UNICORN_PAGE_SIZE = 0x1000; private long align_page_down(long x){ return x & ~(UNICORN_PAGE_SIZE - 1); } private long align_page_up(long x){ return (x + UNICORN_PAGE_SIZE - 1) & ~(UNICORN_PAGE_SIZE - 1); } private void map_segment(long address, long size, int perms){ long mem_start = address; long mem_end = address + size; long mem_start_aligned = align_page_down(mem_start); long mem_end_aligned = align_page_up(mem_end); if (mem_start_aligned < mem_end_aligned){ emulator.getBackend().mem_map(mem_start_aligned, mem_end_aligned - mem_start_aligned, perms); } } private void load_context(String dump_dir) throws IOException, DataFormatException, IOException { Backend backend = emulator.getBackend(); Memory memory = emulator.getMemory(); String context_file = dump_dir + "\\" + "_index.json"; InputStream is = new FileInputStream(context_file); String jsonTxt = IOUtils.toString(is, "UTF-8"); JSONObject context = JSONObject.parseObject(jsonTxt); JSONObject regs = context.getJSONObject("regs"); backend.reg_write(ArmConst.UC_ARM_REG_R0, Long.parseUnsignedLong(regs.getString("r0").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R1, Long.parseUnsignedLong(regs.getString("r1").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R2, Long.parseUnsignedLong(regs.getString("r2").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R3, Long.parseUnsignedLong(regs.getString("r3").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R4, Long.parseUnsignedLong(regs.getString("r4").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R5, Long.parseUnsignedLong(regs.getString("r5").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R6, Long.parseUnsignedLong(regs.getString("r6").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R7, Long.parseUnsignedLong(regs.getString("r7").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R8, Long.parseUnsignedLong(regs.getString("r8").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R9, Long.parseUnsignedLong(regs.getString("r9").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R10, Long.parseUnsignedLong(regs.getString("r10").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R11, Long.parseUnsignedLong(regs.getString("r11").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_R12, Long.parseUnsignedLong(regs.getString("r12").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_SP, Long.parseUnsignedLong(regs.getString("sp").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_LR, Long.parseUnsignedLong(regs.getString("lr").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_PC, Long.parseUnsignedLong(regs.getString("pc").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_CPSR, Long.parseUnsignedLong(regs.getString("cpsr").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_FPSCR, Long.parseUnsignedLong(regs.getString("fpscr").substring(2), 16)); memory.setStackPoint(Long.parseUnsignedLong(regs.getString("sp").substring(2), 16)); backend.enableVFP(); JSONArray segments = context.getJSONArray("segments"); for (int i = 0; i < segments.size(); i++) { JSONObject segment = segments.getJSONObject(i); String path = segment.getString("name"); long start = segment.getLong("start"); long end = segment.getLong("end"); String content_file = segment.getString("content_file"); JSONObject permissions = segment.getJSONObject("permissions"); int perms = 0; if (permissions.getBoolean("r")){ perms |= UnicornConst.UC_PROT_READ; } if (permissions.getBoolean("w")){ perms |= UnicornConst.UC_PROT_WRITE; } if (permissions.getBoolean("x")){ perms |= UnicornConst.UC_PROT_EXEC; } String[] paths = path.split("/"); String module_name = paths[paths.length - 1]; List<String> white_list = Arrays.asList(new String[]{"libkwsgmain.so", "libc.so"}); if (white_list.contains(module_name)){ int size = (int)(end - start); map_segment(start, size, perms); String content_file_path = dump_dir + "\\" + content_file; File content_file_f = new File(content_file_path); if (content_file_f.exists()){ InputStream content_file_is = new FileInputStream(content_file_path); byte[] content_file_buf = IOUtils.toByteArray(content_file_is); // 解压 Inflater decompresser = new Inflater(); decompresser.setInput(content_file_buf, 0, content_file_buf.length); byte[] result = new byte[size]; int resultLength = decompresser.inflate(result); decompresser.end(); backend.mem_write(start, result); } else { System.out.println("not exists path=" + path); byte[] fill_mem = new byte[size]; Arrays.fill( fill_mem, (byte) 0 ); backend.mem_write(start, fill_mem); } } } } private void doCommandNative() { emulator.traceCode(); List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); DvmObject<?> thiz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary").newObject(null); list.add(vm.addLocalObject(thiz)); DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App", vm.resolveClass("android/app/Application")).newObject(null); // context vm.addLocalObject(context); list.add(10418); StringObject urlObj = new StringObject(vm, "/rest/n/encode/androidffdb4a81067a6f7e20afbc3ec76652b3"); vm.addLocalObject(urlObj); ArrayObject arrayObject = new ArrayObject(urlObj); vm.addLocalObject(arrayObject); StringObject appkey = new StringObject(vm,"d7b7d042-d4f2-4012-be60-d97ff2429c17"); vm.addLocalObject(appkey); DvmInteger intergetobj = DvmInteger.valueOf(vm, -1); vm.addLocalObject(intergetobj); DvmBoolean boolobj = DvmBoolean.valueOf(vm, false); vm.addLocalObject(boolobj); StringObject whitestr = new StringObject(vm,""); vm.addLocalObject(whitestr); ArrayObject objlist = new ArrayObject(arrayObject, appkey, intergetobj, boolobj, context, null, boolobj, whitestr); list.add(vm.addLocalObject(objlist)); // 这里获取 dump 时的 pc 地址作为模拟执行起始地址 long ctx_addr = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_PC).longValue(); // 开始模拟执行 Number result = Module.emulateFunction(emulator, ctx_addr + 1, list.toArray()); } }发现执行了一条指令就出现了异常，看下错误的地址，转换到10进制，打开DumpContext文件夹中的_index.json，看看是缺少什么内存对比发现是缺少[anon:stack_and_tls:19161]（图里指错了），将这个添加到分配内存的白名单中，再次模拟执行执行了一点指令，但是出现了新的内存未分配错误，同样还是计算下查找是缺少哪块内存，补上再次模拟执行接下来依次检查，发现需要添加以下内存分段[anon:.bss][vdso][vvar]然后出现了Invalid instruction (UC_ERR_INSN_INVALID)异常，说是指令错误mrrc p15, #1, r6, r7, c14这个指令我也不太明白不过可以看看这个指令是哪块内存的，可以看到就是前面载入的[vdso]查看前面的指令，可以找到明显不一样范围的地址，对比可以知道是libkwsgmain.so根据内存信息，可以计算出偏移是hex(0xd790a240 - 3616550912) = 0x7240原来是调用gettimeofday，那我们去hook住libc的gettimeofday函数就行了添加一个hook_libc函数，根据配置文件找到libc的基址，把gettimeofday函数hook上hook之后相当于操作就直接做完了，所以要把LR写入PC在，这样就会直接返回之前的地址继续执行private void hook_libc() { long libc_gettimeofday_addr = 3964436480L + 0x33534; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { System.out.println("gettimeofday"); UnidbgPointer tv_ptr = emulator.getContext().getPointerArg(0); ByteBuffer tv = ByteBuffer.allocate(8); tv.order(ByteOrder.LITTLE_ENDIAN); tv.putInt(0,1659890020); tv.putInt(4, 2891054); byte[] data = tv.array(); tv_ptr.write(0,data,0,8); backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_gettimeofday_addr, libc_gettimeofday_addr, null); }继续模拟执行，出现了新的内存异常，发现是需要加载[anon:scudo:primary]然后是缺少libc++_shared.so，添加后再次模拟执行，再次出现Invalid instruction (UC_ERR_INSN_INVALID)经过分析发现是调用libc.so的malloc出现了问题，我们改成自己实现long libc_malloc_addr = 3964436480L + 0x294F8; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { int msize = backend.reg_read(ArmConst.UC_ARM_REG_R0).intValue(); MemoryBlock block = emulator.getMemory().malloc(msize, true); backend.reg_write(ArmConst.UC_ARM_REG_R0, block.getPointer().toUIntPeer()); backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_malloc_addr, libc_malloc_addr, null);free emm就啥也不处理，直接返回long libc_free_addr = 3964436480L + 0x29490; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_free_addr, libc_free_addr, null);还有pthread_mutex_lock和pthread_mutex_unlocklong libc_pthread_mutex_lock_addr = 3964436480L + 0x812EC; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { backend.reg_write(ArmConst.UC_ARM_REG_R0, 0L); backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_pthread_mutex_lock_addr, libc_pthread_mutex_lock_addr, null); long libc_pthread_mutex_unlock_addr = 3964436480L + 0x8166C; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { backend.reg_write(ArmConst.UC_ARM_REG_R0, 0L); backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_pthread_mutex_unlock_addr, libc_pthread_mutex_unlock_addr, null);接着是calloclong libc_caloc_addr = 3964436480L + 0x2943C; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { int num = backend.reg_read(ArmConst.UC_ARM_REG_R0).intValue(); int msize = backend.reg_read(ArmConst.UC_ARM_REG_R1).intValue(); MemoryBlock block = emulator.getMemory().malloc(num * msize, true); backend.reg_write(ArmConst.UC_ARM_REG_R0, block.getPointer().toUIntPeer()); backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libc_caloc_addr, libc_caloc_addr, null);这次运行了很久，然后出现一个熟悉的需要补JNI环境的错误提示@Override public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "java/lang/Boolean->booleanValue()Z":{ return (boolean) dvmObject.getValue(); // return false; } } return super.callBooleanMethodV(vm, dvmObject, signature, vaList); }然后运行得更久了，最后出现一个内存读取错误，但是很奇怪，地址是0x78通过对比计算地址偏移，发现是libc++_shared.so调用libc.so的pthread_getspecific出现了问题往前回溯，可以发现是std::__libcpp_snprintf_l调用sub_87BB0出现的异常粗略分析，可以知道这里就是在做一个字符串格式化，至于前后的线程有关操作，都用unicorn跑了那还管什么，直接跳过两个函数的执行long libcpp_patch_addr = 3620732928L + 0x87BB0; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { backend.reg_write(ArmConst.UC_ARM_REG_PC, backend.reg_read(ArmConst.UC_ARM_REG_LR).longValue()); } @Override public void onAttach(UnHook unHook) { } @Override public void detach() { } }, libcpp_patch_addr, libcpp_patch_addr, null);出现了新的内存错误，对比发现需要载入[anon:scudo:secondary]再次模拟执行，这次执行了很久(traceCode太耗时了)，然后完整完成了执行去掉traceCode，只保留JNI相关的日志，可以看到最终拿到了结果这个结果和前面的hook结果对不上，经过测试发现和gettimeofday有关，所以这是正常的完整代码参见：https://gist.github.com/SeeFlowerX/94b7c52e8bc3d6ddb9108ac5003c6570总结模拟执行补环境做了以下几件事：遇到内存异常提示，根据异常地址定位需要加载的内存块遇到指令异常提示，回溯分析是什么函数出现了问题，采用pacth或者自行实现的方式解决实现了libc.so的gettimeofday malloc free pthread_mutex_lock pthread_mutex_unlock calloc补充了一个JNI调用对libc++_shared.so一处函数进行了patch这里和上一篇文章提到的略有出入是因为当时测试的传入参数不一样，不过总体都是差不多的小结：要实现dump上下文，首先要过掉APP的反调试，要能拿到上下文的内存数据快手似乎没有反调试，但是其他APP可能没有这么顺利dump上下文后模拟执行需要补的环境很少，实现的都是常见的几个函数文章中通过内存异常地址定位需要载入的内存块的方法比较简陋，这一点完全可以改为写代码预先从配置文件中解析各个模块地址范围，再比较地址落在哪个内存块即可文章中手动实现了几个libc.so的函数，更好的方案是unidbg加载自带的libc.so，再把模拟执行过程中的libc调用关联过去，这样最省力
前言以绿洲 v4.5.6为例（64位），目标是com.weibo.xvideo.NativeApi的s函数通过frida hook RegisterNative，可以得到其native函数位于liboasiscore.so + 0x116CC在上一篇文章中，已经实现了APP运行到liboasiscore.so + 0x116CC时的上下文dump了现在可以开始尝试使用unidbg加载上下文并模拟执行，这里使用当前最新的0.9.7版本可能出现的问题如果开启traceCode出现null的异常，请修改AssemblyCodeDumper中的代码：maxLengthLibraryName = memory.getMaxLengthLibraryName().length();改为：maxLengthLibraryName = 32;步骤先搭个框架如下，目前这里不需要载入任何so，仅仅初始化虚拟机即可public class NativeApi extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; NativeApi() { emulator = AndroidEmulatorBuilder.for64Bit() .setProcessName("com.sina.oasis") .addBackendFactory(new Unicorn2Factory(true)) .build(); // 创建模拟器实例，要模拟32位或者64位，在这里区分 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 vm = emulator.createDalvikVM(); // 创建Android虚拟机 vm.setJni(this); vm.setVerbose(true); // 设置是否打印Jni调用细节 } public static void main(String[] args) throws Exception { NativeApi mNativeApi = new NativeApi(); mNativeApi.load_context("unidbg-android/src/test/resources/DumpContext_20220806_130827"); mNativeApi.s(); } private void load_context(String dump_dir) throws IOException, DataFormatException, IOException { } private void s() { } }Q: 先思考一个问题，已经有上下文了，那么加载上下文，使用unidbg接着上下文运行，需要做什么？A: 首先当然是还原上下文状态，即设置寄存器，加载内存A: 其次是从上下文的地方开始运行Q 再想想为什么要使用unidbg而不是直接使用unicorn呢？A unidbg对jni和syscall的支持相对完善，接着上下文运行前，我们将JNIEnv和JavaVM替换为unidbg的，那么后续遇到jni调用或是syscall调用也能处理，而unicorn不具备这样的优势在load_context通过下面的代码恢复上下文的寄存器，这里只处理了通用寄存器，浮点寄存器暂时没有写这里一定要向TPIDR_EL0写入刚才dump得到的那个寄存器的值Q: 为什么要向CPACR_EL1写入0x300000？A: 没有仔细研究...但是unidbg的enableVFP是这样的写的:(backend.reg_write(Arm64Const.UC_ARM64_REG_CPACR_EL1, 0x300000L); backend.reg_write(Arm64Const.UC_ARM64_REG_TPIDR_EL0, 0x0000006d4aa68000L);Q: x29和x30呢，为什么没有写？A: FP就是x29，LR就是x30简略代码如下：Backend backend = emulator.getBackend(); Memory memory = emulator.getMemory(); String context_file = dump_dir + "\\" + "_index.json"; InputStream is = new FileInputStream(context_file); String jsonTxt = IOUtils.toString(is, "UTF-8"); JSONObject context = JSONObject.parseObject(jsonTxt); JSONObject regs = context.getJSONObject("regs"); backend.reg_write(Arm64Const.UC_ARM64_REG_X0, Long.parseUnsignedLong(regs.getString("x0").substring(2), 16)); // ... backend.reg_write(Arm64Const.UC_ARM64_REG_X28, Long.parseUnsignedLong(regs.getString("x28").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_FP, Long.parseUnsignedLong(regs.getString("fp").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_LR, Long.parseUnsignedLong(regs.getString("lr").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_SP, Long.parseUnsignedLong(regs.getString("sp").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_PC, Long.parseUnsignedLong(regs.getString("pc").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_CPSR, Long.parseUnsignedLong(regs.getString("cpsr").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_CPACR_EL1, 0x300000L); backend.reg_write(Arm64Const.UC_ARM64_REG_TPIDR_EL0, 0x0000006d4aa68000L); // 好像不设置这个也不会有什么影响 memory.setStackPoint(Long.parseUnsignedLong(regs.getString("sp").substring(2), 16));在load_context通过下面的代码恢复部分上下文内存dump下来的内存很多，如果全部加载会非常耗时，实际上我们需要模拟执行的代码往往只有一小部分，所以通过白名单机制加载（代码中搜white_list）对于没有名字则可以通过content_file来加载这里从配置文件中读取配置，只加载下面这些内存段libc.soliboasiscore.so[anon:stack_and_tls:32529][anon:.bss][anon:scudo:primary]Q: 这些内存段是如何确定下来的呢？A: 最开始可以只加载libc.so和liboasiscore.so相关的，然后直接模拟执行看哪些地方出现报错，再去计算确定是哪些段没有加载（将报错的内存地址转为10进制，在_index.json文件中比较各个分段的start和end，看是什么分段的地址）一般会涉及到libc.so和上下文位置的so然后是[anon:stack_and_tls:32529]，这个需要执行测试确定然后是[anon:scudo:primary]和[anon:scudo:secondary](Android 11之前是[anon:libc_malloc])再然后是[anon:.bss]有很多分段都是相同名字，在完成算法成功模拟执行后，可以记录下访问了那些块，再改为通过content_file来加载内存段还能减少内存占用逻辑很简单，就是根据配置文件+白名单机制先调用mem_map开辟内存空间，再调用mem_write写入内存数据记得写内存数据之前先从文件中解压~部分代码片段如下：int UNICORN_PAGE_SIZE = 0x1000; private long align_page_down(long x){ return x & ~(UNICORN_PAGE_SIZE - 1); } private long align_page_up(long x){ return (x + UNICORN_PAGE_SIZE - 1) & ~(UNICORN_PAGE_SIZE - 1); } private void map_segment(long address, long size, int perms){ long mem_start = address; long mem_end = address + size; long mem_start_aligned = align_page_down(mem_start); long mem_end_aligned = align_page_up(mem_end); if (mem_start_aligned < mem_end_aligned){ emulator.getBackend().mem_map(mem_start_aligned, mem_end_aligned - mem_start_aligned, perms); } }JSONArray segments = context.getJSONArray("segments"); for (int i = 0; i < segments.size(); i++) { JSONObject segment = segments.getJSONObject(i); String path = segment.getString("name"); long start = segment.getLong("start"); long end = segment.getLong("end"); String content_file = segment.getString("content_file"); JSONObject permissions = segment.getJSONObject("permissions"); int perms = 0; if (permissions.getBoolean("r")){ perms |= UnicornConst.UC_PROT_READ; } if (permissions.getBoolean("w")){ perms |= UnicornConst.UC_PROT_WRITE; } if (permissions.getBoolean("x")){ perms |= UnicornConst.UC_PROT_EXEC; } String[] paths = path.split("/"); String module_name = paths[paths.length - 1]; List<String> white_list = Arrays.asList(new String[]{"liboasiscore.so", "libc.so", "[anon:stack_and_tls:32529]", "[anon:.bss]", "[anon:scudo:primary]"}); if (white_list.contains(module_name)){ int size = (int)(end - start); map_segment(start, size, perms); String content_file_path = dump_dir + "\\" + content_file; File content_file_f = new File(content_file_path); if (content_file_f.exists()){ InputStream content_file_is = new FileInputStream(content_file_path); byte[] content_file_buf = IOUtils.toByteArray(content_file_is); // zlib解压 Inflater decompresser = new Inflater(); decompresser.setInput(content_file_buf, 0, content_file_buf.length); byte[] result = new byte[size]; int resultLength = decompresser.inflate(result); decompresser.end(); backend.mem_write(start, result); } else { System.out.println("not exists path=" + path); byte[] fill_mem = new byte[size]; Arrays.fill( fill_mem, (byte) 0 ); backend.mem_write(start, fill_mem); } } }自此已经完成上下文的恢复了现在可以开始模拟执行了，还记得前面的问题吗，我们要替换掉原来的JNIEnv和JavaVM，还有其他传入参数不过遇到的第一个问题是没有module，用过unidbg的同学都知道肯定会在开始的时候加载某个so，不过这里我们并没有加载任何so经过分析，发现可以使用Module.emulateFunction这样来调用，这个问题顺利解决于是修改参数，接着上下文的状态模拟执行的代码实现如下：private void s() { List<Object> list = new ArrayList<>(4); // 参数一 JNIEnv* env list.add(vm.getJNIEnv()); // 参数二 jobject thiz DvmClass NativeApiobj = vm.resolveClass("com/weibo/xvideo/NativeApi"); list.add(NativeApiobj.hashCode()); // 参数三 java 层的参数一 String data = "aid=01AxlUJKR0Ty44wiNo-ebcin69clFdov931m6rKA-DoQZ7Pkk.&cfrom=28C7295010&cuid=0&noncestr=g8g1N6V3t49z943Hx80395kb63f42A&platform=ANDROID&timestamp=1659164634618&ua=Google-Pixel4__oasis__4.5.6__Android__Android11&version=4.5.6&vid=2007759688214&wm=2468_90123"; ByteArray input_array = new ByteArray(vm, data.getBytes(StandardCharsets.UTF_8)); vm.addLocalObject(input_array); list.add(input_array.hashCode()); // 参数四 java 层的参数二 boolean flag = false; list.add((Boolean) flag ? VM.JNI_TRUE : VM.JNI_FALSE); // 这里获取 dump 时的 pc 地址作为模拟执行起始地址 long ctx_addr = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_PC).longValue(); // 开始模拟执行 Number result = Module.emulateFunction(emulator, ctx_addr, list.toArray()); // 获取返回结果 String sign_str = (String) vm.getObject(result.intValue()).getValue(); System.out.println("sign_str=" + sign_str); }这里的关键点在于模拟执行的起始地址是dump上下文时的pc地址模拟执行结果和hook的结果一致总结在拿到上下文之后，使用unidbg的JNIEnv、JavaVM和其他由unidbg构造的参数对原上下文的参数进行替换一定程度上完成了原上下文的接管虽然没有主动完成上下文初始化（即libc和目标so的初始化），但这样操作可以使后续jni调用和syscall都落入unidbg的逻辑之中相比自己去做目标so的初始化，通过dump上下文，可以一定程度上减少工作量，同时更贴近于真实情况除了unidbg，也可以使用相同的手法接入ExAndroidNativeEmuQ: 还有什么需要处理？A: 经过实践，对于Android 10之后的系统，libc.so有些不一样，无法完全模拟执行，推测是unicorn本身对一些指令支持不够完善。关于这个问题，我想可以先用unidbg初始化自己的libc，然后dump下来的模拟执行过程中如果遇到了libc的调用，将这些调用交给unidbg的libc处理Q: 涉及复杂的函数效果如何？A: 对于算法来说，一般不会特别特别复杂，通常只需要hook住libc的部分函数，再针对性hook一些函数即可经过实践，以快手的sig来说（10.6.50.26734），使用dump上下文方案，再分别对以下点位hook和补充后，就能跑出结果libc.so gettimeofday 手动实现libc.so pthread_mutex_lock 跳过执行libc.so pthread_mutex_unlock 跳过执行libc.so malloc 手动实现libc.so calloc 手动实现libc.so free 手动实现libc++_shared.so std::__libcpp_snprintf_l 特殊处理替换原有com/kuaishou/android/security/internal/common/ExceptionProxy类 特殊处理两个需要补充的jni调用 常规补环境完整代码package com.weibo.xvideo; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Backend; 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.DvmClass; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.linux.android.dvm.array.ByteArray; import com.github.unidbg.memory.Memory; import org.apache.commons.io.IOUtils; import unicorn.Arm64Const; import unicorn.ArmConst; import unicorn.UnicornConst; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.DataFormatException; import java.util.zip.Inflater; public class NativeApi extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; NativeApi() { emulator = AndroidEmulatorBuilder.for64Bit() .setProcessName("com.sina.oasis") .addBackendFactory(new Unicorn2Factory(true)) .build(); // 创建模拟器实例，要模拟32位或者64位，在这里区分 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 vm = emulator.createDalvikVM(); // 创建Android虚拟机 vm.setJni(this); vm.setVerbose(true); // 设置是否打印Jni调用细节 } public static void main(String[] args) throws Exception { NativeApi mNativeApi = new NativeApi(); mNativeApi.load_context("unidbg-android/src/test/resources/DumpContext_20220806_130827"); mNativeApi.s(); } int UNICORN_PAGE_SIZE = 0x1000; private long align_page_down(long x){ return x & ~(UNICORN_PAGE_SIZE - 1); } private long align_page_up(long x){ return (x + UNICORN_PAGE_SIZE - 1) & ~(UNICORN_PAGE_SIZE - 1); } private void map_segment(long address, long size, int perms){ long mem_start = address; long mem_end = address + size; long mem_start_aligned = align_page_down(mem_start); long mem_end_aligned = align_page_up(mem_end); if (mem_start_aligned < mem_end_aligned){ emulator.getBackend().mem_map(mem_start_aligned, mem_end_aligned - mem_start_aligned, perms); } } private void load_context(String dump_dir) throws IOException, DataFormatException, IOException { Backend backend = emulator.getBackend(); Memory memory = emulator.getMemory(); String context_file = dump_dir + "\\" + "_index.json"; InputStream is = new FileInputStream(context_file); String jsonTxt = IOUtils.toString(is, "UTF-8"); JSONObject context = JSONObject.parseObject(jsonTxt); JSONObject regs = context.getJSONObject("regs"); backend.reg_write(Arm64Const.UC_ARM64_REG_X0, Long.parseUnsignedLong(regs.getString("x0").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X1, Long.parseUnsignedLong(regs.getString("x1").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X2, Long.parseUnsignedLong(regs.getString("x2").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X3, Long.parseUnsignedLong(regs.getString("x3").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X4, Long.parseUnsignedLong(regs.getString("x4").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X5, Long.parseUnsignedLong(regs.getString("x5").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X6, Long.parseUnsignedLong(regs.getString("x6").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X7, Long.parseUnsignedLong(regs.getString("x7").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X8, Long.parseUnsignedLong(regs.getString("x8").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X9, Long.parseUnsignedLong(regs.getString("x9").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X10, Long.parseUnsignedLong(regs.getString("x10").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X11, Long.parseUnsignedLong(regs.getString("x11").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X12, Long.parseUnsignedLong(regs.getString("x12").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X13, Long.parseUnsignedLong(regs.getString("x13").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X14, Long.parseUnsignedLong(regs.getString("x14").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X15, Long.parseUnsignedLong(regs.getString("x15").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X16, Long.parseUnsignedLong(regs.getString("x16").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X17, Long.parseUnsignedLong(regs.getString("x17").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X18, Long.parseUnsignedLong(regs.getString("x18").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X19, Long.parseUnsignedLong(regs.getString("x19").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X20, Long.parseUnsignedLong(regs.getString("x20").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X21, Long.parseUnsignedLong(regs.getString("x21").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X22, Long.parseUnsignedLong(regs.getString("x22").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X23, Long.parseUnsignedLong(regs.getString("x23").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X24, Long.parseUnsignedLong(regs.getString("x24").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X25, Long.parseUnsignedLong(regs.getString("x25").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X26, Long.parseUnsignedLong(regs.getString("x26").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X27, Long.parseUnsignedLong(regs.getString("x27").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_X28, Long.parseUnsignedLong(regs.getString("x28").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_FP, Long.parseUnsignedLong(regs.getString("fp").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_LR, Long.parseUnsignedLong(regs.getString("lr").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_SP, Long.parseUnsignedLong(regs.getString("sp").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_PC, Long.parseUnsignedLong(regs.getString("pc").substring(2), 16)); backend.reg_write(ArmConst.UC_ARM_REG_CPSR, Long.parseUnsignedLong(regs.getString("cpsr").substring(2), 16)); backend.reg_write(Arm64Const.UC_ARM64_REG_CPACR_EL1, 0x300000L); backend.reg_write(Arm64Const.UC_ARM64_REG_TPIDR_EL0, 0x0000006d4aa68000L); // 好像不设置这个也不会有什么影响 memory.setStackPoint(Long.parseUnsignedLong(regs.getString("sp").substring(2), 16)); JSONArray segments = context.getJSONArray("segments"); for (int i = 0; i < segments.size(); i++) { JSONObject segment = segments.getJSONObject(i); String path = segment.getString("name"); long start = segment.getLong("start"); long end = segment.getLong("end"); String content_file = segment.getString("content_file"); JSONObject permissions = segment.getJSONObject("permissions"); int perms = 0; if (permissions.getBoolean("r")){ perms |= UnicornConst.UC_PROT_READ; } if (permissions.getBoolean("w")){ perms |= UnicornConst.UC_PROT_WRITE; } if (permissions.getBoolean("x")){ perms |= UnicornConst.UC_PROT_EXEC; } String[] paths = path.split("/"); String module_name = paths[paths.length - 1]; List<String> white_list = Arrays.asList(new String[]{"liboasiscore.so", "libc.so", "[anon:stack_and_tls:32529]", "[anon:.bss]", "[anon:scudo:primary]"}); if (white_list.contains(module_name)){ int size = (int)(end - start); map_segment(start, size, perms); String content_file_path = dump_dir + "\\" + content_file; File content_file_f = new File(content_file_path); if (content_file_f.exists()){ InputStream content_file_is = new FileInputStream(content_file_path); byte[] content_file_buf = IOUtils.toByteArray(content_file_is); // 解压 Inflater decompresser = new Inflater(); decompresser.setInput(content_file_buf, 0, content_file_buf.length); byte[] result = new byte[size]; int resultLength = decompresser.inflate(result); decompresser.end(); backend.mem_write(start, result); } else { System.out.println("not exists path=" + path); byte[] fill_mem = new byte[size]; Arrays.fill( fill_mem, (byte) 0 ); backend.mem_write(start, fill_mem); } } } } private void s() { List<Object> list = new ArrayList<>(4); // 参数一 JNIEnv* env list.add(vm.getJNIEnv()); // 参数二 jobject thiz DvmClass NativeApiobj = vm.resolveClass("com/weibo/xvideo/NativeApi"); list.add(NativeApiobj.hashCode()); // 参数三 java 层的参数一 String data = "aid=01AxlUJKR0Ty44wiNo-ebcin69clFdov931m6rKA-DoQZ7Pkk.&cfrom=28C7295010&cuid=0&noncestr=g8g1N6V3t49z943Hx80395kb63f42A&platform=ANDROID&timestamp=1659164634618&ua=Google-Pixel4__oasis__4.5.6__Android__Android11&version=4.5.6&vid=2007759688214&wm=2468_90123"; ByteArray input_array = new ByteArray(vm, data.getBytes(StandardCharsets.UTF_8)); vm.addLocalObject(input_array); list.add(input_array.hashCode()); // 参数四 java 层的参数二 boolean flag = false; list.add((Boolean) flag ? VM.JNI_TRUE : VM.JNI_FALSE); // 这里获取 dump 时的 pc 地址作为模拟执行起始地址 long ctx_addr = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_PC).longValue(); // 开始模拟执行 Number result = Module.emulateFunction(emulator, ctx_addr, list.toArray()); // 获取返回结果 String sign_str = (String) vm.getObject(result.intValue()).getValue(); System.out.println("sign_str=" + sign_str); } }
前言假定你已经完成下面的操作，那么现在要实际开始操作了termux + lldb的环境准备好了写好了dump上下文的lldb脚本2023的补充最新版的termux中lldb已经是16版本了，根据反馈可能出现image list只有几个库的情况，请尝试改为使用remote方式步骤首先在PC上进入termux的环境中（root用户）记得要进入/data/data/com.termux/files/home执行命令，并且dump脚本也放到这个目录，以确保能正常读写文件以绿洲 v4.5.6为例（64位），目标是com.weibo.xvideo.NativeApi的s函数通过frida hook RegisterNative，可以得到其native函数位于liboasiscore.so + 0x116CC先查看其主进程pidps -ef | grep oasis然后执行lldb进入lldb交互界面再执行attach {pid}附加到主进程这个时候APP会被暂停，此时再按c即可恢复运行恢复运行后我们依然可以执行命令，这点相比gdb要好很多，gdb附加真的很慢而且只能在暂停的时候执行命令（？）有时候会遇到这样的信号，这个时候APP就突然暂停了通过下面的命令查看信号处理设置process handle SIGSEGV输出如下NAME PASS STOP NOTIFY =========== ===== ===== ====== SIGSEGV true true true那么可以在刚附加的时候就把这个信号PASS动作设置为true，STOP动作设置为false，这样可以避免APP突然暂停process handle SIGSEGV -p true -s false如果其他APP存在反调试，可能会发SIGQUIT SIGSTOP SIGKILL之类的信号暂停的时候可以打调用栈看看，说不定能找到反调试信息可以用下面的命令查看目标so的基址信息image list -o -f liboasiscore.so这里只是演示下，lldb可以直接通过下面的命令实现对模块名+偏移的断点设置，相比gdb的操作方便得多breakpoint set -s liboasiscore.so -a 0x116CC现在断点下好了，将APP切换到后台，再进入APP就能触发s函数的调用如果前面没有载入脚本，那么先通过下面的命令载入command script import lldb_dumper.py然后执行dumpctx命令即可开始dump上下文提前做了过滤，所以不会太花时间最后脚本会将dump好的内容打包如果是用的ttyd+浏览器远程，那现在可以使用lsz命令直接下载文件，如果是adb方式，记得手动pull出来现在就完成了dump上下文了这里还没有结束查看liboasiscore.so + 0x116CC的反汇编代码，可以看到这样的代码v35 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);对应的汇编是MRS X8, #3, c13, c0, #2也就是在获取TPIDR_EL0这个系统寄存器，有关说明可以参考下面的文章Android逆向中的Canary机制还有哪些系统寄存器？请参考https://github.com/TrungNguyen1909/aarch64-sysreg-ida/blob/main/aarch64_sysreg.py不过我们只需要获取到TPIDR_EL0就足以应付后续的模拟执行了为了能稳妥的模拟执行，必须拿到这个寄存器的值，经过实践，发现无法直接在最开始断点的时候就拿到只能在执行完MRS这条指令之后去获取x8的值所以还需要在MRS的下一行断点，也就是liboasiscore.so + 0x116F4breakpoint set -s liboasiscore.so -a 0x116F4下完断点后按c执行，等断在0x116F4，然后通过register read x8读取TPIDR_EL0的值，记录下来注意，不一定都是把值赋给x8，请结合实际情况确定是哪个寄存器现在才算是完成上下文dump了如果要结束附加进程，建议使用下面的命令（最好取消断点，再按c让APP恢复运行）process detach直接quit也能退，但是会导致APP崩溃或者无响应，不建议
前言提到dump，（做安卓逆向）大家可能会首先想到是dump内存中的so，然后修复，接着再去逆向或者模拟执行dump后模拟执行，按道理说比自己做so解析并完整模拟会更有优势，毕竟省去了初始化so的工作但我也只见到过几篇文章有这样做，都是使用frida去dump，然后修复so后再模拟执行我想问题在于这样的dump操作没有拿到准确的上下文，构造参数仍然是一件很麻烦的事情不过好在已经有人做过这件事了，只是大家可能没有注意到，亦或是脚本年久失修...afl-unicorn: Fuzzing The 'Unfuzzable'注意，执行测试脚本的时候需要先附加到进程脚本编写主要参考下面的脚本，这个脚本已经是两年前的了，存在诸多问题https://github.com/AFLplusplus/AFLplusplus/blob/stable/unicorn_mode/helper_scripts/unicorn_dumper_lldb.pyQ: 为什么选择lldb而不是gdbA: 经过实践lldb更快，更方便，更现代化起初我是想进行简单修补上面的脚本，后面发现还是得重写下下面就详细讲解如何编写可以dump上下文的lldb脚本编写过程中用到的API可以在下面的文档中查询https://lldb.llvm.org/use/python-reference.htmllldb的python库源码在线版本，可以点击下面网页的source code查看https://lldb.llvm.org/python_reference/如果你已经在termux安装好lldb了，那么你也可以在下面的路径找到lldb库源代码/data/data/com.termux/files/usr/lib/python3.10/site-packages/lldb/__init__.py建议把这个脚本拿出来，改名为lldb.py然后新建一个lldb_dumper.py文件，把上面的lldb.py放在同一级目录在lldb_dumper.py中写上如下代码，这样使用vscode或者其他IDE即可获得代码提示from typing import TYPE_CHECKING if TYPE_CHECKING: from lldb import * import lldb我希望命中断点后，进程都停下来，这个时候只需要执行一个命令就可以dump上下文根据Create a new lldb command using a Python function小节https://lldb.llvm.org/use/python-reference.html#create-a-new-lldb-command-using-a-python-function可以知道应该在代码中添加一个__lldb_init_module函数，然后通过debugger.HandleCommand去添加自定义命令def __lldb_init_module(debugger: 'SBDebugger', internal_dict: dict): debugger.HandleCommand('command script add -f lldb_dumper.helloworld hello')command script add -f lldb_dumper.helloworld hello这个命令的意思是，当执行hello命令的时候，lldb会去调用lldb_dumper脚本中的helloworld函数即lldb_dumper.py被视为一个模块根据文档，可以知道被调用的函数接收下面这些参数，详细解释请参考文档debuggercommandexe_ctxresultinternal_dict对于dump上下文来说，最重要的就是exe_ctx了，其类型为SBExecutionContext，看到这个就知道它肯定和上下文紧密相关写个helloworld如下def helloworld(debugger: 'SBDebugger', command: str, exe_ctx: 'SBExecutionContext', result: 'SBCommandReturnObject', internal_dict: dict): print('helloWorld debugger:', debugger) print('helloWorld command:', command) print('helloWorld exe_ctx:', exe_ctx)进入lldb，通过下面的命令加载脚本command script import lldb_dumper.py这个时候执行hello you命令可以看到有输出信息，其中command是you，这个可以方便增强脚本特性现在你已经会helloword了，现在开始正式编写dump上下文脚本吧~获取架构信息通过下面的代码，得到triple是aarch64-unknown-linux-android这样的字符串target = exe_ctx.GetTarget() # type: SBTarget triple = target.GetTriple()triple 的一般格式为<arch>-<vendor>-<sys>-<abi>arch => x86_64, i386, arm, thumb, mipssub => v5, v6m, v7a, v7mvendor => pc, apple, nvidia, ibmsys => none, linux, win32, darwin, cudaabi => eabi, gnu, android, macho, elf可以根据第一个-之前的部分来判断是何种架构，记得区分大小端（不过一般都是小端）代码片段如下：def dump_arch_info(target: 'SBTarget'): triple = target.GetTriple() logger.debug(f'[dump_arch_info] triple => {triple}') # 'aarch64', 'unknown', 'linux', 'android' arch, vendor, sys, abi = triple.split('-') if arch == 'aarch64' or arch == 'arm64': return 'arm64le' elif arch == 'aarch64_be': return 'arm64be' elif arch == 'armeb': return 'armbe' elif arch == 'arm': return 'armle' else: return '' target = exe_ctx.GetTarget() # type: SBTarget arch_long = dump_arch_info(target)获取寄存器信息查阅文档，要获取寄存器，需要按以下几步完成通过SBExecutionContext拿到SBFrame调用SBFrame的GetRegisters方法拿到全部寄存器是一个SBValueList对象，通过GetName可以知道是通用寄存器还是浮点寄存器遍历全部寄存器，通过GetName获取名字，通过GetValue取值这里有一个重点：直接获取SBValue拿到的值通常得到的是贴近人类可读的字符串，比如浮点寄存器打印出来是科学计数法形式的，但是对于浮点寄存器来说我们应该拿到最为精确的值通过查阅文档，可以将SBValue格式指定为lldb.eFormatUnsigned读取，这样就是最完整的数值了但是GetValue拿到的类型依然是字符串 在使用时还需要一次转换因此这里干脆指定为lldb.eFormatHex，即16进制的字符串，这样更加符合逆向人员的阅读习惯https://lldb.llvm.org/python_api_enums.html#format代码片段如下：def dump_regs(frame: 'SBFrame'): regs = {} # type: Dict[str, int] registers = None # type: List[SBValue] for registers in frame.GetRegisters(): # - General Purpose Registers # - Floating Point Registers logger.debug(f'registers name => {registers.GetName()}') for register in registers: register_name = register.GetName() register.SetFormat(lldb.eFormatHex) register_value = register.GetValue() regs[register_name] = register_value logger.info(f'regs => {json.dumps(regs, ensure_ascii=False, indent=4)}') return regs frame = exe_ctx.GetFrame() # type: SBFrame regs = dump_regs(frame)获取内存分段信息这部分拿到的信息一般用不到，似乎是排查bug用的，但是原脚本有这部分，所以一并重写了简单来说就是遍历SBTarget的module，再遍历每个module的section收集记录全部section信息def dump_memory_info(target: 'SBTarget'): logger.debug('start dump_memory_info') sections = [] # 先查找全部分段信息 for module in target.module_iter(): module: SBModule for section in module.section_iter(): section: SBSection module_name = module.file.GetFilename() start, end, size, name = get_section_info(target, section) section_info = { 'module': module_name, 'start': start, 'end': end, 'size': size, 'name': name, } # size 好像有负数的情况 不知道是什么情况 logger.info(f'Appending: {name}') sections.append(section_info) return sections target = exe_ctx.GetTarget() # type: SBTarget sections = dump_memory_info(target)读取保存内存分段信息代码如下，参考原脚本的逻辑，就是先不断遍历当前进程的内存区域通过process.GetMemoryRegionInfo获取信息，记录名字，权限，起始和结束地址然后再通过process.ReadMemory去读取读取之前加入了一些黑名单过滤规则，这样可以加快dump速度另外保存内存数据的时候会进行压缩，因为有很多区域是连续的0，压缩可以极大减少体积并且保存时以md5作为文件名，存在多个内存分段都是4096字节内容全0的情况，这样也能省下不少空间所以有个要点是，在使用的时候记得解压后再加载到unicorn的内存中去代码片段如下：def dump_memory(process: 'SBProcess', dump_path: Path, black_list: Dict[str, List[str]], max_seg_size: int): logger.debug('start dump memory') memory_list = [] mem_info = lldb.SBMemoryRegionInfo() start_addr = -1 next_region_addr = 0 while next_region_addr > start_addr: # 从内存起始位置开始获取内存信息 err = process.GetMemoryRegionInfo(next_region_addr, mem_info) # type: SBError if not err.success: logger.warning(f'GetMemoryRegionInfo failed, {err}, break') break # 获取当前位置的结尾地址 next_region_addr = mem_info.GetRegionEnd() # 如果超出上限 结束遍历 if next_region_addr >= sys.maxsize: logger.info(f'next_region_addr:0x{next_region_addr:x} >= sys.maxsize, break') break # 获取当前这块内存的起始地址和结尾地址 start = mem_info.GetRegionBase() end = mem_info.GetRegionEnd() # 很多内存块没有名字 预设一个 region_name = 'UNKNOWN' # 记录分配了的内存 if mem_info.IsMapped(): name = mem_info.GetName() if name is None: name = '' mem_info_obj = { 'start': start, 'end': end, 'name': name, 'permissions': { 'r': mem_info.IsReadable(), 'w': mem_info.IsWritable(), 'x': mem_info.IsExecutable(), }, 'content_file': '', } memory_list.append(mem_info_obj) # 开始正式dump for seg_info in memory_list: try: start_addr = seg_info['start'] # type: int end_addr = seg_info['end'] # type: int region_name = seg_info['name'] # type: str permissions = seg_info['permissions'] # type: Dict[str, bool] # 跳过不可读 之后考虑下是不是能修改权限再读 if seg_info['permissions']['r'] is False: logger.warning(f'Skip dump {region_name} permissions => {permissions}') continue # 超过预设大小的 跳过dump predicted_size = end_addr - start_addr if predicted_size > max_seg_size: logger.warning(f'Skip dump {region_name} size:0x{predicted_size:x}') continue skip_dump = False for rule in black_list['startswith']: if region_name.startswith(rule): skip_dump = True logger.warning(f'Skip dump {region_name} hit startswith rule:{rule}') if skip_dump: continue for rule in black_list['endswith']: if region_name.endswith(rule): skip_dump = True logger.warning(f'Skip dump {region_name} hit endswith rule:{rule}') if skip_dump: continue for rule in black_list['includes']: if rule in region_name: skip_dump = True logger.warning(f'Skip dump {region_name} hit includes rule:{rule}') if skip_dump: continue # 开始读取内存 ts = datetime.now() err = lldb.SBError() seg_content = process.ReadMemory(start_addr, predicted_size, err) tm = (datetime.now() - ts).total_seconds() # 读取成功的才写入本地文件 并计算md5 # 内存里面可能很多地方是0 所以压缩写入文件 减少占用 if seg_content is None: logger.debug(f'Segment empty: @0x{start_addr:016x} {region_name} => {err}') else: logger.info(f'Dumping @0x{start_addr:016x} {tm:.2f}s size:0x{len(seg_content):x}: {region_name} {permissions}') compressed_seg_content = zlib.compress(seg_content) md5_sum = hashlib.md5(compressed_seg_content).hexdigest() + '.bin' seg_info['content_file'] = md5_sum (dump_path / md5_sum).write_bytes(compressed_seg_content) except Exception as e: # 这里好像不会出现异常 因为前面有 SBError 处理了 不过还是保留 logger.error(f'Exception reading segment {region_name}', exc_info=e) return memory_list # 设置过滤黑名单 符合下面条件的跳过dump black_list = { 'startswith': ['/dev', '/system/fonts', '/dmabuf'], 'endswith': ['(deleted)', '.apk', '.odex', '.vdex', '.dex', '.jar', '.art', '.oat', '.art]'], 'includes': [], } # 设置单个内存分段dump大小上限 max_seg_size = 64 * 1024 * 1024 # dump内存 process = exe_ctx.GetProcess() # type: SBProcess segments = dump_memory(process, dump_path, black_list, max_seg_size)至此完成了上下文dump的脚本编写注意 既然是上下文dump，应当在命中断点的时候执行命令脚本可以提前加载，因为加载的时候只是起到一个注册命令的作用本文已包含完整dump脚本的全部代码，你只需要跟着本篇教程整合即可