【逆向分析】全新思路,Xmind macOS & Windows (23.05|2005) 通杀方案
2023-5-18 11:7:50 Author: 利刃信安攻防实验室(查看原文) 阅读量:128 收藏

寂静的夜晚月光和一个寂寞的灵魂。
山崖之巅,秋城落叶躺在草地上,👄+🥬最终叼着一根青草,任由那微微的酸涩在口中爆开。

“那Xmind财团不仅夺走了我的一切,还扶持了一个同样的天才少年,真是可恶!怎么才能...”少年恶狠狠的想着,看着模糊的夜色,突然眼中一亮,一个恶毒的计划在心中开始酝酿...

却说那 Xmind乃是思维导图行业小有盛名的公司,而旗下的 Xmind 产品更是使用了 Node 字节码技术保护了主程序,让无数天才少年铩羽而归,一举奠定了反破解的巅峰!

而此刻昏黄的灯光下,少年面前的MacBook Pro 中运行的正是XMind!
少年此时正在操作 asar 进行解包,那 Xmind 却是依赖了 Electron 技术,本质上还是 Vue3 + Pinia 实现了全平台。

随后少年用 Visual Studio Code 打开 app 文件夹,开始查阅反编译的代码.

经过少年的观察,这里的升级至Pro很有重大嫌疑。

经过进一步的搜索,发现 Xmind 4412 行代码中认为this.activationStatus === u.ACTIVATION_STATUS.VALID 表示激活,否则显示激活按钮。

而this.activationStatu来自 e.status ,搜索一番后发现:

E = (0, i.computed)(() => {const e = (0, s.useAccountStore)();if (!e.rawSubscriptionData) return null;try {  return N(e.rawSubscriptionData);} catch (e) {  return null;}});S = (0, i.computed)(() => {if (!E.value) return c.ACTIVATION_STATUS.TRIAL;{  const { status: e, expireTime: t } = E.value && E.value;  if (e && e === c.SUBSCRIPTION_SERVER_STATUS.EXPIRED)    return c.ACTIVATION_STATUS.EXPIRED;  if (e && e === c.SUBSCRIPTION_SERVER_STATUS.VALID)    return t && p.value && new Date(t) < new Date(p.value)      ? c.ACTIVATION_STATUS.EXPIRED      : c.ACTIVATION_STATUS.VALID;}}),

代码来自这里,E.value 如果为 NULL,则返回试用ACTIVATION_STATUS.TRIAL,所以我们需要关注N(e.rawSubscriptionData)这个数据。

函数 N 如下:

d=String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45);
N = (e) => {const t = Buffer.from(e, "base64"), n = a.default.publicDecrypt( { key: d, padding: a.default.constants.RSA_PKCS1_PADDING }, t );return JSON.parse(n.toString());};

而 d 实际上值为

-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDYH31l0llicBavbUZRg0y1LnI\n2JJuPZak0498wGmK0N+ksqCzA0XUfCgQ5E9itYyPuT+z6Pz/+0q6NeApkWcnC/Th\nWQY6ZlEOMonrhPub8zsWYOZzckQutx3jn6k+6ZXx7yUbbkxIk+wqWgnlQxnx6TMd\nS3rgo3r4blFTWi6EEQIDAQAB\n-----END PUBLIC KEY-----

由此可见,这是一个 RSA 公钥,然后我们用他解密我们的e.rawSubscriptionData 试试。
e.rawSubscriptionData 来自于抓包。

{"status": "Trial", "expireTime": 0, "ss": "", "deviceId": ""},status 如果为“sub”表示订阅有效,expireTime 表示到期时间,其他值不管。

所以我们只要伪造返回数据即可,自己生成一对密钥,然后伪造加密数据:
我的密钥对

