
这个样本是一道面试题,先来看看题目要求吧
面试题:
要求1.guid溯源2.定位到oz生成的方法3.还原算法guid 溯源
先扫一下这个 apk 文件,看看有哪些 sdk 的 so 值得注意一下
PS C:\Users\p1913> ApkCheckPack_windows_amd64.exe -f C:\Users\p1913\Desktop\面试题02\base.apkAPK检测工具 - 扫描配置:- 文件路径: C:\Users\p1913\Desktop\面试题02\base.apk- 检测类型: ROOT(true) 模拟器(true) 反调试(true) 代理(true) SDK(true) 硬编码(false) 证书(true)- 最大文件大小: 500 MB- 递归扫描: true---------------------------------------------------正在扫描APK文件: C:\Users\p1913\Desktop\面试题02\base.apk
- 加固特征扫描结果 - 未发现加固特征
- 安全检测特征扫描结果 - ROOT检测特征 - classes.dex -> /system/xbin/ (常见Root工具目录(分段检测)) - classes.dex -> com.kingroot.kinguser (KingRoot主程序) - classes2.dex -> /data/local/bin/su (SU二进制文件常见路径) - classes2.dex -> /data/local/su (SU二进制备用路径) - classes2.dex -> /data/local/xbin/su (Xposed框架SU路径) - classes2.dex -> /sbin/su (系统分区SU文件) - classes2.dex -> /su/bin/su (Systemless SU路径) - classes2.dex -> /system/app/Superuser.apk (Superuser安装包) - classes2.dex -> /system/bin/failsafe/su (故障安全模式SU) - classes2.dex -> /system/bin/su (系统内置SU) - classes2.dex -> /system/sd/xbin/su (SD卡扩展SU路径) - classes2.dex -> /system/xbin/su (常见SU存放路径) - classes2.dex -> Superuser.apk (Superuser安装包(分段检测)) - classes2.dex -> /system/xbin/ (常见Root工具目录(分段检测)) - 模拟器检测特征 - classes.dex -> 000000000000000 (模拟器默认IMEI) - classes.dex -> /dev/socket/qemud (QEMU守护进程socket) - classes.dex -> /dev/qemu_pipe (QEMU管道通信接口) - classes.dex -> emulator (模拟器标识) - classes.dex -> goldfish (Android模拟器内核标识) - classes.dex -> emulator (模拟器标识) - classes2.dex -> 000000000000000 (模拟器默认IMEI) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> 000000000000000 (模拟器默认IMEI) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> test-keys (测试版系统特征) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> emulator (模拟器标识) - classes2.dex -> test-keys (测试版系统特征) - classes3.dex -> emulator (模拟器标识) - classes4.dex -> 000000000000000 (模拟器默认IMEI) - classes4.dex -> 000000000000000 (模拟器默认IMEI) - 反调试检测特征 - classes2.dex -> /proc/self/status (TracerPid状态检测) - classes2.dex -> ro.debuggable (系统调试属性检测) - classes3.dex -> ptrace (Ptrace调试检测) - classes3.dex -> ptrace (Ptrace调试检测) - classes4.dex -> /proc/self/status (TracerPid状态检测) - 代理检测特征 - classes.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器) - classes.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器) - classes3.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器) - classes4.dex -> Ljavax/net/ssl/X509TrustManager; (自定义证书信任管理器) - classes4.dex -> Lokhttp3/internal/proxy/NullProxySelector; (OkHttp空代理选择器)
- 第三方SDK特征扫描结果 - TDS 腾讯端服务 - Bugly -> lib/arm64-v8a/libBugly_Native.so - Bugly -> lib/arm64-v8a/libbugly_dumper.so - Bugly -> lib/arm64-v8a/libbuglybacktrace.so - Bugly -> lib/armeabi/libBugly_Native.so - Bugly -> lib/armeabi/libbugly_dumper.so - Bugly -> lib/armeabi/libbuglybacktrace.so - Tencent - MMKV -> lib/arm64-v8a/libmmkv.so - MMKV -> lib/armeabi/libmmkv.so - Mars -> lib/arm64-v8a/libmarsxlog.so - Mars -> lib/armeabi/libmarsxlog.so - 腾讯云 HTTPDNS -> lib/arm64-v8a/libhttpdns.so - 腾讯云 HTTPDNS -> lib/armeabi/libhttpdns.so - 腾讯云语音识别 -> lib/arm64-v8a/libqcloud_asr_realtime.so - sisong - HDiffPatch -> lib/arm64-v8a/libhpatchz.so - HDiffPatch -> lib/armeabi/libhpatchz.so - 腾讯灯塔 - 腾讯灯塔终端 ID 精准识别体系 -> lib/arm64-v8a/libqimei.so - 腾讯灯塔终端 ID 精准识别体系 -> lib/armeabi/libqimei.so - 证书扫描结果 - 证书文件: META-INF/ANDROIDR.RSA - 解析证书失败: x509: malformed tbs certificate
- [!]内嵌APK文件: assets/EmptyRes1.apk - 内嵌APK大小: 0.01 MB - 读取限制: 0.01 MB
- 加固特征扫描结果 - 未发现加固特征
- 安全检测特征扫描结果 - 未发现安全检测特征
- 证书扫描结果 - 证书文件: META-INF/CERT.RSA - 解析证书失败: x509: malformed tbs certificate
- [!]内嵌APK文件: assets/kuikly/AppDetailPage.apk - 内嵌APK大小: 0.36 MB - 读取限制: 0.36 MB
- 加固特征扫描结果 - 未发现加固特征
- 安全检测特征扫描结果 - 未发现安全检测特征
- 证书扫描结果 - 未发现证书文件
- [!]内嵌APK文件: assets/kuikly/BattlePassPage.apk - 内嵌APK大小: 0.20 MB - 读取限制: 0.20 MB
- 加固特征扫描结果 - 未发现加固特征
- 安全检测特征扫描结果 - 未发现安全检测特征
- 证书扫描结果 - 未发现证书文件
- [!]内嵌APK文件: assets/kuikly/DiscoverTopicPage.apk - 内嵌APK大小: 0.14 MB - 读取限制: 0.14 MB
- 加固特征扫描结果 - 未发现加固特征
- 安全检测特征扫描结果 - 未发现安全检测特征
- 证书扫描结果 - 未发现证书文件libqimei.so 需要注意一下

