JS逆向入门第一课:从看懂F12的“乱码”到实战定位加密逻辑(保姆级教程)

你有没有过这种经历:打开一个网站,按下 F12 点进 Sources 面板,找到那个看起来最关键的 JS 文件,然后你就傻眼了。满屏的 abc_0x1234,函数套函数,变量名像乱码。你心想:“写这个网站的程序员是不是不会起名?”

其实不是他不会起名,是你看到的东西,根本就不是他写的。

这篇文章先解决最劝退的几个问题:为什么我们逆向时看到的 JS 代码,和“正常代码”长得完全不一样? 以及:Chrome DevTools 里的 Call Stack、函数跳转、蓝色下划线这些到底怎么用? 中间会用北京大学登录页当案例走一遍完整流程,最后讲 XHR/Fetch 断点——也就是关键词搜索失效时的兜底方案,带你入门 JS 逆向。

假设你已经会按 F12 打开开发者工具。如果还不会,那你现在会了。


一、JS 要不要学?

你不需要会写 JS,但得能读 JS。逆向本质上是阅读理解,你看的是别人写好的代码,不需要自己从零写一个项目。就像你看得懂菜单但不会炒菜,不妨碍你点菜。

1. 变量
var a = 1let b = "hello"const c = 123var / let / const 的区别在逆向里不重要,你只要知道它是在给一个名字赋值。

2. 函数
function f(x) { return x + 1 }。定义一个函数叫 f,接收参数 x,返回 x + 1。如果在别处看到 f(5),就是在调用它,结果是 6。

3. 对象
{ name: "张三", age: 18 }。花括号包起来的一堆 key: value。你看到的 d.e("phone").value 就是在访问对象 d 的属性 e,然后调用它,再取 .value

4. 箭头函数
(x) => x + 1,等价于 function(x) { return x + 1 }。压缩后的代码里大量出现,见多了就习惯了。

5. 回调
$.ajax({ success: function(res) { console.log(res) } }),简单说就是把一个函数当参数传给另一个函数。登录请求、加密操作几乎都是回调的形式。

6. 内置函数
浏览器自带、不用引入就能用。setTimeout(fn, 1000) 延时执行,setInterval(fn, 1000) 重复执行,无限 debugger 就拿它搞你。JSON.stringify(obj) 对象转字符串,JSON.parse(str) 反向操作,加密前必调。document.getElementById("id").value 取输入框值,$("#id").val() 是 jQuery 的等价写法,北大案例里会出现。

遇到不认识的语法直接搜,或者丢给 AI 问“这段 JS 是什么意思”就行。别被吓跑,你只需要能读,不需要会写。

如果你连 var a = 1 都看不懂,那你现在会了。


二、第一个真相:你看到的从来不是「源码」

一个正常的前端项目,开发时是这样写的:

// 开发者写的代码(源码)
function handleSubmitButtonClick(event) {
    const userPhoneNumber = document.getElementById("phone").value;
    const encryptedPassword = md5(document.getElementById("password").value);
    sendLoginRequest(userPhoneNumber, encryptedPassword);
}

变量名叫 userPhoneNumber,函数名叫 handleSubmitButtonClick,见名知意,清清爽爽。

但你 F12 看到的是这样的:

function a(b){var c=d.e("phone").value,f=g(d.e("password").value);h(c,f)}

abcdefgh,每个名字不超过两个字母。

这不是加密,不是混淆,甚至不算是刻意防你。 这是前端工程的标配操作,叫压缩(Minification)

为什么网站要这样做?

你的浏览器在打开网站时,要先下载所有 JS 文件。一个中大型项目可能有几千行代码,变量名 handleSubmitButtonClick 有 26 个字符,改成 a 只有 1 个字符。几千个变量、函数名全部缩成单字母,文件体积能减少 40%–60%。

