吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4149|回复: 45
上一主题 下一主题
收起左侧

[Android 原创] 《安卓逆向这档事》第二十四课、Unidbg之补完环境我就睡(上)

  [复制链接]
跳转到指定楼层
楼主
正己 发表于 2025-9-7 15:03 回帖奖励
本帖最后由 正己 于 2025-9-7 15:23 编辑

![|600]https://pic.rmb.bdstatic.com/bjh/241224/a5a71662ce7a80f210e94738b801fcf09656.png)

一、课程目标

1.了解unidbg_trace,精准控制追踪,提升逆向分析效率
2.配置 mcp 服务,利用AI辅助逆向,加速关键算法分析
3.unidbg 之补 jni,模拟各类Java层调用   

二、工具

1.教程Demo
2.IDEA
3.IDA
4.Cursor  

三、课程内容

一.Unidbg_Trace

1.Trace 的分类

Trace 类型 描述
函数 Trace 包括无差别的函数 Trace、导出函数 Trace、库函数 Trace、系统调用 Trace 等,用于分析算法执行流、应对 OLLVM 混淆与动态跳转。
基本块 Trace 也叫 Block Trace,用于控制流分析与反混淆。
汇编 Trace 包括指令级 Trace、特定指令 Trace 等,尤其适用于分析自定义算法、花指令;它也是最重要的 Trace 类型,基于它可实现其他类型 Trace。

2.Unidbg Trace 的优劣

维度 表现
易用性 必须掌握 Unidbg 的使用,但一旦会用,就能得到目标函数完整执行的 Trace。开启指令追踪十分简单,只需 emulator.traceCode(begin, end) 一句码。  <br>对比:Frida Stalker、IDA Instruction trace 的启动更复杂。
效率 速度中等:测试环境下约可达 40w 行执行流/分钟(2400w/小时)。  <br>简单样本约 10 分钟即可完成 Trace,中等样本则需 2 小时,复杂、混淆严重的样本可能长达 12 小时甚至更多。  <br>与 IDA  trace 相比快很多,但比 Stalker 等动态重编译方慢。  <br>实际速度会因范围过滤等处理而折减至理论值的 1/4 或更低。
稳定性 稳定性好:基于 Unicorn,引发崩溃或异常 Trace 的情况较少。  <br>对比:IDA Trace 容易中断或导致应用崩溃,Stalker 对某些指令支持有 Bug。
兼容性 支持 ARM32ARM64。<br>比多数指令追踪方案(如 Stalker 不完全支持 ARM32)更全面。  <br>相比 IDA Trace 对 X86、MIPS 等架构的支持仍有不足。
展示效果 信息维度丰富:包括时间、绝对地址、模块、相对偏移、机器码、汇编代码、执行前后寄存器变化等。  <br>信息处理也很出色,细节优化多。

3.Trace实例

1. 基础用法
在调用目标函数前,只需简单地调用 emulator.traceCode() 即可开启对后续所有指令的追踪。

// 在调用目标函数前开启指令追踪
emulator.traceCode();
boolean result = security.callStaticJniMethodBoolean(emulator, "check", "123456");

2. 约束追踪范围
通常我们只关心目标 SO 的执行流,而非 libc 等系统库的内部执行。traceCode 的重载方法 traceCode(long begin, long end) 允许我们精确设定追踪的内存地址范围。

// 获取目标模块对象
Module module = dm.getModule();
// 仅追踪目标模块地址范围内的指令
emulator.traceCode(module.base, module.base + module.size); 
// 注意:第二个参数是结束地址,不是长度!这是一个常见误区。

