12345下一页
返回列表 发新帖

《安卓逆向这档事》二十四、Unidbg之补完环境我就睡(上)

  [复制链接]

91

主题

3807

回帖

2万

积分

博士后

萌新

Rank: 8Rank: 8

金币
10363
好评
335
信誉
278

考神MT论坛帅哥MT论坛最佳新人MT论坛活跃会员

QQ
发表于 2025-9-20 09:34:26 | 显示全部楼层 | 阅读模式  来自 福建
本帖最后由 正己 于 2025-9-20 09:37 编辑




一、课程目标
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。                                                                                                           |
| **兼容性**  | 支持 **ARM32** 与 **ARM64**。<br>比多数指令追踪方案(如 Stalker 不完全支持 ARM32)更全面。  <br>相比 IDA Trace 对 X86、MIPS 等架构的支持仍有不足。                                                                                         |
| **展示效果** | 信息维度丰富:包括时间、绝对地址、模块、相对偏移、机器码、汇编代码、执行前后寄存器变化等。  <br>信息处理也很出色,细节优化多。                                                                                                                                 |
3.Trace实例

**1. 基础用法**
在调用目标函数前,只需简单地调用 `emulator.traceCode()` 即可开启对后续所有指令的追踪。
  1. java
  2. // 在调用目标函数前开启指令追踪
  3. emulator.traceCode();
  4. boolean result = security.callStaticJniMethodBoolean(emulator, "check", "123456");
复制代码

**2. 约束追踪范围**
通常我们只关心目标 SO 的执行流,而非 libc 等系统库的内部执行。`traceCode` 的重载方法 `traceCode(long begin, long end)` 允许我们精确设定追踪的内存地址范围。
  1. java
  2. // 获取目标模块对象
  3. Module module = dm.getModule();
  4. // 仅追踪目标模块地址范围内的指令
  5. emulator.traceCode(module.base, module.base + module.size);
  6. // 注意:第二个参数是结束地址,不是长度!这是一个常见误区。
复制代码

**3. 精确控制追踪时机**
  * **追踪 JNI\_OnLoad**: 将 `traceCode` 放在 `dm.callJNI_OnLoad(emulator)` 之前。
  * **追踪初始化函数 (init\_array)**: `JNI_OnLoad` 之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用 `ModuleListener`。
  1. java
  2. memory.addModuleListener(new ModuleListener() {
  3.     @Override
  4.     public void onLoaded(Emulator<?> emulator, Module module) {
  5.         // 当我们关心的模块被加载时,立即开启trace
  6.         if("lib52pojie.so".equals(module.name)){
  7.             emulator.traceCode(module.base, module.base + module.size);
  8.         }
  9.     }
  10. });
  11. // 这之后再加载模块
  12. DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so"), true);
复制代码

**4. 预估追踪耗时**
等待一个未知的 Trace 过程是痛苦的。我们可以通过一个轻量级的 `CodeHook` 快速统计指令总数,从而预估总耗时。这基本不含反汇编和寄存器打印的开销,所以速度极快。
  1. java
  2. private long instructionCount = 0; // 用于指令计数
  3. emulator.getBackend().hook_add_new(new CodeHook() {
  4.     @Override
  5.     public void hook(Backend backend, long address, int size, Object user) {
  6.         instructionCount++;
  7.     }
  8.     @Override
  9.     public void onAttach(UnHook unHook) {}
  10.     @Override
  11.     public void detach() {}
  12. }, module.base, module.base + module.size, null);
  13. System.out.println("总共执行ARM指令数: " + test.instructionCount);
复制代码

**PS:**:在较好的测试条件下,unidbg 每分钟约可追踪 40 万行执行流。你可以根据统计出的总行数来判断是泡杯咖啡等几分钟,还是需要去睡一觉等几个小时。
**5. 限定特定函数**
结合断点,可以实现对单个函数内部的精确追踪。
  * **场景一:只关心某次特定调用**
    在函数调用指令处下断点开启 Trace,在调用指令的下一条指令处下断点关闭 Trace。
  1. java
  2. long callAddr = module.base + 0xE53C; // 假设这是BL指令的地址
  3. final TraceHook[] traceHook = new TraceHook[1];
  4. // 在调用处开启追踪
  5. emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() {
  6.     @Override
  7.     public boolean onHit(Emulator<?> emulator, long address) {
  8.         traceHook[0] = emulator.traceCode(module.base, module.base + module.size);
  9.         return true;
  10.     }
  11. });
  12. // 在调用返回后关闭追踪
  13. emulator.attach().addBreakPoint(callAddr + 4, new BreakPointCallback() {
  14.     @Override
  15.     public boolean onHit(Emulator<?> emulator, long address) {
  16.         if (traceHook[0] != null) {
  17.             traceHook[0].stopTrace();
  18.         }
  19.         return true;
  20.     }
  21. });
