网址:第二题js 混淆 动态cookie 1 - 猿人学爬虫逆向练习平台

虽然这道题通过mcp的方式得到了最终答案,但是这里面还有很多知识没有搞懂,继续接着上次的绕过反调试继续逆向cookie的值,其中反调试的可以参考js 混淆 - 动态cookie 1(猿人学 match/2 反调试绕过)-CSDN博客

首先抓包复制到curl中得到基础代码

import requests

cookies = {
    'Hm_lvt_f80b2b389f44bbfb3bfe1704817d44e0': '1785761802',
    'HMACCOUNT': '1199FDD53B1CA0B3',
    'sessionid': '75hgdzj80qyff0kugfugonjluaelwxi0',
    'Hm_ck_1786344794346': '42',
    'RM4hZBv0dDon443M': 'SisbB0JEswOIglSOS68YhAk495aFH+ZvZEsebU4qzFUsU5T8BbF3yiwR0soXlYBQX3YxlkHZtXyLadVDn6gn4tqJzDsWo6ZihtdnDiAqShu/UNAzJ2ZO/ZcrapuvnbOZF69yKgOeHNUYUe2Udz5DU/V53KoC5jucfOsTAdv5s6U5gk0HECtsscytKP9SEqrTP/6E9W4tylR3GRvFIldFm4uXdXP7ChKwnoFhgPk7agQ=',
    # 'm': '500a625ac065dad8b1a016afddd02c1e|1786453180783',
    'Hm_lpvt_f80b2b389f44bbfb3bfe1704817d44e0': '1786453185',
}

headers = {
    'accept': 'application/json, text/javascript, */*; q=0.01',
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'cache-control': 'no-cache',
    'dnt': '1',
    'pragma': 'no-cache',
    'priority': 'u=1, i',
    'referer': 'https://match.yuanrenxue.cn/match/2',
    'sec-ch-ua': '"Not=A?Brand";v="99", "Microsoft Edge";v="151", "Chromium";v="151"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-origin',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0',
    'x-requested-with': 'XMLHttpRequest',
    # 'cookie': 'Hm_lvt_f80b2b389f44bbfb3bfe1704817d44e0=1785761802; HMACCOUNT=1199FDD53B1CA0B3; sessionid=75hgdzj80qyff0kugfugonjluaelwxi0; Hm_ck_1786344794346=42; RM4hZBv0dDon443M=SisbB0JEswOIglSOS68YhAk495aFH+ZvZEsebU4qzFUsU5T8BbF3yiwR0soXlYBQX3YxlkHZtXyLadVDn6gn4tqJzDsWo6ZihtdnDiAqShu/UNAzJ2ZO/ZcrapuvnbOZF69yKgOeHNUYUe2Udz5DU/V53KoC5jucfOsTAdv5s6U5gk0HECtsscytKP9SEqrTP/6E9W4tylR3GRvFIldFm4uXdXP7ChKwnoFhgPk7agQ=; m=500a625ac065dad8b1a016afddd02c1e|1786453180783; Hm_lpvt_f80b2b389f44bbfb3bfe1704817d44e0=1786453185',
}

params = {
    'page': '1',
    'pageSize': '10',
    'kw': '',
}

response = requests.get('https://match.yuanrenxue.cn/api/question/2', params=params, cookies={}, headers=headers)
print(response.status_code)
print(response.json())

通过观察可以发现第一个请求的结果是一段字符串,js乱码,通过注释掉可以发现这里的cookie是没有用的,可以直接拿到数据,数据在第二个请求中,继续分析第二个请求。继续抓包之后发现两个数据包的携带参数都是一样的,这里让人很费解。豆包之后,才明白第一次请求得到后的是js加密逻辑,这时候是没有cookie的,后面继续发送请求,才得到最后的数据,下面是页面的伪代码(不是页面的逻辑,豆包生成帮助理解的)

// 伪代码,还原网页真实逻辑
async function loadData() {
  const url = "/2?page=1&pageSize=10&kw=";

  // ==========第一次请求==========
  // 此时cookie里没有有效的m,或者是旧的m
  const res1 = await fetch(url);
  // 注意!这里拿到的不是json,是一段混淆JS字符串
  const jsCode = await res1.text();

  // 核心:把后端返回的字符串,当成JS代码运行
  // 运行的时候,这段混淆代码内部会执行 document.cookie="m=xxxx"
  eval(jsCode);  

  // ==========第二次请求==========
  // eval执行完!浏览器cookie已经被注入全新有效的 m=xxx
  // 再次请求完全相同url,浏览器自动带上刚刚生成的cookie
  const res2 = await fetch(url);
  const realData = await res2.json();
  
  console.log("真正业务数据:", realData);
}

loadData();

通过定位找到两次请求的位置