3. 精确控制追踪时机

  • 追踪 JNI_OnLoad: 将 traceCode 放在 dm.callJNI_OnLoad(emulator) 之前。
  • 追踪初始化函数 (init_array): JNI_OnLoad 之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用 ModuleListener
    memory.addModuleListener(new ModuleListener() {
    @Override
    public void onLoaded(Emulator<?> emulator, Module module) {
        // 当我们关心的模块被加载时,立即开启trace
        if("lib52pojie.so".equals(module.name)){
            emulator.traceCode(module.base, module.base + module.size);
        }
    }
    });
    // 这之后再加载模块
    DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so"), true);

    4. 预估追踪耗时
    等待一个未知的 Trace 过程是痛苦的。我们可以通过一个轻量级的 CodeHook 快速统计指令总数,从而预估总耗时。这基本不含反汇编和寄存器打印的开销,所以速度极快。

    private long instructionCount = 0; // 用于指令计数
    emulator.getBackend().hook_add_new(new CodeHook() {  
    @Override  
    public void hook(Backend backend, long address, int size, Object user) {  
        instructionCount++;  
    }  
    @Override  
    public void onAttach(UnHook unHook) {}  
    @Override  
    public void detach() {}  
    }, module.base, module.base + module.size, null);
    System.out.println("总共执行ARM指令数: " + test.instructionCount);

    PS::在较好的测试条件下,unidbg 每分钟约可追踪 40 万行执行流。你可以根据统计出的总行数来判断是泡杯咖啡等几分钟,还是需要去睡一觉等几个小时。
    5. 限定特定函数
    结合断点,可以实现对单个函数内部的精确追踪。

  • 场景一:只关心某次特定调用
    在函数调用指令处下断点开启 Trace,在调用指令的下一条指令处下断点关闭 Trace。

    long callAddr = module.base + 0xE53C; // 假设这是BL指令的地址
    final TraceHook[] traceHook = new TraceHook[1];
    // 在调用处开启追踪
    emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        traceHook[0] = emulator.traceCode(module.base, module.base + module.size);
        return true;
    }
    });
    // 在调用返回后关闭追踪
    emulator.attach().addBreakPoint(callAddr + 4, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        if (traceHook[0] != null) {
            traceHook[0].stopTrace();
        }
        return true;
    }
    });

    6.函数调用追踪 (debugger.traceFunctionCall) 详解
    traceFunctionCall 是 unidbg 0.9.6 版本后新增的强大功能,用于追踪函数粒度的调用关系,对理清高层逻辑和对抗 OLLVM 等混淆有奇效。
    1. 基础用法
    它属于 Debugger 的一部分,使用时需要先 attach() 到模拟器。

    // 获取调试器实例
    Debugger debugger = emulator.attach();
    // 追踪目标模块内的所有函数调用
    debugger.traceFunctionCall(module, new FunctionCallListener() {
    @Override
    public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {
        // 函数调用前(相当于Frida onEnter)
        System.out.println("onCall: " + UnidbgPointer.pointer(emulator, callerAddress) + " -> " + UnidbgPointer.pointer(emulator, functionAddress));
    }
    @Override
    public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {
        // 函数调用后(相当于Frida onLeave)
        // 注意:这里的args是寄存器R0-R7(x0-x7)的值,不完全等同于函数参数
    }
    });

    2. 构建函数调用树
    通过获取栈回溯深度,我们可以格式化输出,形成一个清晰的函数调用树,这对于分析复杂的调用逻辑极其有用。
    首先修改一下源码,路径:unidbg-api\src\main\java\com\github\unidbg\unwind
    添加以下方法:

/**  
 * 将给定的内存地址格式化为包含详细信息、可读的字符串。  
 * 这个方法会尝试解析地址,并提供尽可能多的上下文信息,如模块名、函数名(符号)、偏移量等。  
 *  
 * @Param address 要格式化的绝对内存地址。  
 * @Return 一个包含地址详细信息的格式化字符串。  
 */  
public String formatAddressDetails(long address) {  
    // 1. 尝试根据地址查找其所属的模块(例如,一个 .so 文件)。  
    Module module = emulator.getMemory().findModuleByAddress(address);  

    // 2. 如果地址位于一个已加载的模块内:  
    if (module != null) {  
        // 2.1. 在模块中查找离该地址最近的符号(即函数或全局变量的名称)。  
        //      `true` 参数表示也查找非导出的内部符号。  
        Symbol symbol = module.findClosestSymbolByAddress(address, true);  

        // 2.2. 如果找到了一个符号:  
        if (symbol != null) {  
            // 2.2.1. 创建一个 demangler 实例,用于将 C++ "mangled"(混淆)的符号名还原为可读的函数签名。  
            GccDemangler demangler = DemanglerFactory.createDemangler();  
            // 2.2.2. 对符号名进行 demangle 操作。  
            String demangledName = demangler.demangle(symbol.getName());  
            // 2.2.3. 计算当前地址相对于符号起始地址的偏移量。  
            long offset = address - symbol.getAddress();  
            // 2.2.4. 返回最详细的格式化字符串,例如:"[libnative.so] JNI_OnLoad + 0x10 (at 0x...)"  
            return String.format("[%s] %s + 0x%x (at 0x%x)", module.name, demangledName, offset, address);  
        } else {  
            // 2.3. 如果在模块内但没有找到具体的符号,则将其视为一个未命名的子程序(subroutine)。  
            //      计算地址相对于模块基地址的偏移量。  
            long offset = address - module.base;  
            // 2.3.1. 返回一个通用的子程序格式,例如:"[libnative.so] sub_c80 (at 0x...)"  
            return String.format("[%s] sub_%x (at 0x%x)", module.name, offset, address);  
        }  
    }  

    // 3. 如果地址不属于任何模块,则尝试查找它是否位于一个已知的内存区域中(例如,栈或堆)。  
    MemRegion region = emulator.getSvcMemory().findRegion(address);  
    if (region != null) {  
        // 3.1. 如果找到了,返回区域名称,例如:"[stack] (at 0x...)"  
        return String.format("[%s] (at 0x%x)", region.getName(), address);  
    }  

    // 4. 如果以上所有尝试都失败了,返回一个表示“未知”的通用格式。  
    return String.format("[unknown] (at 0x%x)", address);  
}

调用实例:

Debugger debugger = emulator.attach();  
System.out.println("函数调用关系追踪器已附加,结果将输出到日志文件。");  
debugger.traceFunctionCall(null, new FunctionCallListener() {  
    private int depth = 0;  
    private String getPrefix(int currentDepth) {  
        if (currentDepth <= 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (int i = 0; i < currentDepth - 1; i++) {  
            sb.append("│  ");  
        }  
        sb.append("├─ ");  
        return sb.toString();  
    }  
    @Override  
    public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {  
        String prefix = getPrefix(depth + 1);  
        String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  
        traceStream.printf("%sCALL -> %s%n", prefix, details);  
        depth++;  
    }  
    @Override  
    public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {  
        depth--;  
        String prefix = getPrefix(depth + 1);  
        String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  
        Backend backend = emulator.getBackend();  
        Number retVal = emulator.is64Bit() ? backend.reg_read(Arm64Const.UC_ARM64_REG_X0) : backend.reg_read(ArmConst.UC_ARM_REG_R0);  
        long retValLong = retVal.longValue();  
        // 尝试将返回值作为指针解析  
        String retValFormatted = String.format("0x%x", retValLong);  
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator, retValLong);  
        if (pointer != null) {  
            String cstring = safeReadCString(pointer);  
            // 如果是一个可打印的字符串,则附加到日志中  
            if (isPrintable(cstring)) {  
                retValFormatted += String.format(" -> \"%s\"", cstring);  
            }  
        }  
        traceStream.printf("%sRET  <- %s, ret=%s%n", prefix, details, retValFormatted);  
    }  
});

完整代码:  

package com.example.ndkdemo;  

import com.github.unidbg.*;  
import com.github.unidbg.arm.backend.Backend;  
import com.github.unidbg.arm.backend.CodeHook;  
import com.github.unidbg.arm.backend.UnHook;  
import com.github.unidbg.arm.backend.Unicorn2Factory;  
import com.github.unidbg.arm.context.Arm64RegisterContext;  
import com.github.unidbg.debugger.BreakPointCallback;  
import com.github.unidbg.debugger.Debugger;  
import com.github.unidbg.debugger.FunctionCallListener;  
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;  
import com.github.unidbg.linux.android.AndroidResolver;  
import com.github.unidbg.linux.android.dvm.DalvikModule;  
import com.github.unidbg.linux.android.dvm.DvmClass;  
import com.github.unidbg.linux.android.dvm.VM;  
import com.github.unidbg.memory.Memory;  
import com.github.unidbg.pointer.UnidbgPointer;  
import unicorn.Arm64Const;  
import unicorn.ArmConst;  

import java.io.*;  

public class MainActivity {  
    private final AndroidEmulator emulator;  
    private final VM vm;  
    private final Module module;  
    private final DvmClass security;  
    private final boolean logging;  
    private long instructionCount = 0; // 用于指令计数  

    MainActivity(boolean logging) {  
        this.logging = logging;  

        // 1. 创建模拟器实例  
        emulator = AndroidEmulatorBuilder.for64Bit()  
                .setProcessName("com.example.ndkdemo")  
                .addBackendFactory(new Unicorn2Factory(true))  
                .build();  

        // 2. 设置内存和系统库解析  
        final Memory memory = emulator.getMemory();  
        memory.setLibraryResolver(new AndroidResolver(23));  

        // 3. 创建Dalvik虚拟机并加载SO文件  
        vm = emulator.createDalvikVM();  
        vm.setVerbose(logging);  
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/libndkdemo.so"), false);  
        dm.callJNI_OnLoad(emulator);  
        module = dm.getModule();  

        // 4. 获取DVM类  
        security = vm.resolveClass("com/example/ndkdemo/MainActivity");  

        // 5. 设置指令计数钩子 (可选)  
        emulator.getBackend().hook_add_new(new CodeHook() {  
            @Override  
            public void hook(Backend backend, long address, int size, Object user) {  
                instructionCount++;  
            }  
            @Override  
            public void onAttach(UnHook unHook) {}  
            @Override  
            public void detach() {}  
        }, module.base, module.base + module.size, null);  

        // 6.设置函数追踪与x0寄存器检查  
        attachTraceAndInspectX0(module.base + 0x15F8);  
    }  

    /**  
     * 【模块化功能】: 附加一个函数调用追踪器 (traceFunctionCall)。  
     * 此功能会监听所有的函数调用,并以树状结构打印出调用关系。  
     * @param traceStream 日志输出流  
     */  
    public void attachFunctionCallTracer(final PrintStream traceStream) {  
        Debugger debugger = emulator.attach();  
        System.out.println("函数调用关系追踪器已附加,结果将输出到日志文件。");  

        debugger.traceFunctionCall(null, new FunctionCallListener() {  
            private int depth = 0;  

            private String getPrefix(int currentDepth) {  
                if (currentDepth <= 0) {  
                    return "";  
                }  
                StringBuilder sb = new StringBuilder();  
                for (int i = 0; i < currentDepth - 1; i++) {  
                    sb.append("│  ");  
                }  
                sb.append("├─ ");  
                return sb.toString();  
            }  

            @Override  
            public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {  
                String prefix = getPrefix(depth + 1);  
                String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  
                traceStream.printf("%sCALL -> %s%n", prefix, details);  
                depth++;  
            }  

            @Override  
            public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {  
                depth--;  
                String prefix = getPrefix(depth + 1);  
                String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  

                Backend backend = emulator.getBackend();  
                Number retVal = emulator.is64Bit() ? backend.reg_read(Arm64Const.UC_ARM64_REG_X0) : backend.reg_read(ArmConst.UC_ARM_REG_R0);  
                long retValLong = retVal.longValue();  

                // 尝试将返回值作为指针解析  
                String retValFormatted = String.format("0x%x", retValLong);  
                UnidbgPointer pointer = UnidbgPointer.pointer(emulator, retValLong);  
                if (pointer != null) {  
                    String cstring = safeReadCString(pointer);  
                    // 如果是一个可打印的字符串,则附加到日志中  
                    if (isPrintable(cstring)) {  
                        retValFormatted += String.format(" -> \"%s\"", cstring);  
                    }  
                }  

                traceStream.printf("%sRET  <- %s, ret=%s%n", prefix, details, retValFormatted);  
            }  
        });  
    }  

