12345下一页
返回列表 发新帖

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

  [复制链接]

91

主题

3807

回帖

2万

积分

博士后

萌新

Rank: 8Rank: 8

金币
10363
好评
335
信誉
278

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

QQ
发表于 2025-11-8 16:06:13 | 显示全部楼层 | 阅读模式  来自 福建
本帖最后由 正己 于 2025-11-9 12:23 编辑


一、课程目标
1. 理解文件访问补环境的 `IOResolver` 机制,并掌握不同 `FileIO` 的应用场景
2. 掌握 `/proc/self/maps`、`/proc/self/status` 等关键文件的核心处理策略
3. 理解补齐库函数的原理,掌握使用 `Dobby` Hook `__system_property_get` 等底层函数以伪造系统属性的方法

二、工具

1.教程 Demo
2.IDEA
3.IDA
三、课程内容

一. 补文件访问
#一、文件访问补环境的核心概念
文件访问 (File Access) 补环境是 Unidbg 应用中仅次于 JNI 补环境的重要环节。当 Unidbg 模拟执行的原生库(. So 文件)尝试通过文件 I/O 操作访问文件系统时——例如读取设备信息 (`/proc/cpuinfo`)、检测运行环境 (`/proc/self/maps`、`/xbin/su`) 或校验自身完整性 (APK 文件)——Unidbg 必须能够对这些访问请求作出响应。在一个纯净的 Unidbg 环境中,这些被访问的目标文件通常是不存在的。如果一个关键的文件访问失败或返回了不符合预期的内容,原生库可能会改变其执行逻辑、触发风控机制或直接中断执行。因此,文件访问补环境的目的就是通过 Unidbg 的 `IOResolver` 责任链机制拦截这些文件系统调用,并提供一个内容、权限都符合原生库逻辑预期的虚拟文件或目录。
#二、核心机制:责任链模式与 `IOResolver`
Unidbg 并未将所有文件处理逻辑写死,而是采用了一种灵活的**责任链模式**。当代码尝试访问一个文件时,请求会依次通过一个处理器链,直到被成功处理或最终失败。
这个处理链的顺序通常是:
1. **用户自定义的 `IOResolver`**:你添加的文件处理器,后添加的拥有更高优先级。
2.  **`AndroidResolver`**:Unidbg 内置的默认处理器。
3.  **虚拟文件系统(VFS)**:处理映射到文件系统的真实文件。
这样的设计使得我们可以注入自定义逻辑来处理文件访问,而无需修改 Unidbg 源码,保证了项目的可移植性和环境的整洁性。


`添加自定义文件处理器:`
你可以通过 `emulator.getSyscallHandler().addIOResolver()` 来添加一个文件处理器。这个操作必须在加载 SO 库(loadLibrary)之前完成才能生效。
  1. // BiliIOResolver.java - 独立的处理器类
  2. public class BiliIOResolver implements IOResolver<AndroidFileIO> {
  3.     @Override
  4.     public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
  5.         // 打印所有文件访问请求,无论是否处理
  6.         System.out.println("File open request: " + pathname);
  7.         // 在这里添加具体的文件处理逻辑
  8.         return null; // 返回null表示未处理,交由下一个处理器
  9.     }
  10. }
  11. // Main.java - 在主类中添加处理器
  12. public class Bili extends AbstractJni {
  13.     public Bili() {
  14.         // ...模拟器初始化代码...
  15.         emulator.getSyscallHandler().addIOResolver(new BiliIOResolver());
  16.         // ...加载SO和后续操作...
  17.     }
  18. }
复制代码


