吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1434|回复: 18
上一主题 下一主题
收起左侧

[Web逆向] 某蜂窝w_tsfp参数分析

  [复制链接]
跳转到指定楼层
楼主
kylin1020 发表于 2024-4-21 18:46 回帖奖励
本帖最后由 kylin1020 于 2024-4-22 10:22 编辑

某蜂窝w_tsfp参数分析

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除.

目标地址

aHR0cHM6Ly93d3cubWFmZW5nd28uY24v

参数来源分析

新建新的无痕窗口,打开devtools并浏览目标地址,浏览器停在了一处动态加载的debugger代码中, 该段js代码可以分析出由Function.constructor动态构造得到, 所以直接在console中hook掉debugger的构造函数,使其失效:


Function.prototype.original_constructor= Function.prototype.constructor;
Function.prototype.constructor=function(){
    if (arguments && typeof arguments[0]==="string"){
        if (arguments[0]==="debugger")
            return;
    }
        return Function.prototype.original_constructor.apply(this, arguments);
};


点击继续:


查看网络请求可以知道,请求了两次目标地址,其中第一次请求只返回了一个空的x-waf-captcha-referer参数

两次目标地址请求间加载了一个probe.js文件,之后发起第二次请求, 此时携带了一个w_tsfp参数并且成功返回网页内容.



由此可以基本断定w_tsfp参数在probe.js中生成.

解混淆

打开probe.js, 在js文件加载入口函数处打上断点, 然后对其进行分析.


可以知道该js文件进行了一些常规混淆, 例如大量使用了字符串函数(指调用了某个函数返回特定字符串的函数,该函数目的是为了隐藏字符串), 控制流平坦化等.

1.1 还原字符串函数

通过分析可以发现,probe.js任意一个字符串函数都源自a1i根字符串函数并且参数都是透传的,没有加上任何偏移:




因此可以先使用babel遍历所有字符串函数,得到所有参数变化列表,之后在console中计算得到所有字符串值;根据这些字符串值再将原js中的字符串函数调用替换为对应字符串。

首先需要找到字符串函数调用的特征, 通过观察结构可以知道任何一个字符串函数调用都带有两个参数,其中第一个参数是一个十六进制数字,第二个参数是一个字符串,并且函数名长度很短, 总是2(除了a1i函数)。


因此可以根据这些特征找到所有字符串函数调用:

const parser = require("@babel/parser");
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const fs = require("fs");

const input_file = "probe.js";
const output_file = "probe.output.js";

const js_content = fs.readFileSync(input_file, "utf-8");
const ast = parser.parse(js_content);

const stringFuncItems = [];

traverse(ast, {
    CallExpression: {
        // 找出所有字符串函数调用并记录参数
        enter: function (path) {
            const { node } = path;

            // 只有两个参数的函数调用
            if (!types.isIdentifier(node.callee) || node.arguments.length !== 2) {
                return;
            }

            // 函数名长度不大于3
            if (node.callee.name.length > 3) {
                return;
            }

            const arg0 = node.arguments[0];
            const arg1 = node.arguments[1];

            // 第一个参数是数字,第二个参数是字符串
            if (!types.isNumericLiteral(arg0) || !types.isStringLiteral(arg1)) {
                return;
            }

            stringFuncItems.push([arg0.value, arg1.value]);
        }
    }
});

console.log(JSON.stringify(stringFuncItems));


之后将结果拷贝到console中,断点停在js文件函数入口处或任意一处含有字符串函数的地方, 准备调用实际字符串函数得到每个参数对应的字符串:

words = {}
for (let [i, v] of items) {
    words[`${i}-${v}`] = a1i(i, v);
}

得到所有字符串:


将所有字符串拷贝到代码中,准备开始替换:

const parser = require("@babel/parser");
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const fs = require("fs");

const input_file = "probe.js";
const output_file = "probe.output.js";

const js_content = fs.readFileSync(input_file, "utf-8");
const ast = parser.parse(js_content);

const words = {
  // 这里仅是示例,实际为上一步得到的所有字符串
 // 这里仅是示例,实际为上一步得到的所有字符串
};