复制代码


**6.函数调用追踪 (`debugger.traceFunctionCall`) 详解**
`traceFunctionCall` 是 unidbg 0.9.6 版本后新增的强大功能,用于追踪函数粒度的调用关系,对理清高层逻辑和对抗 OLLVM 等混淆有奇效。
**1. 基础用法**
它属于 `Debugger` 的一部分,使用时需要先 `attach()` 到模拟器。
  1. java
  2. // 获取调试器实例
  3. Debugger debugger = emulator.attach();
  4. // 追踪目标模块内的所有函数调用
  5. debugger.traceFunctionCall(module, new FunctionCallListener() {
  6.     @Override
  7.     public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {
  8.         // 函数调用前(相当于Frida onEnter)
  9.         System.out.println("onCall: " + UnidbgPointer.pointer(emulator, callerAddress) + " -> " + UnidbgPointer.pointer(emulator, functionAddress));
  10.     }
  11.     @Override
  12.     public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {
  13.         // 函数调用后(相当于Frida onLeave)
  14.         // 注意:这里的args是寄存器R0-R7(x0-x7)的值,不完全等同于函数参数
  15.     }
  16. });
复制代码

**2. 构建函数调用树**
通过获取栈回溯深度,我们可以格式化输出,形成一个清晰的函数调用树,这对于分析复杂的调用逻辑极其有用。
首先修改一下源码,路径:unidbg-api\src\main\java\com\github\unidbg\unwind
添加以下方法:
  1. java
  2. /**
  3. * 将给定的内存地址格式化为包含详细信息、可读的字符串。
  4. * 这个方法会尝试解析地址,并提供尽可能多的上下文信息,如模块名、函数名(符号)、偏移量等。
  5. *
  6. * @param address 要格式化的绝对内存地址。
  7. * [url=home.php?mod=space&uid=134981]@Return[/url] 一个包含地址详细信息的格式化字符串。
  8. */
  9. public String formatAddressDetails(long address) {
  10.     // 1. 尝试根据地址查找其所属的模块(例如,一个 .so 文件)。
  11.     Module module = emulator.getMemory().findModuleByAddress(address);

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

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

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

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

复制代码


调用实例:
  1. java
  2. Debugger debugger = emulator.attach();
  3. System.out.println("函数调用关系追踪器已附加,结果将输出到日志文件。");
  4. debugger.traceFunctionCall(null, new FunctionCallListener() {
  5.     private int depth = 0;
  6.     private String getPrefix(int currentDepth) {
  7.         if (currentDepth <= 0) {
  8.             return "";
  9.         }
  10.         StringBuilder sb = new StringBuilder();
  11.         for (int i = 0; i < currentDepth - 1; i++) {
  12.             sb.append("│  ");
  13.         }
  14.         sb.append("├─ ");
  15.         return sb.toString();
  16.     }
  17.     @Override
  18.     public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {
  19.         String prefix = getPrefix(depth + 1);
  20.         String details = emulator.getUnwinder().formatAddressDetails(functionAddress);
  21.         traceStream.printf("%sCALL -> %s%n", prefix, details);
  22.         depth++;
  23.     }
  24.     @Override
  25.     public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {
  26.         depth--;
  27.         String prefix = getPrefix(depth + 1);
  28.         String details = emulator.getUnwinder().formatAddressDetails(functionAddress);
  29.         Backend backend = emulator.getBackend();
  30.         Number retVal = emulator.is64Bit() ? backend.reg_read(Arm64Const.UC_ARM64_REG_X0) : backend.reg_read(ArmConst.UC_ARM_REG_R0);
  31.         long retValLong = retVal.longValue();
  32.         // 尝试将返回值作为指针解析
  33.         String retValFormatted = String.format("0x%x", retValLong);
  34.         UnidbgPointer pointer = UnidbgPointer.pointer(emulator, retValLong);
  35.         if (pointer != null) {
  36.             String cstring = safeReadCString(pointer);
  37.             // 如果是一个可打印的字符串,则附加到日志中
  38.             if (isPrintable(cstring)) {
  39.                 retValFormatted += String.format(" -> "%s"", cstring);
  40.             }
  41.         }
  42.         traceStream.printf("%sRET  <- %s, ret=%s%n", prefix, details, retValFormatted);
  43.     }
  44. });
复制代码

已有1人评分金币 理由
青春向上 + 1 感谢分享。

查看全部评分 总评分:金币 +1 

回复

使用道具 举报

91

主题

3807

回帖

2万

积分

博士后

萌新

Rank: 8Rank: 8

金币
10363
好评
335
信誉
278

考神MT论坛帅哥MT论坛最佳新人MT论坛活跃会员

QQ
发表于 2025-9-20 09:38:17 | 显示全部楼层  来自 福建
剩下内容放这里
完整代码:
  1. java
  2. package com.example.ndkdemo;

  3. import com.github.unidbg.*;
  4. import com.github.unidbg.arm.backend.Backend;
  5. import com.github.unidbg.arm.backend.CodeHook;
  6. import com.github.unidbg.arm.backend.UnHook;
  7. import com.github.unidbg.arm.backend.Unicorn2Factory;
  8. import com.github.unidbg.arm.context.Arm64RegisterContext;
  9. import com.github.unidbg.debugger.BreakPointCallback;
  10. import com.github.unidbg.debugger.Debugger;
  11. import com.github.unidbg.debugger.FunctionCallListener;
  12. import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
  13. import com.github.unidbg.linux.android.AndroidResolver;
  14. import com.github.unidbg.linux.android.dvm.DalvikModule;
  15. import com.github.unidbg.linux.android.dvm.DvmClass;
  16. import com.github.unidbg.linux.android.dvm.VM;
  17. import com.github.unidbg.memory.Memory;
  18. import com.github.unidbg.pointer.UnidbgPointer;
  19. import unicorn.Arm64Const;
  20. import unicorn.ArmConst;

  21. import java.io.*;

  22. public class MainActivity {
  23.     private final AndroidEmulator emulator;
  24.     private final VM vm;
  25.     private final Module module;
  26.     private final DvmClass security;
  27.     private final boolean logging;
  28.     private long instructionCount = 0; // 用于指令计数

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

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

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

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

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

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


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

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

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

  71.             private String getPrefix(int currentDepth) {
  72.                 if (currentDepth <= 0) {
  73.                     return "";
  74.                 }
  75.                 StringBuilder sb = new StringBuilder();
  76.                 for (int i = 0; i < currentDepth - 1; i++) {
  77.                     sb.append("│  ");
  78.                 }
  79.                 sb.append("├─ ");
  80.                 return sb.toString();
  81.             }

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

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

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

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

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

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

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

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

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

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

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

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

  149.                 return true;
  150.             }
  151.         });
  152.     }

  153.     private static String safeReadCString(UnidbgPointer p) {
  154.         try {
  155.             return p.getString(0);
  156.         } catch (Exception e) {
  157.             return null;
  158.         }
  159.     }

  160.     private static boolean isPrintable(String s) {
  161.         if (s == null || s.isEmpty()) {
  162.             return false;
  163.         }
  164.         int printableChars = 0;
  165.         for (int i = 0; i < s.length(); i++) {
  166.             char c = s.charAt(i);
  167.             if ((c >= 32 && c <= 126) || Character.isWhitespace(c)) {
  168.                 printableChars++;
  169.             }
  170.         }
  171.         return s.length() >= 2 && (double) printableChars / s.length() > 0.8;
  172.     }

  173.     private static String prettyHexDump(byte[] data, long baseAddr) {
  174.         StringBuilder sb = new StringBuilder();
  175.         for (int i = 0; i < data.length; i += 16) {
  176.             sb.append(String.format("%016x: ", baseAddr + i));
  177.             StringBuilder hexPart = new StringBuilder();
  178.             StringBuilder asciiPart = new StringBuilder();
  179.             for (int j = 0; j < 16; j++) {
  180.                 if (i + j < data.length) {
  181.                     byte b = data[i + j];
  182.                     hexPart.append(String.format("%02x ", b));
  183.                     char c = (b >= 32 && b <= 126) ? (char) b : '.';
  184.                     asciiPart.append(c);
  185.                 } else {
  186.                     hexPart.append("   ");
  187.                 }
  188.                 if (j == 7) {
  189.                     hexPart.append(" ");
  190.                 }
  191.             }
  192.             sb.append(hexPart).append(" |").append(asciiPart).append("|\n");
  193.         }
  194.         return sb.toString();
  195.     }

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

  201.     void destroy() {
  202.         try {
  203.             emulator.close();
  204.             if (logging) {
  205.                 System.out.println("模拟器已成功关闭");
  206.             }
  207.         } catch (IOException e) {
  208.             e.printStackTrace();
  209.         }
  210.     }

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


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


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