#三、文件处理的三种返回结果
在 `resolve` 方法中,你可以返回一个 `FileResult` 对象,它有三种状态:
4.  `FileResult.success(FileIO)`: 表示文件访问成功,返回一个 `FileIO` 对象。这是最常用的方式。
5.  `FileResult.failed(errno)`: 表示文件访问失败,并返回一个指定的错误码,如 `UnixEmulator.EACCES` (权限不足)。
6.  `FileResult.fallback()`: 表示回退,只有当其他所有处理器都无法处理时,才会由它来处理。
#四、常规文件处理与 `FileIO` 对象
处理文件访问需要结合三方面的知识:Linux/Android 文件系统知识、对业务(风控/检测)的经验以及 Unidbg 的实现方法。
##1. 使用 `SimpleFileIO` 处理物理文件
当需要返回一个真实存在的文件时,`SimpleFileIO` 是最佳选择。你需要将文件从真机上导出,并放置在你的项目路径下。
**案例:补 `boot_id` 和 APK 文件**
`boot_id` 常用于生成设备指纹,而 APK 文件访问常用于签名校验或资源读取,两者都必须处理。
`boot_id` 文件在开机时生成,在设备关机前不会改变内容。我们将这个文件从真机上 push 出来
  1. C:\Users\zhengji>adb shell
  2. * daemon not running; starting now at tcp:5037
  3. * daemon started successfully
  4. vermeer:/ $ cat /proc/sys/kernel/random/boot_id
  5. c7de54b2-f238-481d-b8e1-41c05413b2cd
  6. vermeer:/ $ cp /proc/sys/kernel/random/boot_id /sdcard
  7. vermeer:/ $ exit

  8. C:\Users\zhengji>adb pull /sdcard/boot_id D:\unidbg-master\unidbg-android\src\test\resources
  9. /sdcard/boot_id: 1 file pulled, 0 skipped. 0.0 MB/s (37 bytes in 0.012s)
复制代码
  1. @Override
  2. public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
  3.     switch (pathname) {
  4.         // 注意:这里的路径需要和样本访问的完全一致
  5.         case "/proc/sys/kernel/random/boot_id":{
  6.                 return FileResult.<AndroidFileIO>success(new SimpleFileIO(oflags, new File("unidbg-android/src/test/resources/cpu/boot_id"), pathname));
  7.             }
  8.     }
  9.     return null;
  10. }
复制代码

##2. 使用 `ByteArrayFileIO` 动态生成文件内容
当文件内容需要动态生成或随机化时(例如躲避基于设备指纹的风控),`ByteArrayFileIO` 非常有用。它直接接收一个字节数组作为文件内容。
**案例:随机化 `boot_id` 和补 CPU 频率**
  1. @Override
  2. public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
  3.     switch (pathname) {
  4.         case "/proc/sys/kernel/random/boot_id": {
  5.             // 动态生成UUID作为boot_id
  6.             String randomBootId = UUID.randomUUID().toString();
  7.             return FileResult.<AndroidFileIO>success(new ByteArrayFileIO(oflags, pathname, randomBootId.getBytes(StandardCharsets.UTF_8)));
  8.         }
  9.         case "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq": {
  10.             // 直接返回一个字符串作为文件内容
  11.             return FileResult.<AndroidFileIO>success(new ByteArrayFileIO(oflags, pathname, "1766400".getBytes()));
  12.         }
  13.     }
  14.     return null;
  15. }
复制代码

> **注意**:`ByteArrayFileIO` 不支持写入操作,如果样本需要写入文件,使用它会抛出 `UnsupportedOperationException`。
##4. 使用 `DirectoryFileIO` 和虚拟文件系统处理目录
  * **`DirectoryFileIO`**: 当样本明确访问一个目录时,可以使用它,并传入一个本地文件夹。
  * **虚拟文件系统(VFS)**: 这是处理目录访问更好的选择。你可以在初始化模拟器时设置一个根目录,然后将需要的文件按真实路径结构放置其中。这对于样本需要遍历目录(如 `/system/fonts`)的场景非常高效。
##4. 处理失败的文件访问
多数情况下,对于环境检测类的文件(如 `su` 文件),我们什么都不做,让它自然失败即可。但在某些特殊场景,需要手动返回失败。
**案例:模拟无 Root 权限**
有些 Root 检测会尝试访问 `/data` 目录的权限。由于 Unidbg 的虚拟文件系统会自动创建 `/data` 目录导致访问成功,我们需要手动拦截并返回“权限不足”。
  1. @Override
  2. public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {
  3.     if ("/data".equals(pathname)) {
  4.         return FileResult.failed(UnixEmulator.EACCES); // EACCES = 13, Permission denied
  5.     }
  6.     return null;
  7. }
复制代码

#五、关键点