    /**  
     * 在指定地址设置断点,实现对一个函数调用的追踪,并在函数返回后检查 x0 寄存器的内容。  
     *  
     * @param callAddress 函数调用指令(例如 BL, B)的绝对地址  
     */  
    private void attachTraceAndInspectX0(long callAddress) {  
        final TraceHook[] traceHook = new TraceHook[1];  

        emulator.attach().addBreakPoint(callAddress, (emu, address) -> {  
            traceHook[0] = emu.traceCode(module.base, module.base + module.size);  
            return true;  
        });  

        long returnAddress = callAddress + 4;  
        emulator.attach().addBreakPoint(returnAddress, new BreakPointCallback() {  
            @Override  
            public boolean onHit(Emulator<?> emu, long address) {  
                if (traceHook[0] != null) {  
                    traceHook[0].stopTrace();  
                    System.out.println();  
                }  

                Arm64RegisterContext ctx = emu.getContext();  
                long x0 = ctx.getXLong(0);  
                System.out.printf("[+] 检查地址: 0x%x, x0寄存器值: 0x%x\n", address, x0);  

                UnidbgPointer pointer = UnidbgPointer.pointer(emu, x0);  
                if (pointer == null) {  
                    System.out.println("[-] x0的值不是一个有效的指针或指向未映射的内存");  
                    return true;  
                }  

                String cstring = safeReadCString(pointer);  
                if (cstring != null && isPrintable(cstring)) {  
                    System.out.println("[+] x0指向的字符串: " + cstring);  
                } else {  
                    System.out.println("[-] x0指向的内容不是一个可打印的字符串");  
                }  

                int dumpSize = 256;  
                byte[] data = pointer.getByteArray(0, dumpSize);  
                System.out.println("[+] x0指向内存的HexDump (前" + dumpSize + "字节):");  
                System.out.println(prettyHexDump(data, x0));  
                System.out.println("--- x0寄存器检查结束 ---\n");  

                return true;  
            }  
        });  
    }  

    private static String safeReadCString(UnidbgPointer p) {  
        try {  
            return p.getString(0);  
        } catch (Exception e) {  
            return null;  
        }  
    }  

    private static boolean isPrintable(String s) {  
        if (s == null || s.isEmpty()) {  
            return false;  
        }  
        int printableChars = 0;  
        for (int i = 0; i < s.length(); i++) {  
            char c = s.charAt(i);  
            if ((c >= 32 && c <= 126) || Character.isWhitespace(c)) {  
                printableChars++;  
            }  
        }  
        return s.length() >= 2 && (double) printableChars / s.length() > 0.8;  
    }  

    private static String prettyHexDump(byte[] data, long baseAddr) {  
        StringBuilder sb = new StringBuilder();  
        for (int i = 0; i < data.length; i += 16) {  
            sb.append(String.format("%016x: ", baseAddr + i));  
            StringBuilder hexPart = new StringBuilder();  
            StringBuilder asciiPart = new StringBuilder();  
            for (int j = 0; j < 16; j++) {  
                if (i + j < data.length) {  
                    byte b = data[i + j];  
                    hexPart.append(String.format("%02x ", b));  
                    char c = (b >= 32 && b <= 126) ? (char) b : '.';  
                    asciiPart.append(c);  
                } else {  
                    hexPart.append("   ");  
                }  
                if (j == 7) {  
                    hexPart.append(" ");  
                }  
            }  
            sb.append(hexPart).append(" |").append(asciiPart).append("|\n");  
        }  
        return sb.toString();  
    }  

    private void crack() {  
        emulator.traceCode();  
        boolean result = security.callStaticJniMethodBoolean(emulator, "check", "1234567");  
        System.out.println("函数调用结束,返回结果: " + result);  
    }  