`环境配置:`

  1. pip install git+https://github.com/mrexodia/ida-pro-mcp
复制代码

Ps: 如果你现有的 python 版本低于十一,可以单独下载一个版本然后安装到 ida 的目录下

接着打开 `idapyswitch` 切换 3.11 的版本

然后 cmd 窗口打开新安装的 python.exe 再运行 pip 命令

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

接下来,再运行命令:

  1. ida-pro-mcp --install
复制代码

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

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

最后运行:

  1. ida-pro-mcp --config
  2. 报错则运行下面这个:
  3. D:\IDA\IDA9.0\python311\Scripts\ida-pro-mcp.exe --config
复制代码


接下来打开 cursor,发现 ida_pro_mcp 已经亮绿灯了,说明 mcp 服务正常了。如果没有你就复制 ida-pro-mcp --config 输出的那串 json 到 mcp.json


最后在 IDA 中将 server 端开启即可。


三.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**:
   
  1. java
  2.     public class ChallengeTenUnidbg extends AbstractJni {
  3.     }
  4.    
复制代码

2. **关联 Jni 类与虚拟机 (VM)** :
    - 在创建了 `DalvikVM` 实例后,将 AbstractJni的实例设置给它。
    - **强烈建议开启详细日志** (`vm.setVerbose(true)`),这会在控制台打印出所有 JNI 调用的详细信息(包括方法签名),是定位哪个方法需要补全的关键。
   
  1. java
  2.     VM vm = emulator.createDalvikVM();
  3.     vm.setJni(this);
  4.     vm.setVerbose(true); // 开启详细日志
  5.    
