第1知识点:关于json请求体
第2知识点:关于精准请求(如何排除干扰请求)
第3知识点:入口定位
一、关键字方法
(1) 方法关键字 encrypt decrypt
(2) key关键字
第4知识点:断点与断点调试
普通断点
XHR断点
条件断点
日志断点
脚本断点
逆向爬虫实战
JS逆向爬虫是一种通过分析和执行JavaScript代码来提取动态网页数据的技术。随着现代网站越来越多地使用JavaScript进行内容加载,传统的爬虫方法(如仅使用HTTP请求)常常无法获取所需的数据,因此需要使用逆向工程技术来解析JavaScript。
1. Python逆向解密
某目标资源交易平台首页,PageList即目标接口
先通过Python基础爬虫抓取该响应数据,这里可以复制curl信息去转换为爬虫代码,这里给大家推家一个不错的网站:
https://curlconverter.com/

但是很明显,服务器对该URL线路返回的数据做了某种加密,客户端浏览器该URL请求拿到的也是加密数据,但是,浏览器的该网站窗口在发起第一次首页请求的时候获取了大量的JS逻辑代码,即客户端和该网站交流的正确姿势,所以目标请求(PageList
)返回的加密数据,会有后续的某段JS代码进行解密,那么怎么从千千万万行JS代码中找到解密的位置呢,我们在JS逆向中主要有几下几种技巧
- 关键字搜索
- 请求堆栈
- hook钩子
我们先来学习第一个,关于关键字搜索,即客户端浏览器接收到的该网站所有文件(主要是JS文件)中全局搜索关键字,那么搜索关键字,常用的有以下几种
- 方法关键字(比如encrypt,decrypt,JSON.stringify,JSON.parse等)
- key关键字
- 路径关键字
- header关键字
- 拦截方法关键字
我们一般首选的是方法关键字,这背后的逻辑就是我们怀疑JS加密代码也会用到encrypt这个名字,解密会用到decrypt,那有没有可能人家写的JS并有没有用呢,当然,所以这只是思路的一种,我们爬虫逆向就是有逻辑的连蒙带猜,没有一招屡试不爽,只能按经验不断测。因为我们要找解密的算法,所以搜索一下decrypt那么怎么全局搜索呢?如图所示
搜到了太多,不好排查,所以我们要进一步加限制,那么接下来的思路就是如果JS解密用到decrypt,应该大概率(注意没有绝对一说)是作为一个函数调用,所以我们尝试搜索decrypt(
,这样就只有8个嫌疑点
,方便后续排查了
加下来就是最精彩的地方了,这八个嫌疑犯
到底谁是解密的元凶呢?我们可以通过断点技术进行排查,即给这八个位置都加上断点,然后重新触发PageList请求的事件,那么请求发出,数据返回,本地JS解密,如果元凶
在这八个嫌疑犯中,那么一定会被执行,又因为加了断点,即一定会被断下来,所以结论是,能被断下来的99%就是解密位置,所以先加断点:
我们可以发现,这里明显用得是AES算法,通过鼠标悬浮显示或者控制台打印,可以判断出t就是解密的数据,r["e"]就是key,r["i"]就是iv
至此,知道了是aes算法,又找到了key和iv,我们就可以回到开始的代码中完成解密实现了!
2. Python逆向加密
爬虫的核心是海量抓取数据,虽然现在破解了解密逻辑,但是我们可以批量爬取数据吗?
事实是我们改变data中的任何一个参数,比如我们想修改pageSize值,即获取其它页数的数据时,就直接被服务器告知咱们是恶意请求了,它是怎么知道这是一个爬虫程序的呢?原因很简单,浏览器执行本地的本地JS发出这次请求前以这些数据为参数生成了一个加密值,即请求头中的portal-sign
,也就是json_data
和portal-sign
是对应的,关联的。json_data
和portal-sign
一起发到服务器,服务器会解密portal-sign
再和json_data
比对,如果不一致,说明不是正常请求,有数据篡改,所以响应我们一个警告。
有同学可能会问,那我们不要改数据和sign值不就一致了嘛,但是问题在于,不修改查询你怎么批量爬虫呢,我想要每一页的数据,理论上我们更改pageSize就好了,但是对方服务器做出了反爬,所以我们必须根据不同的page等查询参数计算出与之配对的sign才能通过服务器的校验。所以必须破解客户端JS代码根据参数如何生成的sign值。
上面的解密是ajax请求回来,本地的JS做的解密,这一步我们要破解的是发送ajax请求,携带的参数portal-sign的加密函数,所以思路也可以想逆向解密,方法关键字,搜关键字encrypt,同样因为JS做加密时大概率调用算法库,函数名很大概率是叫encrypt,于是全局搜索
给这13个位置都加上断点,然后刷新页面,触发目标请求,发现没有任何一个端点断下来,说明这个方法关键字失效了,这很正常,没有什么技巧一招鲜吃遍天。接下来我们换key关键字。
key关键字的核心是这个sign值在本地生成,直接搜是搜不到的,但是我们想一下,这个值生成后紧接着的操作应该是什么呢?是不是应该大概率要和键portal-sign
组成键值对
所以我们全局搜索portal-sign,右边如何是个函数调用再赋值就很可能是我们的加密函数,格式可如下:
portal-sign = xxx()
或者
{
portal-sign: xxx()
}
全局搜索:
搜索出一个位置,加上断点确认,刷新页面,果然被断住,参数检查也的确是查询参数,所以这个f.getSign就是我们要找的关于portal-sign的生成位置。
这个d函数内代码就是sign生成的逻辑。
断点d函数第一行,进入当前断点,解析d的算法逻辑。
- 判断参数对象的各个键的值有没有空值(一般没有空值,所以这一步对数据实际没什么操作)
- r["a"]是一个固定字符串,每次请求都一样
- u(t)是将参数对象t排序后组装成指定格式的字符串,比如将对象{a:1,b:2}组装成"a1b2"
- 将r["a"]的固定字符串和u(t)生成的参数字符串拼接在一起,得到n
最关键的是最后一步s(n)做了什么,接下来在s(n)的位置加上断点,进入到s(n)中

发现s函数就是md5的摘要算法,即最后一步就是将n字符串计算md5值作为portal-sign。
至此,关于portal-sign的加密逻辑就完全破解了。
所以我们接下来只需要在python的爬虫代码中像JS那样根据具体的参数生成固定的portal-sign即可请求通过。
代码如下:
from hashlib import md5
import time
import requests
import base64
import json
from Crypto.Util.Padding import pad, unpadfrom Crypto.Cipher import AEStimer = int(time.time() * 1000)cookies = {# 略
}headers = {# 略
}def decrypt(response):base64_encrypt_data = response.json().get("Data")# 一、 base64的解码encrypt_data = base64.b64decode(base64_encrypt_data)print("encrypt_data:", encrypt_data)# 二、解密数据# (1) 确认key和iv必须保证是16或者24,或者32key = 'EB444973714E4A40876CE66BE45D5930'.encode()iv = 'B5A8904209931867'.encode()# (2) 构建一个aes对象aes = AES.new(key, AES.MODE_CBC, iv)# (3) 对数据解密data = aes.decrypt(encrypt_data)data = unpad(data, 16).decode()data = json.loads(data).get("Table")print(data)# 根据data生成sign值def get_sign(data):# (1) 固定字符串s = 'B3978D054A72A7002063637CCDF6B2E5'# (2) 将参数整理成某称格式字符串l = sorted(data.items(), key=lambda i: i[0])data_str = ""for key, val in l:data_str += key + str(val)print("data_str:::", data_str)# (3)s = s + data_strmd5_obj = md5()md5_obj.update(s.encode())return md5_obj.hexdigest()def main():data = {'type': '12','IS_IMPORT': 1,'pageSize': 6,'ts': timer,}sign = get_sign(data)print("sign:::", sign)headers["portal-sign"] = signurl = 'https://xxxxx.fj.gov.cn/FwPortalApi/Article/PageList'response = requests.post(url, cookies=cookies, headers=headers,json=data)decrypt(response)if __name__ == '__main__':main()
这样我们就完成了整个加密逆向和解密逆向的全部破解,实现了批量明文数据抓取的目的。
3. JS逆向加密与解密
上面我们找到加密函数get_sign后,进入内部,找到了d函数,通过读取d函数的加密逻辑,再用Python代码去实现,这种方式叫Python逆向,但是这里有一个问题,就是d函数如果非常复杂呢,有一百个步骤,我们一是读取逻辑困难,二是通过Python复现麻烦,所以这种方式并不理想,我们真正的常见的玩法是JS逆向,这才是”主角“终于登场。
所谓JS逆向的概念很简单,放在这个案例中,就是当我们找到d函数,不要再去“收拾”里面的逻辑,而是把整个d函数“铲走”到本地,然后用一个叫node.js的解释器来运行出结果portal-sign,为爬虫代码所调用。
我们在本地先创建一个名为main.js
的文件
function d(t) {for (var e in t)"" !== t[e] && void 0 !== t[e] || delete t[e];var n = r["a"] + u(t);return s(n).toLocaleLowerCase()
}// 测试d函数
// t就是模拟的查询参数对象
t = {"ts": 1727168121817,"type": "12","IS_IMPORT": 1,"pageSize": 3
}
sign = d(t)
console.log(sign)
可以右击run执行该js文件,也可以在终端通过node main,js
来运行,结果报错:

找不到r变量,因为我们分析知道r["a"]是固定值,所以直接替换就可以了,然后继续报错:

这个很好理解,我们只拷贝了d函数,而d函数虽然是生成sign的主函数,但依赖了外部的其他函数或者变量,所以我们需要将报错的依赖也拷贝过来,保证d函数的顺利执行
u函数中又用到l,所以一起拷贝到本地,如下:
function l(t, e) {return t.toString().toUpperCase() > e.toString().toUpperCase() ? 1 : t.toString().toUpperCase() == e.toString().toUpperCase() ? 0 : -1
}function u(t) {for (var e = Object.keys(t).sort(l), n = "", a = 0; a < e.length; a++)if (void 0 !== t[e[a]])if (t[e[a]] && t[e[a]] instanceof Object || t[e[a]] instanceof Array) {var i = JSON.stringify(t[e[a]]);n += e[a] + i} elsen += e[a] + t[e[a]];return n
}function d(t) {for (var e in t)"" !== t[e] && void 0 !== t[e] || delete t[e];var n = 'B3978D054A72A7002063637CCDF6B2E5' + u(t);return s(n).toLocaleLowerCase()
}// 测试d函数
// t就是模拟的查询参数对象
t = {"ts": 1727168121817,"type": "12","IS_IMPORT": 1,"pageSize": 3
}
sign = d(t)
console.log(sign)
最后报错:

最后我们通过进入s函数中知道是md5算法,涉及到算法,就到了最后一步,不用搬移算法函数,因为我们可以在本地调用js的算法库去“平替”该代码:
const CryptoJS = require('crypto-js');function l(t, e) {return t.toString().toUpperCase() > e.toString().toUpperCase() ? 1 : t.toString().toUpperCase() == e.toString().toUpperCase() ? 0 : -1
}function u(t) {for (var e = Object.keys(t).sort(l), n = "", a = 0; a < e.length; a++)if (void 0 !== t[e[a]])if (t[e[a]] && t[e[a]] instanceof Object || t[e[a]] instanceof Array) {var i = JSON.stringify(t[e[a]]);n += e[a] + i} elsen += e[a] + t[e[a]];return n
}function d(t) {for (var e in t)"" !== t[e] && void 0 !== t[e] || delete t[e];var n = 'B3978D054A72A7002063637CCDF6B2E5' + u(t);return CryptoJS.MD5(n).toString().toLocaleLowerCase()
}// 测试d函数
// t就是模拟的查询参数对象
t = {"ts": 1727168121817,"type": "12","IS_IMPORT": 1,"pageSize": 3
}
sign = d(t)
console.log(sign)
结果生成:

接下来就是Python怎么调用这个JS代码了,这里用到的是一个Python的模块:
解密其实也是一样的,定位到解密的b函数,直接拷贝走

key和iv是固定的,直接平替,h.a是算法库对象,也用本地npm的替换,结果如下:
function b(t) {var e = CryptoJS.enc.Utf8.parse('EB444973714E4A40876CE66BE45D5930'), n = CryptoJS.enc.Utf8.parse('B5A8904209931867'), a = CryptoJS.AES.decrypt(t, e, {iv: n,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return a.toString(CryptoJS.enc.Utf8)
}
最后在Python中爬到加密数据通过调用b函数实现解密,整个过程就是完整的JS逆向。