数瑞Cookie混淆是如何工作的
我的样本文件在此
本次分析文件特征
请求HTML文件包含:
set-cookie: 36501JSESSIONID与set-cookie: lD01YhBPHVTHO, 在dev-tools的应用面板中查到cookie: lD01YhBPHVTHPHTML引入JS文件
cIZgBeQvEQK9/AiHJXIs5GyJH.dee59c7.jsJS文件开头为
$_ts...$_ts['dee59c7'], 其中dee59c7为版本HTML中JS多为如下形式
function _$lt(_$EZ) { var _$aS = _$EZ.length; var _$$N, _$VC = new _$XD(_$aS - 1), _$vC = _$EZ.charCodeAt(0) - 97; for (var _$Vw = 0, _$JP = 1; _$JP < _$aS; ++_$JP) { _$$N = _$EZ.charCodeAt(_$JP); if (_$$N >= 40 && _$$N < 92) { _$$N += _$vC; if (_$$N >= 92) _$$N = _$$N - 52; } else if (_$$N >= 97 && _$$N < 127) { _$$N += _$vC; if (_$$N >= 127) _$$N = _$$N - 30; } _$VC[_$Vw++] = _$$N; } return _$yn.apply(null, _$VC); }
大致工作原理
- 获取HTML文件, HTML文件携带两个Cookie
 - HTML请求JS文件(JS文件为乱码)
 - HTML中JS解密JS文件, 得到JS字符串
 - 使用
eval执行JS字符串, JS字符串计算并设置加密Cookie(lD01YhBPHVTHP) - 离谱的是
RUISHUTESTFUNCTIONENTRY每次请求获取值不同(其中除了$_ts之外变量名都是变化的, 但是保证每次计算结果相同) 
解决思路
将代码从压缩(单行)形式转为格式化后的多行模式, 方便debug
准备工作(解决动态JS)
由于JS与HTML中变量都是动态的, 打开网站后Ctrl+S保存全部文件. 在VSCode中打开HTML文件, 使用格式化工具格式化, 得到如下形式HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <!-- ... -->
  <meta content="{qXVS74Mnw/*...*/03336qqqqqqqq">
  <script type="text/javascript" src="../cIZgBeQvEQK9/AiHJXIs5GyJH.dee59c7.js"></script>
  <script>
    (function () {
    var _$XI = 16,
    // _$_0 = [
      // [14, 10, 13, 15, 12, 1, 0, 8, 5, 12, 7, 3, 2, 9, 12, 11, 5],
      // [
    })()
  </script>