打开 jadx 看一下吧,一开始分析到一个 native 函数 x 了,但是那儿没感觉到它在做什么特别的事情,就像是专门用来搜集设备指纹的函数,根据传入的 key,去 system property 中查一下系统属性,感觉没做个啥,但是题目既然叫 guid 溯源,我不应该往后面走,应该往前面找
看一下这玩意儿的交叉引用,往前面找
会发现很多都会去调用 getPhoneGuidAndGen 这个方法,可能这儿会进行赋值啥的
去到 genPhoneGuid 中
发现最后的值会读取私有目录下的.pid 这个文件
这个值应该也是 app 初始化自动设置的
同时另外一些地方也能够发现 guid 的赋值是跟私有目录下的.pid 文件有关,那也要找哪儿设置的,直接搜索这个值 mPhoneGuid,看在哪儿赋值的

看一下交叉引用,发现只有一个
定位到这儿,发现就是服务端返回的

还想在这儿进行写入的

总结:
服务端响应 -> RspHead.phoneGuid-> Global.setPhoneGuid(head.phoneGuid)-> Global.mPhoneGuid-> FileUtil.write(files/.pid)-> 下次启动 Global.genPhoneGuid() 再读取这个.pid文件的值oz 算法分析
下列是 unidbg 补环境的代码
package com.yinyongbao;
import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Emulator;import com.github.unidbg.Module;import com.github.unidbg.arm.context.RegisterContext;import com.github.unidbg.file.FileResult;import com.github.unidbg.file.IOResolver;import com.github.unidbg.hook.hookzz.HookEntryInfo;import com.github.unidbg.hook.hookzz.HookZz;import com.github.unidbg.hook.hookzz.HookZzArm64RegisterContext;import com.github.unidbg.hook.hookzz.IHookZz;import com.github.unidbg.hook.hookzz.WrapCallback;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.SystemPropertyHook;import com.github.unidbg.linux.android.SystemPropertyProvider;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.api.Bundle;import com.github.unidbg.memory.Memory;import com.github.unidbg.pointer.UnidbgPointer;import com.github.unidbg.virtualmodule.android.AndroidModule;import com.sun.jna.Pointer;
import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintStream;import java.util.HashMap;import java.util.Map;
public class OZ extends AbstractJni implements IOResolver {
// ================= 配置区 =================
// true 表示加载 arm64-v8a;false 表示加载 armeabi-v7a private static final boolean IS_64BIT = true;
// 模拟进程名 / 包名 private static final String PROCESS_NAME = "com.tencent.android.qqdownloader";
// APK 路径。若不需要加载 APK,可设为 null private static final String APK_PATH = "apks/yyb/base.apk";
// so 加载方式说明: // false = 按 SO_PATH 指定的完整路径加载 // true = 按 SO_NAME 让 unidbg 从 APK 中解析并加载 private static final boolean LOAD_BY_NAME = true; private static final String SO_PATH = ""; // LOAD_BY_NAME=false 时使用 private static final String SO_NAME = "qimei"; // LOAD_BY_NAME=true 时使用,不带 lib 前缀和 .so 后缀
// 是否打印 JNI 调用日志 private static final boolean VERBOSE = true;
// ========================================
public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module;
private static final String TARGET_STR = "F3LI7aF/FS8taYJD7vm5YwZvcozu1G3c8Ryl/ZRsqqk=";
// 这里可用于缓存 Bundle 模拟数据 private final Map<String, String> bundleData = new HashMap<>();
public OZ() { // 1. 创建模拟器 emulator = IS_64BIT ? AndroidEmulatorBuilder.for64Bit().setProcessName(PROCESS_NAME).build() : AndroidEmulatorBuilder.for32Bit().setProcessName(PROCESS_NAME).build();
// 2. 获取内存对象 memory = emulator.getMemory();
// 3. 设置 Android SDK 版本 memory.setLibraryResolver(new AndroidResolver(23));
// 4. 创建 Dalvik VM vm = APK_PATH != null ? emulator.createDalvikVM(new File(APK_PATH)) : emulator.createDalvikVM(); vm.setJni(this); vm.setVerbose(VERBOSE);
// 5. 注册系统模块并加载目标 so new AndroidModule(emulator, vm).register(memory); DalvikModule dm = LOAD_BY_NAME ? vm.loadLibrary(SO_NAME, false) : vm.loadLibrary(new File(SO_PATH), false);
// 6. 调用 JNI_OnLoad dm.callJNI_OnLoad(emulator);
// 7. 保存模块对象,后续下断点 / trace 要用 module = dm.getModule(); }
private static String safeReadCString(Pointer p) { if (p == null) { return "null"; } try { return p.getString(0); } catch (Throwable e) { return "<bad-ptr:" + p + ">"; } }
@Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "com/qq/AppService/RealAstApp->getPackageName()Ljava/lang/String;": { return new StringObject(vm, "com.tencent.android.qqdownloader"); } case "com/qq/AppService/RealAstApp->getContentResolver()Landroid/content/ContentResolver;": { return vm.resolveClass("android/content/ContentResolver").newObject(null); }
case "android/content/ContentResolver->call(Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;": { // 这里记录 ContentResolver.call 的 method / arg,后续在 Bundle.getString 再补返回值 String lastMethod = vaList.getObjectArg(1).getValue().toString(); String arg = vaList.getObjectArg(2).getValue().toString();
System.out.println("[*] ContentResolver.call => method=" + lastMethod + ", arg=" + arg);
// 返回一个 Bundle 对象,供后续 native 继续取值 return vm.resolveClass("android/os/Bundle").newObject(null); } case "android/os/Bundle->getString(Ljava/lang/String;)Ljava/lang/String;": { String key = vaList.getObjectArg(0).getValue().toString(); System.out.println("[*] Bundle.getString => key=" + key);
// libqimei.so 在这里取 settings 中的 android_id // 从 trace 可知:调用 GET_secure 后,会继续读取 key="value" if ("value".equals(key)) { String androidId = "22577096caea2b28"; System.out.println("[+] 返回模拟的 Android ID: " + androidId); return new StringObject(vm, androidId); }
// 其余 key 默认返回空字符串,避免 native 层拿到 null return new StringObject(vm, ""); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
@Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "android/net/Uri->parse(Ljava/lang/String;)Landroid/net/Uri;": { String urlString = vaList.getObjectArg(0).getValue().toString(); return vm.resolveClass("android/net/Uri").newObject(urlString); } } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); }
@Override public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature) { case "android/os/Bundle->EMPTY:Landroid/os/Bundle;": { return vm.resolveClass("android/os/Bundle").newObject(null); } } return super.getStaticObjectField(vm, dvmClass, signature); }
public void z() { DvmClass appClass = vm.resolveClass( "com/qq/AppService/RealAstApp", vm.resolveClass("android/app/Application")); DvmObject<?> context = appClass.newObject(null); DvmClass uClass = vm.resolveClass("com/tencent/qimei/uin/U");
DvmObject<?> ret = uClass.callStaticJniMethodObject( emulator, "z(Landroid/content/Context;)Ljava/lang/String;", context);
System.out.println("[+] U.z() 返回值: " + ret.getValue()); }
public void consoleDebugger() { emulator.attach().addBreakPoint(module.base + 0x6BB94);// emulator.attach().addBreakPoint(module.base + 0xD0734); }
public void traceCode() { String traceFile = "unidbg-android/src/test/java/com/yinyongbao/traceCode.log"; PrintStream traceStream = null; try { traceStream = new PrintStream(new FileOutputStream(traceFile), true); } catch (FileNotFoundException e) { throw new RuntimeException(e); }
// 对整个 libqimei.so 做指令级 trace,并重定向到日志文件 emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream); }
public static void main(String[] args) { OZ oz = new OZ(); oz.z(); }
@Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println("[IO] 访问路径: " + pathname); return null; }}这里能够返回出相应的值了