复制代码

3. **重写关键方法以拦截 JNI 调用**:
    - 根据 SO 文件调用的 Java 方法类型(静态/实例、返回类型),重写 `AbstractJni` 中对应的方法。
    - **方法命名规律**:`call[Static]XXXMethodV`,其中 `Static` 表示静态方法,`XXX` 表示返回类型(如 `Object`, `Boolean`, `Int`, `Void` 等)。
    - **核心参数**:
        - `signature`: 完整的方法签名字符串,如 `"com/example/MyClass->getAppContext()Landroid/content/Context;"`,用于 `switch` 或 `if` 判断具体是哪个方法被调用。
        - `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` | 线程间对象共享                       |

4. **方法实现示例**:

    - **模拟静态方法 (`callStaticObjectMethodV`)** :
      
  1. java
  2.         @Override
  3.         public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
  4.             switch (signature) {
  5.                 case "com/example/MyClass->getAppContext()Landroid/content/Context;":
  6.                     // 返回一个模拟的 Context 对象
  7.                     return vm.resolveClass("android/content/Context").newObject(null);
  8.                 // 可以添加更多 case 来处理其他需要补全的 Java 方法
  9.                 default:
  10.                     // 对于未处理的方法,务必调用父类的默认实现
  11.                     return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
  12.             }
  13.         }
  14.         
复制代码

    - **模拟构造方法 (`newObjectV`)** :
      
  1. java
  2.         @Override
  3. public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
  4.     switch (signature) {
  5.         case "java/util/HashMap-><init>()V": {
  6.             return ProxyDvmObject.createObject(vm, new HashMap<>());
  7.         }

  8.         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": {
  9.             System.out.println("【补环境 Level 3】拦截到 UserInfo 构造方法");
  10.             Map<String, DvmObject<?>> userInfoData = new HashMap<>();
  11.             userInfoData.put("status", vaList.getObjectArg(4));
  12.             userInfoData.put("properties", vaList.getObjectArg(5));
  13.             return dvmClass.newObject(userInfoData);
  14.         }
  15.     }
  16.     return super.newObjectV(vm, dvmClass, signature, vaList);
  17. }
  18.         
