在网上冲浪,看到了一个网站的JS加密,下面有一句话:
乍一看这句话吓一跳,我去这么猛,然后就很有兴趣想看看究竟是怎样一种加密算法。
对于破解JS加密算法的时候,都是先输入一个简单的语句然后分析加密后语句的规律,这里先输入一个简单的打印log:
代码拷出来格式化一下:
var __encode = 'sojson.com', _0xb483 = ["\x5F\x64\x65\x63\x6F\x64\x65", "\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x73\x6F\x6A\x73\x6F\x6E\x2E\x63\x6F\x6D\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x6F\x62\x66\x75\x73\x63\x61\x74\x6F\x72\x2E\x68\x74\x6D\x6C"]; (function(_0xd642x1) { _0xd642x1[_0xb483[0]] = _0xb483[1] })(window); var _0xdc02 = ["\x6F\x6B", "\x6C\x6F\x67"]; console[_0xdc02[1]](_0xdc02[0])
这个时候规律已经挺明显了,不急先将十六进制、Unicode等转为易读形式:
<html> <head> <meta charset="UTF-8"/> <title>JavaScript 16进制、Unicode解码</title> </head> <body> <form action="#"> <textarea id="js-code" cols="100" rows="30"></textarea> <button id="decode-btn" type="button">DECODE</button> </form> <script type="text/javascript"> !function () { document.getElementById("decode-btn").addEventListener("click", event => { const jsCodeBox = document.getElementById("js-code"); // 可能会有中文的unicode,要能够兼容 jsCodeBox.value = jsCodeBox.value.replace(/\\x..|\\u.{4}/g, hex => decode(hex)); }); function decode(hex) { try { const c = parseInt(hex.substring(2), 16); return String.fromCharCode(c); } catch (e) { console.log("parse " + hex + " error."); } return hex; } }(); </script> </body> </html>
十六进制、Unicode解码之后:
var __encode = 'sojson.com', _0xb483 = ["_decode", "http://www.sojson.com/javascriptobfuscator.html"]; (function(_0xd642x1) { _0xd642x1[_0xb483[0]] = _0xb483[1] })(window); var _0xdc02 = ["ok", "log"]; console[_0xdc02[1]](_0xdc02[0])
这个时候规律已经超级明显了,就是将属性访问、字符串常量等乱七八糟的提取到一个字典中,然后再引用这个字典,可是这种既做不到不可逆(我下面会写一个专破工具),也做不到体积小,因为len("[_0xdc02[1]]")===12,而len(".log")===4,只在属性名超过11(12-1)并且被引用多次时才能够节省空间,这里的设计一大败笔就是字典名字出现频率极高,但是给的变量名字还是这么长,哈夫曼编码了解一下呗。至于运行速度没有差别,这点时间差别人当然是感觉不出来但多做了一次数组访问肯定是慢了。
下面是针对此网站的高级JS加密写的一个专破工具,基本能够实现还原:
<html> <head> <meta charset="UTF-8"/> <title>sojson js高级加密专破工具, https://www.sojson.com/javascriptobfuscator.html</title> </head> <body> <form action="#"> <textarea name="js-code" id="js-code" cols="100" rows="30"></textarea> <button id="decode-btn" type="button">DECODE</button> </form> <script type="text/javascript"> !function () { document.getElementById("decode-btn").addEventListener("click", event => { const jsCodeBox = document.getElementById("js-code"); const rawJs = jsCodeBox.value; let decodeJs = replaceDictionaryIndexReference(rawJs); decodeJs = squareBracketsToDot(decodeJs); decodeJs = dropSignature(decodeJs); jsCodeBox.value = decodeJs; /** * 字典引用替换为字面值常量 * * @param rawJs * @returns {*} */ function replaceDictionaryIndexReference(rawJs) { const dictionaryNameSet = extractDictionaryNames(rawJs); let decodeJs = rawJs; dictionaryNameSet.forEach(dicName => { // 将字典声明于当前上下文环境 const dicCode = new RegExp("(var\\s+|)" + dicName + "\\s*=\\s*\\[.+?\\];").exec(decodeJs)[0]; eval(dicCode); // 将访问到此变量的地方引用替换为字面值 let isChange = false; decodeJs = decodeJs.replace(new RegExp(dicName + "\\[\\d+\\]", "g"), index => { const dicIndex = parseInt(/\[(\d+)]/.exec(index)[1]); let result = eval(dicName + "[" + dicIndex + "]"); // 对于文本,需要加上双引号 if (!result.match(/^\d+$/)) { result = "\"" + result + "\""; } isChange = true; return result; }); // 如果此变量被使用过,则将其从原文中清除 if (isChange) { decodeJs = decodeJs.replace(dicCode, ""); } }); return decodeJs; } /** * 抽取出所有字典名称 * * @param rawJs * @returns {Set} */ function extractDictionaryNames(rawJs) { const re = /(_+\w+?)\s*=\s*\[.+?]/g; // const re = /(_0x\w+?)\s*=/g; const dictionaryNameSet = new Set(); while (dicName = re.exec(rawJs)) { dictionaryNameSet.add(dicName[1]); } return dictionaryNameSet; } /** * 方法调用尽量由["foo"]的形式转为点调用 * * @param decodeJs * @returns {string | void | *} */ function squareBracketsToDot(decodeJs) { return decodeJs.replace(/\w+\["\w+"]/g, call => { const nameAndAttr = call.replace("[\"", " ").replace("\"]", "").split(" "); try { // 只替换name在当前上下文中已存在并且attr的类型是function if (typeof eval(nameAndAttr[0] + "." + nameAndAttr[1]) === "function") { return nameAndAttr[0] + "." + nameAndAttr[1]; } } catch (e) { console.log("cannot replace: " + call); } return call; }); } /** * 丢弃作者的签名 * * @param encodeJs * @returns {string | void | *} */ function dropSignature(encodeJs) { return encodeJs.replace(/^var __encode.+?\(window\);/, ""); } }); }(); </script> </body> </html>
效果演示:
结论:
只是很简单的变量名混淆+字典压缩,而且字典变量名长度是个很严重的瑕疵,关于字典压缩可以参考JS Packer,我之前也写过一篇关于JS Packer的文章。
.