-----BEGIN RSA PUBLIC KEY-----MIGJAoGBALuQXELwuGkDD+IYTrrSNgztWK6pdbM3bBDWVtQeP6oJWbY10PCP24Wykxo/6nn0xRDFM4Y+QKMrztEb64JvpKxUI4QFnn67PDJtW3QnvvQNJKzO+xWFDNKZwB5kPZ0WGROBgAWjp2/v6hUN/1+JWoIwDilpa0LwHZ8OMvcyu/6lAgMBAAE=-----END RSA PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----MIICXAIBAAKBgQC7kFxC8LhpAw/iGE660jYM7ViuqXWzN2wQ1lbUHj+qCVm2NdDwj9uFspMaP+p59MUQxTOGPkCjK87RG+uCb6SsVCOEBZ5+uzwybVt0J770DSSszvsVhQzSmcAeZD2dFhkTgYAFo6dv7+oVDf9fiVqCMA4paWtC8B2fDjL3Mrv+pQIDAQABAoGAVJClyFiYDGChDKNA++JDFFj+nuEwe/kE9CJvS3vH4HYOyKRC6/MwWntE75TZttqw7vq6XFA8/FSIDqez6z9C0tlo1Gj1qIVFSmqeDaq1DoECFtkAIfSKmMbea8nLAshUlPiKZ7msDq38+GQmVIHvfOrN8iiyC3Jr39Z2szEN8BECQQDt8m8evi1PFoNgTgO4a+szLHGt85ztHDOgm3OfftqSC1TL9hpAgRyIrjCukfIYNGQhyAm6RfhmE3Fu06xFkRhbAkEAyctaIMSC9FPY/CL1MYKSRvS7ZZYoHh8DZF/NCnt9EmyEM3KPM/xJIKTO6UxKiqfGAtAUMLiBoyu9Y0rU5Fr0/wJANMTdC85VMgLmI8dpX87fHDwxAcjS9mqYsHeJDsgNJPJKXek4LTH06ALpXO2U6PVFd5BrR9oYmlqZf2CGBe+FnQJBAJCc0IwnCAn8hMW8b6b5gcaj4CAfCcT8SLwIA7L9aFZpuhv8fy+sHuPr9/QtHkZbkYW2hKGduBmtYN3lZMf5fxUCQHRhDYJe4nVVw7spQRf5zwni4xUuTFicDMaiMLedTLBFI7a+DNlOoXgdhlO4uivv4IPcWaRCe3/HdzJobZ8FmxQ=-----END RSA PRIVATE KEY-----

我伪造的信息
{"status": "sub", "expireTime": 4093057076000, "ss": "", "deviceId": ""}

加密后信息

eeZRXhL4ZY6ftIFDi1JU9XA1mqJaUuiJFgmZySEz50u/HW31e4Tucf4jkCXPRJO3fsLcUYXgK9fjY4H6FnUK4Wh5xBxAdUx+3p986xXZg85fEKtyxyZmuCAff8MNvOBsOLxmJkN2i4+iyuDGQkmhhFx3k60RkeczyV80BM9lbWI=

下面就是考虑怎么替换这个返回值。

少年微微皱眉,分析到现在,却没有任何实质性进展,不禁有些急躁。

如本文标题所见,本文主要是通杀 Windows 版本,而 Windows 版本有 bytecode字节码加密,所以不能像 macOS 上修改 js 一样轻松。

