首页
社区
课程
招聘
Riru原理浅析和EdXposed入口分析
发表于: 2020-10-27 17:36 25966

Riru原理浅析和EdXposed入口分析

2020-10-27 17:36
25966

因为最近在用EdXposed,对于magisk和riru很是好奇,之前也大致了解过edxp通过riru实现zygote注入进而完成ART Hook实现类Xposed,但是在准备看源码的时候发现不知道入口在哪,本来想找找有没有现成的大佬总结,发现貌似没有,于是自力更生,从magisk插件开发到riru插件到riru加载逻辑,一步步找到了edxp的代码入口。

根据面具的官方文档(https://topjohnwu.github.io/Magisk/guides.html)一个Magisk插件模块是/data/adb/modules下的一个文件夹,其中至少包含着以下几个文件:

EdXposed在gradle构建出产物时,会将整个项目打包成一个magisk module installer,该installer与magisk module的区别在于installer为一个zip压缩文件,同时包含META-INF文件夹,其中的update-binary.sh将作为安装前执行的脚本。

但是我们在EdXposed中并没有搜到相关的module.prop文件,只能看到module.prop.tpl模版文件,那么这两个文件是如何关联的呢。

在edxp-core的build.gradle中可以看到声明了一系列构建Task,从EdXposed的构建命令./gradlew clean :edxp-core:[zip|push]YahfaRelease开始追踪,

可以发现 pushTask只是将构建产物推送到设备sdcard下,真正打包任务在zipTask中,其依赖于prepareMagiskFilesTask,该task的作用是:

但是EdXposed作为一个Magisk插件,插件加载时所会执行的update-binary、post-fs-data.sh、customize.sh等脚本中并没有相关执行EdXp的命令,那么EdXposed是如何在Zygote进程加载时所执行呢。

EdXposed实际上还是一个riru插件,即基于riru注入zygote进程时机和riru相关的API声明来实现zygote进程执行时的加载。

Riru模块本质上是一个Magisk插件,额外的地方在于新增了一个目录/data/adb/riru/modules(旧版本在/data/misc/riru/modules)下多了一个插件目录,该目录的作用在于在riru加载时通过该目录名称加载对应的插件so,具体逻辑下面具体分析。

首先看一个Riru模块如何编译构建的,入口依然是build.gradle,即从构建命令zipMagiskMoudle入手:

源码:https://github.com/RikkaApps/Riru-ModuleTemplate/blob/master/module/build.gradle

可以看到主要做了以下操作:

一句话总结:将项目下template/magisk_module下的如动态库、module.prop、post-fs-data.sh等magisk插件相关文件打包成magisk module install所需的zip格式。剩下的就是安装到手机中由Magisk进行加载。

一个典型的riru插件的加载顺序可以简单理解为:

update_binary -> customize.sh -> post-fs-data.sh

其中customize.sh是riru重写update_binary进行执行的,其他都是遵循magisk的插件执行顺序。

接下来就通过这些shell脚本的执行来找到riru是如何定位插件、解析插件、执行插件和完成zygote注入的。

首先看riru是如何被magisk执行的,因为riru也是一个magisk插件,所以直接先看post-fs-data.sh,可以看到:

这个版本的riru直接通过/system/etc/public.libraries.txt自动完成so的dlopen加载。

因此可以直接奔向riru的so,找到.initarray方法(\_attribute__((constructor))),可以看到两个关键方法调用,分别为

XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);

load_modules();

具体.init_array代码可以看main.cpp中:

这里XHOOK_REGISTER是一个宏定义,

XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);

实际上相当于

new_jniRegisterNativeMethods和old_jniRegisterNativeMethods则是通过NEW_FUNC_DEF宏定义来进行声明:

可以看到在com/android/internal/os/Zygote注册JNI方法时会回调onRegisterZygote方法获取新的jniMethods列表进行替换,在onRegisterZygote方法中主要替换了三个方法的fnPtr指针并兼容不同的安卓版本:

这三个方法是应用进程或者系统服务进程被fork 出来的时候会调用的方法,这里以nativeForkAndSpecialize举例分析nativeForkAndSpecialize的AOP逻辑和模块中声明的forkAndSpecializePre/Post等系列方法如何被调用及调用时机。