// 匿名自执行函数,把jQuery的$传入,隔离内部变量,防止污染全局作用域
(function ($) {
  // ====================== 全局常量定义 ======================
  var API="/api/question/2", // 目标接口地址
      TP=5,                  // 总页数 totalPage,一共5页数据
      PS=10,                 // 每页条数 pageSize,一页10条
      p=1;                   // 当前页码,默认从第1页开始

  // 获取页面上各个DOM元素,缓存jQuery对象,后面直接操作页面
  var $r=$("#pgxPager"),        // 分页外层容器
      $l=$("#pgxList"),         // 数据列表容器,用来展示数字
      $pg=$("#pgxPages"),       // 页码按钮容器(1 2 3 4 5)
      $pre=$("#pgxPrev"),       // 上一页按钮
      $nxt=$("#pgxNext"),       // 下一页按钮
      $kw=$("#pgxKw"),          // 搜索输入框
      $s=$("#pgxStatus"),       // 状态文字显示(就绪/请求中...)
      $dbg=$("#pgxDebug");      // debug调试信息展示框

  /**
   * @function ui 更新分页按钮UI状态
   * 控制上一页、下一页是否禁用,高亮当前页码
   */
  function ui(){
    // 如果当前页码<=1,禁用【上一页】按钮
    $pre.prop("disabled", p<=1);
    // 如果当前页码>=总页数5,禁用【下一页】按钮
    $nxt.prop("disabled", p>=TP);
    // 遍历所有页码按钮,给当前页加上高亮class is‑active
    $pg.find(".pgx-page").each(function(){
      // $(this).data("page") 获取按钮上存的页码数字
      $(this).toggleClass("is-active", +$(this).data("page")===p);
    });
  }

  /**
   * @function loading 加载状态开关
   * @param {Boolean} on true=正在请求;false=请求结束
   */
  function loading(on){
    // 修改状态文字:请求中... / 就绪
    $s.text(on?"请求中...":"就绪");
    // 分页容器透明度,加载时变半透明
    $r.css("opacity", on?0.92:1);
    // 批量禁用/启用:上一页、下一页、页码按钮,请求过程不能点击
    $pre.add($nxt).add($pg.find(".pgx-page")).prop("disabled", on);
  }

  /**
   * @function render 渲染页面列表
   * @param {Array} a 后端返回的数据数组
   */
  function render(a){
    // 兜底,没有数据就空数组;最多只展示PS=10条
    a=(a||[]).slice(0,PS);
    // 把数组数据渲染成html字符串,插入列表容器
    $l.html('<div class="pgx-line">'+(a.length
      // 有数据:循环数组,每个数字套上span标签
      ? a.map(n=>'<span class="pgx-num">'+n+'</span>').join("")
      // 数组为空,显示暂无数据
      : '<span class="pgx-empty">暂无数据</span>')+'</div>');
  }

  /**
   * @function req 核心入口:发起分页数据请求
   * @param {Number} np 用户想要跳转的目标页码
   */
  function req(np){
    // 页码边界处理:不能小于1,不能大于总页数TP=5
    p=Math.min(TP,Math.max(1,np));
    ui(); // 更新分页按钮样式

    // 如果全局有window.request函数,就执行(混淆脚本注入的钩子)
    window.request&&window.request();

    // 组装get请求参数
    var d={
      page:p,                  // 当前页码
      pageSize:PS,             // 每页数量10
      kw:$.trim($kw.val()||""),// 搜索框输入内容,去除前后空格
      m:window.match1          // ⭐关键参数 m,来自全局变量window.match1,由eval混淆js生成
    };

    loading(true); // 开启加载状态,按钮禁用
    // 如果debug框存在,把本次请求url打印到页面上
    $dbg&&$dbg.text("GET "+API+"?"+$.param(d));
    // 页面先渲染10个“加载中...”占位
    render(Array(10).fill('加载中...'));

    /**
     * @function load_data 递归AJAX请求函数
     * 第一次请求拿到混淆JS字符串 → eval执行生成m → 递归调用自己再次请求拿真实数据
     */
    function load_data(){
        $.ajax({
          url:API,
          type:"GET",
          dataType:"json",  // 📌告诉jQuery,把响应自动解析成json对象
          data:d,           // get查询参数
          timeout:1e4,      // 超时时间10000毫秒=10秒

          // 请求成功回调
          success:function(res){
              // ✅分支1:res.data是数组,代表拿到真实业务数据
              if (res.data instanceof Array){
                  render(res&&res.data); // 把数据渲染到页面
                  $s.text("加载完成");
                  window.load_finish = false;
              }else {
                  // 🚨分支2:res.data不是数组,是【混淆JS字符串】,后端返回{"data":"xxx混淆代码"}
                  // window.load_finish 防止无限递归死循环
                  if (window.load_finish){
                      failedAlert("发生了不期待的错误,即将刷新页面重置")
                      setTimeout(function (){location.reload()}, 2000)
                  }
                  eval(res.data);   // ✨执行后端返回的混淆JS脚本!!运行后会给全局赋值 window.match1
                  load_data();      // 🔥递归!立刻再次调用本函数,发第二次AJAX请求
                  window.load_finish = true; // 标记已经跑过一次eval
              }
          },

          // 请求出错:网络错误、超时、跨域等
          error:function(){
              $s.text("接口异常,请使用chrome浏览器开启无痕模式,若错误持续请联系管理员");
          },

          // success/error无论成功失败,最后都会执行complete
          complete:function(){
              loading(false); // 关闭加载状态,按钮恢复可用
              ui();
          }
        });
    }
    load_data() // 第一次调用,发起AJAX
  }

  // ======================绑定页面按钮事件======================
  // 上一页点击:页码大于1的时候执行req,页码-1
  $pre.on("click", ()=>p>1&&req(p-1));
  // 下一页点击:页码小于总页数5,执行req,页码+1
  $nxt.on("click", ()=>p<TP&&req(p+1));
  // 页码按钮点击事件,点击页码,跳转到对应页
  $pg.on("click",".pgx-page", function(){ req(+$(this).data("page")); });
  // 搜索输入框按下回车键,重新加载第1页
  $kw.on("keydown", e=>e.key==="Enter"&&req(1));

  // 页面初始化:更新UI,并且直接请求第1页数据
  ui();
  req(1);
})(window.jQuery);

这里到eval(res.data),可以猜到这里面肯定是生成cookie的地方,但是由于绕过debuggering所以没法调试,到这里我也不知道怎么办了.继续豆包,了解cookie的生成原理,这里hook一下cookie。下面是hook脚本:

// hook脚本,控制台运行,所有写cookie都会断下
(function(){
  let originSetter = Object.getOwnPropertyDescriptor(Document.prototype,'cookie').set;
  Object.defineProperty(document,'cookie',{
    set:function(val){
      debugger; // 直接断在写cookie的地方,看调用栈
      console.log("写入cookie:",val)
      return originSetter.call(this,val)
    }
  })
})()

在eval(res.data)和load_data()之间打上断点,并且在控制台注入hook  cookie脚本,点击F8就会在定住,然后在调用栈里面继续寻找cookie的加密逻辑。但是这个eval(res.data),其中res.data是第一次请求的结果,每次结果都不一样,所以还不可以直接扣代码。参考猿人学 js 混淆动态cookie逆向_js 混淆 - 动态cookie 1猿人学解题-CSDN博客,知道可以直接扣代码,且这代码被ob混淆了,可以通过使用猿人学自带的ob反混淆工具解混淆测试版 (yuanrenxue.cn)解决

通过豆包了解

每次请求返回的 JS 文本都不一样,还有没有必要定位 m 生成逻辑?

区分两种完全不同的现象:

  1. 外层混淆外壳每次变,底层加密算法不变(猿人学 2 真实情况)
  2. 真正动态下发,每一次请求,加密算法本身都被服务器重新生成(瑞数这类高强度反爬)

情况 1:外壳变,算法内核不变(猿人学 match2)

接口返回的字符串每次都不同:

  • RC4 密钥每次换、字符串池打乱、变量名全部重命名、控制流扁平化重新打乱;
  • 但是真正计算 m 的数学逻辑、输入输出规则完全不变:输入page + 时间戳 + ua,输出 m。

👉依然有必要定位、提取核心加密逻辑

  • 虽然混淆外壳千变万化,抠出来的加密函数的数学运算逻辑不变
  • 你只需要还原一次算法,之后不需要每次请求都解析一大段混淆 JS。

举例子: 每次返回的 JS 变量名:$dbsm_0xaaaa / $dbsm_0xbbbb一直在变; 但是内部:魔改md5(page + "|" + timestamp)这个运算逻辑是固定的。

误区:

“每次返回 js 不一样,那我扣出来的代码也没用” ❌错。变的是混淆壳(变量名、字符串池、反调试垃圾代码),不变的是加密运算。


情况 2:真正动态 JS,加密算法本身每次请求服务器重新生成(瑞数 5、部分企业站)

每一次 http 请求,服务器下发一套全新算法,运算逻辑、密钥全部改写。 这种场景:你静态抠一次算法完全没有意义!

  • 你今天抠出来的函数,下一次请求服务器返回另一套算法,直接失效。

✅此时只有两条可行路线:

  1. 运行时执行(模拟浏览器环境) 每次请求拿到服务器返回的动态 JS,交给浏览器 /playwright/vm 沙箱执行,让它现场算出 m,直接拿输出结果,不去人工分析算法

不做静态逆向,只做环境模拟。

  1. 高级方案:AST 动态提取,每次拿到脚本自动解混淆,自动 hook 抓输入输出(开发成本很高)

回到猿人学 2 的真实现象

