HumanifyJS 反混淆实战指南:用 LLM 把压缩 JS 还原成人话

HumanifyJS 反混淆实战指南:用 LLM 把压缩 JS 还原成人话

【免费下载链接】humanifyDeobfuscate Javascript code using ChatGPT项目地址: https://gitcode.com/gh_mirrors/hu/humanify

凌晨两点,你接手了一个老项目。构建产物里躺着一个 200KB 的bundle.min.js,报错堆栈指向第 1 行第 3921 列,打开一看,满屏都是function a(e,t){var n=[]...}。这种场景下,HumanifyJS 这类 AI 辅助的 JavaScript 反混淆工具就是为救你而生的:它调用大语言模型,把压缩代码里那些aet全部重命名为inputStringchunkSize这样的可读名字,让你能在十分钟内读懂原本要花一整天破解的代码。本文不写套话,直接带你从一个真实事故现场出发,把工具的用法、原理和坑位一次讲透。

一段压缩代码引发的"事故现场" 🕳️

先还原一下你的处境。报错堆栈长这样:

at a (bundle.min.js:1:3921) at n.push (bundle.min.js:1:4047)

你找到了对应位置,看到的却是:

function a(e,t){var n=[];var r=e.length;var i=0;for(;i<r;i+=t){if(i+t<r){n.push(e.substring(i,i+t))}else{n.push(e.substring(i,r))}}return n}

四个字母aetn,撑起了整个函数。你想加个断点,却不知道该观察哪个变量;你想搜e.length理解业务逻辑,搜出来的全是不相干的匹配。这不是代码写得烂——是压缩工具干的,它天生就该把名字缩短。

你开始手动重命名:e是入参,t也是入参,n是个数组,r是个长度……五分钟过去,你还原了这个函数,但文件里还有另外几百个这样的标识符。手动逆向显然走不通。

这时候你需要的不是"再努力一点",而是一个能把"起名字"这个体力活外包出去的帮手。

在动手之前,先看清它到底能做什么 🔍

HumanifyJS 的官方定位一句话就能说清:用 LLM 为压缩/混淆后的 JavaScript 重新命名标识符。它的适用人群很明确:

  • 接手含压缩产物、需要读代码排障的前端开发者
  • 分析第三方库、SDK 内部实现的逆向爱好者
  • 需要审计线上dist目录里到底跑了什么逻辑的安全工程师

它有一个很容易被忽略的边界:它只做一件事——改名字。它不会帮你格式化代码、不会拆解 webpack 模块、不会把 UglifyJS 压缩的合并语句还原成原本的写法。格式化你可以交给 Prettier,解包 webpack 产物可以交给 webcrack,而"给几千个标识符起一个贴切的名字"这种需要理解语义的活,恰好是 LLM 的强项,也是 HumanifyJS 专注的领域。

搞清楚边界之后,你会发现"只做一件事"反而是一种美德:流程清晰、输出可预测、出问题也好排查。

3 分钟跑通你的第一条命令 ⚡

v3 版本的 HumanifyJS 是一个 Rust 编写的单文件静态二进制——不需要 Node、不需要 npm、不需要 Python 环境,下载下来就能跑。如果你想从源码构建,一条命令即可:

git clone https://gitcode.com/gh_mirrors/hu/humanify cd humanify cargo build --release

编译完成后,把target/release/humanify放到 PATH 里,然后验证:

humanify --help

运行逻辑非常 Unix 风格:输入可以是一个文件路径,也可以是-(表示从标准输入读取);输出默认打到标准输出,也可以用-o指定文件。六个子命令对应六种 LLM 来源:

humanify <openai|gemini|anthropic|ollama|openrouter|requesty> [FLAGS] <INPUT>

拿最常用的 OpenAI 举例,把环境变量配好之后,你实际敲的命令只有一行:

export OPENAI_API_KEY=你的密钥 humanify openai bundle.min.js -o bundle.readable.js

十几秒后,bundle.readable.js里那些aen就都变成了有含义的名字。整套流程从装好工具到拿到结果,三分钟绰绰有余。

打开引擎盖看流程:它怎么处理一个标识符 🏭

加一个--verbose参数重跑一遍,你就能亲眼看到工具的内部工作日志:

humanify openai bundle.min.js -o bundle.readable.js --verbose --progress

