简介:面向JS逆向学习者,针对盼之decode__1174版本提供补环境项目代码,系统覆盖无限debugger绕过、动态后缀添加分析、加密环境数组明文还原等逆向基础流程,适用于分析加密JS逻辑与调试反调试机制的场景。压缩包内共5个文件,含3个HTML页面、1个inscode配置及1个gitignore文件,整体仅17KB,轻量而完整,目录结构简洁。已有201人学习下载,适合掌握一定JavaScript基础、希望进阶补环境技术的开发者研读;配合浏览器开发者工具,可逐步跟踪代码执行过程。代码紧扣实际检测点,实现localStorage触发后缀、Math.random.constructor构造检测、debugger冲突Hook以及Object.defineProperty描述符重写等关键手段;逐段对照可理清程序运行时的环境校验逻辑,掌握从环境伪造、断点绕过到数据还原的完整思路。HTML页面可在浏览器中直接运行,便于结合具体项目逐行验证补环境效果,是实践JS逆向的轻量参考。 JS逆向里最磨人的往往不是算法本身,而是像 decode__1174 这种编号随机、逻辑绕来绕去的混淆函数。这周我和朋友处理一个游戏交易平台的展示数据采集,目标站就叫盼之吧。接口返回的订单名、角色名全是密文,页面却能正常显示,说明前端必然有一个 decode 函数在做解密。全局搜了一轮,锁定的就是 decode__1174。整个排查过程走下来,从定位、断点、Hook 到用 Python 复现,思路其实完全通用。本文就按这个过程拆开讲,适合正在啃混淆 JS、做参数逆向或者被动态解密接口折磨的朋友参考,核心不是让你背代码,而是教你抓住输入输出,用动态调试反推静态逻辑。
1. 项目背景与总体逆向思路
1.1 decode__1174 在链路中到底干了什么
先交代背景。盼之这个站是比较典型的 SPA 结构,列表数据通过 XHR 拉取,返回 JSON 里的关键字段不是明文,而是类似kY8...的长字符串。页面展示走的是自研组件,你在 Network 面板里看到的响应是密文,屏幕上显示的却是明文。这中间隔着的就是一层 JS 解密。全局搜“decode”能捞出一堆候选,最后定位到的是 decode__1174。1174 这种编号说明它来自构建工具级别的混淆,函数名被统一打平,不是业务手写的命名。也就是说,这类函数大概率不是独立设计的高强度算法,而是一个把 A 转成 B 的工具函数,我们只要能拿到输入和输出,就能把它从黑盒变成白盒。
为什么说先弄清楚角色很重要?因为方向一错,时间全浪费。比如你把焦点放在分析某个自研加密算法上,结果发现它只是对服务端返回做了一层 base64 转码;反过来,你以为只是一个简单解码,里面却嵌套了 AES 和动态 key。所以第一步不是写代码,而是先确认这个函数被谁调用、传进去什么、吐出来什么。
1.2 动态调试优先,静态分析为辅
遇到 decode__1174 这种函数名,已经能说明它是被自动化混淆过的。你去静态读那一大坨_0x3f2a、_0x5b9c,人脑容易被绕晕。我的习惯是优先动态调试,让浏览器自己把答案算出来。具体做法就是下断点、打印调用栈、Hook 返回值。这就好比你想知道自动售货机怎么把硬币换成饮料,与其拆开机箱研究电路,不如投一次币,在取货口接住结果,再在投币口装一个记录仪。
动态调试的最大优势是不用理解每一行代码。只要能在函数入口和出口拿到 input/output,就能确认算法效果。然后你再用静态阅读去还原其中关键几步,效率会高很多。这套流程适用于 decode__1174,也适用于大多数 sign、token、param 类的混淆函数。
1.3 手上要有的工具清单
环境上不需要特别重型的东西,我实际用到的就这些:
- Chrome DevTools:核心工具。Sources 里可以格式化压缩 JS,Network 里能看请求发起者 Initiator,Console 里能随时执行表达式,Overrides 功能可以直接覆写线上 JS 文件。
- Charles 或 Fiddler:用来做请求断点、重写返回,必要时直接替换某个 JS 文件为本地版本。没有的话,用 DevTools 本地替换也能实现类似效果。
- VS Code + Prettier / js-beautify:把大段混淆代码格式化,便于搜索和阅读。注意格式化后的文件要另存,不要直接在线上改。
- Node.js + npm:后面用 Python 复刻算法之前,至少要能本地运行一段 JS 验证。推荐装 pyexecjs 或 jsdom,用来在 Python 里临时调用 JS。
- Python + pycryptodome / requests:批量解密验证用。
先别急着写代码,把这几样准备好。
2. 定位目标函数:从搜索到断点
2.1 在几百个 JS 文件里捞关键字
正式开始。打开 DevTools 的 Sources,找到项目主域名的文件目录,按 Ctrl+Shift+F 全局搜索。搜索词不要一上来就搜 decode__1174,因为函数名可能动态拼接,或者实际压缩后出现在别的分包里。先把范围扩大:搜decode、atob、btoa、fromCharCode、charCodeAt、CryptoJS、AES、RSA、base64。这些是解密类函数的常见特征,命中概率高。
我这里运气不算差,搜decode__直接跳到了定义位置:一个约 300KB 的主业务 JS 文件里,第几万行的位置。压缩后的代码长这样:
function decode__1174(e){var t={}...格式化之后,函数体仍然有一堆_0x...,说明字符串被抽到了一个数组里。但不用怕,先在函数开头、中间、结尾分别打上断点,然后去页面上操作一次触发接口。断点一停,右键当前函数,看 Scope、Call Stack,然后 Console 里打印arguments[0]和decode__1174(arguments[0]),输入输出立刻明确。
如果全局搜索 decode__1174 搜不到,通常意味着函数名是在运行时动态生成的。别慌,改用搜索Function(、eval(、new Function,或者直接看Function.prototype.constructor相关调用。动态生成函数一般会先拼接字符串再执行,在网络请求返回的 JS 里搜1174也常常能捞到蛛丝马迹。
2.2 顺着调用栈锁定真正的入口
光找到函数定义还不够,要搞清楚它是被谁调的、什么时候被调用的。这里有个很实用的功能:在 Network 面板里找到返回密文的 XHR 请求,往下拉看 Initiator 那一列的调用栈。点击栈帧会自动跳转到发起请求或处理响应的源码位置。在响应处理的回调那句下断点,比如JSON.parse(xhr.responseText),然后刷新页面。断点触发后,Console 里输入console.trace()或者直接看 Call Stack 面板,一路点进调用链,直到某一帧出现 decode__1174。
我这次就是在某一帧里看到:一个命名类似renderList的函数拿到服务端返回后,对每个 item 的 name 字段调用了decode__1174(item.name)。此时疑问消失了:它不是生成请求参数的加密函数,而是响应数据的解包函数。它把服务端给的 code 字符串解成可读文本。这一步决定了后面复刻算法的方向。
注意,如果调用链非常深、回调很多,你可以在 decode__1174 的入口断点处直接看 caller。旧版浏览器可以用arguments.callee.caller,新版直接右键 Call Stack 面板里的上层函数跳转。不要试图手动往上翻几万行代码找调用方,那是低效的。
2.3 静态阅读:看懂关键步骤而非全部
定位之后就是静态阅读,目标不是把整段混淆代码逐行翻译,而是挑出影响结果的几个关键动作。我习惯先把函数体复制到本地,用 VS Code 格式化,再逐块标记。
第一块,预处理。看入参 e 是否被取子串、替换某些字符。例如e.substr(4)或者e.replace(/^..../,''),说明密文有一个固定前缀需要跳过去。
第二块,核心算法。看有没有调用atob、CryptoJS.AES.decrypt、new Blob、TextDecoder、decodeURIComponent这类高特征 API。有的话基本就能确定算法族。
第三块,后处理。看解密结果是否有reverse()、replace、JSON.parse、split之类的操作。这一步很容易被忽略,但会导致你用 Python 解出来一坨正确 base64 却得不到正常字符串。
我这里看到的一个细节是:函数最开始用字符串的charCodeAt(0)减去某个偏移量得到前缀长度,再截掉前缀;中间调用了一个从字符串数组里取出来的函数,核心是 atob 加移位;最后把结果逆序。整个下来,本质就是把服务端返回的字符串做了一层自定义编码,再包了一层 base64,而不是真正的加密。到这里,算法轮廓已经清楚。
3. 还原解密逻辑与代码复现
3.1 逐行还原出可读的 JS 逻辑
按照前面的静态分析,我把 decode__1174 还原成了一个可读版本。这里要注意,真实版本里第 3 步不是直接用浏览器原生 atob,而是从一个字符串数组_0x2f1a['...']里取出来的等价函数,底层还是一样。这里直接写成 atob 是为了方便理解:
// 原混淆函数: decode__1174 function decode__1174(input) { // 1. 计算前缀长度:首字符的 charCode - 48 var prefixLen = input.charCodeAt(0) - 48; if (prefixLen < 0 || prefixLen > 8) return ''; // 2. 去掉前缀 var body = input.slice(prefixLen); // 3. base64 解码 var decoded = atob(body); // 4. 逐字符逆序 var result = decoded.split('').reverse().join(''); return result; }如果你遇到的是 AES 这类对称加密,会多出 key 和 iv 两个问题。key 和 iv 往往不是写死的字符串,而是由其它函数动态算出来的。我的建议是:别在 Python 里硬编码 key,先用 Hook 把 key 的真实值打出来,或者干脆在 Python 里用 subprocess 调用 Node 执行一段 JS,把 key 计算出来再喂给解密函数。这样目标站一旦更新 key 生成逻辑,你只需要更新 JS,不用动解密主流程。
3.2 用 Hook 验证你的理解
算法还原得对不对,最好的验证方式是 Hook。如果 decode__1174 是挂在全局对象上的,直接在 Console 执行:
const originDecode = window.decode__1174; window.decode__1174 = function (...args) { const result = originDecode.apply(this, args); console.log('[hook] in:', args[0]); console.log('[hook] out:', result); return result; };这样页面再调用的时候,Console 会打印真实入参和出参。把输出和你自己的还原脚本结果对比,一致就说明算法吃透了。
针对不在全局对象上的函数,有一个更通用的办法:用 DevTools 的 Local Overrides,在源码里临时加一行window.__decode1174 = decode__1174;,刷新后就可以在 Console 操作它。如果你不想改文件,也可以右键行号设置 Logpoint,只打日志不改逻辑。把 Logpoint 放在函数 return 那一行,能直接看到返回值;放在第一行能看到入参。这个技巧在压缩代码里尤其好用。
3.3 用 Python 实现批量解密
算法确认后,接下来就是把这套逻辑搬进自己的采集脚本。我这里用 Python 实现一个简化版本:
import base64 import requests from Crypto.Cipher import AES from Crypto.Util.Padding import unpad # 示例1: 如果是“前缀长度+base64+逆序” def decode_1174_simple(cipher_text: str) -> str: prefix_len = ord(cipher_text[0]) - 48 body = cipher_text[prefix_len:] decoded = base64.b64decode(body).decode('utf-8') return decoded[::-1] # 示例2: 如果是“4字节随机前缀 + AES-CBC” def decode_1174_aes(cipher_text: str, key: bytes, iv: bytes) -> str: payload = cipher_text[4:] enc = base64.b64decode(payload) cipher = AES.new(key, AES.MODE_CBC, iv) plain = unpad(cipher.decrypt(enc), AES.block_size) return plain.decode('utf-8') # 批量验证 resp = requests.get("https://target.example.com/api/list", headers={...}).json() for item in resp["data"]: name = decode_1174_simple(item["name"]) print(name)实战中我强烈建议先跑一个包含 10 条左右记录的小样本,把脚本输出和页面上显示的内容逐条比对。确认完全一致后,再放开全量跑。因为一旦有 1% 的边界情况没覆盖,比如空字符串、null、异常前缀,全量跑会哗啦啦报错。批量处理的代码要增加异常捕获,遇到非法密文直接记录日志而不是中断。
另外,如果出于某种原因你不想在 Python 里重写算法,还可以用 pyexecjs 或者 Node 子进程直接执行原 JS。这是最保真的方式,代价是会慢一点。常见的做法是:把整个加解密模块剪出来,在 Node 环境里挂一个最小化的 window/document mock,然后暴露成 module.exports。注意依赖的浏览器 API 越少,越容易跑起来。
4. 实战中的常见问题与排查技巧
4.1 断点失效、无限 debugger 和反调试
做这种带混淆的站点,必然会碰到反调试。最常见的两个现象:
第一,页面无限进 debugger。点继续之后立刻又停,这种多半是代码里有一段setInterval(function(){debugger;}, 100)或者Function('debugger')()。解决办法是 Local Overrides 把包含 debugger 的 JS 文件替换成本地副本,搜索debugger关键词批量删掉。
第二,调试一开就检测。比较粗暴的站点会用 console 内容差异检测、窗口大小检测、时间差检测。遇到这种,优先考虑用 Puppeteer/Playwright 这类无头浏览器跑,它们天然就是一个真实环境,反调试代码一般不会触发。要是反调试太强,再考虑在 JS 里 Hookconsole、Date、performance这些会被探测的 API。
不要一碰到反调试就退缩。大多数网站的所谓“防护”只是用现成混淆工具开了一层壳,核心还是那几类算法,把壳去掉之后,前面分析的解密流程完全不变。
4.2 混淆变量、闭包和调用时机
另一个常见坑是找不到函数引用。函数被定义在一个立即执行函数(IIFE)里,外部根本访问不到。你按照我上面的做法去window.decode__1174取,返回 undefined,这很正常。解决办法有两个:
- 改源码导出。在函数定义后面加
window.__decode1174 = decode__1174;,然后用 Overrides 刷新页面试试。 - 在函数内部打断点,等断点停住后,在 Console 的 Scope 区域找到
this、decode__1174,手动执行。这也能验证函数。
再就是调用时机。有些 key、iv、盐值是页面加载后通过某个接口动态获取的,如果你在页面还没加载完就手动调用 decode__1174,会得到错误结果。遇到这种情况,先重复触发一次正常流程,确保依赖的数据已经生成,再调用解密函数。
4.3 常见问题速查表
把这次和以往项目中常遇到的问题整理成表,方便你复盘时对照:
| 现象 | 常见原因 | 排查手段 |
|---|---|---|
| 全局搜不到 decode__1174 | 函数名被动态拼接或写在 eval/Function 中 | 搜Function(、eval(、1174,或用断点停到调用后看作用域 |
| 函数在闭包内无法访问 | 定义在 IIFE 里 | 改源码导出到 window,或函数内断点手动调用 |
| 断点一直被跳过 | 检测调试、使用了 release 模式压缩 | 本地替换 JS 去掉 debugger/检测 |
| 解密结果乱码 | key/iv/编码/模式不一致 | 和页面输出比对,试 ECB/CBC、Pkcs7/ZeroPadding |
| 前缀长度算法看错 | charCodeAt 取的是首字符而不是 ASCII 数字 | Console 打印input.charCodeAt(0)验证 |
| Python 调用 JS 报 window 未定义 | 原 JS 依赖浏览器全局对象 | 用 jsdom/VM 模拟 window/document,或只剪出纯函数 |
| 解密成功但列表个别字段为空 | 服务端返回了空串或非标准编码 | 增加异常捕获,记录并跳过 |
表格里第 4、5 条是真实踩过的。尤其是前缀长度那里,我最初以为首字符就是前缀长度,结果发现它把字符的 ASCII 码减了 48 得到数字,如果用int(input[0])去解析,一个字母开头就直接报错。这种细节非常容易让批量脚本崩掉。
4.4 别忘了留底和对比
最后分享一个习惯。定位到 decode__1174 后,我会第一时间在 Console 里执行copy(decode__1174.toString()),把函数源码存成文件,再记下调用它的入口 URL 和参数示例。这样过几天网站升级,函数逻辑变了,我拿新旧代码一 diff,几秒钟就知道它改了哪一行。这个习惯帮我省掉了大量重复逆向时间,也方便把算法变化同步到自己的脚本里。
整套流程走下来,你会发现 decode__1174 这类函数并没有想象中神秘。它大概率就是混淆版的 base64、AES、自研位移,难点不在算法,而在定位。定位的核心思路永远是:先找到输入输出,再用动态调试反推静态逻辑,最后用脚本验证。只要你抓到一条真实的入参和出参,这个黑盒就已经开了 80%。另一个想提醒的是,所有分析和测试我都在自己有权访问的测试环境里做,大家动手练习时也尽量用自己搭的靶场或者官方允许的站点,别拿线上核心接口硬来。
本文还有配套的精品资源,点击获取