抓包看:接口 response 的 base64 串每刷新一次就变。 但本质:

  • 反调试外壳、字符串池、解密器变量名一直在变;
  • m 的生成算法逻辑是固定写死在服务端模板,不会改动

所以:逆向定位 m 的生成逻辑依然有价值,只需要剥离混淆壳,拿到底层运算。

模式适用场景做法优缺点
✅静态抠算法外壳变、内核算法不变(猿人学 2)hook 抓 eval 解密后的源码,剥离反调试,提取加密函数,node 本地复现速度快,不需要浏览器,一次分析永久复用
✅动态执行脚本算法本身每次都变(瑞数类)每次请求拿到 response,丢进沙箱 / 无头浏览器 eval 执行,直接拿 cookie 结果不用分析算法;开销大,需要模拟完整浏览器环境

m的生成算法

通过Hook定位加密位置

解混淆之后的代码


(function $dbsm_0x53e9b3(_0x5f2a15) {
  var _0x365c2b = function () {
    var _0x453ec6 = true;
    return function (_0x4ebd11, _0x764204) {
      var _0x378e93 = _0x453ec6 ? function () {
        if (_0x764204) {
          var _0x5e5566 = _0x764204["apply"](_0x4ebd11, arguments);

          _0x764204 = null;
          return _0x5e5566;
        }
      } : function () {};

      _0x453ec6 = false;
      return _0x378e93;
    };
  }();

  var _0x1517bc = function () {
    var _0x40db21 = true;
    return function (_0x541f69, _0x31a961) {
      var _0x25f7b6 = _0x40db21 ? function () {
        if (_0x31a961) {
          var _0x38961d = _0x31a961["apply"](_0x541f69, arguments);

          _0x31a961 = null;
          return _0x38961d;
        }
      } : function () {};

      _0x40db21 = false;
      return _0x25f7b6;
    };
  }();

  function _0x2a3c1f(_0x8cea22, _0xb9406e) {
    var _0x1fc9f8 = (65535 & _0x8cea22) + (65535 & _0xb9406e);

    return (_0x8cea22 >> 16) + (_0xb9406e >> 16) + (_0x1fc9f8 >> 16) << 16 | 65535 & _0x1fc9f8;
  }

  function _0x4eb501(_0x1c3057, _0x395ca7) {
    return _0x1c3057 << _0x395ca7 | _0x1c3057 >>> 32 - _0x395ca7;
  }

  function _0x4dec7f(_0x58e1f8, _0x5678ce, _0x2a1f02, _0x1fefc4, _0x11c74c, _0x5d475c) {
    return _0x2a3c1f(_0x4eb501(_0x2a3c1f(_0x2a3c1f(_0x5678ce, _0x58e1f8), _0x2a3c1f(_0x1fefc4, _0x5d475c)), _0x11c74c), _0x2a1f02);
  }

  function _0x571b88(_0xea1b37, _0x4e9216, _0x58f233, _0x3b7bfe, _0x1e8a9e, _0x3d837c, _0x5bfab1) {
    return _0x4dec7f(_0x4e9216 & _0x58f233 | ~_0x4e9216 & _0x3b7bfe, _0xea1b37, _0x4e9216, _0x1e8a9e, _0x3d837c, _0x5bfab1);
  }

  function _0x88e491(_0x51e76a, _0x260f89, _0x3fa16d, _0x4baeda, _0x41c592, _0x363d5c, _0x39ba9a) {
    return _0x4dec7f(_0x260f89 & _0x4baeda | _0x3fa16d & ~_0x4baeda, _0x51e76a, _0x260f89, _0x41c592, _0x363d5c, _0x39ba9a);
  }

  function _0x3b02a1(_0x479f3d, _0x4cd151) {
    let _0x5287eb = [99, 111, 110, 115, 111, 108, 101];
    let _0x29f643 = "";

    for (let _0xc6f280 = 0; _0xc6f280 < _0x5287eb["length"]; _0xc6f280++) {
      _0x29f643 += String["fromCharCode"](_0x5287eb[_0xc6f280]);
    }

    return _0x29f643;
  }

  function _0x1c45c4(_0x505ea1, _0x3a6c30, _0x41ae36, _0x2d6a86, _0x598229, _0x2bb4cb, _0x2769ae) {
    return _0x4dec7f(_0x3a6c30 ^ _0x41ae36 ^ _0x2d6a86, _0x505ea1, _0x3a6c30, _0x598229, _0x2bb4cb, _0x2769ae);
  }

  function _0x3919b5(_0x1412b8, _0x39dd43, _0x138d9d, _0x155415, _0x56f326, _0x20f24e, _0x554f9c) {
    return _0x4dec7f(_0x138d9d ^ (_0x39dd43 | ~_0x155415), _0x1412b8, _0x39dd43, _0x56f326, _0x20f24e, _0x554f9c);
  }

  function _0x5d7c44(_0x3dbfb8, _0x17d53e) {
    if (_0x17d53e) {
      return _0x3919b5(_0x3dbfb8);
    }

    return _0x3b02a1(_0x3dbfb8);
  }

  function _0x12fc84(_0x39ad5b, _0x42afbe) {
    let _0xdd6431 = "";

    for (let _0x57ea98 = 0; _0x57ea98 < _0x39ad5b["length"]; _0x57ea98++) {
      _0xdd6431 += String["fromCharCode"](_0x39ad5b[_0x57ea98]);
    }

    return _0xdd6431;
  }

  function _0x1132b0(_0x1879ff, _0x2a0389) {
    var _0x3022f9 = _0x365c2b(this, function () {
      var _0x41c42e = function () {
        var _0x5a97e7 = _0x41c42e["constructor"]("return /\" + this + \"/")()["compile"]("^([^ ]+( +[^ ]+)+)+[^ ]}");

        return !_0x5a97e7["test"](_0x3022f9);
      };

      return _0x41c42e();
    });

    _0x3022f9();

    (function () {
      _0x1517bc(this, function () {
        var _0x14ac66 = new RegExp("function *\\( *\\)");

        var _0x26489a = new RegExp("\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)", "i");

        var _0x53d298 = $dbsm_0x5cc7b0("init");

        if (!_0x14ac66["test"](_0x53d298 + "chain") || !_0x26489a["test"](_0x53d298 + "input")) {
          _0x53d298("0");
        } else {
          $dbsm_0x5cc7b0();
        }
      })();
    })();

    _0x5d7c44();

    qz = [10, 99, 111, 110, 115, 111, 108, 101, 32, 61, 32, 110, 101, 119, 32, 79, 98, 106, 101, 99, 116, 40, 41, 10, 99, 111, 110, 115, 111, 108, 101, 46, 108, 111, 103, 32, 61, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 41, 32, 123, 10, 32, 32, 32, 32, 119, 104, 105, 108, 101, 32, 40, 49, 41, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 111, 114, 40, 105, 61, 48, 59, 105, 60, 49, 49, 48, 48, 48, 48, 48, 59, 105, 43, 43, 41, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 104, 105, 115, 116, 111, 114, 121, 46, 112, 117, 115, 104, 83, 116, 97, 116, 101, 40, 48, 44, 48, 44, 105, 41, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 125, 10, 10, 125, 10, 99, 111, 110, 115, 111, 108, 101, 46, 116, 111, 83, 116, 114, 105, 110, 103, 32, 61, 32, 39, 91, 111, 98, 106, 101, 99, 116, 32, 79, 98, 106, 101, 99, 116, 93, 39, 10, 99, 111, 110, 115, 111, 108, 101, 46, 108, 111, 103, 46, 116, 111, 83, 116, 114, 105, 110, 103, 32, 61, 32, 39, 402, 32, 116, 111, 83, 116, 114, 105, 110, 103, 40, 41, 32, 123, 32, 91, 110, 97, 116, 105, 118, 101, 32, 99, 111, 100, 101, 93, 32, 125, 39, 10];
    eval(_0x12fc84(qz));

    try {
      if (global) {
        console["log"]("人生苦短,何必python?");
      } else {
        while (1) {
          console["log"]("人生苦短,何必python?");
          /*db*/
        }
      }
    } catch (_0x45d8c6) {
      return navigator["vendorSub"];
    }
  }

  setInterval(_0x1132b0(), 500);

  function _0x489ff0(_0x29d3f6, _0xcc9bed) {
    _0x29d3f6[_0xcc9bed >> 5] |= 128 << _0xcc9bed % 32, _0x29d3f6[14 + (_0xcc9bed + 64 >>> 9 << 4)] = _0xcc9bed;

    if (qz) {
      var _0x2159d8,
          _0x578dd8,
          _0x5cc688,
          _0x44b230,
          _0x54a8a5,
          _0x506ae5 = 1732584193,
          _0xce3f15 = -271733879,
          _0x12ae4a = -1732584194,
          _0x480272 = 271733878;
    } else {
      var _0x2159d8,
          _0x578dd8,
          _0x5cc688,
          _0x44b230,
          _0x54a8a5,
          _0x506ae5 = 0,
          _0xce3f15 = -0,
          _0x12ae4a = -0,
          _0x480272 = 0;
    }

    for (_0x2159d8 = 0; _0x2159d8 < _0x29d3f6["length"]; _0x2159d8 += 16) _0x578dd8 = _0x506ae5, _0x5cc688 = _0xce3f15, _0x44b230 = _0x12ae4a, _0x54a8a5 = _0x480272, _0x506ae5 = _0x571b88(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8], 7, -680876936), _0x480272 = _0x571b88(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 1], 12, -389564586), _0x12ae4a = _0x571b88(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 2], 17, 606105819), _0xce3f15 = _0x571b88(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 3], 22, -1044525330), _0x506ae5 = _0x571b88(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 4], 7, -176418897), _0x480272 = _0x571b88(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 5], 12, 1200080426), _0x12ae4a = _0x571b88(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 6], 17, -1473231341), _0xce3f15 = _0x571b88(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 7], 22, -45705983), _0x506ae5 = _0x571b88(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 8], 7, 1770010416), _0x480272 = _0x571b88(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 9], 12, -1958414417), _0x12ae4a = _0x571b88(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 10], 17, -42063), _0xce3f15 = _0x571b88(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 11], 22, -1990404162), _0x506ae5 = _0x571b88(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 12], 7, 1804603682), _0x480272 = _0x571b88(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 13], 12, -40341101), _0x12ae4a = _0x571b88(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 14], 17, -1502882290), _0xce3f15 = _0x571b88(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 15], 22, 1236535329), _0x506ae5 = _0x88e491(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 1], 5, -165796510), _0x480272 = _0x88e491(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 6], 9, -1069501632), _0x12ae4a = _0x88e491(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 11], 14, 643717713), _0xce3f15 = _0x88e491(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8], 20, -373897302), _0x506ae5 = _0x88e491(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 5], 5, -701558691), _0x480272 = _0x88e491(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 10], 9, 38016083), _0x12ae4a = _0x88e491(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 15], 14, -660478335), _0xce3f15 = _0x88e491(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 4], 20, -405537848), _0x506ae5 = _0x88e491(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 9], 5, 568446438), _0x480272 = _0x88e491(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 14], 9, -1019803690), _0x12ae4a = _0x88e491(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 3], 14, -187363961), _0xce3f15 = _0x88e491(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 8], 20, 1163531501), _0x506ae5 = _0x88e491(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 13], 5, -1444681467), _0x480272 = _0x88e491(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 2], 9, -51403784), _0x12ae4a = _0x88e491(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 7], 14, 1735328473), _0xce3f15 = _0x88e491(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 12], 20, -1926607734), _0x506ae5 = _0x1c45c4(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 5], 4, -378558), _0x480272 = _0x1c45c4(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 8], 11, -2022574463), _0x12ae4a = _0x1c45c4(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 11], 16, 1839030562), _0xce3f15 = _0x1c45c4(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 14], 23, -35309556), _0x506ae5 = _0x1c45c4(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 1], 4, -1530992060), _0x480272 = _0x1c45c4(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 4], 11, 1272893353), _0x12ae4a = _0x1c45c4(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 7], 16, -155497632), _0xce3f15 = _0x1c45c4(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 10], 23, -1094730640), _0x506ae5 = _0x1c45c4(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 13], 4, 681279174), _0x480272 = _0x1c45c4(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8], 11, -358537222), _0x12ae4a = _0x1c45c4(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 3], 16, -722521979), _0xce3f15 = _0x1c45c4(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 6], 23, 76029189), _0x506ae5 = _0x1c45c4(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 9], 4, -640364487), _0x480272 = _0x1c45c4(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 12], 11, -421815835), _0x12ae4a = _0x1c45c4(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 15], 16, 530742520), _0xce3f15 = _0x1c45c4(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 2], 23, -995338651), _0x506ae5 = _0x3919b5(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8], 6, -198630844), _0x480272 = _0x3919b5(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 7], 10, 1126891415), _0x12ae4a = _0x3919b5(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 14], 15, -1416354905), _0xce3f15 = _0x3919b5(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 5], 21, -57434055), _0x506ae5 = _0x3919b5(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 12], 6, 1700485571), _0x480272 = _0x3919b5(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 3], 10, -1894986606), _0x12ae4a = _0x3919b5(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 10], 15, -1051523), _0xce3f15 = _0x3919b5(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 1], 21, -2054922799), _0x506ae5 = _0x3919b5(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 8], 6, 1873313359), _0x480272 = _0x3919b5(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 15], 10, -30611744), _0x12ae4a = _0x3919b5(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 6], 15, -1560198380), _0xce3f15 = _0x3919b5(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 13], 21, 1309151649), _0x506ae5 = _0x3919b5(_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272, _0x29d3f6[_0x2159d8 + 4], 6, -145523070), _0x480272 = _0x3919b5(_0x480272, _0x506ae5, _0xce3f15, _0x12ae4a, _0x29d3f6[_0x2159d8 + 11], 10, -1120210379), _0x12ae4a = _0x3919b5(_0x12ae4a, _0x480272, _0x506ae5, _0xce3f15, _0x29d3f6[_0x2159d8 + 2], 15, 718787259), _0xce3f15 = _0x3919b5(_0xce3f15, _0x12ae4a, _0x480272, _0x506ae5, _0x29d3f6[_0x2159d8 + 9], 21, -343485441), _0x506ae5 = _0x2a3c1f(_0x506ae5, _0x578dd8), _0xce3f15 = _0x2a3c1f(_0xce3f15, _0x5cc688), _0x12ae4a = _0x2a3c1f(_0x12ae4a, _0x44b230), _0x480272 = _0x2a3c1f(_0x480272, _0x54a8a5);

    return [_0x506ae5, _0xce3f15, _0x12ae4a, _0x480272];
  }

  function _0x488757(_0x516e25) {
    var _0x17cd7b,
        _0x23d64b = "",
        _0x1a5ab3 = 32 * _0x516e25["length"];

    for (_0x17cd7b = 0; _0x17cd7b < _0x1a5ab3; _0x17cd7b += 8) _0x23d64b += String["fromCharCode"](_0x516e25[_0x17cd7b >> 5] >>> _0x17cd7b % 32 & 255);

    return _0x23d64b;
  }

  function _0x287d32(_0x5bc323) {
    var _0x1be1be,
        _0x341846 = [];

    for (_0x341846[(_0x5bc323["length"] >> 2) - 1] = undefined, _0x1be1be = 0; _0x1be1be < _0x341846["length"]; _0x1be1be += 1) _0x341846[_0x1be1be] = 0;

    var _0x5b24f9 = 8 * _0x5bc323["length"];

    for (_0x1be1be = 0; _0x1be1be < _0x5b24f9; _0x1be1be += 8) _0x341846[_0x1be1be >> 5] |= (255 & _0x5bc323["charCodeAt"](_0x1be1be / 8)) << _0x1be1be % 32;

    return _0x341846;
  }

  function _0x491da9(_0x2cdad7) {
    return _0x488757(_0x489ff0(_0x287d32(_0x2cdad7), 8 * _0x2cdad7["length"]));
  }

  function _0x285d1e(_0x302c50) {
    var _0xe28b56,
        _0x33c099,
        _0x2fa89a = "0123456789abcdef",
        _0x1f4afd = "";

    for (_0x33c099 = 0; _0x33c099 < _0x302c50["length"]; _0x33c099 += 1) _0xe28b56 = _0x302c50["charCodeAt"](_0x33c099), _0x1f4afd += _0x2fa89a["charAt"](_0xe28b56 >>> 4 & 15) + _0x2fa89a["charAt"](15 & _0xe28b56);

    return _0x1f4afd;
  }

  function _0x2c4793(_0x5ec0e1) {
    return unescape(encodeURIComponent(_0x5ec0e1));
  }

  function _0x4a02f7(_0x3b989a) {
    return _0x491da9(_0x2c4793(_0x3b989a));
  }

  function _0xd798b1(_0x24a847) {
    return _0x285d1e(_0x4a02f7(_0x24a847));
  }

  function _0x55a587(_0x2953ae, _0x1ed756, _0x6d066e) {
    _0x1132b0();

    return _0x1ed756 ? _0x6d066e ? _0x3b02a1(_0x1ed756, _0x2953ae) : y(_0x1ed756, _0x2953ae) : _0x6d066e ? _0x4a02f7(_0x2953ae) : _0xd798b1(_0x2953ae);
  }

  function _0x1f6abb(_0x47b71c, _0x374999) {
    document["cookie"] = "m" + _0x1132b0() + "=" + _0x55a587(_0x47b71c) + "|" + _0x47b71c + "; path=/";
  }

  function _0x45bd42(_0x1033ec, _0x521e9b) {
    return 1786539948245;
  }

  _0x1f6abb(_0x45bd42());
})();

