文章介绍了使用大型语言模型(LLMs)如ChatGPT和llama2来逆向解析缩减后的JavaScript变量名称的方法。文章讨论了代码缩写的概念,包括无损缩写和非关键数据的丢失,并重点介绍了变量名的丢失及其逆向恢复的问题。文章还详细说明了如何使用LLMs进行变量名的恢复,并提供了完整的JavaScript还原流程。同时,文章提到了控制LLM输出的方法和不应直接使用AI修改代码的原因。最后,给出了一个具体的示例和相关的前端早读课信息。
代码缩写主要用于减少JavaScript文件体积,加快网络传输速度。其中,变量名的缩写是信息丢失最严重的地方。
使用Guidance(引导)和Outlines(大纲)等工具以及正则表达式来确保LLM的输出符合预期格式。
前言
利用大型语言模型(LLMs)如 ChatGPT 和 llama2 来逆向解析缩减后的 JavaScript 变量名称,同时保持代码的语义完整性。今日前端早读课文章由 @Jesse Luoto 分享,@飘飘翻译。
译文从这开始~~
介绍了一种使用像 ChatGPT 和 llama2 这样的大型语言模型(LLM)来逆向压缩后的 JavaScript 的新方法,同时保持代码的语义完整。该方法的代码是开源的,可以在 GitHub 项目 Humanify 中找到。
Humanify:
https://github.com/jehna/humanify
什么是代码缩写(Minification)?
代码缩写是一种优化技术,旨在减少 JavaScript 文件的体积,以加快网络传输速度。从逆向工程的角度来看,代码缩写有不同的级别,每个级别都会带来更大的解析难度:
【第2998期】逆向分析了Github Copilot
无损缩写(Lossless Minification)
大多数代码缩写都是无损的,比如
true
被转换成
!0
时,并不会丢失任何信息。对于这种情况,可以使用 Babel 转换来轻松还原。此外,市面上也有很多专门用于恢复这类无损转换的工具。
非关键数据的丢失(Unimportant Data Loss)
在压缩过程中会丢失一些数据,但这些数据可能很容易重新生成。一个很好的例子就是空白字符;使用 Prettier(或类似工具)将压缩代码的缩进和空白重新格式化为人类可读的格式是轻而易举的。大多数时候,开发人员在原始代码中也使用了类似的工具,所以空白字符数据可以高度自信地重新生成。
变量名的丢失(Variable Names)
最重要的信息丢失发生在变量名和函数名上。代码缩写工具会将所有可能的变量和函数名称替换为短小的代号,以节省空间。到目前为止,还没有什么好办法来逆转这一过程;当你把变量从 crossProduct 重命名为 a 时,要逆转这一过程就没什么办法了。
如何逆向恢复代码的?
许多逆向工程师会训练自己从代码上下文中识别某些特征,并根据这些特征合理推测代码的功能。让我们来看一个简单的示例:
function a(b) {
return b * b;
}
如何为函数 a 重新命名?在特定的代码上下文中,我们可以大致推测出 a 这个函数的原始名称。例如,如果它的功能是计算一个数的平方,那么它的原名很可能是 square。但要做出这样的判断,我们需要理解函数的内部逻辑。
让我们试着将重命名函数的过程规范化:
对于传统的计算机程序来说,从 “将 b 乘以自身” 推理出 “求一个数的平方” 是一项极其困难的任务。但幸运的是,LLM(大型语言模型)的发展已经让这一步骤变得简单甚至是理所当然的。
实际上,步骤 2 可以被看作是 “改写” 或 “翻译”(如果把 JavaScript 视为一种类自然语言的话),而 LLM 在这方面表现非常出色。
另一方面,LLM 也擅长摘要提取,这正是 步骤 3 所需的能力。唯一的特殊要求是:输出的变量名需要足够简洁,并符合驼峰命名法(camelCase)。
如何控制 LLM 的输出?
使用 LLM 生成变量名的问题在于:它的输出并不是确定性的。从本质上讲,LLM 可以被看作一个非常复杂的马尔可夫链 —— 它会基于之前的单词,尽最大可能预测下一个最合适的单词。
这意味着,即使我们设计了一个优秀的提示词(prompt),最终的输出仍然可能有所不同。例如:
Are all roses red? Please answer only "Yes" or "No".
LLM 有时可能会生成类似 “不,但……”、“我不知道”,或者 “很抱歉,作为一个 AI 语言模型,我无法……” 之类的回答。
这曾经是个问题,但幸运的是,现在已经有一些方法可以用来控制 LLM 的输出,比如 Guidance(引导)和 Outlines(大纲)。这些工具采用不同的技术,确保 LLM 生成符合预期格式的内容。
另外,
JavaScript
变量名的格式是固定的,我们可以使用 正则表达式(Regular Expression) 来验证 LLM 的输出,确保它是一个有效的 JavaScript 变量名,从而避免不必要的错误。
不要让 AI 直接修改代码
虽然 LLM 在改写和总结方面非常擅长,但它在编写和修改代码方面(至少目前)仍然不够可靠。由于 LLM 具有随机性,它并不适合直接执行变量重命名或修改代码的任务。
幸运的是,在 JavaScript 作用域内重命名变量已经是一个成熟的问题,可以使用 Babel 这样的传统工具来解决。Babel 首先将代码解析为抽象语法树(AST,代码的机器表示形式),使用表现良好的算法对其进行修改非常容易。
相比让 LLM 直接修改代码文本,这种方法更加可控,能够确保只进行特定的重命名操作,不会影响代码的逻辑和功能。最终,代码的行为保持不变,仍然可以被计算机正确执行。
完整的 JavaScript 还原流程
那么,我们该如何逆向还原(un-minify) JavaScript 代码呢?以下是完整的流程:
-
解包 Webpack 代码:使用 webcrack 还原 Webpack 打包后的代码。
-
代码美化(Beautify):运行 transform-beautifier 以及一些自定义的 Babel 插件,逆向还原无损缩写(lossless minification)。
-
变量语义分析:遍历代码中的所有变量,使用 LLM 分析它们的作用,并根据描述生成更有意义的变量名。
-
变量重命名:使用 Babel 在 AST 级别进行变量重命名,确保代码结构不变。
-
代码格式化:最后,再运行 Prettier,确保代码缩进和空格格式整洁美观。
就这样! 假设我们有以下代码:
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 chunkedString(inputStringToBeSliced, chunk){
var chunkBuffer =[];
var sliceSize = inputStringToBeSliced.length;
var currentCharIndex =0;
for(; currentCharIndex < sliceSize; currentCharIndex += chunk){
if(currentCharIndex + chunk < sliceSize){
chunkBuffer.push(
inputStringToBeSliced.substring(
currentCharIndex,
currentCharIndex + chunk
)
);
}else{
chunkBuffer.