`/proc` 目录下的文件访问频率极高,且容易出错,需要特殊对待。
##1. `/proc/self/cmdline`
  * **用途**:获取当前进程名,常用于反调试或环境检测。
  * **处理要点**:
      * 必须补,否则可能导致关键逻辑失败。
      * 由于进程 ID (pid) 每次运行都可能变化,硬编码 `/proc/12345/cmdline` 会失效。因此,**最佳实践是直接拦截符号链接路径 `/proc/self/cmdline`**,这样可以无需关心动态的 pid。
      * 返回的进程名字符串末尾应包含一个 `\0` (NULL) 字符,这是 cmdline 的格式规范,不加的话在解析时存在出错的可能性。
  *
  1. case "/proc/self/cmdline":
  2.     return FileResult.success(new ByteArrayFileIO(oflags, pathname, PACKAGE_NAME.getBytes()));
复制代码


##2. `/proc/self/status`
  * **用途**:获取进程状态,尤其是 `TracerPid` 字段,用于检测是否被调试(gdb/ida)。
  * **处理要点**:
      * 最简单的处理是只返回 `TracerPid: 0`。
      * 更稳妥的方式是返回一个从真机 `status` 文件中复制的、内容完整模板。
  *
  1. case "/proc/self/status":
  2.     // 返回一个包含 "TracerPid: 0" 的文件内容,表示未被调试
  3.     String statusContent = "Name:\t" + PACKAGE_NAME + "\n" +
  4.             "Umask:\t0077\n" +
  5.             "State:\tS (sleeping)\n" +
  6.             "Tgid:\t12345\n" +
  7.             "Pid:\t12345\n" +
  8.             "PPid:\t1\n" +
  9.             "TracerPid:\t0\n"; // 关键行
  10.     return FileResult.success(new ByteArrayFileIO(oflags, pathname, statusContent.getBytes()));
复制代码


##3. `/proc/net/*` (tcp, udp, unix)
  * **建议**:**一律不要补!**
  * **原因**:
    1.  Android 10 及以上版本已禁止普通应用访问此目录,不补文件符合高版本系统的行为。
    2.  这些文件常用于检测网络代理、Frida/IDA 等工具的端口。如果从真机直接拷贝,可能会把这些危险信息带入模拟环境,反而导致检测失败。

##4\. `/proc/self/maps` (重中之重)

`maps` 文件记录了进程的内存映射,是所有文件访问中处理起来最复杂也最重要的一项。原生库通过读取它来检查环境中是否存在 Frida、Xposed 等 Hook 框架的特征模块,或者获取自身 APK 文件的路径以进行签名校验。
  * **Unidbg 的默认处理 (`fakeMaps`)**:如果你不处理 `maps` 访问,Unidbg 会默认返回一个 `MapsFileIO` 对象。这个 `fakeMaps` 只包含当前加载的 SO 及其依赖库的内存信息,非常干净简洁。
      * **优点**:可以完美绕过对 `frida-agent.so`, `xposed.so` 等风险模块的检测,因为 `fakeMaps` 中根本没有它们。
  * **`fakeMaps` 的问题**:`fakeMaps` 是真实 `maps` 的一个**子集**。它缺少很多信息,例如:
      * 缺少 APK 文件的映射,导致基于 `maps` 寻找 APK 路径并进行签名校验的逻辑失败。
      * 缺少其他 SDK 或系统库的映射,导致检测特定 SDK 是否加载的逻辑失败。
  * **真实 `maps` 的问题**:直接使用从真机导出的 `maps` 文件虽然能解决上述问题,但会引入新的、更严重的问题。如果样本解析 `maps` 文件获取内存地址并尝试读取该地址的内容(如进行 inline hook 检测、扫描内存特征),将会导致 Unidbg 崩溃。因为真实 `maps` 中的内存地址在 Unidbg 的模拟环境中绝大部分是不存在的。这无异于“**拿着北京的地图在南京开车**”。

**处理 `maps` 的三种核心策略:**
面对 `maps` 的复杂性,理论上的最佳方案是完全逆向分析其所有检测点,然后构造一份完美的 `maps` 文件。但在实践中,我们可以采用以下三种更高效的策略,并根据情况灵活选择。
###策略一:使用 Unidbg 默认 `fakeMaps`
  * **做法**:在你的 `IOResolver` 中不添加任何对 `/proc/self/maps` 的处理逻辑。
  * **优点**:最简单,能绕过所有基于 `maps` 内容的风险模块(Frida 等)检测。
  * **缺点**:缺少 APK 等关键路径信息,如果 App 会校验 `maps` 中的 APK 路径,此方法会失效。
  * **适用场景**:
    1.  确定 App 的 `maps` 检测逻辑比较简单,不关心 APK 路径。
    2.  当其他策略导致未知错误时,退回此策略作为保底方案。