对于日活百万的网站,这就意味着页面秒开、带宽和 CDN 流量费省了一大截。所以这不是网站“在防你”,而是网站在给所有用户省流量,顺带让你逆向时看起来像天书 😂。

压缩还做了什么?

除了重命名,压缩工具(Webpack、Vite、esbuild、Terser 等)还会:

操作效果
删除所有空格和换行整个文件变成一行
删除注释所有 ///* */ 消失
缩短变量名userPhoneNumbera
缩短函数名handleSubmitButtonClickb
合并语句能并成一行就并成一行
Tree Shaking删掉所有没被用到的代码

所以你看到的 abcd 天书,是经过打包 + 压缩两道工序之后的最终产物。

至于真正的「代码混淆」(Obfuscation),比如把字符串编码、把控制流打乱、加入死代码,那是更进一步的操作,留到进阶篇讲。


三、Chrome DevTools 的介绍

在开始分析之前,先搞懂 DevTools 里哪几个东西是逆向的核心工具。

image1.png

1. Sources 面板(核心战场)

按 F12 → 点 Sources 标签。左侧是文件树,中间是代码查看区,右侧是调试面板。你接下来 90% 的时间都在这个面板里。

2. 进来先用 Pretty Print 格式化

打开一个压缩成一行的 JS 文件后,点击代码区左下角的 {} 按钮(叫 "Pretty print")。

image2.png

格式化前:

function a(b){var c=d.e("phone").value,f=g(d.e("password").value);h(c,f)}

格式化后:

function a(b) {
    var c = d.e("phone").value,
        f = g(d.e("password").value);
    h(c, f)
}

这是你要做的第一个动作。不格式化,什么都别想看懂。

3. 右侧调试面板

重点关注这三个区域:

区域英文名干什么用的
调用堆栈Call Stack看「谁调用了谁」,逆向溯源的核心
作用域Scope看当前变量值,断点调试时用
断点Breakpoints管理你打的所有断点

四、Call Stack(调用堆栈),逆向的导航地图

什么是堆栈?

假设你的代码长这样:

function 做早饭() {
    煎蛋();
    热牛奶();
}

function 煎蛋() {
    打鸡蛋();
    倒油();
    开火();
}

function 打鸡蛋() {
    console.log("啪");
}

做早饭();

当程序执行到 console.log("啪") 这一行时,调用堆栈从下到上是:

打鸡蛋        ← 你现在在这里
煎蛋          ← 煎蛋调用了打鸡蛋
做早饭        ← 做早饭调用了煎蛋
(全局)      ← 最开始运行的代码

这就是 Call Stack:记录「谁调用了谁」,从入口一路到当前位置的完整轨迹。

在 DevTools 里怎么用?

image3.png

在 Sources 面板右侧的 Call Stack 区域,当代码在断点处停下时,它会显示当前调用链。

操作方式:

  1. 在 Sources 面板中,找到你怀疑的关键函数,点击行号打一个断点(行号上会出现蓝色箭头)
  2. 触发那个函数(刷新页面、点击按钮等)
  3. 代码停在断点处,右侧 Call Stack 显示完整调用链
  4. 点击堆栈中的任意一帧,代码区会自动跳转到那一帧对应的位置

这就是“顺着堆栈往上追”。你可以从一个底层函数出发,顺着堆栈一层层找到是谁调用了它,最终定位到业务逻辑。

业务逻辑一般在 Call Stack 的中间几帧,最上面是请求/监控层,最下面是事件触发层。 找到带业务文件名的帧,重点看那几层就够了。

先记住这个概念就行。Call Stack 什么时候真正派上用场,放到第七节讲,在那之前,有更简单的方法。


五、为什么 a() 点进去跳到了 b(){}?

这是初学者最困惑的现象之一。

在 DevTools 中,你把鼠标悬停在一个函数调用上(比如 a()),会弹出一个浮层,里面有一个蓝色带下划线的函数名(比如 b)。点击它,代码区会跳转到函数定义,然后你看到的是:

function b(c, d) {
    // ...
}

你调用的是 a(),怎么跳到 b 了?

其实 a 只是个变量名,不是函数名

在压缩后的代码里,一个函数通常不是直接以「函数声明」的形式存在的。它往往是这样的:

// 原来的源码(你猜的)
function sendLoginRequest(phone, encryptedPwd) {
    axios.post("/api/login", { phone: phone, password: encryptedPwd });
}

// 压缩后(实际看到的样子)
var a = function (b, c) {
    d.post("/api/login", { phone: b, password: c });
};

这里 a 是一个变量,它的是一个函数。DevTools 在显示函数引用时,显示的是这个函数本身的内部名称。你悬停 a,它告诉你 a 指向的那个函数实体叫 b,跳过去自然看到 function b(){}

更常见的是类似这样的多级赋值:

var e = function () { /* 真正干活的代码 */ };
var d = e;
// ...
d();  // ← 你在这里悬停,显示的是 e,跳过去也看到 e

d 指向 e,所以悬停 d() 时显示的是 e

那为什么有的函数悬停后什么都没有?

两种情况:

  1. 它是浏览器内置函数。 比如 setTimeoutJSON.stringifydocument.getElementById。这些都是浏览器原生实现的,没有 JS 源码可以跳转。
  2. 它是动态生成的匿名函数。 比如 new Function("return 1") 创建的,本身就没有名字。

六、以北京大学登录页为例走一遍完整实战

前面都是铺垫,现在用北京大学统一身份认证登录页(iaaa.pku.edu.cn)走一遍完整流程。

第一步:搜关键词

F12 → Sources → Ctrl+Shift+F 全局搜索 encrypt

搜索结果第二条就看到了一行非常可疑的代码:

encryptedPWD = crypt.encrypt($("#password").val());

第二步:打断点

点进那个文件,在这一行上点击行号打一个蓝色断点。

第三步:触发

在登录页输入任意密码,点击登录。代码停在了断点处。

第四步:看结果

断点停住后,看右侧 Scope 面板,你会看到 $("#password").val() 的值就是你输入的密码明文。crypt.encrypt() 就是加密函数,密码明文传进去,加密结果赋值给 encryptedPWD

这时候再看右侧 Call Stack

crypt.encrypt    ← 加密调用
oauthLogon       ← 登录函数
onClick          ← 按钮点击

只有两层有用帧。这是好事,说明代码结构简单,你不需要顺着堆栈一层层追,断点处的代码已经一目了然。

总结一下最常用的找加密套路

这就是 JS 逆向里找加密逻辑最常用的套路:

  1. Ctrl+Shift+F 搜索 passwordencrypt 等关键词
  2. 找到可疑函数,打上断点
  3. 在网页上触发操作(输入密码点登录)
  4. 断点触发,看 Scope 里的变量值
  5. 如果当前帧不够清楚,看 Call Stack 往上追

北大登录、B 站登录,以及你能遇到的大多数网站,用这个方法都够了。


七、搜不到关键词怎么办?试试 XHR/Fetch 断点

上一节的套路有一个前提:关键词还在源码里。 但如果网站压缩得太狠,或者额外加了混淆,encryptpassword 这些词在源码里根本不存在,你搜什么都是空的。

那这时候就得换一种思路:不看代码,看行为。 不管代码被压缩成什么样,登录请求最终总是要发出去的。在请求发出的那一刻把它断住,然后从 Call Stack 反向追溯。

这就是 Call Stack 真正派上用场的地方。

XHR 和 Fetch 是什么?

网页发网络请求,必须用这两个 API 之一:

API特点
XMLHttpRequest(XHR)老一辈的请求方式,jQuery 时代的主流
fetch新一代的请求方式,Vue/React 等现代框架默认用这个

不知道网站用哪个?两个断点都打上就行。

怎么打 XHR/Fetch 断点?