traverse(ast, {
    CallExpression: {
        // 找出所有字符串函数调用并记录参数
        enter: function (path) {
            const { node } = path;

            // 只有两个参数的函数调用
            if (!types.isIdentifier(node.callee) || node.arguments.length !== 2) {
                return;
            }

            if (node.callee.name.length > 3) {
                return;
            }

            const arg0 = node.arguments[0];
            const arg1 = node.arguments[1];

            // 第一个参数是数字,第二个参数是字符串
            if (!types.isNumericLiteral(arg0) || !types.isStringLiteral(arg1)) {
                return;
            }

            // 得到字符串
            const value = words[`${arg0.value}-${arg1.value}`];
            if (!value) {
                return;
            }

            //替换
            path.replaceWith(types.stringLiteral(value));

            // 打印替换的结果
            console.log(`${generator(node).code} 替换为字符串: "${value}"`);
        }
    }
});

const {code} = generator(ast);

fs.writeFileSync(output_file, code);


查看probe.output.js文件可以知道,字符串函数已经替换完成:

1.2 MemberExpression常量传播

还原字符串函数之后, 可以看到有很多访问变量某个属性的情况且这些属性的值是不变的,可能是字符串或一些数字, 这些MemberExpression表达式可以替换为对应的字符串或数字,使得理解代码逻辑更简单些:

// 尝试获取获取常量
function tryGetConstant(value, scope, binding, targetPropertyName) {

    // 字符串类型直接返回值
    if (types.isStringLiteral(value)) {
        return value.value;
    }

    // 数字类型直接返回值
    if (types.isNumericLiteral(value)) {
        return value.value;
    }

    // 变量的话查找其初始值
    if (types.isIdentifier(value)) {
        binding = scope.getBinding(value.name);
        if (!binding) {
            return;
        }
        return tryGetConstant(binding.path.node.init, binding.scope, binding);
    }

    // memberExpression的话需要其object属性的变量初始值,如果初始值中有对应property的属性,则可以直接拿其属性,否则
    // 找所有赋值语句,看赋值语句中有没有property属性
    if (types.isMemberExpression(value)) {
        if (!types.isIdentifier(value.object)) {
            return;
        }
        binding = scope.getBinding(value.object.name);
        if (!binding) {
            return;
        }
        const objectInit = binding.path.node.init;
        targetPropertyName = targetPropertyName || value.property.value || value.property.name;
        if (types.isObjectExpression(objectInit)) {
            const properties = objectInit.properties;
            for (const pro of properties) {
                const key = pro.key.name || pro.key.value;
                if (key === targetPropertyName) {
                    return tryGetConstant(pro.value, binding.scope, binding);
                }
            }
            // 从赋值语句中查找
            for (const ref of binding.referencePaths) {
                if (!types.isAssignmentExpression(ref.parentPath.parent)) {
                    continue;
                }
                const assign = ref.parentPath.parent;
                if (!types.isMemberExpression(assign.left)) {
                    continue;
                }
                if (assign.left.object !== value.object) {
                    continue;
                }
                const property = assign.left.property;
                if (types.isIdentifier(property) && property.name === targetPropertyName) {
                    return tryGetConstant(assign.right, binding.scope, binding);
                }
            }
        } else if (types.isIdentifier(objectInit)) {
            // object是变量的话,继续找
            binding = scope.getBinding(objectInit.name);
            if (!binding) {
                return;
            }
            return tryGetConstant(binding.path.node.init, binding.scope, binding, targetPropertyName);
        }
    }
}

// 常量传播
function MemberExpressionConstantPropagation(path) {
    const { node } = path;

    // 只需要类似于 A["xxx"]这样的member expression, 其中xxx是一个字符串, 例如 A["AaWYl"]
    if (!types.isIdentifier(node.object) || !types.isStringLiteral(node.property)) {
        return;
    }
  // 赋值语句排除,例如: A["xxx"] = "hello"; 这种语句不需要替换。
    if (types.isAssignmentExpression(path.parent) && node === path.parent.left) {
        return;
    }
    const binding = path.scope.getBinding(node.object.name);
    const value = tryGetConstant(node, path.scope, binding);
    if (!value) {
        return;
    }
    path.replaceWith(types.valueToNode(value));
    console.log(`常量传播: ${node.object.name}["${node.property.value}"] -> "${value}"`);
}