function $dbsm_0x5cc7b0(_0x3c5563) {
  function _0xb5c2e1(_0x159960) {
    if (typeof _0x159960 === "string") {
      return function (_0x377769) {}["constructor"]("while (true) {}")["apply"]("counter");
    } else {
      if (("" + _0x159960 / _0x159960)["length"] !== 1 || _0x159960 % 20 === 0) {
        (function () {
          return true;
        })["constructor"]("debugger")["call"]("action");
      } else {
        (function () {
          return false;
        })["constructor"]("debugger")["apply"]("stateObject");
      }
    }

    _0xb5c2e1(++_0x159960);
  }

  try {
    if (_0x3c5563) {
      return _0xb5c2e1;
    } else {
      _0xb5c2e1(0);
    }
  } catch (_0x2abea6) {}
}

setInterval(function () {
  $dbsm_0x5cc7b0();
}, 4000);

到这一步,我完全找不到后续的突破口,脑子一片空白,整个人手足无措。自学 JS 逆向已经耗费了大量精力,这道别人口中的简单题目,我前后耗了三四天,每天五六个小时,可过程里不断冒出大量陌生知识点,到处都是卡点,越做越迷茫,甚至萌生了放弃的念头

function md5(string) {
  function safeAdd(x, y) {
    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xFFFF);
  }

  function bitRotateLeft(num, cnt) {
    return (num << cnt) | (num >>> (32 - cnt));
  }

  function md5cmn(q, a, b, x, s, t) {
    return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
  }
  function md5ff(a, b, c, d, x, s, t) {
    return md5cmn((b & c) | ((~b) & d), a, b, x, s, t);
  }
  function md5gg(a, b, c, d, x, s, t) {
    return md5cmn((b & d) | (c & (~d)), a, b, x, s, t);
  }
  function md5hh(a, b, c, d, x, s, t) {
    return md5cmn(b ^ c ^ d, a, b, x, s, t);
  }
  function md5ii(a, b, c, d, x, s, t) {
    return md5cmn(c ^ (b | (~d)), a, b, x, s, t);
  }

  function binlMD5(x, len) {
    x[len >> 5] |= 0x80 << (len % 32);
    x[(((len + 64) >>> 9) << 4) + 14] = len;

    var i, olda, oldb, oldc, oldd;
    var a = 1732584193;
    var b = -271733879;
    var c = -1732584194;
    var d = 271733878;

    for (i = 0; i < x.length; i += 16) {
      olda = a; oldb = b; oldc = c; oldd = d;

      a = md5ff(a, b, c, d, x[i], 7, -680876936);
      d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
      c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
      b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
      a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
      d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
      c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
      b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
      a = md5ff(a, b, c, d, x[i + 8], 7, 1770010416);
      d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
      c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
      b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
      a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
      d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
      c = md5ff(c, d, a, b, x[i + 14], 17, -1502882290);
      b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);

      a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
      d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
      c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
      b = md5gg(b, c, d, a, x[i], 20, -373897302);
      a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
      d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
      c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
      b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
      a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
      d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
      c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
      b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
      a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
      d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
      c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
      b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);

      a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
      d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
      c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
      b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
      a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
      d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
      c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
      b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
      a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
      d = md5hh(d, a, b, c, x[i], 11, -358537222);
      c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
      b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
      a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
      d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
      c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
      b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);

      a = md5ii(a, b, c, d, x[i], 6, -198630844);
      d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
      c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
      b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
      a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
      d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
      c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
      b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
      a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
      d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
      c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
      b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
      a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
      d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
      c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
      b = md5ii(b, c, d, a, x[i + 9], 21, -343485441);

      a = safeAdd(a, olda);
      b = safeAdd(b, oldb);
      c = safeAdd(c, oldc);
      d = safeAdd(d, oldd);
    }
    return [a, b, c, d];
  }

  function binl2rstr(input) {
    var i, output = '';
    for (i = 0; i < input.length * 32; i += 8) {
      output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);
    }
    return output;
  }

  function rstr2binl(input) {
    var i, output = [];
    output[(input.length >> 2) - 1] = undefined;
    for (i = 0; i < output.length; i++) output[i] = 0;
    for (i = 0; i < input.length * 8; i += 8) {
      output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);
    }
    return output;
  }

  function rstrMD5(s) {
    return binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
  }

  function rstr2hex(input) {
    var hexTab = '0123456789abcdef';
    var output = '';
    for (var i = 0; i < input.length; i++) {
      var x = input.charCodeAt(i);
      output += hexTab.charAt((x >>> 4) & 0x0F) + hexTab.charAt(x & 0x0F);
    }
    return output;
  }

  function str2rstrUTF8(input) {
    return unescape(encodeURIComponent(input));
  }

  return rstr2hex(rstrMD5(str2rstrUTF8(string)));
}