</head>
<!-- ... -->其中
<meta>中字符串参与加密- 第一个
<script>请求乱码JS - 第二个
<script>中的立即执行函数是"解码器" 
为了方便分析, 我们将立即执行函数分离到outer.js并引入
<meta content="{qXVS7/*...*/3703336qqqqqqqq">
<script type="text/javascript" src="../cIZgBeQvEQK9/AiHJXIs5GyJH.dee59c7.js"></script>
<script type="text/javascript" src="js/outer.js"></script>在outer.js立即执行函数前加入debugger这样我们可以在解密前暂停
我们使用本地JS文件进行分析, 房子变量换来换去
在保存文件夹下执行
python -m http.server 8081这样在0.0.0.0:8081开启了一个web server
清空自带debugger(解决前端反调试)
瑞数的前端反调试就是注册了几个定时器, 每500ms去eval(function), 这个function获取一个时间戳并debugger. 两个解决方法
在debugger的行号处右键, 选择一律不再此处暂停, 看到出现一个黄色问号表示忽略了这个debugger
借助JS是单线程的, 在debugger时候执行
for(let i = 0;i<999999;i++) clearInterval(i)
劫持cookie与eval
在前面介绍中, 我们知道, 这段JS的作用就是定时set-cookie. 所以我们要将cookie的set与get进行重写, 方便在读写cookie时暂停, 进行堆栈分析. 在outer.js被debugger暂停的时候, 写入
var _cookie = document.__lookupSetter__('cookie');      // 移走cookie
document.__defineSetter__("cookie", function(c) {       // 重设cookie
  debugger;
  _cookie=c;
} );
document.__defineGetter__("cookie", function() {
  debugger;
  return _cookie;
} );同样, 解密函数需要先解密加密串, 然后需要使用eval执行解密JS, 所以需要劫持eval
orig = window.eval;
window.eval=function(str){debugger;orig(str);}
window.eval.toString = function (){return orig.toString();}分析eval入口
在dev-tools中执行代码, 代码从outer.js的首行开始执行, 暂停在了eval, 在调用堆栈中选到上一级_$XC, 看到正在执行的代码
_$aS = _$$N[_$6p[47]](_$qP, _$EZ);_$$N: 未知函数_$6p: 可以看到是一个变量替换表Array(56) 0: "}" 1: "$_ts" 2: "," 3: "random" 4: "substr"_$6p[47]:call_$qP:window_$EZ: 一个172K的代码(function(){var _$1U=0,_$f8=$_ts.scj,_$v4=$_ts.aebi;function _$yB(){var _$vI=[730];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}function _$9j(){var _$vI=[709];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}function _$b_(){var _$vI=[614];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}function _$Th(){var _$vI=[185];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}function _$IT(){var _$vI=[607];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}function _$Hj(){var _$vI=[810];Array.prototype.push.apply(_$vI,arguments);return _$iv.apply(this,_$vI);}/*...*/})()
是这个表达式调用的evla, 所以只能是_$$N是eval, 这个表达式大概意思就是
_$aS = eval.call(window, '一个长长的String')也就是说, 这个String就是待释放的代码. 保存String为inner.js, 格式化代码, 并替换导出
RUISHUTESTFUNCTIONENTRY = function(){
  // ...
}  if (60 === 20 * _$LZ) {
-   _$aS = _$$N[_$6p[47]](_$qP, _$EZ);
+   RUISHUTESTFUNCTIONENTRY();
  } else if (_$LZ * 69 === 69) {相当于我们手动释放了解密函数, 但是使用我们的方法后, 不会出现如下情形: 解密代码释放为单行, 返回堆栈时只能看到一行, 完全无法debug
debugger> XXX...注意, 这不意味着我们可以直接删除AiHJXIs5GyJH.dee59c7.js, 打开就可以看到, 我们解密的字符串开头与其不同
我们解密的
function () { var _$1U = 0, _$f8 = $_ts.scj, _$v4 = $_ts.aebi; function _$yB() { var _$vI = [730]; Array.prototype.push.apply(_$vI, arguments); return _$iv.apply(this, _$vI); } }原文件
$_ts=window['$_ts'];if(!$_ts)$_ts={};$_ts.scj=[];$_ts['dee59c7']
找点特殊代码比对, 我找了
'=a"S%$Y\'tU9q.C,~NQy-^|6rXh:H?M[<@fK;0W+VI2RiJ(FencmskgL#OBT>\\4Gj`P&1_wD7oZxAb]}updv5Ez) *3{!l8/',发现在AiHJXIs5GyJH.dee59c7.js也存在
可以大胆的猜测执行逻辑:
- 加载
AiHJXIs5GyJH.dee59c7.js, 释放变量(虽然变量值可能是乱码) - 加载解密器
 - 解密器解密
AiHJXIs5GyJH.dee59c7.js并获得待释放JS字符查 - 释放JS, 执行计算逻辑
 
我们做的事情就是劫持eval的内容, 并格式化代码, 手动释放, 方便调试. 为此, HTML应变为如下结构
<meta content="{qXVS7/*...*/3703336qqqqqqqq">
<script type="text/javascript" src="../cIZgBeQvEQK9/AiHJXIs5GyJH.dee59c7.js"></script>
<script type="text/javascript" src="js/inner.js"></script>
<script type="text/javascript" src="js/outer.js"></script>重新加载网页, 继续捕获
var _$ET = _$Z5[_$SH[9]](_$qP[_$SH[43]], '; ');检查变量
_$SH[9]:"call"_$Z5:split_$SH: 可以看到是一个变量替换表Array(723) [0 … 99] 0: "prototype" 1: "type" 2: "toString" 3: "readyState" 4: "concat" 5: "indexOf" 6: "string" 7: "body" 8: "slice"_$qP:document_$SH[43]:cookie
也就是这句指令的意思是
_$ET = split['call'](document['cookie'],';')值得注意到是3806行上面的代码, Chrome给出提示_$CG='lD01YhBPHVTHP='. 这就是我们要拼的头啊!
没什么意思...继续执行
暂停到了cookie.setter, 返回上级堆栈_$iv, 继续检查
_$qP:document_$SH[43]:cookie_$qS: "enable_lD01YhBPHVTH=true"
相当于设置了一个cookie, 这个cookie是一个临时的(多刷新几次就发现了), 继续执行
暂停到了eval, 返回上层堆栈_$iv
_$ET = _$qt(_$SH[615]);_$SH[615]:"Z8XHJJY.bmF0aXZlRmlVyUHJ()"_$SH是变量替换表_$qt:ƒ (str){debugger;orig(str);}这就是我们劫持的eval
可惜这次执行失败了, 因为没有Z8XHJJY, 程序进入下面的catch()并出来
继续执行
_$ET = _$qt(_$SH[661]);同上, 落入catch
继续执行, 暂停在cookie.set, 查看堆栈_$iv
_$qP[_$SH[43]] =      // document.cookie =
      _$CG +          // 刚刚遇到过, 是我们需要的"lD01YhBPHVTHP"
      _$SH[47] +      // `=`
      _$41 +          // lD01YhBPHVTHP的值
      _$iv(994) +     // ''
      _$SH[589] +     // ; path...
      _$iv(983, _$s2);// '; expires=Wed, 06 Apr 2022 11:26:36 GMT; Secure'也就是在此执行了一次拼串, 查看调用栈, 看到了我们命名的RUISHUTESTFUNCTIONENTRY()
所有操作都是在这里进行的
至此, 我们有了变量替换表, inner.js, outer.js, <meta>, 如何解密代码呢?
解决方案
根据已有变量替换表, 一行一行理清逻辑...(费头发)
既然我们知道了加密程序就是在反复执行
inner.js, 不如欲擒故纵, 在Node中引入并执行, 但是Node中没有DOM/BOM方法, 所以需要我们实现几个假的, 尤其是query meta的时候(!这是一种非常不安全的方法, 例如加密者完全可以判断当前是否为Node环境, 如果是, 直接执行攻击指令)使用中间人攻击, 结合变量替换表, 实现解密JS
使用模拟爬虫工具
变量替换始终是一种猫鼠游戏. 更好的方法是: 使用
jsdom模块在Node中实现轻量化浏览器环境, 我的实现代码如下function promiseStick() { let res, rej; const p = new Promise((resolve, reject) => { [res, rej] = [resolve, reject]; }); return Object.assign(p, { res, rej }); } async function reqLoginToken(p = promiseStick()) { const cookieMap = new Map(); let dom = await JSDOM.fromURL( 'https://xxx.cn', { resources: 'usable', runScripts: 'dangerously', } ); // 劫持cookie获取生成时机 dom.window._cookie = dom.window.document.__lookupSetter__('cookie'); dom.window.document.__defineSetter__('cookie', function (c) { _cookie = c; const cs = c.split('=', 2); cookieMap.set(cs[0], cs[1]); if (cookieMap.size === 2) { // 一个enable_XX临时token, 一个加密的 p.res({ cookie: Object(dom.cookieJar.store.idx['uaaap.swu.edu.cn']), cookieE: cookieMap.get(config.login.encryptCookie), }); dom.window.close(); dom = null; } }); dom.window.document.__defineGetter__('cookie', function () { return _cookie; }); return p; }
值得学习的东西
- cookie&eval劫持
 - dev-tool的一律不暂停
 - setInterval清除debugger