const crypto = require("crypto");
// 保存原始的 publicDecrypt 函数const originalPublicDecrypt = crypto.publicDecrypt;
const originalPublicDecryptEx = function (message) { let key = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDYH31l0llicBavbUZRg0y1LnI\n2JJuPZak0498wGmK0N+ksqCzA0XUfCgQ5E9itYyPuT+z6Pz/+0q6NeApkWcnC/Th\nWQY6ZlEOMonrhPub8zsWYOZzckQutx3jn6k+6ZXx7yUbbkxIk+wqWgnlQxnx6TMd\nS3rgo3r4blFTWi6EEQIDAQAB\n-----END PUBLIC KEY-----`; const n = originalPublicDecrypt( { key: key, padding: 1, }, message ); return n;};
// 将 publicDecrypt 函数定义为 getter 方法,返回新的实现Object.defineProperty(crypto, "publicDecrypt", { get() { return function myPublicDecrypt(...args) { console.trace("myPublicDecrypt 调用栈"); console.log("秋城落叶Hook Xmind开始");
args[0]["key"] = "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBALuQXELwuGkDD+IYTrrSNgztWK6pdbM3bBDWVtQeP6oJWbY10PCP24Wy\nkxo/6nn0xRDFM4Y+QKMrztEb64JvpKxUI4QFnn67PDJtW3QnvvQNJKzO+xWFDNKZ\nwB5kPZ0WGROBgAWjp2/v6hUN/1+JWoIwDilpa0LwHZ8OMvcyu/6lAgMBAAE=\n-----END RSA PUBLIC KEY-----"; let result; try { result = originalPublicDecrypt.call(this, ...args); let data = JSON.parse(result.toString()); data.status = "sub"; data.expireTime = 4093057076000; result = Buffer.from(JSON.stringify(data)); crypto.log("用自己的密钥解密成功,开始走我的密钥解密流程。", data); } catch (e) { crypto.log("解密出错,开始走官方密钥解密流程。"); result = null; let ori = originalPublicDecryptEx(args[1]); crypto.log( "解密出错", args[1].toString("base64"), "\n官方密钥解密结果", ori, "\n错误细节\n", e ); result = ori; }
// 调用原始的 publicDecrypt 函数 return result; }; },});
Object.defineProperty(crypto, "log", { get() { return function log(...args) { console.log(...args); }; },});
module.exports = crypto;

我们知道 nodejs 中有一个概念叫模块缓存,这是为了优化性能而设计的。
当我们下一次 require 某个模块的时候,会从缓存里去读取模块缓存代码,而这正好为我们的攻击提供了便利。
让我们来看上方一段代码,利用模块重导出技术我们成功 Hook 了 Main.js 并修改了加密函数的 key 为我们自己的 key。

我们只需要在 main.js 文件头部加上一行引入即可。

运行试试:

我们已经成功注入进去代码,实现了无侵入式修改。

下一步,我们伪造 Http 返回值:

利用NodeJS的内部模块,我们直接监听了本地一个 socket 端口,实现了一个简易服务器,地址为: 127.0.0.1:3000.

然后分别判断 req 来源的 path 判断请求的 Http 地址是什么,并返回伪造好的数据。

const http = require("http");const url = require("url");
const hostname = "127.0.0.1";const port = 3000;
const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url, true); const path = parsedUrl.pathname; const method = req.method;
res.setHeader("Content-Type", "application/json; charset=utf-8");
if (path === "/_res/session" && method === "GET") { res.statusCode = 200; res.end( JSON.stringify({ uid: "_xmind_1234567890", group_name: "", phone: "18888888888", group_logo: "", user: "_xmind_1234567890", cloud_site: "cn", expireDate: 4093057076000, emailhash: "1234567890", userid: 1234567890, if_cxm: 0, _code: 200, token: "1234567890", limit: 0, primary_email: "[email protected]", fullname: "[email protected]", type: null, }) ); } else if (path === "/_api/check_vana_trial" && method === "POST") { res.statusCode = 200; res.end(JSON.stringify({ code: 200, _code: 200 })); } else if (path === "/_res/get-vana-price" && method === "GET") { res.statusCode = 200; res.end( JSON.stringify({ products: [ { month: 6, price: { cny: 0, usd: 0 }, type: "bundle" }, { month: 12, price: { cny: 0, usd: 0 }, type: "bundle" }, ], code: 200, _code: 200, }) ); } else if (path === "/_api/events" && method === "GET") { res.statusCode = 200; res.end(JSON.stringify({ code: 200, _code: 200 })); } else if (path === "/_res/user_sub_status" && method === "GET") { res.statusCode = 200; res.end(JSON.stringify({ _code: 200 })); } else if (path === "/piwik.php" && method === "POST") { res.statusCode = 200; res.end(JSON.stringify({ code: 200, _code: 200 })); } else if (path.startsWith("/_res/token/") && method === "POST") { res.statusCode = 200; res.end( JSON.stringify({ uid: "_xmind_1234567890", group_name: "", phone: "18888888888", group_logo: "", user: "_xmind_1234567890", cloud_site: "cn", expireDate: 4093057076000, emailhash: "1234567890", userid: 1234567890, if_cxm: 0, _code: 200, token: "1234567890", limit: 0, primary_email: "[email protected]", fullname: "[email protected]", type: null, }) ); } else if (path === "/_res/devices" && method === "POST") { res.statusCode = 200; res.end( JSON.stringify({ raw_data: "eeZRXhL4ZY6ftIFDi1JU9XA1mqJaUuiJFgmZySEz50u/HW31e4Tucf4jkCXPRJO3fsLcUYXgK9fjY4H6FnUK4Wh5xBxAdUx+3p986xXZg85fEKtyxyZmuCAff8MNvOBsOLxmJkN2i4+iyuDGQkmhhFx3k60RkeczyV80BM9lbWI=", license: { status: "sub", expireTime: 4093057076000, }, _code: 200, }) ); } else { res.statusCode = 404; res.end("Not Found"); }});
server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`);});
require("./hook/crypto");require("./hook/electron");