    void destroy() {  
        try {  
            emulator.close();  
            if (logging) {  
                System.out.println("模拟器已成功关闭");  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) {  
        MainActivity test = new MainActivity(false);  
        String traceFile = "unidbg-android/src/test/resources/traceFunctions.txt";  
        try (PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true)) {  
            // 在调用 crack 之前附加追踪器,持久化日志  
            test.attachFunctionCallTracer(traceStream);  
            // 执行会触发函数调用的模拟代码  
            test.crack();  
        } catch (IOException e) {  
            e.printStackTrace();  
        } finally {  
            // 确保模拟器资源被释放  
            test.destroy();  
            System.out.println("总共执行ARM指令数: " + test.instructionCount);  
        }  
    }  
}

二. Mcp 配置

MCP概念:
MCP(Model Context Protocol,模型上下文协议)是什么?
MCP是一种标准化协议,旨在为人工智能模型(如大语言模型)与外部工具、数据源之间的交互提供统一接口。(为 AI 装上了手脚)
MCP是AI智能体与外部工具的"USB接口",定义了AI模型与外部工具(如API、数据库、文档编辑器等)的交互标准,开发者无需为每个工具单独开发适配代码。
MCP协议旨在实现大型语言模型(LLM)与外部数据源和工具之间的无缝集成,通过提供标准化的接口,使AI应用程序能够安全、可控地与本地或远程资源进行交互。
image.png

项目地址
要求:
1.IDA Pro 8.3 以上,最好是 9
2.python3.11 或更高版本,使用idapyswitch切换到Python 版本
3.支持 Mcp 的客户端,这里以 Cursor 为例  

环境配置:

pip install git+https://github.com/mrexodia/ida-pro-mcp

Ps: 如果你现有的 python 版本低于十一,可以单独下载一个版本然后安装到 ida 的目录下
image.png
接着打开 idapyswitch 切换 3.11 的版本
image.png
然后 cmd 窗口打开新安装的 python.exe 再运行 pip 命令  

D:\IDA\IDA9.0\python311\python.exe -m pip install --upgrade git+https://github.com/mrexodia/ida-pro-mcp

接下来,再运行命令:

ida-pro-mcp --install

这步如果报错,可以打开 11 版本 python 目录下的 Scripts 文件夹,找到 ida-pro-mcp.exe
Cmd 窗口运行:  

这里需要你自己拖动exe文件到cmd窗口
D:\IDA\IDA9.0\python311\Scripts\ida-pro-mcp.exe --install

最后运行:  

ida-pro-mcp --config
报错则运行下面这个:
D:\IDA\IDA9.0\python311\Scripts\ida-pro-mcp.exe --config

image.png
接下来打开 cursor,发现 ida_pro_mcp 已经亮绿灯了,说明 mcp 服务正常了。如果没有你就复制 ida-pro-mcp --config 输出的那串 json 到 mcp.json
image.png
image.png
最后在 IDA 中将 server 端开启即可。
image.png

三.Unidbg补环境

定义:
“Unidbg 补环境”是指在使用 Unidbg 模拟执行 Android 原生 (Native) 代码时,为了使程序能够正常运行并获得与真实设备一致的结果,对 Unidbg 模拟器环境中的缺失或不完善部分进行补充和调整的过程

一. 补 Jni 环境

一、 JNI 补环境的核心概念

JNI (Java Native Interface) 补环境是 Unidbg 应用中的核心环节。当 Unidbg 模拟执行的原生库(. So 文件)尝试通过 JNI 调用 Java 层的代码时,Unidbg 必须能够提供这些 Java 方法的模拟实现。如果 Unidbg 缺少某个方法的实现,或者默认实现不符合预期,程序就会抛出 UnsupportedOperationException 异常,导致模拟中断。因此,JNI 补环境的目的就是拦截这些 JNI 调用,并提供一个符合目标 SO 文件逻辑预期的返回值或行为,从而“欺骗”SO 文件,使其认为自己运行在真实的 Android 环境中。(本质就是缺啥补啥)

二、 Java 层函数补全:继承 AbstractJni

这是最主要的 Java 层补环境方式。通过创建一个继承自com.github.unidbg.linux.android.dvm.AbstractJni 的自定义类,并重写其关键方法,可以模拟任意 JNI 调用。
具体步骤:

  1. extends AbstractJni
    public class ChallengeTenUnidbg extends AbstractJni {
    }
  2. 关联 Jni 类与虚拟机 (VM)
    • 在创建了 DalvikVM 实例后,将 AbstractJni的实例设置给它。
    • 强烈建议开启详细日志 (vm.setVerbose(true)),这会在控制台打印出所有 JNI 调用的详细信息(包括方法签名),是定位哪个方法需要补全的关键。
      VM vm = emulator.createDalvikVM();
      vm.setJni(this); 
      vm.setVerbose(true); // 开启详细日志
  3. 重写关键方法以拦截 JNI 调用
    • 根据 SO 文件调用的 Java 方法类型(静态/实例、返回类型),重写 AbstractJni 中对应的方法。
    • 方法命名规律call[Static]XXXMethodV,其中 Static 表示静态方法,XXX 表示返回类型(如 Object, Boolean, Int, Void 等)。
    • 核心参数
      • signature: 完整的方法签名字符串,如 "com/example/MyClass->getAppContext()Landroid/content/Context;",用于 switchif 判断具体是哪个方法被调用。
      • vaList: 包含了调用时传递的所有参数。
JNI 原型 AbstractJni 覆写方法 典型用途
jobject CallStaticObjectMethodV callStaticObjectMethodV 返回 Context, Application
jint CallIntMethodV callIntMethodV 返回版本号、随机数
jobject NewObjectV newObjectV 构造 ZipFile, Cipher
void Set<Object>Field setObjectField SO 写回 Java 字段
jint RegisterNatives invokeRegisterNatives 动态注册补环境
jint NewGlobalRef newGlobalRef / deleteGlobalRef 线程间对象共享
  1. 方法实现示例

    • 模拟静态方法 (callStaticObjectMethodV)