日志会告诉你:一共找到了多少个标识符,当前在处理第几个,每个标识符原来的名字和建议的新名字分别是什么。这一层"进度可见性"特别适合排查问题——比如某个名字一直没被改,你就能定位到是哪一步出了问题。

而它的处理顺序也藏着设计心思:从最大的作用域开始,逐层向内。先把整个函数/模块级别的标识符定下来,再处理里面的局部变量。这就像先给一栋楼贴好楼层号,再给每扇门装门牌,避免内层名字先定、外层一改名又把语境搅乱。

每个标识符的处理路径大致是:

  1. 从代码中截取该标识符所在作用域的上下文(默认截取 500 个字符)
  2. 把"这段代码 + 这个标识符"发给 LLM,要求返回一个 JSON 格式的建议名字
  3. 收到建议后做合法性检查——LLM 偶尔会返回foo barthis.x甚至static这种不能直接用的名字,工具会把它规范化成合法的标识符
  4. 检查作用域冲突,必要时加数字后缀
  5. 在符号表里完成改名,输出时自动保证所有引用同步更新

如果 LLM 调用失败或者返回了没法用的名字,工具会保留原名字继续跑,而不是中断整个任务。这个"宁可不动,不可改错"的策略,保证了任何情况下输出都是可运行的有效 JavaScript。

为什么"AI 负责起名,AST 负责动手"最稳妥 🛡️

很多人第一次听到"用 AI 重写代码"会本能地担心:它会不会顺手改了我的逻辑?

HumanifyJS 的分工设计恰好把这种风险压到了最低。整条链路的核心代码在src/rename/目录下:解析用的是 oxc(高性能 JS 解析器),重命名发生在抽象语法树(AST)层,LLM 全程只做一件事——给出一个名字建议。它没有权限改动代码结构,代码生成器会忠实地把 AST 重新打印出来。

这个分工很像"外科医生与助手"的关系:助手(LLM)只负责递上工具、给出建议,下刀的是外科医生(AST 重命名器),每一步都精准、可控、可回退。

具体到"安全重命名",有三个细节值得你注意,它们共同构成了防呆网:

  • 上下文窗口:工具不会把整个文件都发给 LLM,而是以每个标识符为圆心,截取其所在作用域的一段代码(默认 500 字符,可用--context-size调整)。这让 LLM 有足够的"侦查范围"理解语义,又不至于因为文件太大而烧掉太多 token。
  • 名称规范化:LLM 可能建议出user account namestatic这类非法标识符。src/rename/safe_name.rs会把它们规范化为userAccountName_static,保证生成的名字永远是合法的 JavaScript 标识符。
  • 作用域感知的冲突处理:两个兄弟函数里的同名局部变量互不干扰,可以都叫index;但同一作用域里撞名时,会通过数字后缀区分——如果 LLM 建议了item2而该名字已被占用,下一个会变成item3,而不是尴尬的item22