load_modules解析如下:

EdXposed本身也是一个riru插件,但是如何证明呢。

我们可以知道如果作为一个riru插件,必然需要

在edxp-core中,依然通过build.gradle为入口分析zip打包的逻辑,可以看到也是一个标准的magisk module installer,其中customize.sh中有一行关键代码创建了riru插件的相关目录:

通过这段脚本可以看出edxp插件在magisk加载时创建了一个riru插件目录,并且把libriru_edxp.so重命名成一个随机后缀的so文件由riru进行加载。

接着分析libriru_edxp.so,通过CMakeLists.txt可以知道是由edxp-core中编译得到,查看edxp-core/src/main/cpp/main/src/main.cpp中可以看到确实声明了riru中的一些方法模版钩子:

等方法,一切都可以想明白了。接下来就是edxp如何进行ART Hook。

 
 
def zipTask = task("zip${backendCapped}${variantCapped}", type: Zip) {
                dependsOn prepareMagiskFilesTask
                archiveName "${module_name}-${project.version}-${variantLowered}.zip"
                destinationDir file("$projectDir/release")
                from "$zipPathMagiskRelease"
            }
 
            task("push${backendCapped}${variantCapped}", type: Exec) {
                dependsOn zipTask
                workingDir "${projectDir}/release"
                def commands = ["adb", "push",
                                "${module_name}-${project.version}-${variantLowered}.zip",
                                "/sdcard/"]
                if (is_windows) {
                    commandLine 'cmd', '/c', commands.join(" ")
                } else {
                    commandLine commands
                }
            }
        }
 
        // backward compatible
        task("zip${variantCapped}") {
            dependsOn "zipYahfa${variantCapped}"
        }
        task("push${variantCapped}") {
            dependsOn "pushYahfa${variantCapped}"
        }
def zipTask = task("zip${backendCapped}${variantCapped}", type: Zip) {
                dependsOn prepareMagiskFilesTask
                archiveName "${module_name}-${project.version}-${variantLowered}.zip"
                destinationDir file("$projectDir/release")
                from "$zipPathMagiskRelease"
            }
 
            task("push${backendCapped}${variantCapped}", type: Exec) {
                dependsOn zipTask
                workingDir "${projectDir}/release"
                def commands = ["adb", "push",
                                "${module_name}-${project.version}-${variantLowered}.zip",
                                "/sdcard/"]
                if (is_windows) {
                    commandLine 'cmd', '/c', commands.join(" ")
                } else {
                    commandLine commands
                }
            }
        }
 
        // backward compatible
        task("zip${variantCapped}") {
            dependsOn "zipYahfa${variantCapped}"
        }
        task("push${variantCapped}") {
            dependsOn "pushYahfa${variantCapped}"
        }
def prepareMagiskFilesTask = task("prepareMagiskFiles${backendCapped}${variantCapped}", type: Delete) {
                dependsOn prepareJarsTask, "assemble${variantCapped}"
                delete file(zipPathMagiskRelease)
                doFirst {
                    copy {
                        from "${projectDir}/tpl/edconfig.tpl"
                        into templateFrameworkPath
                        rename "edconfig.tpl", "edconfig.jar"
                        expand(version: "$version", backend: "$backend")
                    }
                    copy {
                        from "${projectDir}/tpl/module.prop.tpl"
                        into templateRootPath
                        rename "module.prop.tpl", "module.prop"
                        expand(moduleId: "$magiskModuleId", backend: "$backendCapped",
                                versionName: "$version",
                                versionCode: "$versionCode", authorList: "$authorList")
                        filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf"))
                    }
                }
                def libPathRelease = "${buildDir}/intermediates/cmake/${variantLowered}/obj"
                doLast {
                    copy {
                        from "${projectDir}/template_override"
                        into zipPathMagiskRelease
                    }
                    copy {
                        from "$libPathRelease/armeabi-v7a"
                        into "$zipPathMagiskRelease/system/lib"
                    }
                    copy {
                        from "$libPathRelease/arm64-v8a"
                        into "$zipPathMagiskRelease/system/lib64"
                    }
                    copy {
                        from "$libPathRelease/x86"
                        into "$zipPathMagiskRelease/system_x86/lib"
                    }
                    copy {
                        from "$libPathRelease/x86_64"
                        into "$zipPathMagiskRelease/system_x86/lib64"
                    }
                }
            }