      @Override
      public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
          switch (signature) {
              case "com/example/MyClass->getAppContext()Landroid/content/Context;":
                  // 返回一个模拟的 Context 对象
                  return vm.resolveClass("android/content/Context").newObject(null);
              // 可以添加更多 case 来处理其他需要补全的 Java 方法
              default:
                  // 对于未处理的方法,务必调用父类的默认实现
                  return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
          }
      }
    • 模拟构造方法 (newObjectV)

      @Override  
      public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {  
      switch (signature) {  
      case "java/util/HashMap-><init>()V": {  
          return ProxyDvmObject.createObject(vm, new HashMap<>());  
      }  
      
      case "com/zj/wuaipojie/ui/ChallengeTen$UserInfo-><init>(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;Ljava/util/Map;)V": {  
          System.out.println("【补环境 Level 3】拦截到 UserInfo 构造方法");  
          Map<String, DvmObject<?>> userInfoData = new HashMap<>();  
          userInfoData.put("status", vaList.getObjectArg(4));  
          userInfoData.put("properties", vaList.getObjectArg(5));  
          return dvmClass.newObject(userInfoData);  
      }  
      }  
      return super.newObjectV(vm, dvmClass, signature, vaList);  
      }

      模拟字段访问 (Get/Set<Type>Field)

public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {  
    // 匹配枚举类的 PREMIUM 静态字段  
    if ("com/zj/wuaipojie/ui/ChallengeTen$AccountStatus->PREMIUM:Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;".equals(signature)) {  
        System.out.println("【补环境】拦截到获取 AccountStatus.PREMIUM 静态字段");  
        // 创建一个枚举实例(用字符串"PREMIUM"作为其值,方便后续name()方法返回正确结果)  
        DvmObject<?> premium = dvmClass.newObject("PREMIUM");  
        return premium;  
    }  
    return super.getStaticObjectField(vm, dvmClass, signature);  
}

@Override
public void setIntField(BaseVM vm, DvmObject<?> dvmObject, String signature, int value) {
    // signature的格式是:com/example/User->age:I
    if ("com/example/User->age:I".equals(signature)) {
        System.out.println("SO 正在设置 User 对象的 age 字段,值为: " + value);
        // 你可以在这里记录值,或者什么都不做
        return; // 注意set方法是void返回
    }
    super.setIntField(vm, dvmObject, signature, value);
}
  1. 处理不同类型的对象
    • Android 特有类 (如 Context, Application): 使用 vm.resolveClass(className).newObject(value) 创建模拟对象。
    • JDK 标准库类 (如 HashMap): 使用 ProxyDvmObject.createObject(vm, realJavaObject) 来包装一个真实的 Java 对象进行模拟。这在处理集合类(如 Map)时非常有用。
      • 对于 JNI 来说,继承和多态是非常重要的概念。当 Native 代码通过一个父类或接口的引用来调用子类实例的方法时(例如通过 Map 引用调用 HashMap 实例的 put 方法),JNI 的运行时必须能够顺着继承链向上查找,才能找到正确的方法定义和 ID。
三、处理复杂 JNI 数据结构

处理复杂的数据结构(如结构体、数组)是 JNI 补环境的难点和重点。
1. 数组 byte[]String[]int[]

  • 在加密、解密等协议分析场景中非常常见。
  • Unidbg 的 DvmObjectArrayObject 类可以帮助在 Java 的数组 和 Native 层的内存指针之间进行转换。
    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    if ("com/example/Utils->getSignKeys()[Ljava/lang/String;".equals(signature)) {
        // 伪造的Key列表
        StringObject key1 = new StringObject(vm, "key_alpha");
        StringObject key2 = new StringObject(vm, "key_beta");
        // 创建一个包含这些DvmObject的ArrayObject
        return new ArrayObject(key1, key2);
    }
    return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }

    2.结构体 (Struct)

  • 通过 jobject 传递:如果 Java 层有一个类与 C/C++ 结构体对应,Native 代码会通过 JNI 的 Get/Set<Type>Field 系列函数访问字段。在 Unidbg 中,需要模拟这些 JNI 调用,使用DvmObject 提供的 setValuegetValue 方法来操作模拟对象的字段。
  • 通过指针传递:Native 函数有时直接接收或返回一个指向结构体内存的指针(通常是 long 类型)。
    • 补全逻辑
      1. 使用emulator.getMemory().malloc(size, true) 在 Unidbg 的模拟内存中分配空间。
      2. 使用返回的UnidbgPointer 对象,通过 pointer.setInt(offset, value)pointer.setString(offset, value) 等方法填充结构体的各个成员。
      3. 将这个指针的地址(一个long 值)返回给调用方。
        3.Map 等集合对象
  • 在 JNI 调用时,可以通过ProxyDvmObject.createObject(vm, realJavaMap) 将一个真实的 Java HashMap 对象包装成 DvmObject 传给 Native 层 。
  • 当 Native 代码通过 JNI 调用Map.getMap.keySet 等方法时,Unidbg 会拦截到这些调用。你需要在自定义的 AbstractJni 子类中实现这些方法的逻辑,直接操作那个真实的 realJavaMap 对象来返回正确的值。

四. 小结

在本次课程中,我们系统性地学习了 Unidbg 逆向分析中的两项核心技术和 mcp 的配置与辅助分析:

  1. unidbg_trace 精准追踪技术:我们掌握了如何运用指令与函数追踪,对目标算法的执行流程进行深度分析,从而有效应对代码混淆与动态跳转等复杂场景。
  2. MCP 服务与 AI 辅助分析:通过配置 MCP 服务,我们实现了逆向工具与人工智能模型的联动。这项技术代表了逆向工程领域的发展方向,能够显著加速对复杂算法的理解与重构,从而提升分析效率。
  3. Unidbg 的 JNI 环境补全:我们深入探讨了 JNI 补环境的原理与实践,掌握了通过继承 AbstractJni 模拟 Java 层调用的关键方法。这是确保原生库在 Unidbg 中稳定运行的技术基础,也是高级应用场景中必须具备的核心能力。

