
背景:在分析某站點接口時發現以前發現漏洞的JS修復后進行了強混淆,看起來十分抽象。

于是乎擱置在一邊沒有繼續分析,直到前幾日在圖書館發現了小肩膀大佬寫的爬蟲混淆AST對抗,書中描述的幾種混淆方式與該站點使用的十分相似,遂嘗試使用AST對該JS進行一定程度的還原。
本篇針對該JS中的字符串混淆進行還原。
字符串是如何混淆的
解密方式
想要對字符串反混淆就要先分析該樣本是如何對字符串進行混淆的。
以一個字符串的解密為例子,可以發現他將字符串解密拆分成一串函數調用并對立即數進行減法操作來防止通用解密。

而處于全局作用域的_0x1f1a68實際上也是對另一個函數的調用。
function _0x1f1a68(_0x1be822, _0x79fd7, _0x340561, _0x170aa8, _0x35407a) { return _0x4903(_0x35407a - 0x252, _0x340561);}
經過在VSCode中對每個字符串解密函數查找定義,發現所有的字符串解密最終都是調用的_0x4903。
由于每個函數的調用時機跟作用域都不同,獲取每一個字符串解密函數的結果是不明智的。
于是這里需要實現的第一個功能就是將每一個字符串的解析還原成對_0x4903的調用,也就是將不同字符串解密函數的調用替換成對最根本的解密函數_0x4903的冪等形式。
還原
函數調用還原實現
舉個例子:
function _0x3cb10b(_0x9056d3, _0xd6da67, _0x4e8aa3, _0x575cfa, _0x50067e) { return _0x1f1a68(_0x9056d3 - 0x1ca, _0xd6da67 - 0x97, _0x4e8aa3, _0x575cfa - 0x13c, _0xd6da67 - 0x119); }function _0x362f86(_0xeb8495, _0x2bb06b, _0x3bc6ce, _0x59c29b, _0x141499) { return _0x3cb10b(_0xeb8495 - 0x1a0, _0xeb8495 - -0x370, _0x3bc6ce, _0x59c29b - 0x19c, _0x141499 - 0x120);}function _0x1f1a68(_0x1be822, _0x79fd7, _0x340561, _0x170aa8, _0x35407a) { return _0x4903(_0x35407a - 0x252, _0x340561);}
我們的目標是將
_0x362f86(0x9a3, 0xef2, '1vkx', 0x369, 0xb40)
轉換成
_0x3cb10b(0x9a3- 0x1a0, 0x9a3 - -0x370, '1vkx', 0x369 - 0x19c, 0xb40 - 0x120);
繼而轉換成
_0x1f1a68(0x9a3- 0x1a0 - 0x1ca, 0x9a3 - -0x370 - 0x97, '1vkx', 0x369 - 0x19c - 0x13c, 0x9a3 - -0x370 - 0x119);
最終轉換成
_0x4903(0x9a3 - -0x370 - 0x119 - 0x252, '1vkx');


那么如何使用AST實現呢,為了盡可能實現上下文無關減少狀態,這里采用像示例中的一樣一層一層的處理。
在代碼實現上我將其分為了多個部分。
function replaceArgsToIndex(funcargs, arg) { if (arg.type == "BinaryExpression") { return replaceArgsToIndex(funcargs, arg.left); } if (arg.name.startsWith("arg")) { return true; } for (let i = 0; i < funcargs.length; i++) { if (funcargs[i].name == arg.name) { arg.name = "arg" + i; return true; } } console.log("not found arg " + arg.name + " at " + arg.loc?.start.line); return false;}
第一步是將函數內的參數名轉換成參數下標,這樣就可以從CallExpression中直接用下標獲取對應的參數進行表達式替換,這里處理了BinaryExpression是因為參數中存在減法表達式的情況,但變量永遠在第一位,所以遞歸到最左面的變量再進行處理,同時如果參數已經被轉化成argN的形式便不做處理。
這里放一下關于二值表達式的表示:

如圖,每個紅框都是一個二值表達式,外層的二值表達式將內層的二值表達式作為左值,所以當變量為
xxx - 0x123 -0x456 -0x789
形式時我們要遞歸的獲取左值。
轉換后的形式為:
function _0x1f1a68(_0x1be822, _0x79fd7, _0x340561, _0x170aa8, _0x35407a) { return _0x4903(arg4 - 0x252, arg2);}
這樣就可以檢測所有對0x1f1a68的調用,獲取其中的第5個參數和第三個參數并把其放入_0x4903調用的對應位置,然后將0x1f1a68替換為_0x4903。
將參數下標替換成參數的代碼如下:
function convertIndexToArg(funcargs, arg) { if (arg.type == "BinaryExpression") { return btypes.binaryExpression(arg.operator, convertIndexToArg(funcargs, arg.left), arg.right); } if (arg.name.startsWith("arg")) { let index = parseInt(arg.name.substr(3)); if (index < funcargs.length) { return funcargs[index]; } else { console.log("not found arg index with name " + arg.name + " at " + arg.loc?.start.line); } } else { return arg; }}
其中funcargs為CallExpression中的參數,該函數同樣遞歸處理二值表達式。
實現函數展開只需要遍歷所有的函數定義,判斷是否滿足混淆函數的格式,然后通過binding尋找他的調用表達式進行處理,下面為代碼實現:
let doFlatten = { FunctionDeclaration(path) { let refBinding = path.scope.getBinding(path.node.id?.name); if (!refBinding.referenced) { path.remove(); //如果函數沒有被引用則直接刪除并更新作用域 path.scope.crawl(); return; } if (path.node.body.body.length != 1) return; let body = path.node.body.body[0]; if (!btypes.isReturnStatement(body)) return; let callExp = body.argument; if (!btypes.isCallExpression(callExp)) return; //以上三個判斷是否滿足混淆函數的格式 let calleeArgs = callExp.arguments; //混淆函數里面調用函數的參數 let funcArgs = path.node.params; //混淆函數的參數 for (let arg of calleeArgs) { let type = arg.type; switch (arg.type) { case "BinaryExpression": replaceArgsToIndex(funcArgs, (arg as btypes.BinaryExpression).left as btypes.Identifier); //這里可以不case,已經在replaceArgsToIndex中實現了遞歸,這里case是為了防止有未預期的形式,但是經過測試不存在該情況 break; case "Identifier": replaceArgsToIndex(funcArgs, arg as btypes.Identifier); break; default: console.log("callee arg not recognizable at line: " + path.node.loc?.start.line); return; } } let { id } = path.node; let binding = path.scope.getBinding((id as btypes.Identifier).name); for (let refer_path of binding!.referencePaths) { //獲取所有調用 if (!btypes.isCallExpression(refer_path.parent)) { console.log("abnormal reference at line: " + refer_path.node.loc?.start.line); continue; } let args = (refer_path.parent as btypes.CallExpression).arguments; let newArgs: btypes.Expression[] = []; //重組的表調用參數 let argExp: btypes.Expression; for (let arg of calleeArgs) { let type = arg.type; switch (arg.type) { case "BinaryExpression": argExp = convertIndexToArg(args, (arg as btypes.BinaryExpression).left as btypes.Identifier); let exp = btypes.binaryExpression((arg as btypes.BinaryExpression).operator, argExp, (arg as btypes.BinaryExpression).right) newArgs.push(exp); //處理重組,按照嵌套二值表達式的方式組裝并把變量參數放在最左邊 break; case "Identifier": argExp = convertIndexToArg(args, arg as btypes.Identifier); newArgs.push(argExp); break; } } let newCallExp = btypes.callExpression(callExp.callee, newArgs); refer_path.parentPath.replaceWith(newCallExp);//替換callExpression } path.parentPath.scope.crawl(); //console.log("modified code: " + codegen["default"](path.node).code); //path.remove(); } }; traverse["default"](root, doFlatten);
由于每次我們僅處理一層,所以這里多次處理,這樣就不必為先后順序發愁。
for (let level = 0; level < 3; level++) { removeConstFunc(root)}
字符串函數調用
上一步中我們將字符串混淆替換成了形似_0x4903(0x9a3 - -0x370 - 0x119 - 0x252, '1vkx');的調用,這一步中我們要將對該函數的調用還原為字符串。
以下為_0x4903的實現:
function _0x4903(_0x41f1e9, _0x3130bc) { var _0x5e7ec4 = _0x8976(); return _0x4903 = function (_0x899a2d, _0x5835f7) { _0x899a2d = _0x899a2d - 109; var _0x3e8c46 = _0x5e7ec4[_0x899a2d]; if (_0x4903.HfpBsi === undefined) { var _0x1cbb5e = function (_0x50d26d) { var _0x5a42a9 = '', _0x12cc8d = '', _0x5f42a1 = _0x5a42a9 + _0x1cbb5e; for (var _0x2829d6 = 0, _0x49459b, _0x390f91, _0x46a986 = 0; _0x390f91 = _0x50d26d.charAt(_0x46a986++); ~_0x390f91 && (_0x49459b = _0x2829d6 % 4 ? _0x49459b * 64 + _0x390f91 : _0x390f91, _0x2829d6++ % 4) ? _0x5a42a9 += _0x5f42a1.charCodeAt(_0x46a986 + 10) - 10 !== 0 ? String.fromCharCode(255 & _0x49459b >> (-2 * _0x2829d6 & 6)) : _0x2829d6 : 0) { _0x390f91 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='.indexOf(_0x390f91); } for (var _0x42631c = 0, _0x4ab4be = _0x5a42a9.length; _0x42631c < _0x4ab4be; _0x42631c++) { _0x12cc8d += '%' + ('00' + _0x5a42a9.charCodeAt(_0x42631c).toString(16)).slice(-2); } return decodeURIComponent(_0x12cc8d); }; var _0x6b448 = function (_0x1b5efd, _0x1ba8dd) { var _0x45d44d = [], _0xd3ad3a = 0, _0x1e5c57, _0x567392 = ''; _0x1b5efd = _0x1cbb5e(_0x1b5efd); var _0x579ae7; for (_0x579ae7 = 0; _0x579ae7 < 256; _0x579ae7++) { _0x45d44d[_0x579ae7] = _0x579ae7; } for (_0x579ae7 = 0; _0x579ae7 < 256; _0x579ae7++) { _0xd3ad3a = (_0xd3ad3a + _0x45d44d[_0x579ae7] + _0x1ba8dd.charCodeAt(_0x579ae7 % _0x1ba8dd.length)) % 256, _0x1e5c57 = _0x45d44d[_0x579ae7], _0x45d44d[_0x579ae7] = _0x45d44d[_0xd3ad3a], _0x45d44d[_0xd3ad3a] = _0x1e5c57; } _0x579ae7 = 0, _0xd3ad3a = 0; for (var _0x577b0d = 0; _0x577b0d < _0x1b5efd.length; _0x577b0d++) { _0x579ae7 = (_0x579ae7 + 1) % 256, _0xd3ad3a = (_0xd3ad3a + _0x45d44d[_0x579ae7]) % 256, _0x1e5c57 = _0x45d44d[_0x579ae7], _0x45d44d[_0x579ae7] = _0x45d44d[_0xd3ad3a], _0x45d44d[_0xd3ad3a] = _0x1e5c57, _0x567392 += String.fromCharCode(_0x1b5efd.charCodeAt(_0x577b0d) ^ _0x45d44d[(_0x45d44d[_0x579ae7] + _0x45d44d[_0xd3ad3a]) % 256]); } return _0x567392; }; _0x4903.MMnWus = _0x6b448, _0x41f1e9 = arguments, _0x4903.HfpBsi = !![]; } var _0x22abc4 = _0x5e7ec4[0], _0x244987 = _0x899a2d + _0x22abc4, _0x238d8e = _0x41f1e9[_0x244987]; if (!_0x238d8e) { if (_0x4903.CBAcVv === undefined) { var _0xdfbdcc = function (_0xacf633) { this.kDnxVr = _0xacf633, this.IoBPQs = [1, 0, 0], this.wpMLDB = function () {return 'newState';}, this.kJPlqW = '\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*', this.YBMSQk = '[\x27|\x22].+[\x27|\x22];?\x20*}'; //這是正則表達式 }; _0xdfbdcc.prototype.NgtXeG = function () { var _0x18c3d0 = new RegExp(this.kJPlqW + this.YBMSQk), _0xa664a8 = _0x18c3d0.test(this.wpMLDB.toString()) ? --this.IoBPQs[1] : --this.IoBPQs[0]; //這里檢測函數文本是否滿足正則,實際上是檢測JS有沒有被格式化,在這里將wpMLDB手動的改回了最小化的格式繞過檢測 return this.vbKnou(_0xa664a8); }, _0xdfbdcc.prototype.vbKnou = function (_0x561c7b) { if (!Boolean(~_0x561c7b)) return _0x561c7b; return this.DtzlIA(this.kDnxVr); //檢測到被格式化,調用該函數溢滿內存 }, _0xdfbdcc.prototype.DtzlIA = function (_0x386581) { for (var _0x2adaa0 = 0, _0x5245a5 = this.IoBPQs.length; _0x2adaa0 < _0x5245a5; _0x2adaa0++) { this.IoBPQs.push(Math.round(Math.random())), _0x5245a5 = this.IoBPQs.length; } return _0x386581(this.IoBPQs[0]); }, new _0xdfbdcc(_0x4903).NgtXeG(), _0x4903.CBAcVv = !![]; } _0x3e8c46 = _0x4903.MMnWus(_0x3e8c46, _0x5835f7), _0x41f1e9[_0x244987] = _0x3e8c46; } else _0x3e8c46 = _0x238d8e; return _0x3e8c46; }, _0x4903(_0x41f1e9, _0x3130bc); } (function (_0x4dbad8, _0x3b7f07) { var _0x37755d = _0x4dbad8(); while (!![]) { try { var _0x39c0da = parseInt(_0x4903(4069, 'u]yp')) / 1 * (parseInt(_0x4903(2125, 'Jx@]')) / 2) + parseInt(_0x4903(140, 'kFVy')) / 3 + -parseInt(_0x4903(2566, 'j1TD')) / 4 + parseInt(_0x4903(3272, 'a*Xk')) / 5 + -parseInt(_0x4903(2587, 'EnP@')) / 6 + -parseInt(_0x4903(743, '5h*C')) / 7 + parseInt(_0x4903(5102, 'dVlJ')) / 8; if (_0x39c0da === _0x3b7f07) break;else _0x37755d.push(_0x37755d.shift()); } catch (_0x696156) { _0x37755d.push(_0x37755d.shift()); } } })(_0x8976, 214580); //字符串數組順序還原,_0x8976為一個返回全局數組的函數,數組太長了就不放上來了
這里不關心她如何實現,只要能夠調用就好了,不過也進行了分析寫在注釋中。
繞過其中的檢測后就可以放到文件里然后直接引入該js執行了。
為其添加導出。
module.exports = { _0x4903}
import { _0x4903 } from './strdeec'function evalDecryptStr(root){ traverse["default"](root, { CallExpression(path) { let { callee } = path.node; if (btypes.isIdentifier(callee) && callee.name == "_0x4903") { //判斷是否為字符串解密函數 //console.log(codegen["default"](path.node).code,"loc:",path.node.loc?.start.line); let args = path.node.arguments; if(!btypes.isNumericLiteral(args[0]) || !btypes.isStringLiteral(args[1])) return; let str = (args[0] as btypes.NumericLiteral).value; let key = (args[1] as btypes.StringLiteral).value; //獲取函數調用表達式的參數 //console.log("decrypt str: " + str + " with key: " + key); let result = _0x4903(str, key); //調用解密JS path.replaceWith(btypes.stringLiteral(result)); //將函數調用替換成返回的字符串 } } });}
效果
原JS
var _0x36ac29 = { 'NaqJs': function(_0x23b608, _0x3b3abb) { return _0x23b608 + _0x3b3abb; }, 'UJTOv': _0x174a07(0xf38, 'LzP4', 0xfd8, 0x636, 0x1585), 'KunTi': _0x513b7d(0x130c, 0xeca, 0x876, 'lsex', 0x51b), 'qCfPd': _0x513b7d(-0x735, 0x161, 0x4cf, 'KFQS', 0x9d0) + 'n', 'CGJyE': _0x3b7723(-0x374, 'k2r*', 0xc46, 0x41c, 0x16c) + _0x1c1190(0xef5, 'Dvut', 0x841, 0x960, 0xae9) + _0x3b7723(-0x165, 'a*Xk', -0x5d6, 0x3a5, 0x743) + ')', 'aXujP': _0x174a07(0x5b9, 'a*Xk', 0x976, 0xf31, 0x697) + _0x1c1190(0xb61, 'Jx@]', 0x10c9, 0x7cb, 0x8db) + _0x1c1190(0x3e4, '5h*C', 0x39f, 0x36d, 0xc81) + _0x139616(0xd98, 0x90e, '[q9T', 0x1244, 0x4e3) + _0x174a07(0xfc6, '$TWJ', 0x1020, 0xbef, 0xbc5) + _0x174a07(0x801, 'mPMe', 0xcce, 0x8d3, 0xbb) + _0x513b7d(0x605, 0x1d, -0x31c, '[q9T', -0x3c5), 'MTufy': function(_0x43fade, _0x361ab2) { return _0x43fade(_0x361ab2); }, 'cqyFN': _0x513b7d(0x6c5, 0xcb7, 0x1631, 'A$dO', 0xc76), 'ITrER': function(_0x7d61e2, _0x18ffb2) { return _0x7d61e2 + _0x18ffb2; }, 'mYpth': _0x3b7723(0x3f8, 'vrb1', 0x487, 0xa64, 0x32f), 'ySBMh': _0x174a07(0x10a4, 'LzP4', 0x10fb, 0x1589, 0xc04), 'fhGPp': function(_0x10918e) { return _0x10918e(); }, 'BPPTg': function(_0xbea0a9, _0xf05a6b) { return _0xbea0a9 === _0xf05a6b; }, 'aFUAX': _0x1c1190(0x1105, 'DZK9', 0xa48, 0x101f, 0x17c1), 'YkQUD': function(_0x3d394f, _0x304d01) { return _0x3d394f !== _0x304d01; }, 'XUKwk': _0x1c1190(0x53c, 'u]yp', 0x1b9, 0xa84, 0x7fb), 'hGUML': _0x174a07(0xbc5, '[Kpm', 0xb6a, 0x3bd, 0xa68), 'FZvDE': function(_0x3899f7, _0x4782b9) { return _0x3899f7 === _0x4782b9; }, 'LCVgl': _0x1c1190(0x925, 's*!&', 0x2f8, 0x4d4, -0x3e5), 'FFifH': _0x174a07(0x1437, 'Thp]', 0x17dc, 0x1c99, 0x1031) };
還原后
var _0x36ac29 = { 'NaqJs': function (_0x23b608, _0x3b3abb) { return _0x23b608 + _0x3b3abb; }, 'UJTOv': "debu", 'KunTi': "gger", 'qCfPd': "action", 'CGJyE': "function *\\( *\\)", 'aXujP': "\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)", 'MTufy': function (_0x43fade, _0x361ab2) { return _0x43fade(_0x361ab2); }, 'cqyFN': "init", 'ITrER': function (_0x7d61e2, _0x18ffb2) { return _0x7d61e2 + _0x18ffb2; }, 'mYpth': "chain", 'ySBMh': "input", 'fhGPp': function (_0x10918e) { return _0x10918e(); }, 'BPPTg': function (_0xbea0a9, _0xf05a6b) { return _0xbea0a9 === _0xf05a6b; }, 'aFUAX': "WrmJg", 'YkQUD': function (_0x3d394f, _0x304d01) { return _0x3d394f !== _0x304d01; }, 'XUKwk': "SBXpo", 'hGUML': "GJQSO", 'FZvDE': function (_0x3899f7, _0x4782b9) { return _0x3899f7 === _0x4782b9; }, 'LCVgl': "spaxN", 'FFifH': "QeYEB" };
至此成功還原字符串加密,此部分結束。
系統安全運維