def prepareMagiskFilesTask = task("prepareMagiskFiles${backendCapped}${variantCapped}", type: Delete) {
                dependsOn prepareJarsTask, "assemble${variantCapped}"
                delete file(zipPathMagiskRelease)
                doFirst {
                    copy {
                        from "${projectDir}/tpl/edconfig.tpl"
                        into templateFrameworkPath
                        rename "edconfig.tpl", "edconfig.jar"
                        expand(version: "$version", backend: "$backend")
                    }
                    copy {
                        from "${projectDir}/tpl/module.prop.tpl"
                        into templateRootPath
                        rename "module.prop.tpl", "module.prop"
                        expand(moduleId: "$magiskModuleId", backend: "$backendCapped",
                                versionName: "$version",
                                versionCode: "$versionCode", authorList: "$authorList")
                        filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf"))
                    }
                }
                def libPathRelease = "${buildDir}/intermediates/cmake/${variantLowered}/obj"
                doLast {
                    copy {
                        from "${projectDir}/template_override"
                        into zipPathMagiskRelease
                    }
                    copy {
                        from "$libPathRelease/armeabi-v7a"
                        into "$zipPathMagiskRelease/system/lib"
                    }
                    copy {
                        from "$libPathRelease/arm64-v8a"
                        into "$zipPathMagiskRelease/system/lib64"
                    }
                    copy {
                        from "$libPathRelease/x86"
                        into "$zipPathMagiskRelease/system_x86/lib"
                    }
                    copy {
                        from "$libPathRelease/x86_64"
                        into "$zipPathMagiskRelease/system_x86/lib64"
                    }
                }
            }
 
 
 
android.libraryVariants.all { variant ->
    def task = variant.assembleProvider.get()
    task.doLast {
        // clear
        delete { delete magiskDir }
 
        // copy from template
        copy {
            from "$rootDir/template/magisk_module"
            into magiskDir.path
            exclude 'riru.sh'
        }
        // copy riru.sh
        copy {
            from "$rootDir/template/magisk_module"
            into magiskDir.path
            include 'riru.sh'
            filter { line ->
                line.replaceAll('%%%RIRU_MODULE_ID%%%', moduleId)
                        .replaceAll('%%%RIRU_MIN_API_VERSION%%%', moduleMinRiruApiVersion.toString())
                        .replaceAll('%%%RIRU_MIN_VERSION_NAME%%%', moduleMinRiruVersionName)
            }
            filter(FixCrLfFilter.class,
                    eol: FixCrLfFilter.CrLf.newInstance("lf"))
        }
        // copy .git files manually since gradle exclude it by default
        Files.copy(file("$rootDir/template/magisk_module/.gitattributes").toPath(), file("${magiskDir.path}/.gitattributes").toPath())
 
        // generate module.prop
        def modulePropText = ""
        magiskModuleProp.each { k, v -> modulePropText += "$k=$v\n" }
        modulePropText = modulePropText.trim()
        file("$magiskDir/module.prop").text = modulePropText
 
        // generate module.prop for Riru
        def riruModulePropText = ""
        moduleProp.each { k, v -> riruModulePropText += "$k=$v\n" }
        riruModulePropText = riruModulePropText.trim()
        file(riruDir).mkdirs()
 
        // module.prop.new will be renamed to module.prop in post-fs-data.sh
        file("$riruDir/module.prop.new").text = riruModulePropText
 
        // copy native files
        def nativeOutDir = file("build/intermediates/cmake/$variant.name/obj")
 
        file("$magiskDir/system").mkdirs()
        file("$magiskDir/system_x86").mkdirs()
        renameOrFail(file("$nativeOutDir/arm64-v8a"), file("$magiskDir/system/lib64"))
        renameOrFail(file("$nativeOutDir/armeabi-v7a"), file("$magiskDir/system/lib"))
        renameOrFail(file("$nativeOutDir/x86_64"), file("$magiskDir/system_x86/lib64"))
        renameOrFail(file("$nativeOutDir/x86"), file("$magiskDir/system_x86/lib"))
 
        // generate sha1sum
        fileTree("$magiskDir").matching {
            exclude "README.md", "META-INF"
        }.visit { f ->
            if (f.directory) return
            file(f.file.path + ".sha256sum").text = calcSha256(f.file)
        }
    }
    task.finalizedBy zipMagiskMoudle
}
 