###策略二:使用真实 `maps` 文件
  * **做法**:从与你模拟环境相近的真机上,抓取目标 App 运行时的 `maps` 文件 (`cat /proc/<pid>/maps`),并用 `SimpleFileIO` 返回它。
  * **优点**:信息最全,能够应对需要检查各种库(如特定 SDK)是否加载的场景。
  * **缺点**:**风险极高**。一旦 App 根据 `maps` 中的地址去读取内存,几乎必然导致 Unidbg 因访问非法地址而崩溃。
  * **适用场景**:
    1.  作为一种快速试探手段,当你怀疑 App 仅仅是简单地搜索 `maps` 文件中的某些字符串,而不会进行后续的内存操作时。
    2.  **出现内存异常时,必须立即切换**:如果在使用了真实 `maps` 后,程序执行中出现内存访问异常(Bad Address, segmentation fault 等),应立刻放弃此策略。

###策略三:按需构造极简 `maps` 内容
  * **核心思想**:**“它要什么,就给什么”**。通过日志或简单逆向,分析出 App 访问 `maps` 文件的真实意图,然后用 `ByteArrayFileIO` 动态构造一个只包含它所关心的那几行内容的 `maps` 字符串。
  * **做法**:假设我们分析出 App 读取 `maps` 只是为了找到 `base.apk` 的路径,那么我们的处理如下:
  
  1.     // 在 IOResolver 的 resolve 方法中
  2.     case "/proc/self/maps": {
  3.             final String APK_PATH = "/data/app/com.zj.wuaipojie-1/base.apk";
  4.             String maps = "7fbe852000-7fbe853000 r-xp 00000000 00:00 0 " + APK_PATH + "\n";
  5.             return FileResult.success(new ByteArrayFileIO(oflags, pathname, maps.getBytes(StandardCharsets.UTF_8)));
  6. }
  7.    
复制代码

  * **优点**:
      * **精准绕过**:提供了 App 关心的 APK 路径,成功绕过校验。
      * **安全无风险**:由于没有提供任何真实的内存地址,从根本上杜绝了 App 进行非法内存读写导致崩溃的可能。
      * **环境干净**:`maps` 内容完全由我们控制,干净且不含任何风险模块特征。
  * **适用场景**:绝大多数需要处理 `maps` 的情况。它只需要很小的分析成本,就能获得稳定且安全的结果。

二. 补库函数
#1. 什么是“库函数”?

首先,一个应用程序(例如一个 App 的 `.so` 文件)通常不会自己从零开始实现所有功能。它会依赖大量由操作系统或第三方提供的**共享库(Shared Libraries)**,在 Android/Linux 中,这些库通常是 `.so` 文件(Shared Object)。
**库函数**就是这些共享库中提供的、可供应用程序调用的、预先编译好的函数。
- **例子**:
    - `libc.so` (C 标准库): 提供了 `fopen` (打开文件), `printf` (打印字符串), `malloc` (分配内存), `gettid` (获取线程 ID) 等基础功能。
    - `liblog.so` (安卓日志库): 提供了 `__android_log_print` (打印 logcat) 功能。
    - `libz.so` (压缩库): 提供了 `compress`, `uncompress` 等数据压缩功能。

当你的 so 文件调用 `fopen` 时,它实际上是在请求操作系统加载 `libc.so`,并执行其中的 `fopen` 函数代码。

#2. 案例讲解
**案例 1:补 getProperty**
**`__system_property_get` 函数详解**
在 Android 系统中,App 获取设备信息(如型号、品牌、系统版本等)最常用、最底层的手段之一就是调用 `libc.so` 库中的 `__system_property_get` 函数。理解并控制这个函数,是 Unidbg 环境模拟的重中之重。
**1. 它是做什么的?**
`__system_property_get` 是一个 C 函数,原型如下:
  1. int __system_property_get(const char* name, char* value);
复制代码

- **功能**:根据传入的属性**名称**(`name`,一个字符串,例如 `"ro.product.model"`),查询对应的属性**值**,并将结果存入调用者提供的内存缓冲区(`value`)。
- **返回值**:
    - 如果成功找到属性,函数返回属性值的**长度**。
    - 如果属性不存在,函数返回 `0`。