复制代码

    - **模拟字段访问 (`Get/Set<Type>Field`)** :
  1. java
  2. @Override
  3. public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
  4.     // 匹配枚举类的 PREMIUM 静态字段
  5.     if ("com/zj/wuaipojie/ui/ChallengeTen$AccountStatus->PREMIUM:Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;".equals(signature)) {
  6.         System.out.println("【补环境】拦截到获取 AccountStatus.PREMIUM 静态字段");
  7.         // 创建一个枚举实例(用字符串"PREMIUM"作为其值,方便后续name()方法返回正确结果)
  8.         DvmObject<?> premium = dvmClass.newObject("PREMIUM");
  9.         return premium;
  10.     }
  11.     return super.getStaticObjectField(vm, dvmClass, signature);
  12. }

  13. @Override
  14. public void setIntField(BaseVM vm, DvmObject<?> dvmObject, String signature, int value) {
  15.     // signature的格式是:com/example/User->age:I
  16.     if ("com/example/User->age:I".equals(signature)) {
  17.         System.out.println("SO 正在设置 User 对象的 age 字段,值为: " + value);
  18.         // 你可以在这里记录值,或者什么都不做
  19.         return; // 注意set方法是void返回
  20.     }
  21.     super.setIntField(vm, dvmObject, signature, value);
  22. }
复制代码
回复

使用道具 举报

91

主题

3807

回帖

2万

积分

博士后

萌新

Rank: 8Rank: 8

金币
10363
好评
335
信誉
278

考神MT论坛帅哥MT论坛最佳新人MT论坛活跃会员

QQ
发表于 2025-9-20 09:39:20 | 显示全部楼层  来自 福建
5. **处理不同类型的对象**:
    - **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 的 `DvmObject` 和 `ArrayObject` 类可以帮助在 Java 的数组 和 Native 层的内存指针之间进行转换。
  1. java
  2. @Override
  3. public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
  4.     if ("com/example/Utils->getSignKeys()[Ljava/lang/String;".equals(signature)) {
  5.         // 伪造的Key列表
  6.         StringObject key1 = new StringObject(vm, "key_alpha");
  7.         StringObject key2 = new StringObject(vm, "key_beta");
  8.         // 创建一个包含这些DvmObject的ArrayObject
  9.         return new ArrayObject(key1, key2);
  10.     }
  11.     return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
  12. }
复制代码