task zipMagiskMoudle(type: Zip) {
    from magiskDir
    archiveName zipName
    destinationDir outDir
}
android.libraryVariants.all { variant ->
    def task = variant.assembleProvider.get()
    task.doLast {
        // clear
        delete { delete magiskDir }
 
        // copy from template
        copy {
            from "$rootDir/template/magisk_module"
            into magiskDir.path
            exclude 'riru.sh'
        }
        // copy riru.sh
        copy {
            from "$rootDir/template/magisk_module"
            into magiskDir.path
            include 'riru.sh'
            filter { line ->
                line.replaceAll('%%%RIRU_MODULE_ID%%%', moduleId)
                        .replaceAll('%%%RIRU_MIN_API_VERSION%%%', moduleMinRiruApiVersion.toString())
                        .replaceAll('%%%RIRU_MIN_VERSION_NAME%%%', moduleMinRiruVersionName)
            }
            filter(FixCrLfFilter.class,
                    eol: FixCrLfFilter.CrLf.newInstance("lf"))
        }
        // copy .git files manually since gradle exclude it by default
        Files.copy(file("$rootDir/template/magisk_module/.gitattributes").toPath(), file("${magiskDir.path}/.gitattributes").toPath())
 
        // generate module.prop
        def modulePropText = ""
        magiskModuleProp.each { k, v -> modulePropText += "$k=$v\n" }
        modulePropText = modulePropText.trim()
        file("$magiskDir/module.prop").text = modulePropText
 
        // generate module.prop for Riru
        def riruModulePropText = ""
        moduleProp.each { k, v -> riruModulePropText += "$k=$v\n" }
        riruModulePropText = riruModulePropText.trim()
        file(riruDir).mkdirs()
 
        // module.prop.new will be renamed to module.prop in post-fs-data.sh
        file("$riruDir/module.prop.new").text = riruModulePropText
 
        // copy native files
        def nativeOutDir = file("build/intermediates/cmake/$variant.name/obj")
 
        file("$magiskDir/system").mkdirs()
        file("$magiskDir/system_x86").mkdirs()
        renameOrFail(file("$nativeOutDir/arm64-v8a"), file("$magiskDir/system/lib64"))
        renameOrFail(file("$nativeOutDir/armeabi-v7a"), file("$magiskDir/system/lib"))
        renameOrFail(file("$nativeOutDir/x86_64"), file("$magiskDir/system_x86/lib64"))
        renameOrFail(file("$nativeOutDir/x86"), file("$magiskDir/system_x86/lib"))
 
        // generate sha1sum
        fileTree("$magiskDir").matching {
            exclude "README.md", "META-INF"
        }.visit { f ->
            if (f.directory) return
            file(f.file.path + ".sha256sum").text = calcSha256(f.file)
        }
    }
    task.finalizedBy zipMagiskMoudle
}
 
task zipMagiskMoudle(type: Zip) {
    from magiskDir
    archiveName zipName
    destinationDir outDir
}
 
 
 
 
LIBRARIES_FILE='/system/etc/public.libraries.txt'
mkdir -p "$MODDIR/system/etc"
cp -f $LIBRARIES_FILE "$MODDIR/$LIBRARIES_FILE"
grep -qxF 'libriru.so' "$MODDIR/$LIBRARIES_FILE" || echo 'libriru.so' >> "$MODDIR/$LIBRARIES_FILE"
LIBRARIES_FILE='/system/etc/public.libraries.txt'
mkdir -p "$MODDIR/system/etc"
cp -f $LIBRARIES_FILE "$MODDIR/$LIBRARIES_FILE"
grep -qxF 'libriru.so' "$MODDIR/$LIBRARIES_FILE" || echo 'libriru.so' >> "$MODDIR/$LIBRARIES_FILE"
PS:
 
