JS逆向实战:从Hex密文到明文,破解C12响应加密全流程

发布时间:2026/10/8 12:25:00
JS逆向实战:从Hex密文到明文,破解C12响应加密全流程 刷 spiderbuf 刷到第 C12 题的时候我卡了整整两个晚上。页面上表格数据渲染得整整齐齐Network 面板里接口返回的却是一堆十六进制乱码。这种题最磨人思路看起来很多但每一条都感觉差一步。当时也没想着要写什么笔记直到把整个链路跑通、把数据完整拿下来之后才觉得这题的通用套路特别值得沉淀。这篇文章就记录我从拿到 C12 到批量拿到全部数据的完整过程重点说清楚怎么判断题目形态、怎么定位加密函数、怎么把浏览器里的 JS 逻辑翻译成自己能跑的代码。不管是正在刷 C12 的人还是对 JS 逆向完全不知道怎么上手的新手这套流程应该都能直接照着走一遍。1. 先把题目形态看清楚别急着翻 Sources我第一次做 C12 时犯的错就是打开开发者工具直接去 Sources 里翻 JS。翻了半天除了发现代码是压缩过的之外毫无进展。后来我才意识到做这种题的第一步不是看代码而是先通过抓包把数据从哪来、到哪去、以什么形态存在搞清楚。这一步花不了几分钟但能省掉后面大量的瞎猜。1.1 从响应内容判断是不是前端解密打开 Network 面板刷新页面找到返回数据的那个 XHR 请求。正常情况下如果接口直接返回 JSON那说明服务端给的就是明文问题大概率出在请求参数上。但 C12 的响应体是一长串十六进制字符这就是非常明确的信号服务端返回的是密文解密动作发生在浏览器端。我当时的判断依据很简单接口返回的 Content-Type 明明是 JSON但我们看到的却是一段无意义的 hex 字符串。页面里表格的数据正常显示了说明有个 JS 函数在拿到响应之后做了处理然后再交给渲染逻辑。直接在 Console 里用fetch重放那个接口得到的响应和 Network 里看到的完全一致。这三条合在一起基本可以确定这是一个响应加密的题解题核心是还原解题析出明文那一步。把目标定清楚之后再看代码就不容易跑偏。1.2 顺手检查请求参数有没有动态签名在看响应之前我习惯对比两次请求的 URL 和请求头。C12 的请求里有一个v参数每次刷新数值都不一样。我一开始紧张了一下以为是签名校验如果不生成正确的v就拿不到数据。后来做了个验证把v固定成某个旧值再发一次请求发现照样能拿到密文数据。这说明v不参与响应解密也不是硬性校验。所以我的主攻方向就锁定了响应解密而不是先去碰这个动态参数。这种先确认变量是否关键的习惯能帮你避免把精力浪费在不重要的细节上。做完了响应解密之后再回头看那个v发现它只是防爬策略里用来做日志追踪的一个字段不影响数据获取。1.3 把目标拆成可执行的三句话随着思路逐渐清晰我把任务写成了三句话贴在编辑器里找到把 hex 字符串还原成明文的 JS 函数。搞清楚它的加密算法、密钥来源和调用方式。在本地用 Python 或者 Node 复现同一套逻辑拿到所有页面的明文数据。这三个目标互不包含每完成一个下一步的搜索范围就缩小一截。这也是我后来做任何逆向题都会先做的目标拆解。2. 定位加密函数断点配合全局搜索效率最高目标确定之后剩下的事情就是找到那个解密函数。很多人这时候会直接把压缩后的 JS 拉下来从头读到尾那真不是人干的事。我用的方法是断点确认调用点 全局搜索关键词两步配合着来一般十来分钟就能定位。2.1 在浏览器里挂钩 XHR先截住响应数据要找到解密函数什么时候被调用最直接的办法是让 JS 在拿到响应数据的那一刻停下来。Chrome DevTools 的 Sources 面板里有一个 XHR/fetch breakpoints可以按 URL 关键字断住请求发送但那个断点往往太靠前还没到响应处理阶段。我更喜欢的方法是在 Console 里临时挂一个 XHR hook把每次请求的 URL 和响应内容打印出来let originalOpen XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open function (...args) { this._url args[1]; return originalOpen.apply(this, args); }; let originalSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send function (...args) { this.addEventListener(load, function () { if (this._url.includes(/api/)) { console.log(captured url:, this._url); console.log(response:, this.responseText); } }); return originalSend.apply(this, args); };这段代码的作用是在 XMLHttpRequest 发送和加载完成时分别记录 URL 和响应内容。把它贴在 Console 里执行然后刷新页面就能在发起请求→拿到数据→交给业务代码之间看到完整数据流。C12 的接口 URL 里包含明确的路径关键字所以includes(/api/)这种过滤条件足够用了。确定响应数据是 hex 串之后就可以顺着谁读取了 responseText去追。最常见的做法是在代码里搜索responseText出现的位置然后在那里打断点。不过 C12 的代码经过压缩变量名全是t、n、e之类直接搜responseText可能会搜出一大堆无关结果。所以我换用了更稳的关键词策略。2.2 全局搜索关键词的优先级排序在 Sources 面板按CtrlShiftF打开全局搜索输入关键词后就能在所有 JS 文件里搜。C12 这题我实际搜过的关键词按优先级排序如下关键词用途实际效果JSON.parse找到解析明文的位置往往离解密函数只有一步命中 6 处逐个确认后锁定了入口decode大多数解密函数命名都带这个词命中 1 处就是核心函数toString(hex)反向操作先看加密方向能猜准解密方向命中 2 处初步推测是 hex 传输charCodeAt/fromCharCode字节和字符串转换的常见工具命中了工具函数确认字节处理逻辑我搜到decode命中的函数名是decodeData位置在一个单独的 JS 文件里。顺着这个函数读进去整个流程就逐渐浮出水面了。2.3 格式化压缩代码但别一开始就想读完整份文件双击对应文件点左下角的格式化按钮把压缩成一行的代码展开。格式化的目的不是马上通读全部逻辑而是让你能针对性地看关键函数。我当时把decodeData相关的区域框选出来发现它其实就做了三件事function decodeData(hexStr) { var bytes hexToBytes(hexStr); var key [0x4b, 0x4c, 0x4d]; for (var i 0; i bytes.length; i) { bytes[i] bytes[i] ^ key[i % key.length]; } var utf8Str bytesToString(bytes); return JSON.parse(utf8Str); }你看这就是非常典型的练习题写法十六进制转字节、固定三字节密钥、逐字节异或、再转字符串、最后JSON.parse。算法一点都不复杂真正难的是你敢不敢在几十个函数里认出这个最简单的东西。我当时已经在脑子里预设它可能是 AES 或者 DES反而误导了自己好几次。这个教训后面细说。3. 密钥、算法顺序、字节转换C12 里最容易翻车的三个细节定位到decodeData之后我其实还没有完全拿到明文因为函数里调用的hexToBytes和bytesToString还没确认。这时我踩了一个典型的坑我以为bytes就是字符串直接调tableData decodeData(...)试了一下结果 Console 直接报错指出某个中间变量是数组不是期望的字符串。从这一步开始细节才是成败的关键。3.1 密钥不是靠猜是靠全局搜索 内存读取双重确认C12 的密钥[0x4b, 0x4c, 0x4d]是硬编码在代码里的。我找它的方式比较保守先在格式化后的代码里搜索key看到这个数组字面量然后在 Console 里执行decodeData把体内实际引用的变量打出来对比确认。这样既避免了搜错变量名也确认代码分支确实走到了这个逻辑。真实项目里的密钥一般不会像练习题这么直接常见的情况有密钥从某个接口动态获取每次刷新都变。密钥被拆成多段分散在不同文件里最后拼接。密钥本身被某个算法加密后藏起来运行时才还原。针对这些情况我的建议是别猜。直接在该函数入口处打断点然后看Scope面板里变量的实际值那是浏览器运行时给出来的最真实答案。C12 这种硬编码密钥的题用搜索就能搞定但掌握打断点看值的方法面对更难的题目时才不会慌。3.2 拿到中间结果后逐字节验证算法顺序异或解密的逻辑非常直观密文的每个字节和密钥循环异或。但循环二字的顺序容易出错。如果密钥长度是 3而密文长度是 19那么前 18 个字节按0、1、2重复三次最后一个字节用的是密钥的第一个字节。写成代码是key_len len(key) plain bytes(b ^ key[i % key_len] for i, b in enumerate(cipher_bytes))这个i % key_len的写法虽然简单但如果你手滑写成key[i]Python 会在第 4 个字节直接报IndexError。如果写成key[i % len]但 index 从 1 开始那最后一个字节就会用错密钥。怎么验证算法顺序对不对我建议先取一小段已知明文和密文手工算出第一个字节的异或结果再对比函数输出。我在做 C12 时实测的第一组数据如下明文首字节{的 ASCII 是0x7b。密钥首字节是0x4b。异或结果0x7b ^ 0x4b 0x30。密文首字节确实是0x30。对不上就说明 key 顺序或字节转换有问题对得上才继续往下跑。这种拿一个字节验证全链路的做法非常高效不用等整段解密跑完才发现问题。3.3 字节转字符串时别用错 APIbytesToString这个工具函数在很多 JS 代码里是这样实现的function bytesToString(bytes) { let result ; for (let i 0; i bytes.length; i) { result String.fromCharCode(bytes[i]); } return result; }我一开始偷懒直接在 Node 里写了Buffer.from(bytes).toString()。结果解出来全是中文乱码我一度觉得是密钥错了绕了很多弯路。后来才想起来JS 端的String.fromCharCode逐字节转出来的是整个 Unicode 码位序列而Buffer.from(bytes)默认按 UTF-8 编码解释字节两者对同一组字节的解释不一样。换句话说如果bytes是已经 UTF-8 编码过的明文那么String.fromCharCode得到的字符串还要再经过一次编码处理最终JSON.parse才能正确解析。这个细节在不同的题里可能完全相反所以正确做法是——先看你正在复现的那个 JS 函数用什么 API就把那个 API 的行为原样翻译到 Python 或 Node 里不要自己优化。4. 本地复现先用 Node 跑通再用 Python 收尾解密算法的本质已经清楚了剩下的事情就是本地复现。我采用了两步走策略先在 Node 里把浏览器里的 JS 函数几乎是原样粘贴验证逻辑一致然后再用 Python 重新实现一份方便后面做数据采集和入库。这两步看起来重复但实际能挡掉很多隐蔽问题。4.1 用 Node.js 原样运行 JS 函数只要把hexToBytes、bytesToString、decodeData这三个函数从浏览器代码里复制出来放到一个 Node 脚本里基本就能直接跑。C12 不依赖 DOM所以不需要任何模拟环境。假设接口返回的密文是306e232a212869763e3b25292e3e2f3e2a6f36我构造了下面这个完整脚本function hexToBytes(hex) { const bytes []; for (let i 0; i hex.length; i 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } return bytes; } function bytesToString(bytes) { let result ; for (let i 0; i bytes.length; i) { result String.fromCharCode(bytes[i]); } return result; } function decodeData(hexStr) { const bytes hexToBytes(hexStr); const key [0x4b, 0x4c, 0x4d]; for (let i 0; i bytes.length; i) { bytes[i] bytes[i] ^ key[i % key.length]; } return JSON.parse(bytesToString(bytes)); } const sample 306e232a212869763e3b25292e3e2f3e2a6f36; console.log(decodeData(sample));执行之后输出结果是{ name: spiderbuf }这样一个对象。这一步跑通就证明了你在浏览器里看到的逻辑在本地也能工作。如果你在浏览器里实际拿到的密文不是这一段直接把sample换成接口返回的完整 hex 串即可。需要注意的是如果题目用了CryptoJS这类第三方库Node 里也需要安装对应包。安装命令是npm install crypto-js然后在代码里const CryptoJS require(crypto-js)。C12 没用到 CryptoJS但很多同平台的题会用比如后面难度上去的 AES 题。4.2 用 Python 重写一份方便批量采集Node 脚本适合验证逻辑但真要采集几十页数据、存进数据库我习惯用 Python。C12 的异或解法翻译成 Python 非常干净import json def hex_to_bytes(hex_str: str) - bytes: return bytes.fromhex(hex_str) def decode_data(hex_str: str, key: bytes) - dict: cipher hex_to_bytes(hex_str) key_len len(key) plain bytes( cipher[i] ^ key[i % key_len] for i in range(len(cipher)) ) return json.loads(plain.decode(utf-8)) key b\x4b\x4c\x4d sample 306e232a212869763e3b25292e3e2f3e2a6f36 print(decode_data(sample, key))运行结果同样是{name: spiderbuf}。Python 的bytes.fromhex可以直接转换十六进制字符串bytes类型的异或操作通过生成器逐字节完成逻辑上和 JS 版一一对应。这里的plain.decode(utf-8)对应 JS 里的JSON.parse(bytesToString(bytes))但注意一定要在确认了原函数确实是在做 UTF-8 解码之后才能这么写。C12 是这样的换一道题就不一定了。4.3 解密结果和页面渲染不一致时先查编码再查算法我在调试过程中遇到过一个很迷的现象同样一段密文Node 解出来是完整 JSONPython 解出来后面多了两个字符的乱码。排查了半天最后发现是 Python 脚本读取 hex 字符串时不小心把换行符也带进了变量。这种低级错误很常见但特别浪费时间。如果你遇到本地解出来乱码的情况按这个顺序排查密文是否原样复制有没有多空格、换行或截断。十六进制转字节时用的是bytes.fromhex还是int(hex, 16)加循环前者对错误更敏感。解密后的字节解码用的什么编码是 UTF-8 还是 latin-1。密钥长度是否和密文长度匹配有没有读错 key 数组。绝大多数算法不对的感觉实际上都是上面四个环节里某个小问题导致的。我在 C12 上至少浪费了一个小时在怀疑算法结果只是试错批次中一次粘贴少了一位 hex。5. 翻页、动态参数与批量采集C12 后半程的实用处理单页数据解密成功之后紧接着就是翻页。C12 这类题通常会有多个分页每一页的密文都不相同但解密逻辑完全一致。比较麻烦的是这个平台部分题目的分页请求里带着动态参数如果你只是在本地写死一个密钥直接循环请求可能会在某一页被限流或者拿不到数据。5.1 先判断动态参数是不是必填项我当时先手动请求第二页的 URL把请求里的动态参数v分别替换成空值和固定值对比响应。C12 的v字段即使固定住服务端仍然正常返回密文说明它只是混淆项不参与校验。那我就不需要为它单独写生成逻辑直接把上一次响应的 cookie 带上、每页间隔两秒循环请求就足够了。但如果遇到动态参数必填的题方法也很成熟定位那个生成v的 JS 函数在 Node 里调用同一套逻辑。由于这个函数通常没有 DOM 依赖从浏览器复制到 Node 几乎不需要改动。生成一次打印出来和浏览器里的值对比一致后再放进请求参数。5.2 把解密函数封装成独立模块为了让后续采集代码保持整洁我会把解密逻辑单独放在一个文件里比如c12_decrypt.pyclass C12Decryptor: def __init__(self, key: bytes): self.key key def decrypt_hex(self, hex_str: str) - dict: cipher bytes.fromhex(hex_str.strip()) key_len len(self.key) plain bytes( cipher[i] ^ self.key[i % key_len] for i in range(len(cipher)) ) return json.loads(plain.decode(utf-8))然后在采集主脚本里只负责请求页面和调用这个类。这样做的好处是万一某一天平台改了算法你只需要改decrypt_hex一个方法其他代码不用动。对练习来说也许不重要但一旦这套流程延伸到一个需要长期维护的采集任务代码边界清晰会省很多事。5.3 请求频率和去重应该从一开始就考虑spiderbuf 是练习平台但也讲究别把练习题当成压测目标。我建议每抓一页至少间隔一到两秒控制在合理频率内。数据落库之前做一次去重用数据里的主键字段比如题目编号做UNIQUE约束或者先查库再插入。这样做既保护平台也能避免你自己在调试过程中重复抓取造成的数据混乱。C12 的数据拿到之后我习惯性地存了一份 JSON 和一份 SQLite。这两个格式对后续的整理、验证都很方便。如果你只是想验证解密逻辑直接打印到控制台就够了如果想留着做数据分析落一份结构化文件是值得的。6. 复盘 C12 的几个通用教训做完 C12 之后我把它和之前刷过的其他题放在一起做了个对比发现有些坑几乎是共通的。把这些经验写下来比单纯记住这道题的解法要有用得多。6.1 先定响应加密还是参数加密再决定看代码的方向很多人在蛛丝马迹还不充分的时候就打开代码乱搜结果在错误的方向上越走越深。拿 C12 来说如果我一开始就认定是参数加密去追v参数的生成逻辑可能两天都做不完。正确的顺序是先抓包、看响应形态、对比不同请求之间哪些字段在变然后再决定看哪个方向的代码。这个顺序适用于绝大多数前端加密题。6.2 压缩混淆的 JS 不可怕怕的是你不知道找什么格式化代码之后变量名还是t、n、e读起来依然痛苦。但我后来发现读压缩代码的技巧不是从头读懂每一行而是先找关键函数再局部理解。JSON.parse、decrypt、decode、charCodeAt这些关键词天然指向数据处理的核心位置。找到一个函数之后用 Console 直接调用验证比逐行读代码快得多。6.3 浏览器是最顺手的调试工具别急着写脚本我的习惯流程是在 Console 里调用找到的函数验证它可以解出明文。在函数内部打断点查看密钥和中间变量的实际值。单步执行两遍确认每一轮循环的变量变化符合预期。确认无误之后才把它搬到 Node 或 Python。这套流程每一步都在做验证每验证一步后面写脚本时的信心就多一分。直接跳过浏览器验证、上来就写脚本一旦结果不对你可能分不清是算法没搞对还是代码写错了。6.4 练习平台的边界和底线最后说一点题外话。spiderbuf 这类平台存在的意义就是让人在干净的环境里练手。C12 的教学价值在于训练抓包—定位—复现—验证这一整套逻辑这套逻辑放到工作中也是在处理自己公司或者明显授权的接口时才有意义。未经授权去抓别人的数据无论在哪个国家、什么场景下都需要非常谨慎。我在做这类练习时给自己定了几条底线只在公开的练习平台测试不针对任何未授权的真实站点做逆向尝试批量请求控制在低频范围拿到数据后只用于自身学习整理不对外传播。守住这些底线才能让逆向技术始终待在合法、合规的范围里。C12 这道题现在回头看难度真的不算高但它逼我养成了一个非常重要的习惯每一次猜测都要有验证动作每一个中间值都要亲眼看到再往下走。做完它之后我再去碰那些带 AES、RSA 参数的题明显感觉到心智负担小了很多因为我知道不管算法怎么变量级怎么涨拆解的框架是固定的。如果你也卡在 C12 的半路上建议先别去看别人的答案回到 Network 面板把数据流重新看一遍大概率能自己找到突破口。