traverse(ast, {
    MemberExpression: {
        exit: [
            MemberExpressionConstantPropagation
        ]
    }
});


前后对比:

1.3 消除一句话函数



可以看到js代码中很多上图这种函数调用,其函数代码中只有一句return 语句是有用的(通常只有一行return语句),这些函数作用是隐藏各种运算表达式,例如:

B = {
                // 其他函数

      'eViYz': function (M, i) {
        return M(i);
      }
    };

 // 函数调用
B["eViYz"](t, 0x0);

其中B["eViYz"](t, 0x0)应该简化为t(0x0)




通过观察其函数特征可以知道这种函数其callee一般是MemberExpression并且函数的body只有一行或只有两行,其中一行是完全没用的变量定义,据此可以筛选出这些一句话函数并进行替换:

function tryGetFunction(v, scope, targetName, binding) {
    if (types.isIdentifier(v)) {
        binding = scope.getBinding(v.name);
        if (!binding) {
            return ;
        }
        if (binding.kind === "param") {
            return;
        }
        return tryGetFunction(binding.path.node.init, binding.scope, targetName, binding);
    }
    if (types.isObjectExpression(v)) {
        const properties = v.properties;
        for (const pro of properties) {
            const key = pro.key.name || pro.key.value;
            if (key === targetName) {
                return [pro.value, scope];
            }
        }

        if (!binding) {
            return ;
        }

        // 从赋值语句中查找
        for (const ref of binding.referencePaths) {
            if (!types.isAssignmentExpression(ref.parentPath.parent)) {
                continue;
            }
            const assign = ref.parentPath.parent;
            if (!types.isMemberExpression(assign.left)) {
                continue;
            }
            if ((assign.left.property.name || assign.left.property.value) === targetName) {
                return [assign.right, ref.parentPath.parentPath.scope];
            }
        }
    }
}

function handleSimpleCallExpression(path) {

    const {node} = path;
    if (!types.isMemberExpression(node.callee) || !types.isIdentifier(node.callee.object)) {
        return;
    }
    const calleeBinding = path.scope.getBinding(node.callee.object.name);
    if (!calleeBinding) {
        return;
    }
    let func = null;
    let funcScope = null;

    const targetName = node.callee.property.name || node.callee.property.value;
    const info = tryGetFunction(calleeBinding.path.node.init, calleeBinding.scope, targetName, calleeBinding.scope.getBinding(node.callee.object.name));
    if (Array.isArray(info)) {
        func = info[0];
        funcScope = info[1];
    }

    if (func === null || funcScope === null) {
        return;
    }
    if (!types.isFunctionExpression(func)) {
        return;
    }
    let returnStatement = null;
    // 只有一个return语句
    if (func.body.body.length === 1 && types.isReturnStatement(func.body.body[0])) {
        returnStatement = func.body.body[0];

    } else if (func.body.body.length === 2) {
        // 可能有一个return语句, 一个var语句, 例如:
        // FhNDr: function (t, r) {
        //                       return ut["AaWYl"](t, r);
        //                       var n, e, o, i;
        //                     },
        // eljBu: function (t, r) {
        //                       return ut["UlYjQ"](t, r);
        //                       var n, e;
        //                     }
        if (types.isReturnStatement(func.body.body[0]) && types.isVariableDeclaration(func.body.body[1])) {
            returnStatement = func.body.body[0];
        } else if (types.isReturnStatement(func.body.body[1]) && types.isVariableDeclaration(func.body.body[0])) {
            returnStatement = func.body.body[1];
        }
    }

      if (!returnStatement) {
        return;
    }

    if (!types.isCallExpression(returnStatement.argument) && !types.isBinaryExpression(returnStatement.argument) && !types.isLogicalExpression(returnStatement.argument)) {
        return;
    }
}

traverse(ast, {
    CallExpression: {
        exit: [
            handleSimpleCallExpression
        ]
    }
});

开始遍历替换:

function handleSimpleCallExpression(path) {
  // 其余代码...

  // 寻找function的path
    let funcPath = null;
    funcScope.path.traverse({
        FunctionExpression: function (mpath) {
            const {node: mnode} = mpath;
            if (mnode === func) {
                funcPath = mpath;
                mpath.skip();
            }
        }
    });

    if (funcPath === null) {
        return;
    }

    // 得到传入参数节点和函数参数节点的对应关系, 准备将return表达式中的所有设计这些函数参数的替换为实际的传入参数.
    const paramToArgument = {};
    for (let i = 0; i < func.params.length; i++) {
        const param = func.params[i];
        const argument = node.arguments[i];
        if (typeof argument === "undefined") {
            break;
        }
        paramToArgument[param.name] = argument;
    }

    const beforeCode = generator(node).code;

    // 替换先实现保留原来的function定义
    const originalFunc = types.cloneNode(func, true);

    // 遍历替换对应节点
    funcPath.traverse({
        Identifier: function (mpath) {
            const { node: mnode } = mpath;
            if (paramToArgument[mnode.name]) {
                mpath.replaceWith(paramToArgument[mnode.name]);
                mpath.skip();
            }
        }
    });

    const afterCode = generator(funcPath.node.body.body[0].argument).code;

    // 替换函数调用为return语句中的argument表达式
    path.replaceInline(returnStatement.argument);

    // 还原原来的function
    funcPath.replaceInline(originalFunc);

    console.log(`简化函数调用: ${beforeCode.slice(0, 30)}... -> ${afterCode.slice(0, 30)}...`);
}


前后对比效果:

然后再将BinaryExpression可以计算出常量的语句替换为常量,例如:

1.4 BinaryExpression常量计算

针对一些可以直接得到结果的BinaryExpression表达式,使用babel path自带的evaluate计算得到值,在这里只应用字符串类型:

function BinaryExpressionConstantCalculation(path) {
    const { confident, value } = path.evaluate();
    if (!confident) {
        return;
    }
    // 只应用字符串类型
    if (typeof value !== "string") {
        return;
    }
    console.log(`常量计算: ${generator(path.node).code} -> ${value}`);
    path.replaceWith(types.valueToNode(value));
}


此时阅读代码逻辑已经基本很清晰了,还有一些控制流混淆和一些无用代码没有使用babel 整理,但是都比较简单,基本不影响理解其代码逻辑,在此不展开了,感兴趣或想练手的朋友继续编写babel操作ast还原的代码。

参数分析

原本打算直接使用overwrite content替换为还原后的js代码进行断点分析,但是发现替换后会使得页面变为空白,不过这不影响,直接参照还原后的js找到对应的代码行进行断点分析即可。


代码中搜索关键词"w_tsfp"得到15处搜索结果,找出其中可能有生成逻辑的代码:

经过查找发现其中一处有诸多参数生成逻辑,极有可能是w_tsfp参数的生成逻辑代码:


之后找到原js中对应的代码段打上断点,如果有经过这里,基本敲定是这里生成的w_tsfp:

经过断点确认确实是这里,因此只需要分析这段代码的逻辑即可:

2.1 function(G, C)函数

先来看function(G, C)这个函数


很容易看出来是一个rc4加密算法,G是key,C是value, 其中key是固定的:



如果事先不知道rc4算法,导致不知道这段代码的做什么操作,也可以先将控制流手动还原下,把代码抠出来问下大模型即可:

由于已知key,因此可以尝试将w_tsfp解密出来校验下是不是真的是rc4算法:

2.2 basets/loadts/timestamp