riru目前存在了三种已知的app_process注入实现:
1. 最早通过替换libmemtrack.so
2. 后来通过/system/etc/public.libraries.txt
3. 目前通过native bridge即设置系统属性ro.dalvik.vm.native.bridge
PS:
 
riru目前存在了三种已知的app_process注入实现:
1. 最早通过替换libmemtrack.so
2. 后来通过/system/etc/public.libraries.txt
3. 目前通过native bridge即设置系统属性ro.dalvik.vm.native.bridge
 
 
extern "C" void constructor() __attribute__((constructor));
 
// _init_array libriru.so被dlopen后最先执行的函数
void constructor() {
#ifdef DEBUG_APP
    hide::hide_modules(nullptr, 0);
#endif
 
    if (getuid() != 0)
        return;
 
    char cmdline[ARG_MAX + 1];
    get_self_cmdline(cmdline, 0);
 
    if (strcmp(cmdline, "zygote") != 0
        && strcmp(cmdline, "zygote32") != 0
        && strcmp(cmdline, "zygote64") != 0
        && strcmp(cmdline, "usap32") != 0
        && strcmp(cmdline, "usap64") != 0) {
        LOGW("not zygote (cmdline=%s)", cmdline);
        return;
    }
 
    LOGI("Riru %s (%d) in %s", RIRU_VERSION_NAME, RIRU_VERSION_CODE, cmdline);
 
    LOGI("config dir is %s", CONFIG_DIR);
 
    if (access(CONFIG_DIR "/disable", F_OK) == 0) {
        LOGI("%s exists, do nothing", CONFIG_DIR "/disable");
        return;
    }
 
    read_prop();
 
    // 通过GOT表hook libandroid_runtime.so中对jniRegisterNativeMethods方法的调用,因为libandroid_runtime.so中所有JNI方法都是通过该方法进行注册,然后再通过手动调用registeNatives来替换
    // 因此通过hook该方法可以在com.android.internal.os.Zygote#nativeForkAndSpecialize和com.android.internal.os.Zygote#nativeForkSystemServer注册时进行替换
    // Riru也因此完成了zygote进程的注入
    XHOOK_REGISTER(".*\\libandroid_runtime.so$", jniRegisterNativeMethods);
 
    if (xhook_refresh(0) == 0) {
        xhook_clear();
        LOGI("hook installed");
    } else {
        LOGE("failed to refresh hook");
    }
 
    // 加载插件
    load_modules();
 
    status::writeToFile();
}
extern "C" void constructor() __attribute__((constructor));
 
// _init_array libriru.so被dlopen后最先执行的函数
void constructor() {
#ifdef DEBUG_APP
    hide::hide_modules(nullptr, 0);
#endif
 
    if (getuid() != 0)
        return;
 
    char cmdline[ARG_MAX + 1];
    get_self_cmdline(cmdline, 0);
 
    if (strcmp(cmdline, "zygote") != 0
        && strcmp(cmdline, "zygote32") != 0
        && strcmp(cmdline, "zygote64") != 0
        && strcmp(cmdline, "usap32") != 0
        && strcmp(cmdline, "usap64") != 0) {
        LOGW("not zygote (cmdline=%s)", cmdline);
        return;
    }
 
    LOGI("Riru %s (%d) in %s", RIRU_VERSION_NAME, RIRU_VERSION_CODE, cmdline);
 
    LOGI("config dir is %s", CONFIG_DIR);
 
    if (access(CONFIG_DIR "/disable", F_OK) == 0) {
        LOGI("%s exists, do nothing", CONFIG_DIR "/disable");
        return;
    }
 
    read_prop();
 
    // 通过GOT表hook libandroid_runtime.so中对jniRegisterNativeMethods方法的调用,因为libandroid_runtime.so中所有JNI方法都是通过该方法进行注册,然后再通过手动调用registeNatives来替换
    // 因此通过hook该方法可以在com.android.internal.os.Zygote#nativeForkAndSpecialize和com.android.internal.os.Zygote#nativeForkSystemServer注册时进行替换
    // Riru也因此完成了zygote进程的注入
    XHOOK_REGISTER(".*\\libandroid_runtime.so$", jniRegisterNativeMethods);
 
    if (xhook_refresh(0) == 0) {
        xhook_clear();
        LOGI("hook installed");
    } else {
        LOGE("failed to refresh hook");
    }
 
    // 加载插件
    load_modules();
 
    status::writeToFile();
}
 