我们首先通过 Network 面板,抓包找到登录接口的 URL,然后填入 XHR/Fetch 断点。这里以某东登录页为例。

image4.png

  1. 打开 Sources 面板
  2. 在右侧调试面板中找到 XHR/fetch Breakpoints 区域(和 Call Stack、Scope、Breakpoints 并列)
  3. 点击 + 号,添加断点规则
  4. 填入 URL 的关键片段。先用 Network 面板看一次登录请求的地址(比如 https://xxx.com/api/user/login),取其中独特的片段(比如 login)填进去。这样只有包含 login 的请求会断住
  5. 勾选启用

怎么触发它?又该怎么用 Call Stack 顺藤摸瓜?

在登录页面输入账号密码、点击登录,代码停在 fetchXMLHttpRequest.send 处,在这个时候,代码会停在浏览器底层发送网络请求的核心位置。

这时候右侧 Call Stack 会长得多,一般像这样:

监控/请求拦截层       ← XHR 断点通常停在这里(跳过)
jQuery send / ajax
加密/签名函数         ← 你要找的目标
login 登录函数        ← 重点关注这几帧
proceedWithLogin
验证码回调 / 事件分发  ← 触发源(跳过)

💡 业务逻辑一般在 Call Stack 的中间几帧。最上面几帧往往是请求发送层(jQuery、监控 SDK 之类的),最下面几帧是事件触发层(按钮点击、验证码回调之类的),两头都可以跳过。你要找的加密逻辑,通常就在中间那几个带业务文件名的帧里。

但怎么快速区分哪些帧该看、哪些该跳? 不用点进去看代码,光看右边的文件名就能判断:

文件名特征大概率是什么操作
sdkmin、版本号的(如 trace-chain-sdk.umd.min.js第三方 SDK跳过
一看就是库名的(jquery-1.6.4.jsvue.js框架跳过
名字像乱码的(jcap_ujb96b.js混淆后的 SDK跳过
名字是正常英文单词的(login2024.jsuser.jspay.js业务代码⭐ 重点看

找到带业务关键词的文件名,点进去看那几帧就够了。

逐帧点击,观察每一层的代码和 Scope 中的变量。你最终会找到这样一个函数:它的代码里出现了 MD5SHA256AESsign 之类的关键词,或者生成了你看不懂的字符串。那就是加密逻辑。

下面我们以某东为例:
image4.png

怎么知道找没找对?

把提取的加密函数在 Node.js 里跑一下,和抓包结果对比:

const crypto = require("crypto");
const result = crypto.createHash("md5").update("123456").digest("hex");
console.log(result);  // e10adc3949ba59abbe56e057f20f883e

一致就说明你找对了 🎉。

实在不行?试试 Event Listener 断点

如果 XHR/Fetch 断点不适合你的场景,还有一个备选:

image6.png

在 Sources 面板右侧找到 Event Listener Breakpoints → 展开 Mouse → 勾选 click。之后任何鼠标点击事件都会触发断点。从 Call Stack 往下追,一样能找到加密逻辑。


🎉 到这里,你已经算是正式入门 JS 逆向了。

能打开 DevTools、看懂压缩后的代码、找到关键的加密函数,这些就是逆向最核心的基本功。别小看这一步,很多人卡在“打开 F12 之后不知道该看哪”就直接放弃了,而你已经走到这里了。


📖 这一篇解决了「看懂代码」+「找到关键函数」。下一篇:

《JS逆向入门第二课:财联社逆向实战》

用第一课学到的东西,实战逆向财联社,从抓包到定位加密逻辑完整走一遍。


⚠️ 免责声明:本文介绍的技术(Chrome DevTools 调试、代码分析)是前端开发和安全研究的通用技能,广泛应用于性能分析、Bug 排查和合法安全测试。请勿将其用于非法入侵、未授权数据抓取或其他违反法律法规的行为。