本节完整代码:

package com.zj.wuaipojie.util;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;

import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;

public class ChallengeTenOne extends AbstractJni {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    public ChallengeTenOne() {
        emulator = AndroidEmulatorBuilder.for64Bit().build();
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(true);

        File soFile = new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so");
        DalvikModule dm = vm.loadLibrary(soFile, true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }

    public void callUnidbgLevel1() {
        System.out.println("====== 开始执行 unidbg_level1 函数 ======");
        DvmClass securityUtilClass = vm.resolveClass("com/zj/wuaipojie/util/SecurityUtil");
        DvmObject<?> configObject = vm.resolveClass("com/zj/wuaipojie/util/SecurityUtil$Config").newObject(null);
        //注册Map和HashMap,并声明二者的关系
        DvmClass mapClass=vm.resolveClass("java/util/Map");
        DvmClass hashMapClass= vm.resolveClass("java/util/HashMap",mapClass);
        StringObject result = securityUtilClass.callStaticJniMethodObject(emulator,
                "unidbg_level1(Lcom/zj/wuaipojie/util/SecurityUtil$Config;)Ljava/lang/String;",
                configObject);
        System.out.println("====== 函数执行完毕 ======");
        System.out.println("JNI 函数返回结果: " + result);

    }

    public static void main(String[] args) throws FileNotFoundException {
        ChallengeTenOne challenge = new ChallengeTenOne();
        challenge.callUnidbgLevel1();
    }

    /**
     * 拦截静态方法调用 (对应 C++ 关卡 1)
     */
    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        if ("com/zj/wuaipojie/ui/ChallengeTen->getLevel1Key()Ljava/lang/String;".equals(signature)) {
            System.out.println("【补环境 Level 1】拦截到静态方法调用: " + signature);
            // C++ 代码期望得到 "InitialKey_From_Kotlin"
            return new StringObject(vm, "InitialKey_From_Kotlin");
        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }

    /**
     * 拦截实例字段读取 (对应 C++ 关卡 2 和 4)
     */
    @Override
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        switch (signature) {
            case "com/zj/wuaipojie/util/SecurityUtil$Config->deviceId:Ljava/lang/String;": {
                System.out.println("【补环境 Level 2】拦截到获取实例字段: " + signature);
                // C++ 代码期望得到 "unidbg_patched_device"
                return new StringObject(vm, "unidbg_patched_device");
            }
            // 将 UserInfo 的字段处理逻辑合并,使其更优雅和可扩展
            case "com/zj/wuaipojie/ui/ChallengeTen$UserInfo->status:Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;":
            case "com/zj/wuaipojie/ui/ChallengeTen$UserInfo->properties:Ljava/util/Map;": {
                // 这是我们为 UserInfo 对象设计的核心:从其后端存储 (一个Map) 中获取字段
                Map<String, DvmObject<?>> userInfoData = (Map<String, DvmObject<?>>) dvmObject.getValue();

                // 从签名中动态解析字段名 ("status" 或 "properties")
                String fieldName = signature.substring(signature.indexOf("->") + 2, signature.indexOf(":"));
                System.out.println("【补环境 Level 4】获取 UserInfo." + fieldName + " 字段 (从内部Map中)");

                return userInfoData.get(fieldName);
            }
        }
        return super.getObjectField(vm, dvmObject, signature);
    }

    /**
     * 拦截静态对象字段获取(对应 C++ 中获取 AccountStatus.PREMIUM 枚举实例)
     */
    @Override
    public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        // 匹配枚举类的 PREMIUM 静态字段
        if ("com/zj/wuaipojie/ui/ChallengeTen$AccountStatus->PREMIUM:Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;".equals(signature)) {
            System.out.println("【补环境】拦截到获取 AccountStatus.PREMIUM 静态字段");
            // 创建一个枚举实例(用字符串"PREMIUM"作为其值,方便后续name()方法返回正确结果)
            DvmObject<?> premium = dvmClass.newObject("PREMIUM");
            return premium;
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }

    /**
     * 拦截对象创建 (对应 C++ 关卡 3 中 new HashMap 和 new UserInfo)
     */
    @Override
    public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature) {
            case "java/util/HashMap-><init>()V": {
                return ProxyDvmObject.createObject(vm, new HashMap<>());
            }

            case "com/zj/wuaipojie/ui/ChallengeTen$UserInfo-><init>(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;Ljava/util/Map;)V": {
                System.out.println("【补环境 Level 3】拦截到 UserInfo 构造方法");
                Map<String, DvmObject<?>> userInfoData = new HashMap<>();
                userInfoData.put("status", vaList.getObjectArg(4));
                userInfoData.put("properties", vaList.getObjectArg(5));
                return dvmClass.newObject(userInfoData);
            }
        }
        return super.newObjectV(vm, dvmClass, signature, vaList);
    }

    /**
     * 拦截实例方法调用 (对应 C++ 关卡 3 和 4 中 map.put/get 和 status.name)
     */
    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {

        switch (signature) {
            // 处理自定义Map的put方法
            case "java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;":{
                Map map = (Map) dvmObject.getValue();
                Object key = vaList.getObjectArg(0).getValue();
                Object value = vaList.getObjectArg(1).getValue();
                return ProxyDvmObject.createObject(vm, map.put(key, value));
            }

            case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;": {
                Map map = (Map) dvmObject.getValue();
                Object key = vaList.getObjectArg(0).getValue();
                Object val = map.get(key);
                return ProxyDvmObject.createObject(vm, val);
            }

            // 处理 AccountStatus.name()
            case "com/zj/wuaipojie/ui/ChallengeTen$AccountStatus->name()Ljava/lang/String;": {
                System.out.println("【补环境 Level 4】拦截到 AccountStatus.name() 调用");
                return new StringObject(vm, dvmObject.getValue().toString());
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

}


```



四、请作者喝杯咖啡

图片

六、视频及课件地址

百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压

七、其他章节

《安卓逆向这档事》一、模拟器环境搭建
《安卓逆向这档事》二、初识APK文件结构、双开、汉化、基础修改
《安卓逆向这档事》三、初识smail,vip终结者
《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡
《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩
《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向
《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-Xposed快速上手(上)模块编写,常用Api
《安卓逆向这档事》八、Sorry,会Hook真的可以为所欲为-xposed快速上手(下)快速hook
《安卓逆向这档事》九、密码学基础、算法自吐、非标准加密对抗
《安卓逆向这档事》十、不是我说,有了IDA还要什么女朋友?
《安卓逆向这档事》十二、大佬帮我分析一下
《安卓逆向这档事》番外实战篇1-某电影视全家桶
《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)
《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)
《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)
《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)
《安卓逆向这档事》十七、你的RPCvs佬的RPC
《安卓逆向这档事》番外实战篇2-【2024春节】解题领红包活动,启动!
《安卓逆向这档事》十八、表哥,你也不想你的Frida被检测吧!(上)
《安卓逆向这档事》十九、表哥,你也不想你的Frida被检测吧!(下)
《安卓逆向这档事》二十、抓包学得好,牢饭吃得饱(上)
《安卓逆向这档事》番外实战篇3-拨云见日之浅谈Flutter逆向
《安卓逆向这档事》第二十一课、抓包学得好,牢饭吃得饱(中)
《安卓逆向这档事》第二十二课、抓包学得好,牢饭吃得饱(下)
《安卓逆向这档事》第二十三课、黑盒魔法之Unidbg

八、参考文档

白龙unidbg教程

免费评分

参与人数 30威望 +1 吾爱币 +49 热心值 +30 收起 理由
fireflyga + 1 + 1 谢谢@Thanks!
Devil_Kiss + 1 + 1 谢谢@Thanks!
max2012 + 1 + 1 谢谢@Thanks!
T4DNA + 1 + 1 我很赞同!
longforus + 1 + 1 谢谢@Thanks!
温馨提示 + 1 + 1 热心回复!
lingyun011 + 1 + 1 用心讨论,共获提升!
leokyer + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
warobot + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
xueren114 + 1 + 1 热心回复!
jinzhao666 + 1 + 1 谢谢@Thanks!
BensonDC + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
kseds + 1 + 1 我很赞同!
世忘nb + 1 + 1 用心讨论,共获提升!
C0l0RS + 1 用心讨论,共获提升!
asdly1992 + 1 + 1 用心讨论,共获提升!
debug_cat + 2 + 1 我很赞同!
爱飞的猫 + 1 + 1 佬,补不完,补不完啊啊啊啊
350950537 + 1 + 1 热心回复!
ioyr5995 + 1 + 1 热心回复!
xiaodu3335 + 1 + 1 正己 正己
jiaokai + 1 + 1 启蒙老师,膜拜大神!
KKei + 1 + 1 我很赞同!
风子09 + 1 + 1 谢谢@Thanks!
qck + 1 + 1 谢谢@Thanks!
zhangxu888 + 1 + 1 我很赞同!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
0504yck + 1 谢谢@Thanks!
OVVO + 2 + 1 import 正己

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| 正己 发表于 2025-9-7 15:04 |楼主
好久没更新,水一章
推荐
 楼主| 正己 发表于 2025-9-15 22:37 |楼主
Sn0r1ax 发表于 2025-9-15 20:19
老大那种app免登录是怎么实现的啊,就登陆进去就是默认用户

简单的就是修改一下islogin的校验,复杂的就是抓包获取正常的用户信息,写入到apk里
推荐
天空宫阙 发表于 2025-9-7 16:31
大佬B站的视频啥时候更新呀,催更

点评

明天上视频  详情 回复 发表于 2025-9-7 20:06
推荐
yjsai 发表于 2025-9-7 15:25
大佬的风范,虽然看不懂,还是喜欢看这类文章
5#
buluo533 发表于 2025-9-7 15:28
已学,催更正己佬冲冲冲
6#
kemess 发表于 2025-9-7 17:22
大佬,膜拜!!!
7#
OVVO 发表于 2025-9-7 17:27
import 正己
8#
mulltf 发表于 2025-9-7 17:38
虽然看不懂,不过还是顶一个
9#
cyhcuichao 发表于 2025-9-7 18:32
老大整个夸克盘
10#
qck 发表于 2025-9-7 19:57
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-2-1 13:21

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表