当一个 App 的 Java 代码调用 `android.os.SystemProperties.get("ro.product.model")` 时,其底层最终就会调用到这个 Native 函数来完成查询。
**2. 为什么它在补环境中如此重要?**
几乎所有的风控和反调试检测都会利用这个函数来“审问”当前的运行环境。它们会查询一系列的属性,来判断设备是否真实、是否被 Root、是否处于调试状态。常见的检测点包括:
- **设备标识**:`ro.product.model`, `ro.product.brand`, `ro.build.fingerprint`
- **硬件信息**:`ro.board.platform`, `ro.hardware`
- **调试状态**:`ro.debuggable` (值应为 `0`), `ro.secure` (值应为 `1`)
- **ABI/CPU 类型**:`ro.product.cpu.abi`
如果不对这个函数进行处理,Unidbg 环境下的默认返回值可能为空或不符合预期,导致 App 识别出模拟器环境,从而触发风控或直接退出。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

91

主题

3807

回帖

2万

积分

博士后

萌新

Rank: 8Rank: 8

金币
10363
好评
335
信誉
278

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

QQ
发表于 2025-11-8 16:06:31 | 显示全部楼层  来自 福建
剩下内容放这里
**3.修补方法**
  1. java
  2. //方式1:使用unidbg自带类SystemPropertyHook
  3. SystemPropertyHooksystemPropertyHook=newSystemPropertyHook(emulator);
  4. systemPropertyHook.setPropertyProvider(newSystemPropertyProvider(){
  5.     @Override
  6.     publicStringgetProperty(Stringkey){
  7.         //在这里根据请求的key,返回伪造的value
  8.         switch(key){
  9.             case"ro.board.platform":{
  10.                 return"kalama";
  11.             }
  12.             case"ro.product.model":{
  13.                 return"23113RKC6C";
  14.             }
  15.         }
  16.         //如果返回null,则会尝试使用Unidbg的默认属性
  17.         returnnull;
  18.     }
  19. });
  20. //将Hook注册到内存中
  21. memory.addHookListener(systemPropertyHook);


  22. //方式2:使用Dobby直接Hook原生C函数
  23. privatevoidhookSystemPropertyGet(){
  24.     Modulelibc=emulator.getMemory().findModule("libc.so");
  25.     SymbolpropertyGetSymbol=libc.findSymbolByName("__system_property_get");

  26.     Dobbydobby=Dobby.getInstance(emulator);
  27.     dobby.replace(propertyGetSymbol,newReplaceCallback(){
  28.         @Override
  29.         publicHookStatusonCall(Emulator<?>emulator,HookContextcontext,longoriginFunction){
  30.             //C函数原型:int__system_property_get(constchar*key,char*value);
  31.             //手动从寄存器获取参数指针
  32.             UnidbgPointerkeyPtr=context.getPointerArg(0);
  33.             UnidbgPointervaluePtr=context.getPointerArg(1);

  34.             Stringkey=keyPtr.getString(0);
  35.             Stringvalue=null;

  36.             //判断关心的key
  37.             if("ro.board.platform".equals(key)){
  38.                 value="kalama";
  39.             }elseif("ro.product.model".equals(key)){
  40.                 value="23113RKC6C";
  41.             }

  42.             //如果是我们处理的key
  43.             if(value!=null){
  44.                 System.out.println("[HOOK]__system_property_get:Interceptedkey="+key+",Fakingvalue="+value);
  45.                 //将伪造的值写入到so传入的value内存缓冲区
  46.                 byte[]valueBytes=value.getBytes(StandardCharsets.UTF_8);
  47.                 valuePtr.write(0,valueBytes,0,valueBytes.length);
  48.                 valuePtr.setByte(valueBytes.length,(byte)0);//写入字符串结束符'\0'

  49.                 //使用HookStatus.LR()直接返回,不再执行原始函数
  50.                 //返回值是字符串的长度
  51.                 returnHookStatus.LR(emulator,valueBytes.length);
  52.             }

  53.             //如果不是我们关心的key,则放行,执行原始的__system_property_get函数
  54.             returnHookStatus.RET(emulator,originFunction);
  55.         }
  56.     });
  57. }
复制代码