但是在经常算法分析的过程中,往往都会调用 memcpy,那一起 hook 上吧
public void hookMemcpy() { final RegisterContext registerContext = emulator.getContext(); Symbol memcpySymbol = module.findSymbolByName("memcpy"); emulator.attach().addBreakPoint(memcpySymbol.getAddress(), new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { System.out.print("call memcpy"); UnidbgPointer srcAddress = registerContext.getPointerArg(1); int length = registerContext.getIntArg(2); Inspector.inspect(srcAddress.getByteArray(0, length), "source:" + srcAddress); return true; } });}首先这个值看起来特别符合 base64 的特征,先使用 cyberchef 来验证一下
发现它的明文值是 1772c8eda17f152f2d698243eef9b963066f728ceed46ddcf11ca5fd946caaa9 这个 hex 数据,看一下 memcpy 有没有复制这个值
有的,有的,看一下这个的写入在哪个地址
emulator.traceWrite(0x125bd000, 0x125bd000 + 32);看一下它的日志呢?
可以看到,它在 ida 中对应的偏移地址是 0x27330,直接跳转到 ida 中去分析一波
进去就看到这个伪码了,处理 4 次,每次处理 4 个字节,而且这儿有点像 AES 最后一轮的轮密钥加
debugger 一下
emulator.attach().addBreakPoint(module.base+0x2732C);
这儿应该就能够判断是 AES 算法的最后一轮的轮密钥加了,测试一下
emulator.attach().addBreakPoint(module.base + 0x2732C, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { int x15 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X15).intValue(); int x16 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X16).intValue(); System.out.printf("x15:: 0x%02x,x16:: 0x%02x%n,xor:: 0x%02x%n", x15, x16,x16^x15); return true; }});得到的结果如下:
x15:: 0xa0,x16:: 0xb7,xor:: 0x17x15:: 0xc9,x16:: 0xbb,xor:: 0x72x15:: 0x58,x16:: 0x90,xor:: 0xc8x15:: 0x40,x16:: 0xad,xor:: 0xedx15:: 0x6f,x16:: 0xce,xor:: 0xa1x15:: 0x7d,x16:: 0x02,xor:: 0x7fx15:: 0x7f,x16:: 0x6a,xor:: 0x15x15:: 0x32,x16:: 0x1d,xor:: 0x2fx15:: 0x94,x16:: 0xb9,xor:: 0x2dx15:: 0x62,x16:: 0x0b,xor:: 0x69x15:: 0xa7,x16:: 0x25,xor:: 0x82x15:: 0x6b,x16:: 0x28,xor:: 0x43x15:: 0x23,x16:: 0xcd,xor:: 0xeex15:: 0xd6,x16:: 0x2f,xor:: 0xf9x15:: 0xbf,x16:: 0x06,xor:: 0xb9x15:: 0x34,x16:: 0x57,xor:: 0x63x15:: 0xa0,x16:: 0xa6,xor:: 0x06x15:: 0xc9,x16:: 0xa6,xor:: 0x6fx15:: 0x58,x16:: 0x2a,xor:: 0x72x15:: 0x40,x16:: 0xcc,xor:: 0x8cx15:: 0x6f,x16:: 0x81,xor:: 0xeex15:: 0x7d,x16:: 0xa9,xor:: 0xd4x15:: 0x7f,x16:: 0x12,xor:: 0x6dx15:: 0x32,x16:: 0xee,xor:: 0xdcx15:: 0x94,x16:: 0x65,xor:: 0xf1x15:: 0x62,x16:: 0x7e,xor:: 0x1cx15:: 0xa7,x16:: 0x02,xor:: 0xa5x15:: 0x6b,x16:: 0x96,xor:: 0xfdx15:: 0x23,x16:: 0xb7,xor:: 0x94x15:: 0xd6,x16:: 0xba,xor:: 0x6cx15:: 0xbf,x16:: 0x15,xor:: 0xaax15:: 0x34,x16:: 0x9d,xor:: 0xa9继续往上面看,发现很多地方都在查表
这里的这个 byte_21C9E1 就是 S 盒,标准的
好,既然是 AES,那就要考虑是什么模式?ECB/CBC,key 是什么,有没有 iv,明文是什么,填充方式是什么?是不是白盒 AES,但这儿不像,流程很清楚的感觉
接着继续往上面看,在最开始的这个地方,16 个字节逐字节进行异或,还是在函数一开始,怀疑明文在与 iv 进行异或