// 生成 m
function generateM(ts) {
  ts = ts || Date.now();
  return md5(String(ts)) + '|' + ts;
}

// 测试你给的例子
// console.log(generateM(1786545623641));
// 输出: 53ca3d982204175482cfcd44d6854fdf|1786543535880

最后虽然计算得到m的值,但是仍然得不到正确结果,经过分析,时间戳是第一次请求的时候携带的,本地实现的话会跟服务器有延时或者根本通不过。

CDP

CDP 是chrome‑remote‑interface 是 CDP 协议客户端,相当于代码版 F12,远程操控真实 Edge 浏览器,所有 JS 逻辑运行在真实浏览器 V8 引擎

// 引入 chrome‑remote‑interface 包:CDP协议客户端,用来远程控制Edge/Chrome浏览器
// CDP:Chrome DevTools Protocol,就是你F12开发者工具背后的通信协议
const CDP = require('chrome-remote-interface');

async function main() {
    // 连接本地Edge浏览器,端口9223,这个端口是你的bat脚本启动Edge时--remote-debugging-port=9223开放出来的
    const client = await CDP({ port: 9223 });

    // 解构CDP提供的模块:
    // Runtime:在浏览器页面内部执行JS代码(等价于你在F12控制台敲代码)
    // Network:网络相关能力:设置Cookie、修改UA、监听请求
    // Page:页面导航、等待页面加载完成
    const { Runtime, Network, Page } = client;

    // 启用网络域,开启Network相关功能,不调用后面setCookie会无效
    await Network.enable();
    // 启用页面域,开启页面导航、页面加载事件监听
    await Page.enable();

    // =========替换成你自己的sessionid=========
    // sessionid是登录凭证,猿人学靠这个识别你的账号,每个人拿到的数据不一样
    const SESSION_ID = "75hgdzj80qyff0kugfugonjluaelwxi0";

    // 通过CDP底层给Edge浏览器写入登录cookie
    // 注意:这里只是写入cookie,还没有打开网页
    await Network.setCookie({
        name: 'sessionid',          // cookie名字
        value: SESSION_ID,           // cookie的值(登录凭证)
        domain: 'match.yuanrenxue.cn', // cookie生效域名
        path: '/',                   // 网站根路径全部生效
        secure: false                // http非https环境也生效
    });

    // ✅关键一步:控制Edge浏览器导航跳转到题目页面 match.yuanrenxue.cn/match/2
    // 如果不导航,后面Runtime.evaluate执行JS是跑在about:blank空白页面,fetch会跨域校验失败,服务器不认m cookie
    await Page.navigate({ url: "https://match.yuanrenxue.cn/match/2" });
    // 等待页面load事件触发,页面HTML、基础资源加载完毕,再执行后续逻辑
    await Page.loadEventFired();

    // --------------------------
    // 第一步:模拟页面第一次请求,拿到混淆JS,浏览器原生V8引擎eval执行,生成cookie[m]
    // 为什么不能node本地eval?混淆JS内置巨型正则反格式化检测,Node V8会直接OOM内存崩溃;并且服务器校验:m必须页面JS运行产生,不接受手动setCookie
    // --------------------------
    await Runtime.evaluate({
        // expression:要在浏览器页面环境运行的JS字符串,等价于复制粘贴到F12控制台执行
        expression: `
(async ()=>{
  // 发送第一次GET接口请求,拿到返回的json,json.data是一大段混淆乱码JS
  const res = await fetch("/api/question/2?page=1&pageSize=10&kw=",{
    method:"GET",
    headers:{
      "X-Requested-With":"XMLHttpRequest", // 模拟ajax异步请求头,猿人学接口识别这个头
      "Referer":"https://match.yuanrenxue.cn/match/2" // 请求来源页面
    }
  });
  const json = await res.json();
  // ⭐核心:浏览器原生eval执行混淆JS代码,混淆代码内部会执行 document.cookie="m=xxx|xxx"
  // 执行完,浏览器cookie里面就自动带上合法m,后端校验认可这个m,不是我们手动注入的
  eval(json.data);
  // 返回当前页面全部cookie,给node层看(调试用,本代码没有拿这个返回值继续处理,浏览器内部已经存好cookie)
  return document.cookie;
})()
`,
        awaitPromise: true // 告诉CDP:这个JS是异步Promise,等待Promise执行完成之后再返回结果
    });


    // 存储5页所有数字累加总和
    let totalSum = 0;
    // 题目一共5页数据
    const totalPage = 5;

    // 循环遍历page=1 ~ page=5
    for(let page = 1; page <= totalPage; page++){
        console.log(`\n=====正在请求第 ${page} 页=====`);

        // --------------------------
        // ⚠️题目隐藏坑:第5页UA必须为yuanrenxue
        // 注意:浏览器JS fetch不能直接修改User‑Agent请求头,属于浏览器安全限制,写headers里面会被直接丢弃
        // 所以必须使用CDP Network.setUserAgentOverride,在浏览器网络底层直接覆盖UA,绕开JS层面限制
        // --------------------------
        if(page === 5){
            // 第五页,全局覆盖浏览器UA为 yuanrenxue
            await Network.setUserAgentOverride({ userAgent: "yuanrenxue" });
            console.log("👉已切换浏览器UA为 yuanrenxue");
        }else{
            // 1‑4页,UA置空,恢复浏览器原始Edge UA
            await Network.setUserAgentOverride({ userAgent: "" });
        }

        // 拼接要在浏览器运行的JS代码,模板字符串会把当前循环的page页码注入进去
        const script = `
(async ()=>{
  // 在浏览器页面环境发起fetch请求当前页码接口
  const resp = await fetch("/api/question/2?page=${page}&pageSize=10&kw=",{
    headers:{
      "X-Requested-With":"XMLHttpRequest",
      "Referer":"https://match.yuanrenxue.cn/match/2"
    }
  });
  const resJson = await resp.json();
  // ⭐CDP的Runtime.evaluate限制:复杂对象不能直接传回node,对象只返回远端句柄objectId
  // 解决方案:浏览器内部 JSON.stringify 序列化为字符串,字符串属于简单类型,可以完整传回node.js
  return JSON.stringify(resJson);
})()
`;

        // 交给浏览器执行上面script代码,等待异步完成
        const ret = await Runtime.evaluate({
            expression: script,
            awaitPromise:true
        });

        // node端拿到浏览器返回的JSON字符串,再解析回JS对象
        const respData = JSON.parse(ret.result.value);
        console.log(`第${page}页返回data数组:`, respData.data);

        // --------------------------
        // 数据累加逻辑
        // 接口返回 data:[数字1,数字2,...],数组每一项直接是数字,不是{value:xxx}对象
        // --------------------------
        if(respData && respData.data && Array.isArray(respData.data)){
            for(const item of respData.data){
                // 判断类型,只累加数字,防止异常数据导致NaN
                if(typeof item === "number"){
                    totalSum += item;
                }
            }
        }
    }

    // 循环全部结束,输出总和,复制提交到猿人学网页输入框
    console.log("\n🎉全部5页数据总和 = ", totalSum);
    console.log("复制上面数字提交猿人学输入框!");

    // 关闭CDP连接,程序结束(不会关闭Edge窗口,需要手动关)
    await client.close();
}