`小结:`
深入讲解了Unidbg实现此功能的核心机制——基于责任链模式的`IOResolver`,并介绍了如何通过`SimpleFileIO`(处理真实文件)、`ByteArrayFileIO`(动态生成内容)等`FileIO`对象来制定精细化的文件处理策略。课程重点剖析了`/proc/self/maps`、`/proc/self/status`等高频检测文件的处理技巧与策略,强调了按需构造、精准伪造的核心思想。
解释了原生库对`libc.so`等系统共享库函数的依赖,并指出直接Hook这些底层函数是伪造设备信息、绕过风控检测的关键。课程以Android系统中最常见的属性获取函数`__system_property_get`为例,详细演示了如何使用Unidbg自带的`SystemPropertyHook`或更底层的`Dobby`框架来拦截函数调用,从而实现对运行环境的深度伪造。

四、请作者喝杯咖啡


六、视频及课件地址


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

使用道具 举报

18

主题

2773

回帖

7694

积分

硕士生

Rank: 6Rank: 6

金币
950
好评
1
信誉
102
发表于 2025-11-8 16:07:34 来自手机  | 显示全部楼层  来自 河南
看看正己大佬
回复

使用道具 举报

162

主题

8064

回帖

2万

积分

博士后

滑稽

Rank: 8Rank: 8

金币
953
好评
474
信誉
210

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

QQ
发表于 2025-11-8 16:07:45 来自手机  | 显示全部楼层  来自 福建
看看
回复

使用道具 举报

688

主题

1万

回帖

2万

积分

博士后

Rank: 8Rank: 8

金币
3410
好评
164
信誉
119

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

发表于 2025-11-8 16:09:13 来自手机  | 显示全部楼层  来自 广东
看看隐藏
回复

使用道具 举报

6

主题

1051

回帖

3206

积分

大学生

Rank: 5Rank: 5

金币
2254
好评
0
信誉
100
发表于 2025-11-8 16:10:23 来自手机  | 显示全部楼层  来自 江西
看看
回复

使用道具 举报

1

主题

835

回帖

2642

积分

大学生

Rank: 5Rank: 5

金币
12
好评
0
信誉
74
发表于 2025-11-8 16:10:45 来自手机  | 显示全部楼层  来自 江苏
看看
回复

使用道具 举报

0

主题

608

回帖

1509

积分

高中生

Rank: 4

金币
1033
好评
0
信誉
100
发表于 2025-11-8 16:13:00 | 显示全部楼层  来自 山西
看看隐藏
回复

使用道具 举报

282

主题

4131

回帖

1万

积分

博士生

Rank: 7Rank: 7Rank: 7

金币
5838
好评
23
信誉
128

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

发表于 2025-11-8 16:13:39 来自手机  | 显示全部楼层  来自 天津
路过看看
回复

使用道具 举报

35

主题

6576

回帖

1万

积分

博士生

Rank: 7Rank: 7Rank: 7

金币
1499
好评
6
信誉
172

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

QQ
发表于 2025-11-8 16:14:37 来自手机  | 显示全部楼层  来自 河北
感谢分享,
回复

使用道具 举报

10

主题

2046

回帖

7247

积分

硕士生

Rank: 6Rank: 6

金币
6446
好评
3
信誉
100
QQ
发表于 2025-11-8 16:18:47 来自手机  | 显示全部楼层  来自 广东
看看隐藏
回复

使用道具 举报

8

主题

1151

回帖

4249

积分

大学生

Rank: 5Rank: 5

金币
1102
好评
8
信誉
117
发表于 2025-11-8 16:22:13 来自手机  | 显示全部楼层  来自 浙江
大佬又更新了看看
回复

使用道具 举报

8

主题

1151

回帖

4249

积分

大学生

Rank: 5Rank: 5

金币
1102
好评
8
信誉
117
发表于 2025-11-8 16:23:03 来自手机  | 显示全部楼层  来自 浙江
一脸懵逼看不懂
回复

使用道具 举报

20

主题

2498

回帖

8217

积分

硕士生

Rank: 6Rank: 6

金币
1286
好评
2
信誉
100

MT论坛最佳新人MT论坛新人

发表于 2025-11-8 16:37:48 来自手机  | 显示全部楼层  来自 山东
路过看看
回复

使用道具 举报

8

主题

1061

回帖

2970

积分

大学生

Rank: 5Rank: 5

金币
956
好评
1
信誉
100
发表于 2025-11-8 16:40:40 来自手机  | 显示全部楼层  来自 四川
看看
回复

使用道具 举报

发表回复

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

本版积分规则

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