另外两类名字它坚决不碰:对象属性名和类的方法名/私有字段(如#x)。原因很简单——这些名字可能被外部代码以字符串形式访问,改了就会破坏契约。

一个文件的前后对比:从 a(e,t) 到 splitString 📊

把文章开头那个让你崩溃的函数交给它,处理前的样子:

function a(e,t){var n=[];var r=e.length;var i=0;for(;i<r;i+=t){if(i+t<r){n.push(e.substring(i,i+t))}else{n.push(e.substring(i,r))}}return n}

处理之后:

function splitString(inputString, chunkSize) { var chunks = []; var stringLength = inputString.length; var startIndex = 0; for (; startIndex < stringLength; startIndex += chunkSize) { if (startIndex + chunkSize < stringLength) { chunks.push(inputString.substring(startIndex, startIndex + chunkSize)); } else { chunks.push(inputString.substring(startIndex, stringLength)); } } return chunks; }

a变成了splitStringe变成了inputStringt变成了chunkSizen变成了chunks。你没有猜错,这就是一个把字符串按指定长度切片的工具函数——但你不需要靠猜了,名字已经把意图写在了脸上。

注意看:splitString这个函数名本身没有被改动。如果 LLM 判断一个名字已经足够有表达力,它会原样返回,不做无意义的折腾。这也从侧面印证了它改名的依据是语义,而不是"见一个改一个"。

再补一个进阶场景。如果你的目标是 webpack 打包产物,直接处理效果会差很多——因为里面还裹着一层模块加载器。正确的姿势是先拆包、再改名,Unix 管道在这里发挥得淋漓尽致:

npx webcrack < bundle.min.js | humanify openai - -o bundle.js

npx webcrack把模块结构还原成普通脚本,通过标准输出直接喂给humanify,humanify 读-(标准输入),把结果写到bundle.js。两个工具各司其职,拼成一条完整的"webpack 产物可读化"流水线。

关于费用、速度和精力的三笔账 💰

用 LLM 反混淆不是做慈善,你需要对成本有心理预期。最重要的一条:每遇到一个标识符,工具就会调用一次 LLM。一个中等规模的压缩文件大约有 500 个标识符,也就是说一次完整处理会发起约 500 次请求。

  • 费用:500 个标识符的文件,用 OpenAI 的小模型大约花 $0.10~$1.00;用 Gemini 的免费额度通常够用;用本地 Ollama 或 OpenRouter 的免费模型则基本零成本。
  • 速度:取决于标识符数量和模型响应速度。本地模型最慢,某些 CPU 环境下单次请求可能要等很久,所以工具给本地模式设置了长达 1800 秒的请求超时,而云端 API 的超时只有 60 秒。
  • 精力:处理完别急着删原始文件。把原文件、新文件都纳入版本管理,改完跑一遍测试套件,确认行为一致再谈其他。

想粗略估算一下 token 消耗?工具给的公式很朴素:大概等于文件字符数的两倍。用wc -c量一下你的文件就知道大概量级了:

echo "$((2 * $(wc -c < yourscript.min.js)))"

对预算敏感的开发者,推荐从 Gemini 免费层或本地 Ollama 起步,跑通流程后再决定要不要上更强的付费模型。

三个高频坑位与排雷手册 🧯

实战中最容易踩的坑,我帮你提前排掉三个:

坑位一:模型没配好,跑起来全是"原样返回"症状:日志里每个标识符的->前后名字一模一样。 排查:先--verbose看配置是否解析成功(模型名、API Key、base URL),再确认网络能连通。LLM 调用失败时工具会静默保留原名,所以"名字没变"往往是上游出问题的信号,而不是模型觉得名字已经够好。

坑位二:webpack 产物直接处理,效果一塌糊涂症状:改完的代码里还残留大量__webpack_require____d(function(...){...})之类的结构,局部变量改名了但整体依然读不懂。 排查:记住 HumanifyJS 只重命名标识符。先过一遍 webcrack 把模块结构解开,再交给 humanify 改名,两者通过管道串联。

坑位三:处理超大文件时把预算烧穿症状:跑完一看账单,费用远超预期。 排查:先用wc -c估算 token 量,再用--context-size把上下文窗口调小(比如 256),牺牲一点命名准确度换成本可控。也可以先用 webcrack 拆包,让每个文件变小后再分别处理。

再提醒一个容易误判的点:你以为"反混淆会改逻辑"?不会。前面说过,所有改动都发生在 AST 层的标识符重命名,程序的行为保持不变。这正是它可以放心接入 CI/CD 流程的原因——跑完对比测试不红,就能合入。

下一步,把它变成你的日常工具 🔧

你不需要等到半夜加班才想起它。几个可以立刻上手的用法:

  • 拿到陌生库的压缩版源码,先跑一遍再阅读
  • 排查生产环境的报错堆栈,把bundle.min.js还原成可读版本对照行号
  • 配合 webcrack,把历史遗留的 webpack 老产物整理成可维护的源码雏形

最后把命令备忘收藏一下,这一套流程你大概率会反复用到:

# 免费方案:Gemini 免费层 export GEMINI_API_KEY=你的密钥 humanify gemini app.min.js -o app.js # 本地私有方案:Ollama humanify ollama app.min.js -o app.js # 预算充足追求最佳效果:OpenAI humanify openai app.min.js -o app.js

HumanifyJS 是 MIT 许可的开源项目,代码完全开放可审查。想深入了解或参与改进,可以 clone 它的仓库(https://gitcode.com/gh_mirrors/hu/humanify)看看src/rename/下的实现,那里藏着它所有"防呆设计"的细节。

现在,去把那个让你头疼的bundle.min.js拖进来跑一遍吧。十分钟之后你会回来感谢自己——那些aet背后藏着的逻辑,终于不用靠猜了。🚀

【免费下载链接】humanifyDeobfuscate Javascript code using ChatGPT项目地址: https://gitcode.com/gh_mirrors/hu/humanify

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考