关于 js:6. 网络与加密模块
一、AJAX
AJAX(Asynchronous JavaScript And XML) = 异步 JavaScript 与 XML(现在多为 JSON)
它允许网页在不重新加载整个页面的情况下,从服务器请求数据并更新页面内容。
主要用途:
-
提交表单时无需刷新页面。
-
动态加载评论、搜索建议、分页数据。
-
网页内容分块加载,提高用户体验。
1. AJAX 的核心实现方式:XMLHttpRequest
这是最基础、兼容性最强的 AJAX 实现方式(Fetch 是后来的替代方案)。
基本用法:
let xhr = new XMLHttpRequest();
xhr.open("POST", "https://example.com/api", true);
xhr.setRequestHeader("Content-Type", "application/json");xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log("响应结果:", xhr.responseText);}
};xhr.send(JSON.stringify({ username: "admin", password: "123" }));
🔹 let xhr = new XMLHttpRequest();
创建一个新的 XMLHttpRequest
实例对象,用于发起 HTTP 请求(就是 AJAX 请求的载体)。
🔹 xhr.open("POST", "https://example.com/api", true);
open()
不会发起请求,只是初始化。
-
请求方法是
POST
(表示发送数据) -
请求地址是
https://example.com/api
-
第三个参数
true
表示 异步请求(常见做法,浏览器默认)
🔹 xhr.setRequestHeader("Content-Type", "application/json");
设置请求头。告诉服务器:我发送的是 JSON 数据。
🔹 xhr.onreadystatechange = function () { ... }
注册一个回调函数,监听请求的状态变化。
xhr.readyState
会变化,值可能是:
值 | 含义 |
---|---|
0 | 请求未初始化(尚未调用 open) |
1 | 请求已建立(调用了 open) |
2 | 服务器收到请求头 |
3 | 响应体正在下载 |
4 | 完整响应已收到(最重要) |
🔹 xhr.send(JSON.stringify({ username: "admin", password: "123" }));
-
发起真正的请求,并把数据发给服务器。
-
send()
参数必须是字符串(除非是FormData
等对象)。 -
JSON.stringify(...)
将对象转换为 JSON 字符串。
2. 浏览器和服务器交互
[浏览器 JS]
↓
构造参数(如用户名、密码)
↓
执行加密函数(如 AES、MD5、签名等)
↓
xhr.send(JSON.stringify(加密后的参数))
↓
[HTTP 请求:发送到服务器]
↓
[服务器]
接收密文 → 解密/验签 → 校验身份 → 返回数据
3. 重要属性与方法
属性 / 方法 | 作用说明 |
---|---|
open(method, url) | 初始化请求:设置请求方法和 URL |
setRequestHeader() | 设置请求头(如 Content-Type 、Token) |
send(body) | 发起请求,可传请求体 |
readyState | 请求状态:0-4(4为完成) |
status | 响应状态码(如 200) |
responseText | 响应体(字符串) |
onreadystatechange | 监听状态变化 |
onload | 请求成功触发 |
onerror | 请求失败触发 |
4. XHR 请求完整生命周期(readyState
)
readyState | 含义 |
---|---|
0 | UNSENT:未调用 open() |
1 | OPENED:已调用 open() |
2 | HEADERS_RECEIVED:收到响应头 |
3 | LOADING:下载中 |
4 | DONE:完成(可访问响应数据) |
5. 逆向分析中的关键 Hook 点
在逆向中,XHR 经常是参数加密函数的入口。可以这样监听它的行为:
Hook open
和 send
方法:
// Hook open 方法
const rawOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async) {this._url = url; // 保存请求 URLreturn rawOpen.apply(this, arguments);
};// Hook send 方法
const rawSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {console.log("拦截到 XHR 请求:");console.log("URL:", this._url);console.log("Body:", body);debugger; // 可在浏览器中断点分析调用堆栈return rawSend.call(this, body);
};
补充监听 setRequestHeader
const rawSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (key, value) {console.log("Header:", key, value);return rawSetHeader.call(this, key, value);
};
总结
关键点 | 逆向目的 |
---|---|
open(url) | 获取接口地址 |
send(body) | 拦截加密前参数 |
setRequestHeader | 获取 token、sign |
responseText | 如果返回数据加密,这里是解密入口 |
DevTools Initiator | 定位加密函数栈 |
Hook 方法 | 精准抓取请求参数与加密逻辑 |
二、Fetch
Fetch
是现代浏览器提供的基于 Promise 的 HTTP 请求接口,它是 XMLHttpRequest
的替代方案,更简洁、灵活。
1. 基本用法(发起 GET/POST 请求)
GET 请求:
fetch("https://example.com/api/user?id=123").then(response => response.json()).then(data => console.log(data));
POST 请求:
fetch("https://example.com/api/login", {method: "POST",headers: {"Content-Type": "application/json"},body: JSON.stringify({username: "admin",password: "123"})
})
.then(response => response.json())
.then(data => console.log(data));
2. Fetch 请求参数结构
fetch(url, {method: "POST", // 请求方法headers: { // 请求头"Content-Type": "application/json"},body: JSON.stringify(data), // 请求体credentials: "include", // 是否携带 cookiemode: "cors" // 跨域策略
});
参数 | 说明 |
---|---|
method | 请求方法,如 GET/POST |
headers | 请求头,如 token、Content-Type |
body | 请求体(不能用于 GET) |
credentials | 控制是否发送 Cookie:omit / same-origin / include |
mode | 跨域策略:cors / same-origin / no-cors |
3. 浏览器监听 Fetch 请求的方法
fetch
是浏览器原生函数,可以被重写 hook,这是分析参数加密的最佳入口之一。
Hook fetch
方法:
const rawFetch = window.fetch;
window.fetch = async function (...args) {console.log("拦截到 Fetch 请求:");console.log("URL:", args[0]);console.log("配置:", args[1]);debugger; // 断点观察调用栈return rawFetch.apply(this, args);
};
这段代码可以:
-
查看请求地址和加密后的 body
-
定位调用
fetch
前的参数构造函数 -
逆推出加密函数
4. Fetch 请求中的常见参数构造方式
在逆向中,最常见的参数类型包括:
1)JSON.stringify(data)
(大多数接口使用)
body: JSON.stringify({ id: 123, sign: "xxxxx" })
2)FormData
(用于表单上传、模拟 POST)
const form = new FormData();
form.append("username", "admin");
form.append("password", "123");
fetch(url, {method: "POST",body: form
});
3)URLSearchParams
(urlencoded 表单格式)
const params = new URLSearchParams();
params.append("username", "admin");
params.append("password", "123");fetch(url, {method: "POST",headers: { "Content-Type": "application/x-www-form-urlencoded" },body: params
});
4)自定义加密参数
let sign = getSign(username, password, timestamp); // 加密逻辑
body: JSON.stringify({username,password,sign
});
5. Fetch 的响应处理
fetch(url).then(res => res.text()) // 获取原始文本.then(text => {// 有些接口返回加密数据,这里可能是 base64、hex、AES密文console.log("原始响应:", text);});
如果发现响应是乱码或密文,说明:
-
前端还会解密响应数据
-
需要
hook
解密函数或继续抓栈定位
总结
关键点 | 逆向目的 |
---|---|
fetch(url, config) | 拦截请求地址与参数配置 |
body | 发现加密参数、定位加密函数 |
headers | 检查 Token、Content-Type |
debugger + Initiator | 精确找到构造请求的函数 |
fetch hook | 实时监听,抓取请求参数 |
三、XHR
-
AJAX 是一种技术思想,用于“在网页不刷新的情况下与服务器通信”。
-
XHR(XMLHttpRequest) 是实现 AJAX 的核心 API(工具)。
AJAX 是什么?AJAX = Asynchronous JavaScript And XML
中文叫“异步 JavaScript 与 XML”,但其实不只支持 XML,也可以传 JSON、HTML、纯文本。
本质上:AJAX 是一种在网页中异步请求数据,不刷新整个页面的技术方式。
它能实现的效果:
-
用户点击按钮,页面不刷新,但后台偷偷向服务器发请求。
-
比如搜索框自动补全、无限滚动、点赞、评论提交等。
XHR 是什么?XHR(XMLHttpRequest) 是浏览器提供的一个内建对象,用来发 AJAX 请求。
可以用它来:
-
发起 GET、POST 请求
-
设置请求头
-
监听响应
-
获取服务器返回的结果
示例代码:
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com/data", true);
xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log(xhr.responseText); // 响应内容}
};
xhr.send(); // 发起请求
XHR 与 fetch
的区别
对比点 | XHR | fetch |
---|---|---|
是否支持 Promise | 不支持 | 支持 |
是否支持 Stream | 不支持 | 支持 |
请求拦截难度 | 易 hook | 易 hook |
跨域支持 | 支持设置 withCredentials | 使用 credentials |
前端项目中使用频率 | 老项目中多见 | 现代项目主流 |
四、请求监听方式
请求监听(Request Interception) 指的是:我们在浏览器中监听网页内部的网络请求,查看请求内容、加密参数、调用栈,甚至插桩劫持请求逻辑。
逆向的时候,往往看到的是:
xhr.send(加密参数);
但不知道这个加密参数是怎么来的。
监听请求就能:
-
看到最终加密参数的结构
-
追踪调用栈,找到加密函数入口
-
判断是 XHR 还是 Fetch,决定 hook 方案
-
动态打断点、劫持参数、还原加密
方式 1:Chrome DevTools → Network 面板监听
功能:
-
抓包请求(XHR / Fetch / WebSocket)
-
查看请求地址、请求体(payload)、响应内容
-
查看请求是通过什么方式发起的(XHR、Fetch、FormData)
操作:
-
F12 打开控制台
-
切换到 Network
-
刷新页面或触发行为(比如点击按钮)
-
找到类型为
XHR
或Fetch
的请求 -
查看:
-
Request Payload(加密数据)
-
Headers(请求头)
-
Preview / Response(返回数据)
-
Initiator(调用栈,追到加密函数)
-
进阶:
-
右键 → Break on request(触发断点)
-
Initiator → 点击函数跳转到源码
方式 2:Chrome DevTools → Hook XHR / Fetch
Hook XMLHttpRequest:
let open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async) {console.log("拦截请求地址:", method, url);this._url = url; // 保存地址,后面 send 中用return open.apply(this, arguments);
};let send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {console.log("拦截请求体:", this._url, body);debugger; // 打断点分析 bodyreturn send.apply(this, arguments);
};
Hook Fetch:
let realFetch = window.fetch;
window.fetch = function(...args) {console.log("拦截 fetch 请求:", args);debugger;return realFetch.apply(this, args);
};
打开控制台,粘贴运行,然后执行页面动作就能抓到所有请求和加密参数。
方式 3:DevTools → XHR/Fetch 断点监听
操作步骤:
-
打开 DevTools
-
切到 Sources → XHR/Fetch Breakpoints
-
点击右侧的 “Fetch/XHR”
-
执行页面操作,请求发出时自动断点
-
查看调用栈 / 参数加密点 / 函数名
五、FormData
FormData
是浏览器提供的一个类,用来构造 表单格式的请求体,可以模拟 HTML <form>
的提交行为,特别适合文件上传、键值对参数提交。
1. 本质理解:
执行:
let form = new FormData();
form.append("username", "admin");
form.append("password", "123456");
最终发送到服务器的内容是类似于:
Content-Type: multipart/form-data; boundary=----xxx------xxx
Content-Disposition: form-data; name="username"admin
------xxx
Content-Disposition: form-data; name="password"123456
------xxx--
它不是 JSON,不是纯字符串,是“表单格式”,带有 Content-Disposition
和 boundary
边界。
2. 使用方式
基本用法:
let form = new FormData();
form.append("key1", "value1");
form.append("key2", "value2");fetch("https://example.com/api", {method: "POST",body: form
});
上传文件:
let fileInput = document.querySelector("input[type=file]");
let form = new FormData();
form.append("username", "admin");
form.append("avatar", fileInput.files[0]); // 上传文件xhr.send(form); // 以 multipart/form-data 格式发送
3. 如何逆向分析 FormData 参数?
Step 1:在 DevTools → Network → 找到请求类型为 multipart/form-data
会看到:
------WebKitFormBoundary...
Content-Disposition: form-data; name="username"admin
Step 2:Sources 或 Console 中 Hook 或打断点
可以这样 hook:
let append = FormData.prototype.append;
FormData.prototype.append = function(key, value) {console.log("添加参数:", key, value);return append.apply(this, arguments);
};
这段代码会拦截所有 form.append(...)
的行为,打印每个参数,用于分析是否存在加密值或隐藏参数。
总结
特性 | 说明 |
---|---|
自动设置 Content-Type | 浏览器自动添加 boundary,不能手动设置 Content-Type |
支持文件上传 | 可以 .append("file", File对象) |
可用于 XHR 和 fetch | 都可以传入 form 对象作为 body |
不能直接查看字符串内容 | form 不是普通对象,不能 JSON.stringify() |
和 JSON 对比:
项目 | FormData | JSON |
---|---|---|
内容格式 | multipart/form-data | application/json |
是否能传文件 | 是 | 否 |
参数格式 | 键值对(string, Blob) | 任意 JS 对象 |
能否直接 console.log | 否(需遍历) | 是 |
六、URLSearchParams
URLSearchParams
是浏览器提供的内建类,用来构造、解析、修改 URL 查询字符串(?a=1&b=2
这样的格式)。
1. 使用场景
场景 | 示例 |
---|---|
构造请求参数 | POST 请求参数体是 application/x-www-form-urlencoded |
解析 URL | 拿到 location.search 中的参数 |
拼接参数 | 动态生成接口调用的 query 参数 |
示例 1:构造 POST 请求体
let params = new URLSearchParams();
params.append("username", "admin");
params.append("password", "123456");fetch("https://example.com/api", {method: "POST",headers: {"Content-Type": "application/x-www-form-urlencoded"},body: params.toString() // username=admin&password=123456
});
params.toString()
会自动序列化成表单形式的字符串,非常常见于登录、注册、搜索接口。
示例 2:解析 URL 参数
let url = "https://example.com/page?uid=1001&token=abcd123";
let searchParams = new URLSearchParams(new URL(url).search);console.log(searchParams.get("uid")); // "1001"
console.log(searchParams.get("token")); // "abcd123"
2. 常用 API 一览
方法 | 说明 | 示例 |
---|---|---|
append(key, val) | 添加参数 | params.append("a", "1") |
get(key) | 获取参数 | params.get("a") |
set(key, val) | 修改/替换 | params.set("a", "2") |
delete(key) | 删除参数 | params.delete("a") |
has(key) | 是否存在参数 | params.has("a") |
toString() | 序列化字符串 | a=1&b=2 |
for...of 遍历 | 遍历所有参数 | for ([k,v] of params) |
3. 和 FormData
对比
项目 | FormData | URLSearchParams |
---|---|---|
内容类型 | multipart/form-data | application/x-www-form-urlencoded |
是否能传文件 | 是 | 否(只能字符串) |
用于哪类请求 | 上传类 / 文件 / 图片 | 登录、搜索、加密参数 |
能否用 toString() | 否(不能) | 是(生成 query 字符串) |
4. 逆向中如何利用?
在 DevTools 中抓包时会看到:
POST https://example.com/api
Content-Type: application/x-www-form-urlencoded
Request Payload:
username=admin&password=123456&sign=ab12cd34
那么 80% 的概率就是代码里用了:
let p = new URLSearchParams();
p.append("username", "admin");
p.append("password", "123456");
p.append("sign", sign_func("admin123456"));
关键:hook append()
,追踪 sign
来源!
Hook 分析技巧(追踪加密参数):
const _append = URLSearchParams.prototype.append;
URLSearchParams.prototype.append = function(key, value) {console.log("拦截 append 参数:", key, value);debugger; // 查看调用栈,追踪加密return _append.apply(this, arguments);
};
把这段代码贴到控制台后再执行请求,就能看到哪个参数是加密的,再反向分析加密函数。
实战例子:
let token = genToken(username, timestamp);
let params = new URLSearchParams();
params.append("username", username);
params.append("ts", timestamp);
params.append("token", token); // 加密生成xhr.send(params.toString());
抓包看到 token 是 asdu23ask1d21
,就可以在 hook 中抓到 token
的生成值,逆向 genToken()
函数逻辑。
总结
点 | 内容 |
---|---|
用途 | 构造 query 参数字符串(a=1&b=2),用于请求体或 URL |
类型 | application/x-www-form-urlencoded |
特点 | 轻量、易用、可序列化、适合加密拼参 |
逆向核心 | 通过 hook .append() 找加密参数的来源 |
七、Headers 构造方式
Headers
是浏览器提供的接口,用来构造和操作 HTTP 请求头部(Headers),它决定了请求的“身份”、“类型”、“来源”等重要信息。
常见使用场景
-
使用
fetch
或XHR
发起请求时,设置请求头 -
模拟浏览器行为(如添加
User-Agent
,Referer
) -
设置登录凭证(如
Authorization
,Cookie
) -
模拟
Content-Type
(用于表单、JSON、加密数据)
1. 基本用法
1)创建 Headers 对象
let headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", "Bearer token_xyz");
2)传入 fetch 请求
fetch("https://example.com/api", {method: "POST",headers: headers,body: JSON.stringify({ username: "admin", password: "123" })
});
或者简写成:
fetch("https://example.com/api", {method: "POST",headers: {"Content-Type": "application/json","Authorization": "Bearer token_xyz"},body: JSON.stringify({ username: "admin", password: "123" })
});
两种写法等价,第二种是对象字面量自动转 Headers
。
常用请求头字段
Header | 说明 | 示例 |
---|---|---|
Content-Type | 请求体格式 | application/json 、application/x-www-form-urlencoded 、multipart/form-data |
User-Agent | 浏览器身份 | 模拟浏览器 UA |
Referer | 来源页面 | 某些接口必须带上指定页面 |
Cookie | 携带登录信息 | 模拟登录态 |
Authorization | token 或 Basic 认证 | Bearer token_xxx 或 Basic xxxxx== |
X-Requested-With | 区分 AJAX | XMLHttpRequest |
Headers API 方法
方法 | 说明 |
---|---|
append(name, value) | 添加头部 |
set(name, value) | 设置或覆盖头部 |
get(name) | 获取某个头部值 |
has(name) | 判断是否存在 |
delete(name) | 删除某个头部 |
forEach() | 遍历所有头部 |
示例:
headers.forEach((value, name) => {console.log(name + ": " + value);
});
和 XMLHttpRequest
配合
let xhr = new XMLHttpRequest();
xhr.open("POST", "/api", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", "Bearer token_xyz");
xhr.send(JSON.stringify({ data: 123 }));
2. 请求头伪造与逆向中作用
在 JS 逆向/爬虫过程中,经常要模拟浏览器请求头,否则接口会返回 403 或提示“非法请求”:
-
User-Agent
→ 模拟浏览器设备 -
Referer
→ 验证页面来源,防爬关键头部 -
X-Requested-With: XMLHttpRequest
→ AJAX 请求标识 -
Token / Sign / Timestamp
→ 加密参数中放头部
3. 实战逆向技巧
1)DevTools 抓包对比
在 Chrome Network 面板中,点击请求 → Headers → Request Headers:
找到与正常请求差异的头部,例如:
X-Sign: ad83b23a3a 自定义加密头部
X-Time: 17123423452 时间戳
这类参数很可能是 JS 中加密生成的,需要你找到:
headers.append("X-Sign", genSign(data, ts));
2)Hook Headers.append
const oldAppend = Headers.prototype.append;
Headers.prototype.append = function(key, val) {console.log("Header添加:", key, val);debugger; // 查看调用栈,追踪生成过程return oldAppend.apply(this, arguments);
};
拦截加密头部(如 token、sign),反向追踪函数来源。
4. 项目案例
let headers = new Headers();
headers.append("X-Token", encryptToken(username, ts, key));
headers.append("X-Time", ts);
headers.append("Content-Type", "application/json");fetch("/api/protected", {method: "POST",headers: headers,body: JSON.stringify({ username, password })
});
请求被加密了,抓包看到 X-Token
是混淆值,就去找 encryptToken()
的源码,通常是 md5、AES、Base64 自定义函数。
总结
项目 | 内容 |
---|---|
用途 | 构造 HTTP 请求头,设置内容格式、认证、token 等 |
类型 | JS 内建对象,可单独构造,也可直接传对象 |
逆向重点 | 观察 Authorization / X-* 自定义字段是否加密 |
调试技巧 | DevTools 抓包对比 + Hook append() |
八、WebSocket 通信机制与劫持调试
WebSocket 是一种持久化的双向通信协议,适合实时数据交换(如聊天、弹幕、验证码行为上传等),逆向难点在于它绕过了传统 HTTP 抓包,必须使用调试和劫持手段来追踪。
1. WebSocket 与 HTTP 的区别
对比项 | HTTP | WebSocket |
---|---|---|
连接方式 | 请求一次就断开 | 一次连接,持续通信 |
通信方向 | 客户端主动请求 | 双向,服务端也可主动发消息 |
适合场景 | 表单提交、接口调用 | 聊天、弹幕、验证码行为上报 |
请求协议 | http:// / https:// | ws:// / wss:// |
抓包方式 | 普通 HTTP 抓包 | DevTools 或专用代理工具 |
2. WebSocket 使用机制
JS 创建连接
let ws = new WebSocket("wss://example.com/ws");ws.onopen = () => {console.log("连接成功");ws.send(JSON.stringify({ action: "auth", token: "abcd1234" }));
};ws.onmessage = (e) => {console.log("接收到消息:", e.data);
};ws.onerror = (e) => {console.error("发生错误", e);
};ws.onclose = () => {console.log("连接关闭");
};
常见的 WebSocket 用途:
场景 | 数据内容 | 分析目标 |
---|---|---|
验证码行为上传 | 滑动轨迹、坐标、行为时间等 | 找到加密逻辑、格式、字段名 |
聊天通信 | msg、uid、room | 伪造用户发送消息 |
登录验证 | token / device_id | 模拟身份登录 |
3. 如何抓包与调试 WebSocket?
方法一:Chrome DevTools → Network 面板 → WS
-
打开网页
-
按 F12 打开 DevTools → Network → WS(WebSocket)
-
刷新页面
-
找到连接名(一般是
/ws
或/socket
) -
点击查看:
-
Messages:每一条收发的消息(原始/解码后)
-
Frames:查看 WebSocket 的原始数据包
-
Payload:可能是字符串、JSON、加密串
方法二:控制台劫持 WebSocket
拦截发送数据(hook send
方法):
const originSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(data) {console.log("[WS 拦截] 发送内容:", data);debugger; // 追踪发送数据的调用栈return originSend.apply(this, arguments);
};
拦截接收数据(hook onmessage
):
Object.defineProperty(WebSocket.prototype, 'onmessage', {set(fn) {const wrapper = function(event) {console.log("[WS 拦截] 收到消息:", event.data);debugger; // 查看接收的数据结构return fn.call(this, event);};this._onmessage = wrapper;return wrapper;},get() {return this._onmessage;}
});
4. 逆向分析技巧
Step1:确认连接地址和格式
let ws = new WebSocket("wss://xxx.com/socket?token=abc");
有些时候连接参数里就藏着 sign
或者 timestamp
,是加密生成的!
Step2:拦截 send
内容
ws.send(JSON.stringify({type: "track",data: encrypt(轨迹数组)
}));
可以通过 send
hook 抓到:
{"type": "track","data": "ajb19nASDAWJq1da=="
}
然后去找这个加密 encrypt()
函数。
Step3:解构收到的数据(解密)
ws.onmessage = (e) => {let msg = JSON.parse(e.data);let content = decrypt(msg.payload); // 模拟服务端逻辑
};
有些网站是前端解密数据展示,hook onmessage
可以观察服务端主动推送内容。
总结
项目 | 内容 |
---|---|
创建方式 | new WebSocket("wss://xxx") |
通信方式 | send() 发送,onmessage 接收 |
抓包方式 | Chrome DevTools → Network → WS |
劫持方式 | Hook send() / onmessage |
逆向重点 | 抓取并分析发送的数据结构与加密函数 |
九、btoa
/ atob
函数 | 全称 | 作用 |
---|---|---|
btoa() | binary to ASCII | 把字符串 → Base64 编码 |
atob() | ASCII to binary | 把 Base64 → 原始字符串 |
Base64 是一种将二进制数据用 ASCII 字符表示的方法,用于:
-
在网络上传输二进制数据(如图片、密钥)时变为字符串
-
编码数据避免乱码或安全字符(如 POST body、URL 参数)
1. 基本语法
btoa()
let encoded = btoa("hello");
console.log(encoded); // "aGVsbG8="
atob()
let decoded = atob("aGVsbG8=");
console.log(decoded); // "hello"
2. 实际用途举例
示例1:前端加密参数
let data = JSON.stringify({ username: "admin", password: "123" });
let encrypted = btoa(data); // 模拟加密
xhr.send(encrypted); // 加密参数传输
示例2:反爬遇到参数是 Base64
let encoded = "eyJ1c2VybmFtZSI6ImFkbWluIiwicCI6IjEyMyJ9"; // 看起来像一串 base64
let decoded = atob(encoded);
console.log(decoded); // {"username":"admin","p":"123"}
3. 注意事项
限制项 | 说明 |
---|---|
字符集限制 | 只能对 Latin1 范围(0~255)的字符使用,不能直接用于 Unicode 字符(中文) |
中文处理 | 需要用 encodeURIComponent 和 decodeURIComponent 包裹,避免乱码 |
处理中文的写法
// 正确加密中文
function encodeBase64(str) {return btoa(unescape(encodeURIComponent(str)));
}// 正确解密中文
function decodeBase64(base64) {return decodeURIComponent(escape(atob(base64)));
}let encrypted = encodeBase64("你好");
console.log(encrypted); // 5L2g5aW9
console.log(decodeBase64(encrypted)); // 你好
1)encodeURIComponent(str)
- 把 Unicode 字符串编码成 UTF-8 后,以 %XX 形式表示(URL 编码)。
示例:
encodeURIComponent("你好")
// 输出:"%E4%BD%A0%E5%A5%BD"
"你好"
→ UTF-8 编码 → 字节:[0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD]
2)unescape(...)
-
把
%XX
表示的字节还原成 JS 字符串(每字符代表一个字节); -
结果是一个 binary-like 字符串,符合
btoa()
要求。
unescape("%E4%BD%A0%E5%A5%BD")
// 输出:乱码样式的字符串,每个字符的 charCode 在 0~255
3)btoa(...)
- 现在就可以 Base64 编码这个“binary-like”的字符串。
btoa(unescape(encodeURIComponent("你好")));
// 输出:"5L2g5aW9"
这个字符串 "5L2g5aW9"
就是 "你好"
的 UTF-8 → Base64 编码。
总结
方法 | 含义 | 示例 |
---|---|---|
btoa(str) | 字符串 → base64 编码 | btoa('abc') → YWJj |
atob(base64) | base64 → 解码字符串 | atob('YWJj') → abc |
中文处理 | 必须先转 Unicode | encodeURIComponent + unescape |
十、window.crypto
window.crypto
(全名 Web Crypto API)是浏览器提供的原生加密接口,用于执行安全性更高的加密操作,包括:
-
生成随机数(
crypto.getRandomValues
) -
加密/解密(AES、RSA)
-
哈希(SHA-256 等)
-
密钥导入、导出、生成
1. 基本结构
window.crypto.subtle // 提供加密函数
window.crypto.getRandomValues() // 安全随机数生成
-
window.crypto.getRandomValues()
:安全随机数(强于 Math.random) -
window.crypto.subtle
:加密核心接口(SubtleCrypto)
2. 核心用途场景
功能 | 方法 |
---|---|
加密 / 解密 | encrypt , decrypt |
摘要(哈希) | digest |
密钥生成 | generateKey |
密钥导入 / 导出 | importKey , exportKey |
签名 / 验签 | sign , verify |
3. 常用函数详解
1)安全随机数(爬虫滑动轨迹等)
const array = new Uint8Array(10); // 创建一个长度为 10 的 Uint8Array 数组
crypto.getRandomValues(array); // 生成安全随机数并填充数组
console.log(array); // 打印安全随机数数组
2)SHA-256 哈希(常见签名算法)
const data = new TextEncoder().encode("abc123");
// 用 TextEncoder 把字符串 "abc123" 编码成 UTF-8 字节数组(Uint8Array)
// 输出类似:Uint8Array [97, 98, 99, 49, 50, 51]crypto.subtle.digest("SHA-256", data).then(hashBuffer => {
// 使用 Web Crypto API 的 subtle.digest 方法对字节数组进行 SHA-256 哈希计算
// 结果是一个 ArrayBuffer(二进制缓冲区),表示哈希后的原始数据const hashArray = Array.from(new Uint8Array(hashBuffer)); // 把 ArrayBuffer 转换成 Uint8Array,再转成普通数组// 每个元素是一个 0~255 的整数,代表哈希值的每个字节const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // 把每个字节转成两位十六进制字符串,不足两位的用 '0' 补齐// 然后把所有十六进制字符串拼接成一个完整的哈希字符串(长度为64)console.log(hashHex); // 打印最终的 SHA-256 哈希值,格式为 64 位十六进制字符串
});
3)AES-GCM 加密/解密(重点!常用于前端数据加密)
// 生成密钥
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, // 指定生成 AES-GCM 密钥,密钥长度为 256 位true, // 密钥是否可以导出(true 表示可以)["encrypt", "decrypt"] // 密钥的用途是加密和解密
);// 加密数据
const iv = crypto.getRandomValues(new Uint8Array(12)); // 12字节IV
const encoded = new TextEncoder().encode("hello world");const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, // 使用 AES-GCM 加密算法和生成的 IVkey, // 用之前生成的密钥进行加密encoded // 待加密的明文字节数组("hello world")
);// 解密数据
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, // 使用 AES-GCM 解密算法和相同的 IVkey, // 使用之前生成的密钥进行解密ciphertext // 加密后的密文
);console.log(new TextDecoder().decode(decrypted)); // "hello world"
4. 注意事项
限制 | 描述 |
---|---|
只能异步操作 | 所有方法都返回 Promise |
二进制为主 | 输入输出都为 ArrayBuffer 类型 |
无法直接处理字符串 | 需配合 TextEncoder 和 TextDecoder |
5. JS逆向中出现的表现形式
经常会在网站中看到这样的代码片段:
await window.crypto.subtle.digest("SHA-256", data);
await window.crypto.subtle.encrypt({name: "AES-GCM", iv}, key, plaintext);
或者压缩/混淆后是:
e = window.crypto.subtle;
t = e.encrypt({name:"AES-GCM",iv:a}, n, r)
这些往往是在加密签名参数 w、payload、token 前执行的过程,必须跟踪输入输出、密钥、IV(偏移量)来逆推加密逻辑。
总结
功能 | 方法 | 用途 |
---|---|---|
随机数 | getRandomValues | 滑动轨迹、IV 生成 |
哈希 | digest | 签名摘要,如 SHA-256 |
加密 | encrypt | AES / RSA 加密请求参数 |
解密 | decrypt | 解密服务器回传数据 |
密钥导入 | importKey | 字符串密钥导入使用 |
密钥生成 | generateKey | 通信过程生成密钥 |
十一、自定义 base64 / md5 / AES / RSA 实现
1. 自定义 base64
编码/解码
base64
编码实现
function encodeBase64(str) {let charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // 创建 Base64 编码字符集:包含大写字母、小写字母、数字和 "+"、"/" 两个符号。// 这个字符集是 Base64 编码中使用的标准字符集。let binaryStr = ""; // 用于存储将字符串转换成的二进制表示的变量。// 转为二进制字符串for (let i = 0; i < str.length; i++) {binaryStr += str.charCodeAt(i).toString(2).padStart(8, '0');}// 遍历输入字符串的每个字符// `charCodeAt(i)` 获取字符的 Unicode 编码(整数),然后用 `.toString(2)` 转换为二进制字符串// `padStart(8, '0')` 确保每个二进制字符串都是 8 位,不足的补充 '0'// 例如:'h' -> 104 (charCodeAt),转为 '01101000' (8 位)let base64Str = ""; // 用于存储最终的 Base64 编码结果。// 将二进制字符串按 6 位一组进行处理for (let i = 0; i < binaryStr.length; i += 6) {let chunk = binaryStr.slice(i, i + 6); // 每次取 6 位二进制// 例如:'011010'、'000110'、'101100',等let index = parseInt(chunk, 2); // 将 6 位二进制字符串转换为十进制整数// 例如:'011010' -> 26(十进制)base64Str += charSet.charAt(index); // 使用得到的索引在 Base64 字符集(charSet)中找到相应字符// 例如:26 -> 'a'(字符集中的第 27 个字符)}// 添加 "=" 补齐while (base64Str.length % 4) {base64Str += "=";}// Base64 编码的最终结果长度必须是 4 的倍数,若不足,使用 '=' 字符进行补齐。// 例如:如果最后的编码长度是 6,则需要补充 2 个 '=',长度变为 8。return base64Str; // 返回最终的 Base64 编码字符串
}console.log(encodeBase64("hello")); // "aGVsbG8="
base64
解码实现
function decodeBase64(base64Str) {let charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // 创建 Base64 字符集,包含大写字母、小写字母、数字以及 '+' 和 '/' 两个符号。// 这是用于解码 Base64 字符串时的标准字符集。let binaryStr = ""; // 用于存储将 Base64 字符串转换成的二进制字符串。// 去掉补充的 "="base64Str = base64Str.replace(/=/g, ""); // 去掉字符串中的 '=' 补齐符号,因为它们只是为了保证 Base64 字符串的长度为 4 的倍数。// `replace(/=/g, "")` 会去除所有的 '='。// 转回二进制字符串for (let i = 0; i < base64Str.length; i++) {let index = charSet.indexOf(base64Str.charAt(i)); // `charSet.indexOf()` 获取当前 Base64 字符在字符集中的索引位置。// 例如:'a' 在字符集中的索引是 0,'b' 是 1,依此类推。binaryStr += index.toString(2).padStart(6, '0'); // 将索引转换为二进制字符串,并通过 `padStart(6, '0')` 确保每个二进制字符串是 6 位(不足补 '0')。// 例如:索引 0 转换为二进制是 '000000',索引 1 转换为二进制是 '000001',等等。}let decodedStr = ""; // 用于存储最终解码得到的原始字符串。// 将二进制字符串按 8 位一组解码for (let i = 0; i < binaryStr.length; i += 8) {let chunk = binaryStr.slice(i, i + 8); // 每次取 8 位二进制(一个字节)// 例如:'01101000','01100101' 等decodedStr += String.fromCharCode(parseInt(chunk, 2)); // `parseInt(chunk, 2)` 将 8 位二进制转换成十进制整数。// `String.fromCharCode()` 将十进制整数转换为字符。// 例如:'01101000' -> 104 -> 'h','01100101' -> 101 -> 'e',等等。}return decodedStr; // 返回解码后的原始字符串
}console.log(decodeBase64("aGVsbG8=")); // "hello"
2. 自定义 MD5
实现
MD5
(消息摘要算法)是常用于数据完整性校验的哈希算法,通常用于生成数据的唯一标识符。
MD5 算法实现
function md5(str) {// 定义常量和初始化变量const hex_chars = "0123456789abcdef";let K = [];let s = [];let i = 0;let x = [];for (i = 0; i < 64; i++) {if (i < 16) K[i] = Math.pow(2, 32 - i) - 1;else K[i] = Math.pow(2, 64 - i) - 1;}// 对数据进行填充和分块处理str = str + String.fromCharCode(0x80);return str;
}
注意: MD5
的实现较为复杂,这只是一个简化的框架。完整的 MD5
算法需要处理数据填充、分块、初始哈希值等多个步骤,通常我们会用现成的库如 CryptoJS
来实现 MD5
。
3. 自定义 AES
加密/解密
AES
(高级加密标准)是一种对称加密算法,用于加密和解密数据。你可以使用 JavaScript 提供的 Web Crypto API 进行 AES 加密,但也可以自定义实现。下面是自定义 AES
的实现方式:
AES 加密与解密实现(使用 crypto-js
库)
// 引入 CryptoJS 库
// <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1-crypto-js.js"></script>// AES 加密
function encryptAES(plaintext, key) {let encrypted = CryptoJS.AES.encrypt(plaintext, key);return encrypted.toString();
}// AES 解密
function decryptAES(ciphertext, key) {let bytes = CryptoJS.AES.decrypt(ciphertext, key);return bytes.toString(CryptoJS.enc.Utf8);
}let key = "mysecretkey12345"; // 密钥
let plaintext = "hello world"; // 明文let encrypted = encryptAES(plaintext, key);
console.log("Encrypted: ", encrypted);let decrypted = decryptAES(encrypted, key);
console.log("Decrypted: ", decrypted); // 输出: hello world
4. 自定义 RSA
加密/解密
RSA
是一种非对称加密算法,通常用于安全的密钥交换。与对称加密算法(如 AES)不同,RSA
使用一对密钥(公钥和私钥)。下面是 RSA
加密的实现:
RSA 加密/解密实现(使用 jsrsasign
库)
由于 RSA
算法涉及复杂的数学运算,JavaScript 并没有原生的实现。通常我们使用如 jsrsasign
等库来实现。
<script src="https://cdn.rawgit.com/kjur/jsrsasign/master/jsrsasign-all-min.js"></script><script>// 生成 RSA 密钥对var rsa = new RSAKey();rsa.generate(1024, '10001'); // 生成 1024 位的 RSA 密钥对// 使用公钥加密var encrypted = rsa.encrypt("hello world");console.log("Encrypted: ", encrypted);// 使用私钥解密var decrypted = rsa.decrypt(encrypted);console.log("Decrypted: ", decrypted); // 输出: hello world
</script>
总结
1)base64
编码/解码
-
将二进制数据转换为 ASCII 字符串,常用于 HTTP 请求、数据传输中。
-
编码和解码都基于字符集,并通过二进制和 Base64 字符集的转换来实现。
2)MD5
哈希
-
用于生成数据的唯一标识符(哈希值),通常用于校验数据完整性。
-
MD5
会将输入数据转为 128 位哈希值,但具有碰撞漏洞,已经不再推荐用于安全应用。
3)AES
加密/解密
-
对称加密算法,广泛应用于数据保护。使用同一个密钥进行加密和解密。
-
实现时通常需要选择加密模式(如 CBC、GCM 等)。
4)RSA
加密/解密
-
非对称加密算法,公钥用于加密,私钥用于解密。用于保护数据传输或密钥交换。
-
实现时涉及较为复杂的数学运算,常常依赖现成库(如
jsrsasign
)。
十二、流量还原与加密函数逆推
流量还原与加密函数逆推,其目标是分析网页或 APP 的加密请求数据是如何构造出来的,并通过调试还原出参数生成的全过程,以便模拟请求爬虫数据。
1. 目标与关键点
目标:
通过抓包和调试,找出:
-
哪些参数是加密的(如:
sign
,token
,w
,encrypt_data
等) -
参数是如何加密的(逻辑路径、加密算法、依赖变量)
-
还原整个 JS 加密函数(逆向核心)
2. 核心步骤详解
步骤 1:打开 DevTools,抓取目标请求
-
F12 打开 DevTools → 进入「Network」面板
-
找到目标接口(通常是 XHR 或 Fetch)
-
查看请求参数,判断哪些可能是加密参数
例子:
POST https://api.example.com/data
Form Data:
{"username": "admin","timestamp": 1715100000,"sign": "abc1234fdasf1adf3q1rq" ← 可疑(加密)
}
步骤 2:关键点定位:打断点或 console.log
重点跟踪:
-
XMLHttpRequest.prototype.send
-
fetch
-
参数构造位置(FormData / JSON.stringify)
-
CryptoJS.MD5()
,btoa()
,AES.encrypt()
,window.crypto
等调用
方法一:XHR 断点(推荐)
-
在 DevTools → Source 面板,按快捷键
Ctrl+Shift+F
-
搜索关键词:
-
send(
-
fetch(
-
XMLHttpRequest
-
sign
,token
,encrypt
,AES
,MD5
,Base64
-
-
选中一处调用 → 右键 "Add breakpoint"
方法二:Breakpoint → XHR/DOM 改写 hook(高级)
// 拦截 send 函数
const oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {console.log("请求参数:", body); // 找加密入口debugger; // 打断点return oldSend.apply(this, arguments);
};
步骤 3:追踪加密函数
目标:从 send(body)
的 body
里,找到加密字段的生成路径,逐步往上追。
常见路径示例:
let params = {username: "admin",timestamp: Date.now(),
};
params.sign = md5(params.username + params.timestamp + "secret_key");xhr.send(JSON.stringify(params));
要逐步分析:
-
sign 是怎么计算的?
-
有没有 secret_key?在哪里?
-
是 md5、AES、RSA 还是自定义加密?
步骤 4:控制台调试还原逻辑
在控制台里调用关键函数,手动试验参数是否能得到一致结果:
// 控制台验证
md5("admin1715100000secret_key") === 请求中 sign
验证成功 → 说明已经逆向出加密逻辑
步骤 5:封装爬虫逻辑
# 用 Python 模拟构造请求参数
import hashlib, time, requeststimestamp = str(int(time.time() * 1000))
sign = hashlib.md5(("admin" + timestamp + "secret_key").encode()).hexdigest()data = {"username": "admin","timestamp": timestamp,"sign": sign,
}res = requests.post("https://api.example.com/data", json=data)
print(res.text)
3. 实战补充
1)美化/格式化混淆代码
-
页面压缩严重时,使用
Ctrl + P
→ 搜索.js
-
使用 Prettify (右键 → "格式化源代码") 重排代码结构
2)使用 debugger
动态插入断点
if (param.key === "target") {debugger;
}
3)Hook 常用加密函数
// Hook CryptoJS.MD5
CryptoJS.MD5 = (function (oldFn) {return function (input) {console.log("MD5 输入:", input);const res = oldFn(input);console.log("MD5 输出:", res.toString());return res;};
})(CryptoJS.MD5);
4. 常见加密参数入口点
参数名 | 常见算法 | 来源字段 |
---|---|---|
sign | md5 / hmac | token + 时间戳 + secret |
w | AES+Base64 | 行为参数、滑动轨迹 |
token | RSA / AES | 登录时保存的密钥 |
x-token | window.crypto | WebCrypto 生成非对称密钥 |
5. 总结流程图
DevTools - Network 抓包↓
找出加密参数(如 sign)↓
Sources 中全局搜索 send / fetch / sign↓
打断点 + 控制台 log 参数↓
分析加密过程(追函数 + 算法 + 依赖变量)↓
用 Python 模拟复现(MD5/AES/RSA 等)