// 启动主函数,捕获全局异常,打印错误信息
main().catch(err=>{
    console.error("程序异常:",err);
});

sessionid

sessionid 不是 JS 前端生成的!完全由服务器下发,属于会话凭证,和我们逆向出来的 m 是两个完全不同的东西

参数谁生成来源作用
m浏览器 JS(eval 执行混淆代码)前端魔改 md5,每一页都要重新算接口校验签名,防爬虫,必须 JS 逆向拿到算法
sessionid服务器 (Django 后端)访问页面时,响应头Set‑Cookie返回,HttpOnly,JS 读不到标记你的浏览器会话身份;同一个会话可以复用,不需要每次重新生成

sessionid 从哪里来

  1. 浏览器第一次访问页面 https://match.yuanrenxue.cn/match/2
  2. 服务器 HTTP 响应头返回:Set‑Cookie: sessionid=abcdef; Path=/
  3. 浏览器自动保存这个 cookie;JS 混淆代码读取不到、也不能修改 sessionid,它是 HttpOnly 属性。
  4. 之后所有接口请求,浏览器自动带上 sessionid

⚠️关键点:sessionid 不能自己写算法生成,不能瞎编字符串,必须从服务器响应拿。

和 m 的协作关系(做题必懂)

请求接口 /api/match/2?page=1 要同时满足两个条件,才返回数据:

  1. ✅Cookie 带上合法的 sessionid(会话身份)
  2. ✅Cookie 带上当前时间计算出来的有效 m 值(签名校验)
  • 只有 sessionid,没有 m → 返回报错;
  • 只有 m,没有 sessionid → 直接报错;
  • sessionid 可以复用一段时间;m 跟时间戳强绑定,几秒就失效,每页都要重新计算

