Edge/Chrome CDP 远程调试与 Node.js Hook TextDecoder、JSON、AES

Edge/Chrome CDP 远程调试

1. 启动浏览器开启调试端口

# Edge
msedge.exe --remote-debugging-port=9222 --user-data-dir=D:\edge_debug

# Chrome
chrome.exe --remote-debugging-port=9222

2. 获取 WebSocket 调试地址

访问:

http://127.0.0.1:9222/json

返回 JSON,找 webSocketDebuggerUrl

{
  "title": "test page",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ABC123"
}

3. 用 DevTools 前端连接

把 ws 地址传给 DevTools 前端:

devtools://devtools/bundled/devtools_app.html?ws=127.0.0.1:9222/devtools/page/ABC123

在 Edge/Chrome 地址栏直接输入即可,打开完整的开发者工具界面。

核心结构:

浏览器(Edge/Chrome)
  ↓ WebSocket (CDP)
DevTools Frontend(网页 UI)

Node.js TextDecoder 支持情况

Node.js ≥ 11 支持 TextDecoder,Node ≥ 16/18 更稳定:

const { TextDecoder, TextEncoder } = require('util');

const decoder = new TextDecoder('utf-8');
const buf = Buffer.from([0xe4, 0xbd, 0xa0]); // "你"
console.log(decoder.decode(buf)); // 你

Hook TextDecoder.decode

用于拦截 WebSocket / CDP 协议里的二进制转文本:

const { TextDecoder } = require('util');

const _decode = TextDecoder.prototype.decode;

TextDecoder.prototype.decode = function (input, options) {
  const result = _decode.call(this, input, options);
  console.log('[TextDecoder.decode]', result);
  return result;
};

全局 hook(拦截所有实例):

const orig = TextDecoder;

global.TextDecoder = function (...args) {
  const instance = new orig(...args);
  const _decode = instance.decode;
  instance.decode = function (...params) {
    const res = _decode.apply(this, params);
    console.log('[WS decode]', res);
    return res;
  };
  return instance;
};

Hook JSON.parse 并落盘

const fs = require('fs');
const _parse = JSON.parse;

JSON.parse = function (text, reviver) {
  try {
    const result = _parse.call(this, text, reviver);

    // 只保存含 "raw" 字段的数据
    if (result && typeof result === 'object' && 'raw' in result) {
      fs.appendFileSync(
        './json_raw_log.txt',
        JSON.stringify({
          time: Date.now(),
          data: result
        }) + '\n'
      );
    }

    return result;
  } catch (e) {
    return _parse.call(this, text, reviver);
  }
};

按关键字过滤避免刷屏:

// 只记录长度 < 5000、包含关键字的
if (typeof text === 'string' && text.length < 5000 && text.includes('method')) {
  fs.appendFileSync('./log.txt', text + '\n');
}

Hook Node.js AES 加密

Hook crypto.createCipheriv 抓加密前的明文、key、IV:

const crypto = require('crypto');
const _createCipheriv = crypto.createCipheriv;

crypto.createCipheriv = function (algorithm, key, iv, options) {
  console.log('[AES createCipheriv]', {
    algorithm,
    key: Buffer.isBuffer(key) ? key.toString('hex') : key,
    iv: Buffer.isBuffer(iv) ? iv.toString('hex') : iv
  });

  const cipher = _createCipheriv.call(this, algorithm, key, iv, options);
  const _update = cipher.update;
  const _final = cipher.final;

  cipher.update = function (data, inputEncoding, outputEncoding) {
    console.log('[AES encrypt input]', data?.toString?.() ?? data);
    return _update.apply(this, arguments);
  };

  cipher.final = function (outputEncoding) {
    const res = _final.apply(this, arguments);
    console.log('[AES encrypt output]', res);
    return res;
  };

  return cipher;
};

解密 hook(对称):

const _createDecipheriv = crypto.createDecipheriv;

crypto.createDecipheriv = function (algorithm, key, iv) {
  const decipher = _createDecipheriv.call(this, algorithm, key, iv);
  const _update = decipher.update;

  decipher.update = function (data) {
    const res = _update.apply(this, arguments);
    console.log('[AES decrypt result]', res.toString());
    return res;
  };

  return decipher;
};

组合 hook(实战抓包)

WebSocket + 加密接口逆向的标准三件套:

  1. TextDecoder.prototype.decode — 抓二进制转文本
  2. JSON.parse — 抓接口返回 JSON
  3. crypto.createCipheriv/createDecipheriv — 抓 AES 明文和密钥

部分代码直接用 Buffer.toString('utf-8') 而不经过 TextDecoder,还需要额外 hook:

const _toString = Buffer.prototype.toString;

Buffer.prototype.toString = function (encoding, ...rest) {
  const result = _toString.call(this, encoding, ...rest);
  if (encoding === 'utf8' || encoding === 'utf-8') {
    console.log('[Buffer.toString]', result.slice(0, 200));
  }
  return result;
};