寂静的夜晚月光和一个寂寞的灵魂。
山崖之巅,秋城落叶躺在草地上,👄+🥬最终叼着一根青草,任由那微微的酸涩在口中爆开。
“那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+IYTrrSNgztWK6pdbM3bBDWVtQeP6oJWbY10PCP24Wy
kxo/6nn0xRDFM4Y+QKMrztEb64JvpKxUI4QFnn67PDJtW3QnvvQNJKzO+xWFDNKZ
wB5kPZ0WGROBgAWjp2/v6hUN/1+JWoIwDilpa0LwHZ8OMvcyu/6lAgMBAAE=
-----END RSA PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC7kFxC8LhpAw/iGE660jYM7ViuqXWzN2wQ1lbUHj+qCVm2NdDw
j9uFspMaP+p59MUQxTOGPkCjK87RG+uCb6SsVCOEBZ5+uzwybVt0J770DSSszvsV
hQzSmcAeZD2dFhkTgYAFo6dv7+oVDf9fiVqCMA4paWtC8B2fDjL3Mrv+pQIDAQAB
AoGAVJClyFiYDGChDKNA++JDFFj+nuEwe/kE9CJvS3vH4HYOyKRC6/MwWntE75TZ
ttqw7vq6XFA8/FSIDqez6z9C0tlo1Gj1qIVFSmqeDaq1DoECFtkAIfSKmMbea8nL
AshUlPiKZ7msDq38+GQmVIHvfOrN8iiyC3Jr39Z2szEN8BECQQDt8m8evi1PFoNg
TgO4a+szLHGt85ztHDOgm3OfftqSC1TL9hpAgRyIrjCukfIYNGQhyAm6RfhmE3Fu
06xFkRhbAkEAyctaIMSC9FPY/CL1MYKSRvS7ZZYoHh8DZF/NCnt9EmyEM3KPM/xJ
IKTO6UxKiqfGAtAUMLiBoyu9Y0rU5Fr0/wJANMTdC85VMgLmI8dpX87fHDwxAcjS
9mqYsHeJDsgNJPJKXek4LTH06ALpXO2U6PVFd5BrR9oYmlqZf2CGBe+FnQJBAJCc
0IwnCAn8hMW8b6b5gcaj4CAfCcT8SLwIA7L9aFZpuhv8fy+sHuPr9/QtHkZbkYW2
hKGduBmtYN3lZMf5fxUCQHRhDYJe4nVVw7spQRf5zwni4xUuTFicDMaiMLedTLBF
I7a+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