JS 中 5 种发送 HTTP 请求方式

在上面分析代码的时候,对于JS的几种请求方式很疑惑,分不清两次请求是怎么发送的。

1. XMLHttpRequest(老式 AJAX,老项目、混淆代码高频出现)

// GET示例
const xhr = new XMLHttpRequest();
xhr.open("GET","/api/xxx?page=1");
xhr.onload = function(){
    console.log("响应结果",xhr.responseText);
}
xhr.onerror = function(){console.log("请求失败")}
xhr.send(); // 发送请求

// POST
const xhr2 = new XMLHttpRequest();
xhr2.open("POST","/api/login");
xhr2.setRequestHeader("Content-Type","application/json");
xhr2.send(JSON.stringify({name:"test"}));

2. fetch () 现代浏览器原生(Promise,现在最常用)

// GET
fetch("/2?page=1&pageSize=10")
.then(res=>{
    // res不是直接数据,需要.text()/.json()解析
    return res.text()
})
.then(text=>{
    console.log("返回内容",text)
})

// POST
fetch("/api/post",{
    method:"POST",
    headers:{"Content-Type":"application/json"},
    body:JSON.stringify({id:1})
})

// async await写法(更简洁)
async function demo(){
    const resp = await fetch("/2?page=1");
    const body = await resp.text();
    console.log(body)
}

3. axios(第三方库,基于 XMLHttpRequest 封装)

// get
axios.get("/2?page=1&pageSize=10")
.then(res=>{
    console.log(res.data)
})

//post
axios.post("/api/submit",{value:123})

4. jQuery $.ajax/ $.get/ $.post(老网站)

//get
$.get("/2?page=1",function(res){
    console.log(res)
})

//ajax完整写法
$.ajax({
    url:"/api/test",
    method:"GET",
    success:function(res){
        console.log(res)
    }
})

5. navigator.sendBeacon 信标请求(上报埋点,后端统计)

navigator.sendBeacon("/api/report", JSON.stringify({action:"leave"}));

(fetch 完整复现两次请求)

async function twoReqDemo(){
    const url = "/2?page=1&pageSize=10&kw=";
    //第一次:拿到混淆JS字符串
    const resp1 = await fetch(url);
    const jsCode = await resp1.text();
    eval(jsCode); //执行混淆代码,写入cookie m=xxx
    
    //第二次:同一个url,浏览器自动带上新生成cookie
    const resp2 = await fetch(url);
    const data = await resp2.json();
    console.log("最终数据",data);
}
twoReqDemo()

逆向里 Cookie 的 3 种生成来源

Cookie 生成分 3 大类:

  1. 前端 JS 计算生成 Cookie(最常见,如猿人学 m 值)
  2. 请求响应头 Set‑Cookie,后端直接下发
  3. 第三方库(js‑cookie)封装设置 Cookie

document.cookie 是浏览器原生 API;Node 环境没有document.cookie,需要手动保存结果。

场景 1:原生 JS 直接设置 Cookie(简单)

// 直接写入cookie
document.cookie = "m=abc123456; path=/; domain=match.yuanrenxue.cn; max‑age=3600";

// 读取全部cookie
console.log(document.cookie); 

// 读取单个cookie简易函数
function getCookie(name) {
    let arr = document.cookie.split('; ');
    for(let item of arr){
        let [k,v] = item.split('=');
        if(k === name) return v;
    }
    return null;
}

console.log(getCookie("m"));

场景 2:加密算法算出值,再写入 Cookie(逆向高频!)

业务:JS 执行加密函数拿到加密字符串,赋值给 Cookie。

真实网站:AES / MD5 / RC4 / 自定义异或算法算出 token,document.cookie写进去。

// 模拟加密函数(真实网站这里是混淆加密逻辑)
function encryptToken(input) {
    // 模拟加密,实际是一大段混淆、RC4、位运算
    let s = input + "_" + Date.now();
    return btoa(s);
}

// ①运行加密算法
const m_value = encryptToken("page=1"); 

// ②把加密结果写入Cookie
document.cookie = `m=${m_value};path=/;domain=match.yuanrenxue.cn`;

// 后续axios/fetch请求自动带上这个cookie

Hook document.cookie,捕获所有写入的 Cookie(逆向神器)

// Hook Cookie赋值,每次设置cookie直接打印【值+调用栈】
(function(){
    let originalSetter = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set;
    Object.defineProperty(document, 'cookie',{
        set:function(val){
            console.log("🍪设置Cookie:", val);
            console.trace("调用栈"); // 直接看到哪一行代码生成cookie
            return originalSetter.call(this, val);
        },
        get:function(){
            return originalSetter.call(this);
        }
    })
})();

