这篇文章上次修改于 341 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

前言

绿洲 v4.5.6为例(64位),目标是com.weibo.xvideo.NativeApis函数

通过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.so
  • liboasiscore.so
  • [anon:stack_and_tls:32529]
  • [anon:.bss]
  • [anon:scudo:primary]
  • Q: 这些内存段是如何确定下来的呢?
  • A: 最开始可以只加载libc.soliboasiscore.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,也可以使用相同的手法接入ExAndroidNativeEmu

  • Q: 还有什么需要处理?
  • 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);
    }

}