if (xhook_register(".*\\libandroid_runtime.so$", jniRegisterNativeMethods, (void*) new_jniRegisterNativeMethods, (void **) &old_jniRegisterNativeMethods) != 0) \
    LOGE("failed to register hook jniRegisterNativeMethods ."); \
if (xhook_register(".*\\libandroid_runtime.so$", jniRegisterNativeMethods, (void*) new_jniRegisterNativeMethods, (void **) &old_jniRegisterNativeMethods) != 0) \
    LOGE("failed to register hook jniRegisterNativeMethods ."); \
#define NEW_FUNC_DEF(ret, func, ...) \
    static ret (*old_##func)(__VA_ARGS__); \
    static ret new_##func(__VA_ARGS__)
 
NEW_FUNC_DEF(int, jniRegisterNativeMethods, JNIEnv *env, const char *className,
             const JNINativeMethod *methods, int numMethods) {
    api::putNativeMethod(className, methods, numMethods);
 
    LOGD("jniRegisterNativeMethods %s", className);
 
    JNINativeMethod *newMethods = nullptr;
    if (strcmp("com/android/internal/os/Zygote", className) == 0) {
        // com/android/internal/os/Zygote注册时回调onRegisterZygote方法获取新的jniMethods列表进行替换
        newMethods = onRegisterZygote(env, className, methods, numMethods);
    } else if (strcmp("android/os/SystemProperties", className) == 0) {
        // hook android.os.SystemProperties#native_set to prevent a critical problem on Android 9
        // see comment of SystemProperties_set in jni_native_method.cpp for detail
        // 回调onRegisterSystemProperties方法
        newMethods = onRegisterSystemProperties(env, className, methods, numMethods);
    }
 
    int res = old_jniRegisterNativeMethods(env, className, newMethods ? newMethods : methods,
                                           numMethods);
    /*if (!newMethods) {
        NativeMethod::jniRegisterNativeMethodsPost(env, className, methods, numMethods);
    }*/
    delete newMethods;
    return res;
}
#define NEW_FUNC_DEF(ret, func, ...) \

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2020-10-27 17:37 被alienhe编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (18)
雪    币: 3400
活跃值: (14083)
能力值: ( LV9,RANK:230 )
在线值:
发帖
回帖
粉丝
2
TQL
2020-10-27 17:37
0
雪    币: 4105
活跃值: (3507)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
TQL
2020-10-27 17:39
0
雪    币: 1365
活跃值: (3619)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
4
riru负责注入
ed负责hook
2020-10-27 17:44
0
雪    币: 1385
活跃值: (5609)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
5
TQL
2020-10-27 17:54
0
雪    币: 1867
活跃值: (4018)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
6
tql
2020-10-27 18:06
0
雪    币: 6573
活跃值: (3923)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
7
tql
2020-10-28 09:49
0
雪    币: 36
活跃值: (1061)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
8
TQL
2020-10-28 09:50
0
雪    币: 5
活跃值: (116)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
TQL
2020-10-28 11:27
0
雪    币: 211
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
TQL
2020-10-28 17:00
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
TQL
2020-10-28 20:46
0
雪    币: 1636
活跃值: (653)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
12
TQL
2020-10-28 21:42
0
雪    币: 0
活跃值: (57)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
TQL
(虽然不知道啥意思,但是还是跟风发一个吧)
2020-10-29 13:57
0
雪    币: 213
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
TQL
2020-10-29 16:23
0
雪    币: 15
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
谢谢大佬
2020-10-31 21:30
0
雪    币: 2081
活跃值: (2715)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
16
感谢 很有帮助
2020-11-5 17:13
0
雪    币: 866
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
十分感谢 非常有帮助
2021-2-24 16:50
0
雪    币: 3077
活跃值: (4167)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
tql
2021-9-25 22:44
0
雪    币: 438
活跃值: (228)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
19
tql=太强了
2023-7-26 11:28
0
游客
登录 | 注册 方可回帖
返回
//