然后利用上面说到的代码注入技术 hook掉 electron.net.request 包的请求,拦截网络请求并修改域名:

const electron = require("electron");
// 获取原始的 net 模块const originalNet = electron.net;
// 保存原始的 request 函数const originalRequest = originalNet.request;
// 修改 request 函数Object.defineProperty(originalNet, "request", { get() { return function (options, callback) { options["url"] = options["url"].replace( "https://www.xmind.cn", "http://127.0.0.1:3000" ); console.error( "===== Intercepting net.request with options:", options, callback ); const req = originalRequest(options, callback);
// 注册 response 事件监听器 req.on("response", (response) => { let data = ""; response.on( "data", function (chunk) { data += chunk; chunk = "FUCKING data"; this.emit("continue", chunk); }.bind(response) ); response.on( "end", function () { // 将数据添加到缓存 // cache[options.url] = data; // console.log("Response ----- ", data); this.emit("continue"); }.bind(response) ); }); return req; }; return function (options, ...args) { // 对 options 进行修改或者添加自己的逻辑 console.error("===== Intercepting net.request with options:", options);
// { url: 'https://www.xmind.cn/_res/user_sub_status', method: 'GET' } // { url: 'https://www.xmind.cn/_res/devices', method: 'POST' }
// 调用原始的 request 函数 return originalRequest.call(this, options, ...args); }; },});
module.exports = electron;

可以看出菜单已经没有 VIP 提示了,但是主界面还有升级到 Pro 的按钮。

对于这种情况我们分析代码可知:

E = (0, i.computed)(() => {                const e = (0, s.useAccountStore)();                if (!e.rawSubscriptionData) return null;                try {                  return N(e.rawSubscriptionData);                } catch (e) {                  return null;                }              }),

s.useAccountStore这里就是前端的 localStorage 存储,所以肯定是读取的我们伪造的信息毋庸置疑,但是为什么还提示升级?其实是我们的公钥Hook没有覆盖到 renderer 层js代码,所以我们手动替换所有的公钥:

=String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)

替换为

=String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,82,83,65,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,74,65,111,71,66,65,76,117,81,88,69,76,119,117,71,107,68,68,43,73,89,84,114,114,83,78,103,122,116,87,75,54,112,100,98,77,51,98,66,68,87,86,116,81,101,80,54,111,74,87,98,89,49,48,80,67,80,50,52,87,121,10,107,120,111,47,54,110,110,48,120,82,68,70,77,52,89,43,81,75,77,114,122,116,69,98,54,52,74,118,112,75,120,85,73,52,81,70,110,110,54,55,80,68,74,116,87,51,81,110,118,118,81,78,74,75,122,79,43,120,87,70,68,78,75,90,10,119,66,53,107,80,90,48,87,71,82,79,66,103,65,87,106,112,50,47,118,54,104,85,78,47,49,43,74,87,111,73,119,68,105,108,112,97,48,76,119,72,90,56,79,77,118,99,121,117,47,54,108,65,103,77,66,65,65,69,61,10,45,45,45,45,45,69,78,68,32,82,83,65,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)

下载我写好的 js 文件,解压到 main文件夹内,然后在 main.js 文件头部增加一行"require("./hook")"即可。

这里不用 vscode 替换,因为 vscode换完会自动格式化代码,导致代码出现异常,所以用文本编辑器暴力替换。

替换完打包重新运行看看:

成功拿下。

压下内心的激动,少年用力的喝了口自来水。想起父母临别前的叮嘱:“三年内千万不要去报仇!时机尚未成熟,尔等还需隐忍三年!”

少年闭上了双眼,三年.....

没想到Windows 下 Hook 破解的操作竟和 macOS 下一模一样!真的做到的通杀!

临时文件夹的路径=随便找个目录 比如D:/code即可
app.asar的路径=Xmind 安装目录中的 resources 文件夹中的app.asar文件完整路径

只见少年熟练的在 Windows 下安装 nodejs最新版本,cmd执行 npm i -g asar 安装 asar 工具包,随后打开 cmd 执行 "asar extract app.asar的路径 临时文件夹的路径"解包 asar 文件为源代码。

extract表示解包
pack表示打包

得到了 asar 解包后的文件,照旧将附件解压出来的hook.js文件和hook文件夹复制到asar文件解包出来的 main文件夹中

完成后如图所示。