hook 一下
emulator.attach().addBreakPoint(module.base + 0x270A4, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { int x12 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X12).intValue(); int x14 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X14).intValue(); System.out.printf("x12:: 0x%02x,x14:: 0x%02x%n,xor:: 0x%02x%n", x12, x14,x12^x14); return true; }});hook 结果如下:
source:unidbg@0xe4fff6a1, md5=1a5e931a0e2aaf547ade19321f85d811, hex=32323537373039366361656132623238size: 160000: 32 32 35 37 37 30 39 36 63 61 65 61 32 62 32 38 22577096caea2b28^-----------------------------------------------------------------------------^x12:: 0x5a,x14:: 0x32,xor:: 0x68x12:: 0x73,x14:: 0x32,xor:: 0x41x12:: 0x30,x14:: 0x35,xor:: 0x05x12:: 0x6e,x14:: 0x37,xor:: 0x59x12:: 0x74,x14:: 0x37,xor:: 0x43x12:: 0x44,x14:: 0x30,xor:: 0x74x12:: 0x71,x14:: 0x39,xor:: 0x48x12:: 0x47,x14:: 0x36,xor:: 0x71x12:: 0x32,x14:: 0x63,xor:: 0x51x12:: 0x6a,x14:: 0x61,xor:: 0x0bx12:: 0x79,x14:: 0x65,xor:: 0x1cx12:: 0x68,x14:: 0x61,xor:: 0x09x12:: 0x4b,x14:: 0x32,xor:: 0x79x12:: 0x4e,x14:: 0x62,xor:: 0x2cx12:: 0x30,x14:: 0x32,xor:: 0x02x12:: 0x63,x14:: 0x38,xor:: 0x5bx12:: 0x17,x14:: 0x10,xor:: 0x07x12:: 0x72,x14:: 0x10,xor:: 0x62x12:: 0xc8,x14:: 0x10,xor:: 0xd8x12:: 0xed,x14:: 0x10,xor:: 0xfdx12:: 0xa1,x14:: 0x10,xor:: 0xb1x12:: 0x7f,x14:: 0x10,xor:: 0x6fx12:: 0x15,x14:: 0x10,xor:: 0x05x12:: 0x2f,x14:: 0x10,xor:: 0x3fx12:: 0x2d,x14:: 0x10,xor:: 0x3dx12:: 0x69,x14:: 0x10,xor:: 0x79x12:: 0x82,x14:: 0x10,xor:: 0x92x12:: 0x43,x14:: 0x10,xor:: 0x53x12:: 0xee,x14:: 0x10,xor:: 0xfex12:: 0xf9,x14:: 0x10,xor:: 0xe9x12:: 0xb9,x14:: 0x10,xor:: 0xa9x12:: 0x63,x14:: 0x10,xor:: 0x73这里在对明文进行填充,0x10 就是填充的长度,正好是 pkcs7 的填充方式,明文就是补环境的那个 android_id 值,那 x12 就是 iv 了
推出 x12 就是 Zs0ntDqG2jyhKN0c
那最后就还剩下 key 需要验证了,key 是什么呢? 继续看就发现到了这儿
a1+176,在 AES128 中,密钥扩展的长度刚好是 176 个字节,v8 被赋值成 a1+16,16 应该是主密钥的长度,160 是需要扩展的密钥的长度,a1 又是当前函数的参数,直接 hook 一下当前的函数
好了,现在得到了 key,iv,明文,就可以去 cyberchef 中验证一下了
所以,就得到了算法
from Crypto.Cipher import AESfrom Crypto.Util.Padding import padimport base64
def qimei_encrypt(text, key, iv): """ AES-128-CBC 加密 + Base64 编码 """ key_bytes = key.encode('utf-8') iv_bytes = iv.encode('utf-8') cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) padded_data = pad(text.encode('utf-8'), AES.block_size) encrypted_bytes = cipher.encrypt(padded_data) result = base64.b64encode(encrypted_bytes).decode('utf-8') return result
key = "lvcwmSYVr2Axv1gn"iv = "Zs0ntDqG2jyhKN0c"plaintext = "22577096caea2b28"
final_result = qimei_encrypt(plaintext, key, iv)print(f"明文: {plaintext}")print(f"加密结果: {final_result}")总结:
btoa(aes("22577096caea2b28"))
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 125 天
部分内容可能已过时
评论
还没有评论,来写第一条吧。