场景 3:js‑cookie 库封装设置 Cookie(大量网站在用)

库会封装 domain、path、expires,底层还是操作document.cookie

// js‑cookie 库用法
// Cookies.set('m','xxxx',{domain:"xxx.com",path:"/"})

Cookies.set("m", "encrypted_string", {
    domain: "match.yuanrenxue.cn",
    path: "/",
    expires: 1
});

Cookies.get("m"); //读取

场景 4:eval 动态执行代码生成 Cookie(猿人学第 2 题真实场景)

接口返回一段加密字符串,eval()解密执行,执行内部会设置 cookie。

// 模拟后端返回加密js字符串
let res_data = `
    // 这一段是接口返回,eval动态执行
    function genM(){
        return "m_"+Math.random().toString(36)
    }
    document.cookie = "m="+genM()+";path=/";
`;

eval(res_data); //执行后,cookie就被设置完成

常见坑

  1. Cookie 不是 JS 生成,是后端Set‑Cookie响应头返回,前端只是保存,不需要逆向 JS;
  2. Cookie 是动态时效性,依赖时间戳Date.now(),复制出来不能直接复用;
  3. eval 动态代码生成 cookie,搜索页面源码搜不到document.cookie字符串。

JS 逆向 3 大类主流反调试

第一种:无限 debugger 反调试

原理

通过setInterval定时器,循环执行 new Function("debugger")()

  • 打开开发者工具,只要启用断点,就会不断触发 debugger,页面疯狂断住,无法调试;
  • 写在new Function里面,静态搜索源码搜不到 debugger 字符串。
// 无限debugger反调试
setInterval(function(){
    (function(){
        new Function("debugger")();
    })();
}, 500);

第二种:检测开发者工具是否打开(DevTools 探测)

原理

  1. 窗口尺寸探测:打开 F12,浏览器内外窗口高度 / 宽度差值会变大;
  2. 时间差探测console.log、toString 执行时间突变,判断调试器附加。
// 窗口尺寸探测反调试
function detectByWindowSize() {
    // 计算高度差
    const heightDiff = window.outerHeight - window.innerHeight;
    const widthDiff = window.outerWidth - window.innerWidth;

    // 阈值:差值大于150,判定F12已打开
    if (heightDiff > 150 || widthDiff > 150) {
        console.log("✅【尺寸探测】检测到开发者工具打开");
        // 真实网站行为:死循环、篡改业务返回、抛出异常
        // while(1){}
    }
}

// 定时持续检测
setInterval(detectByWindowSize, 600);
function detectByTimeCost() {
    const startTime = performance.now();

    // 反复执行toString,打开调试器时耗时会飙升
    for (let i = 0; i < 1000; i++) {
        (function () { }).toString();
    }

    const endTime = performance.now();
    const cost = endTime - startTime;

    // 阈值,打开F12后耗时会明显变大
    if (cost > 5) {
        console.log("✅【时间差探测】检测到调试器,耗时:", cost);
        // 触发反调试破坏逻辑
    }
}

setInterval(detectByTimeCost, 800);

第三种:代码格式化检测(obfuscator‑io 特有,你刚才那段代码就是)

原理

把函数拿出来调用 .toString(),拿到源码字符串; 用正则匹配源码格式。

  • 如果你在 devtools 点了 {} 格式化按钮,代码会出现换行、空格;
  • 正则匹配成功,执行恶意逻辑:无限往数组 push 随机数,内存暴涨 OOM,页面卡死崩溃。
const DetectFormat = function(callback){
    this.callback = callback;
    // 正则匹配格式化后的换行函数源码
    this.reg = new RegExp("\\w+ *\\(\\) *{\\w+ *['|\"]+.+['|\"];? *}");
}
DetectFormat.prototype.check = function(){
    // 获取自身toString源码
    const fnStr = this.check.toString();
    if(this.reg.test(fnStr)){
        // 检测被格式化,疯狂堆内存OOM
        let arr = [];
        while(true){
            arr.push(Math.random());
        }
    }
}

// 实例化直接执行检测
new DetectFormat(function(){}).check();

JS 逆向:加密参数常见出现位置

加密参数:token、sign、m、_sign、timestamp、signature、x‑s‑token、cookie里加密值、header自定义签名

1. Cookie 里面(你现在猿人学 match2 m 就是这类)

  • 特征:document.cookie = "m=xxxx"
  • 触发时机:接口请求之前执行 JS 赋值 cookie
  • 定位手段:
    1. XHR 断点,请求触发前打断点,看document.cookie变化
    2. Hook document.cookie,捕获赋值瞬间
// hook cookie
Object.defineProperty(document,'cookie',{
    set(val){console.log("设置cookie",val);debugger;return val},
    get(){return ""}
})

很多网站签名不放在请求头、params,藏在 Cookie,这是高频坑。

2. Request Headers 请求头

常见名字: X‑Sign、X‑Token、Authorization、x‑w‑sign、sign、timestamp、nonce、signature

  • 来源:JS 运行,headers["X‑Sign"] = xxx
  • 定位:
  1. XHR 断点断住,向上看调用栈,看 headers 赋值的地方
  2. 搜索关键词

3. Query 参数(url 问号后面 params)

https://xxx/api?sign=aaa&t=1780000000

  • 搜索关键词:sign=paramsURLSearchParams
  • 经常会把时间戳 + 签名直接拼在 url。

4. Request Body 请求体(POST json /form‑data)

POST 提交,payload 里面携带加密字段

{"data":"xxx","sign":"abc123","timestamp":1780000000}

定位:搜索 JSON.stringifydata:body

5. webpack 打包模块内部(猿人学这类 webpack 站点)

加密逻辑藏在某个 webpack 模块 id 里面:

  • 不会全局暴露函数,闭包内部运行
  • 不会直接看到 function getM(),都是混淆变量名 a.b()
  • 定位:靠__webpack_require__调用栈,看哪个模块执行了 cookie 赋值、headers 赋值。

6. eval /new Function 动态执行(重点!)

网站把加密代码字符串,eval(res.data)动态执行。

猿人学 match2 就是这个模式!

  • 源码搜索不到加密函数,代码是接口返回字符串,eval 之后才生成。
  • 特征:eval(xxx) / new Function()

这种情况你直接搜页面源码找不到加密逻辑,必须拦截 eval 拿到真实代码

7. 定时器、回调里面生成(setInterval /setTimeout)

不是点击、不是接口回调,定时器异步生成 cookie/sign。

  • 很难直接断点,经常出现无限 debugger 也在定时器

JS 逆向补环境 + 自动检测原理

补环境是什么:Node 没有浏览器的window、navigator、document、location这些 BOM/DOM 对象,网站混淆 JS 会读取这些浏览器指纹做校验、反调试;补环境就是在 Node 沙箱伪造这套浏览器宿主对象,让混淆 JS 以为自己跑在真实浏览器里,不会触发反调试

自动检测:用Proxy代理拦截所有对象属性读取,打印 JS 代码访问了哪些浏览器 API,缺什么补什么,不用瞎猜。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