打开main.js 在头部加入一行"require("./hook");",记住千万要顶部加入一行,并且结尾要有";"号,防止编译出错。至于为什么不加分号编译会出错,懂得都懂。


复制文件只是第一步注入,第二步是替换所有 js 文件里面的公钥为我自己的公钥:


接下来Sublime Text/VSCode 搜索替换所有js里面的的公钥为我的 RSA 公钥,具体操作和替换的代码在上面有,仔细查看。全部替换并保存所有文件后并打包回 app.asar就完成了!

解包出来所有的js文件批量搜索替换即可,不用一个个去替换。

=String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,102,77,65,48,71,67,83,113,71,83,73,98,51,68,81,69,66,65,81,85,65,65,52,71,78,65,68,67,66,105,81,75,66,103,81,67,68,89,72,51,49,108,48,108,108,105,99,66,97,118,98,85,90,82,103,48,121,49,76,110,73,10,50,74,74,117,80,90,97,107,48,52,57,56,119,71,109,75,48,78,43,107,115,113,67,122,65,48,88,85,102,67,103,81,53,69,57,105,116,89,121,80,117,84,43,122,54,80,122,47,43,48,113,54,78,101,65,112,107,87,99,110,67,47,84,104,10,87,81,89,54,90,108,69,79,77,111,110,114,104,80,117,98,56,122,115,87,89,79,90,122,99,107,81,117,116,120,51,106,110,54,107,43,54,90,88,120,55,121,85,98,98,107,120,73,107,43,119,113,87,103,110,108,81,120,110,120,54,84,77,100,10,83,51,114,103,111,51,114,52,98,108,70,84,87,105,54,69,69,81,73,68,65,81,65,66,10,45,45,45,45,45,69,78,68,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)
替换为
=String.fromCharCode(45,45,45,45,45,66,69,71,73,78,32,82,83,65,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45,10,77,73,71,74,65,111,71,66,65,76,117,81,88,69,76,119,117,71,107,68,68,43,73,89,84,114,114,83,78,103,122,116,87,75,54,112,100,98,77,51,98,66,68,87,86,116,81,101,80,54,111,74,87,98,89,49,48,80,67,80,50,52,87,121,10,107,120,111,47,54,110,110,48,120,82,68,70,77,52,89,43,81,75,77,114,122,116,69,98,54,52,74,118,112,75,120,85,73,52,81,70,110,110,54,55,80,68,74,116,87,51,81,110,118,118,81,78,74,75,122,79,43,120,87,70,68,78,75,90,10,119,66,53,107,80,90,48,87,71,82,79,66,103,65,87,106,112,50,47,118,54,104,85,78,47,49,43,74,87,111,73,119,68,105,108,112,97,48,76,119,72,90,56,79,77,118,99,121,117,47,54,108,65,103,77,66,65,65,69,61,10,45,45,45,45,45,69,78,68,32,82,83,65,32,80,85,66,76,73,67,32,75,69,89,45,45,45,45,45)

说白了就是搜索替换文本,把上面的fromCharCode替换成下面的fromCharCode。


还有,如果搜索结果搜索不到这串公钥说明你搜索范围有问题,我要你搜索的是 main文件夹同级的renderer文件夹内的所有 js 文件,而且不要用 vscode 格式化 js 文件!这些 js 文件应该是压缩好的,格式化js文件后会搜索不到!!

替换示例:

打包: cmd执行 "asar pack 临时文件夹的路径 app.asar的路径"

打开 Xmind 后登录账号 123 密码 123


随后便用力打开xmind:

"这不可能!"Xmind丶顶针珍珠惊呼道,惊骇溢于言表!台下更是爆发出阵阵尖叫!

“回来了,一切都回来了!”


少年站在聚光灯下,享受着这awesome的moment,哈哈狂笑道:“你....输了!”

软件下载地址:

https://dl2.xmind.cn/Xmind-for-Windows-x64bit-23.05.2004.exe
https://dl2.xmind.cn/Xmind-for-macOS-23.05.2005.dmg

原文地址:

https://www.52pojie.cn/thread-1786811-1-1.html

文章来源: http://mp.weixin.qq.com/s?__biz=MzU1Mjk3MDY1OA==&mid=2247504024&idx=1&sn=41ca3518b3caaa4107f3a60a8f206313&chksm=fbfb6055cc8ce9434fa4ebcfd37207736a29f699029e5c1baf7487e631feaf1c4a15934eb54d#rd
如有侵权请联系:admin#unsafe.sh