**2.结构体 (Struct)**
- **通过 `jobject` 传递**:如果 Java 层有一个类与 C/C++ 结构体对应,Native 代码会通过 JNI 的 `Get/Set<Type>Field` 系列函数访问字段。在 Unidbg 中,需要模拟这些 JNI 调用,使用`DvmObject` 提供的 `setValue` 和 `getValue` 方法来操作模拟对象的字段。
- **通过指针传递**: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.get`、`Map.keySet` 等方法时,Unidbg 会拦截到这些调用。你需要在自定义的 `AbstractJni` 子类中实现这些方法的逻辑,直接操作那个真实的 `realJavaMap` 对象来返回正确的值。

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


本节完整代码:

  1. java
  2. package com.zj.wuaipojie.util;

  3. import com.github.unidbg.AndroidEmulator;
  4. import com.github.unidbg.Module;
  5. import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
  6. import com.github.unidbg.linux.android.AndroidResolver;
  7. import com.github.unidbg.linux.android.dvm.*;
  8. import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
  9. import com.github.unidbg.memory.Memory;

  10. import java.io.File;
  11. import java.io.FileNotFoundException;
  12. import java.util.HashMap;
  13. import java.util.Map;

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

  16. public class ChallengeTenOne extends AbstractJni {

  17.     private final AndroidEmulator emulator;
  18.     private final VM vm;
  19.     private final Module module;

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

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

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

  44.     }

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

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

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

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

  80.                 return userInfoData.get(fieldName);
  81.             }
  82.         }
  83.         return super.getObjectField(vm, dvmObject, signature);
  84.     }

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

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

  108.             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": {
  109.                 System.out.println("【补环境 Level 3】拦截到 UserInfo 构造方法");
  110.                 Map<String, DvmObject<?>> userInfoData = new HashMap<>();
  111.                 userInfoData.put("status", vaList.getObjectArg(4));
  112.                 userInfoData.put("properties", vaList.getObjectArg(5));
  113.                 return dvmClass.newObject(userInfoData);
  114.             }
  115.         }
  116.         return super.newObjectV(vm, dvmClass, signature, vaList);
  117.     }

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

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

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

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



  145. }

复制代码
回复

使用道具 举报

123

主题

3512

回帖

1万

积分

博士生

●一只菜鸡●

Rank: 7Rank: 7Rank: 7

金币
3509
好评
85
信誉
122

考神MT论坛新人MT论坛最佳新人MT论坛帅哥MT论坛活跃会员

发表于 2025-9-20 09:35:19 来自手机  | 显示全部楼层  来自 辽宁
回复

使用道具 举报

43

主题

2043

回帖

5540

积分

硕士生

喵~

Rank: 6Rank: 6

金币
856
好评
7
信誉
103

考神MT论坛新人MT论坛最佳新人

QQ
发表于 2025-9-20 09:35:34 来自手机  | 显示全部楼层  来自 广西
看看隐藏喵~
回复

使用道具 举报

78

主题

2431

回帖

9829

积分

硕士生

Rank: 6Rank: 6

金币
4159
好评
57
信誉
116

MT论坛新人考神

QQ
发表于 2025-9-20 09:35:42 来自手机  | 显示全部楼层  来自 江苏
诈尸啦
回复

使用道具 举报

14

主题

1721

回帖

5272

积分

硕士生

春日野穹

Rank: 6Rank: 6

金币
3164
好评
4
信誉
103

MT论坛新人考神MT论坛帅哥MT论坛最佳新人

发表于 2025-9-20 09:36:20 来自手机  | 显示全部楼层  来自 甘肃
看看
回复

使用道具 举报

5

主题

1万

回帖

3万

积分

博士后

萌新

Rank: 8Rank: 8

金币
3415
好评
51
信誉
101

考神MT论坛最佳新人MT论坛帅哥MT论坛新人MT论坛活跃会员

发表于 2025-9-20 09:42:14 来自手机  | 显示全部楼层  来自 广东
感谢分享
回复

使用道具 举报

341

主题

2336

回帖

7958

积分

硕士生

Rank: 6Rank: 6

金币
1046
好评
9
信誉
101
发表于 2025-9-20 09:44:46 来自手机  | 显示全部楼层  来自 浙江
睡了吗?
回复

使用道具 举报

162

主题

8064

回帖

2万

积分

博士后

滑稽

Rank: 8Rank: 8

金币
953
好评
474
信誉
210

MT论坛新人MT论坛最佳新人MT论坛活跃会员考神MT论坛帅哥MT论坛灌水老大

QQ
发表于 2025-9-20 09:44:55 来自手机  | 显示全部楼层  来自 福建
看看
回复

使用道具 举报

402

主题

2858

回帖

1万

积分

博士生

Rank: 7Rank: 7Rank: 7

金币
5047
好评
148
信誉
100

MT论坛新人MT论坛最佳新人MT论坛帅哥考神MT论坛活跃会员MT论坛侠客

发表于 2025-9-20 09:47:55 来自手机  | 显示全部楼层  来自 四川
前排支持大佬
回复

使用道具 举报

36

主题

6072

回帖

1万

积分

博士生

水博士

Rank: 7Rank: 7Rank: 7

金币
4912
好评
7
信誉
97
发表于 2025-9-20 09:53:41 来自手机  | 显示全部楼层  来自 北京
大佬重出江湖
回复

使用道具 举报

54

主题

3588

回帖

1万

积分

博士生

Rank: 7Rank: 7Rank: 7

金币
789
好评
190
信誉
180

MT论坛新人MT论坛最佳新人MT论坛帅哥考神MT论坛活跃会员挂机大佬

发表于 2025-9-20 09:56:07 来自手机  | 显示全部楼层  来自 陕西
感谢分享。
回复

使用道具 举报

128

主题

2542

回帖

1万

积分

博士生

q1592131045

Rank: 7Rank: 7Rank: 7

金币
1393
好评
66
信誉
102

考神MT论坛最佳新人MT论坛活跃会员MT论坛灌水老大MT论坛新人MT论坛侠客MT论坛帅哥

QQ
发表于 2025-9-20 09:59:55 来自手机  | 显示全部楼层  来自 重庆
回复看看
回复

使用道具 举报

42

主题

1292

回帖

4738

积分

大学生

Rank: 5Rank: 5

金币
2207
好评
5
信誉
102

考神MT论坛新人MT论坛帅哥MT论坛最佳新人

发表于 2025-9-20 10:00:04 来自手机  | 显示全部楼层  来自 湖南
感谢分享
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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