这3个参数全部来自c = parseInt(new Date()["getTime"]()

2.3 fingerprint参数

核心代码, 其中c是2.2的timestamp

h = v(JSON["stringify"](window["pacus"]), c)

window["pacus"]可以在console中查看,有一大堆参数:


手动在console中执行下,发现每次都生成不同的32位字符串:

先分析下v函数, 代码从return开始往回看,追踪相关代码:

可以明显看出是一个md5算法,其中四个幻数和MD5 每轮需要加的常数也都对上了,似乎没有魔改。继续往上看:

实际参与运算的是R变量,R变量由Z + J得到, Z和J都是传入进来的参数。

不过如果Z和J如果是固定的值,则md5值应该是不会变得,但是console中每次运算都会返回一个新的32位,因此要找下这两个参数是不是有哪一步被变更了:


通过查看Z的引用可以看到当调用v函数时,如果传入的arguments没有第三个参数,则会给Z加上32位的随机字符串,因此在console中每次执行才会返回不同的32位字符串(只传入了两个参数),所以fingerprint实际上是随机的32位md5字符串。除此之后,还需要校验下md5有没有魔改,如果有魔改则需要找出魔改点并复现: 已知v函数传入三个参数可以不加32位随机字符串,因此可以在console中执行如下操作:


可以看到此时的结果是固定的,而且实际参与运算的只有前两个参数, 其md5值与"12"的md5值完全一致, 因此v函数md5算法没有魔改.

2.4 checksum参数

核心代码, 其中Jwindow["location"]["href"], h是fingerprint参数

A["checksum"] = v(J + h, new Date()["getTime"]())

由于传入的参数只有两个,因此checksum参数也是随机32位md5值

2.5 fingerprint和checksum二次加载分析

通过2.32.4的分析知道fingerprint和checksum都是随机的,这很令人疑惑. 经过继续查看网络请求可以知道,probe.js在第二次请求目标地址之后又重新加载了一次,此时的probe.js代码与第一次加载的probe.js稍有不同:


第二次加载的probe.js混淆方式与第一次加载的大同小异,按照上述方法还原之后全文搜索checksum关键词:


可以知道,其中:

L["checksum"] = z(P, L["timestamp"], I)

传入了三个参数其中P是uri, I是localStorage中存的fingerprint(为第一次生成的随机fingerprint值), v函数变为了z函数,z函数内部似乎也有一些不同,不过还是标准md5算法,只是多了一个参数N4(P和x是传入参数), 这个N4实际是fingerprint值.

感悟总结

probe.js混淆用到的都是很常见的方式,还原很容易,不过调试似乎有点麻烦。

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (52.78 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (212.37 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (165.83 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (144.21 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (59.2 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (29.28 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (173.1 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (104.16 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (285.02 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (174.37 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (54.85 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (74.2 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (100.89 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (185.42 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (202.01 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (245.52 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (198.9 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (80.55 KB, 下载次数: 1)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png (63.26 KB, 下载次数: 0)

1712048977504-3d661aa5-60de-4ffc-be9e-70641b238ff7.png

免费评分

参与人数 8吾爱币 +7 热心值 +8 收起 理由
Cofei430 + 1 + 1 谢谢@Thanks!
janken + 1 + 1 热心回复!
stone102 + 1 我很赞同!
mufeng001 + 1 + 1 谢谢@Thanks!
FitContent + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
zzyzy + 1 + 1 谢谢@Thanks!
BonnieRan + 1 + 1 谢谢@Thanks!
这是追求不是梦 + 1 + 1 热心回复!

查看全部评分

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

沙发
mufeng001 发表于 2024-4-21 20:39
蜂窝的找了两天了,谢谢楼主分享
3#
Lty20000423 发表于 2024-4-22 07:45
4#
Hmily 发表于 2024-4-22 08:59
同学,底部的图片是不是开始那几张,忘记贴进去替换网络地址了?
5#
BonnieRan 发表于 2024-4-22 09:10
楼主这篇教程的ast解混淆逻辑清晰, 过程很详细,拿来练手label很合适
6#
 楼主| kylin1020 发表于 2024-4-22 09:13 |楼主
Hmily 发表于 2024-4-22 08:59
同学,底部的图片是不是开始那几张,忘记贴进去替换网络地址了?

排版乱了,我今天再重新整理下
7#
xixicoco 发表于 2024-4-22 17:24
楼主写的很详细,支持
8#
mufeng001 发表于 2024-4-22 23:04
佬,那个_sn有办法算吗
9#
 楼主| kylin1020 发表于 2024-4-22 23:05 |楼主
mufeng001 发表于 2024-4-22 23:04
佬,那个_sn有办法算吗

哪个sn,
10#
mufeng001 发表于 2024-4-22 23:13

蜂窝的请求参数里有个_sn
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

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

GMT+8, 2024-5-6 14:11

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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