声明:目的是用于记录逆向思路,而非提供结果。仅供学习参考,请勿滥用爬虫
文章目录
前言
学习js逆向,破解广东省公共资源交易平台,详细记录个人探索、踩坑全过程。
网站地址: aHR0cHM6Ly95Z3AuZ2R6d2Z3Lmdvdi5jbi8jLzQ0L2p5Z2c=
一、前戏
首先开始,我个人处理这种文本数据采集基本流程都是这样:
- 检查页面、清除cookie和请求记录、重新刷新页面
- 定位需要的数据请求接口
- 复制接口curl, 转成python请求尝试能否正常使用request/等方式请求
- 关键步骤:
- 看响应内容是否需要解密->先处理解密
- 看是否需要特殊cookie、headers、param参数加密生成
- 编写完整代码
这里我们打开链接、重新刷新后,直接用关键文本去搜(比如标题、链接),这里很显然直接就能搜索出结果:
于是我们就能容易定位到/v2/items 接口,响应数据没有加密,包含了我们想要的内容。
复制curl,通过https://spidertools.cn/#/curl2Request转成python-request请求

执行后能看到正常请求成功(如果403 就是已经过了一会, 已经失效了,重新拿一个就可以)
这里大致看一下请求,很显然包含了cookie、headers中也有些特殊的东西,表单参数没什么特别的
二、cookie生成
尝试将cookie 删除后会直接导致403,所以我们先看下cookie的生成逻辑
- cookie中包含两个值,_horizon_sid和_horizon_uid,全局搜索一下。搜出来只有一个结果,但是可以看到应该是做了映射,那就换成cookie_key_sid、cookie_key_uid重新搜

- 重新搜索,在结果中能够比较明显看出,cookie_key_sid生成的关键逻辑就是genUUID()方法

- 同样的cookie_key_uid

- 直接搜或者打断点再跳转到该方法,比较明显能看出来其实就是用来生成了一些特定格式的随机字符串

- 那么理论上来说 固定cookie也是可以的、或者自己保持格式随便改些数,也没问题。当然这里如果追求严谨,就按它的逻辑抠下来,也完全没问题(只能确定客户端生成逻辑,但在不知道后端校验逻辑的情况下,可以选择保守的方式。只是通常情况下都不需要考虑这么多)。看个人选择
三、headers生成关键函数定位
首先观察headers, 可以比较明显看出来,其中包含几个特别的参数
第一步还是先尝试删除,看是否是必需的。实际尝试后发现这几个都是必需的。
其中X-Dgi-Req-App明显是固定的,X-Dgi-Req-Nonce看起来可能是随机的,X-Dgi-Req-Signature不知道是什么,X-Dgi-Req-Timestamp时间戳就不用说了。所以我们重点确定Nonce和Signature的生成逻辑。
- 先尝试直接全局搜Signature,啥都找不到。搜其他的也是
- 可以先尝试搜索/items定位,确实可以找到一处。但是打上断点调试,调用栈太多了很难看明白,还是尝试换个办法
(以下是header生成核心方法的定位步骤,比较琐碎可以跳过)
-
尝试打上XHR断点,刷新后断在send处

-
然后通过调用堆栈往前找,可以看到这里前几个都是Promise相关的执行逻辑(这里要简单了解Promise即可)。前两个都可以看到header已经生成好了。再往前看到异步调用栈,打个断点看看。

-
这里如果简单理解下代码,可以看出是实现了一个串行异步操作,通过then调用可以看出是依次执行h中的函数

简单从python角度理解就是类似于(并不准确):- 定义了一个函数列表 h : 包含[strip, split, sort ];定义一个c, 初始是"abcdefg";然后就是执行"abcdefg".strip().split().sort()。只是这里是异步的
这里核心在于,我们可以看出c初始是一个Promise对象,通过Promise.resolve(n)定义,而这里n就是一个请求对象的东西
并且从中我们看到这里的headers中还尚未包含我们想要的Signature什么的。
当然这里也会发现请求地址「url」并不是我们想要的,因为我们刷新了页面,其他接口请求前也走到了这里,并不是请求/item时断住的。
但是可以敏锐的察觉到这里或许就是一些请求发起前的预处理,并且在执行h中的某个函数后,就向请求headers中添加了一些东西。
当然我们就需要验证一下, 验证方式就是:- 这里先打个条件断点,让它只有在请求接口是/item时才断住,看下这时headers中有没有Signature
- 如果没有,再然后从这里执行下去,看执行后,headers中会不会有Signature
取消原来的断点,右键点击在原位置改为打上条件断点。

for循环后也打上个断点,可以是return的位置。方便直接跳到最后
这里因为c是Promise链并不能直接看到c的结果,可以逐步向下走,到send处,才能确定。
或者也可以在h列表中的每个函数都打上断点,这样执行到这几个函数时,就可以看到每个函数执行的内容,是否有关于headers的操作。(事实上h的第一个函数执行u(o)就是)
这里过程琐碎,不再细致说明 -
除了通过打上XHR断点,其实还有其他方式。比如我们发现headers中其实包含时间戳Timestamp,那么可以通过搜索new Date、或者Date.now()。比如这里搜索Date.now()

- 可以看到有一堆结果,实际上定位起来也比较麻烦,就是分析代码、打断点,也比较费时间
- 通过以上方法,或者其他方式,最终我们就可以定位到下面这个函数u(),接下来就是抠这个函数了

四、headers生成关键函数逆向
关掉其他断点,只在关键函数u(o)中打个断点,重新刷新页面,断住后,可以看到传入的o是个请求对象,但是url并不是/item。
所以同样,这里我们将普通断点改为条件断点o.url.includes(‘/items’) ,重新刷新
向下走几步,可以看出前面之所以没办法直接搜索出X-Dgi-Req-App是因为是通过qu([56, 62, 52, 11, 23, 62, 39, 18, 16, 62, 54, 25, 25])这种方式实现的,
向下继续走到最后,可以大致分析出以下内容:
- X-Dgi-Req-App 固定
- X-Dgi-Req-Nonce 通过hne(16)方法生成
- 对应/item接口是post请求,只会走到else执行
- X-Dgi-Req-Signature 通过t1方法生成
- 传入四个参数:p是将字典参数转成固定url参数格式,其余就是其他的App、Nonce和固定的’k8tUyS$m’
- 但t1此处结果并不是字符串而是个init对象,包含个words数组
先按整体逻辑将js代码写下来, 整理一下, 删除明显不需要的东西, 补上明显可以看出的东西
function u() {
// _o.inc();
const a = Date.now()
, l = hne(16)
, c = 'k8tUyS$m'
, d = {
'X-Dgi-Req-App': 'ggzy-portal',
'X-Dgi-Req-Nonce': l,
'X-Dgi-Req-Timestamp': a
};
const p = t1({
p: 'type=trading-type&openConvert=false&keyword=&siteCode=44&secondType=A&tradingProcess=&thirdType=%5B%5D&projectType=&publishStartTime=&publishEndTime=&pageNo=1&pageSize=10',
t: a,
n: l,
k: c
});
d['X-Dgi-Req-Signature'] = p
return d
}
console.log(u())
关键就是其中的hne()和t1()方法。重新刷新,开始补
hne()方法没什么特殊的,跳转进入后把对应的缺失的函数和变量补上。会发现其实就是个生成固定长度的随机英文+数字字符串(所以其实固定写死也可以)
function dne(e, t) {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * e + 1, 10);
case 2:
return parseInt(Math.random() * (t - e + 1) + e, 10);
default:
return 0
}
}
const lF = "zxcvbnmlkjhgfdsaqwertyuiop0987654321QWERTYUIOPLKJHGFDSAZXCVBNM"
, fne = lF + "-@#$%^&*+!";
function hne(e) {
return [...Array(e)].map(()=>lF[dne(0, 61)]).join("")
}
t1()函数逆向
t1()函数内部处理相对复杂,这里有好几种方式处理,更简单的和更原始的,这里列举两个例子
- 直接发现是SHA256加密,用SHA256算法替换原来的逻辑
跳转到t1,发现uK函数,其参数就是将已知内容拼接成字符串。pne()方法没什么特别的,直接补就行。

进入uK发现实际调用是_createHelper,搜一下,找到_createHelper,发现是赋值给SHA256方法


这里不管具体逻辑怎么嵌套,已经可以猜测,此处uK实际上就是sha256加密,加密参数也很清晰。那就直接写一个sha256加密试试,对比结果看看
比如用python写一个
import hashlib
def calculate_sha256(message):
sha256_hash = hashlib.sha256()
sha256_hash.update(message.encode('utf-8'))
return sha256_hash.hexdigest()
# 这个message就是u + o + decodeURIComponent(r) + n拼接的
message = 'HKO6UceFvo8xZALuk8tUyS$mkeyword=&openConvert=false&pageNo=1&pageSize=10&projectType=&publishEndTime=&publishStartTime=&secondType=A&siteCode=44&thirdType=[]&tradingProcess=&type=trading-type1712116609325'
sha256_digest = calculate_sha256(message)
print("SHA-256摘要:", sha256_digest)
# 输出:SHA-256摘要: 12d9f16fef71a00c4dd87d50580de92917f2bc8382fd173d281e724c2a791942
浏览器上直接执行到最后,对比下最终请求包含的Signature,显然就是一样的
这里还有个要注意的点是,如果调试时间太长,发起请求是网页会报错什么时间不一致,是因为请求对时间戳有校验超过一定时间差就不行了,重新刷新即可。并且重新刷新前,我们要仅保留最开始的条件断点,使其只有在构造/item接口请求时才断住,不然其他请求走到这也断住了,会产生干扰。条件断点断住后再打开其他的即可
到这里,t1()方法就很清晰了,那就没什么难点的,用js代码实现或者python代码实现都行。
这里就不贴了。
-
通用方式,不管是什么加密,一步步补上
上述使用sha256加密替换的方式,并不适用于所有网站,或者说即使是我们也不一定都能发现。如果是实际开发,当然是怎么简单怎么来,但这里如果我们目的是学习逆向处理思路,还可以参考以下过程。
还是先走到uK方法内,从这里开始处理
这里可以看到创建了一个v.init实例化对象,E是undefined。然后调用finalize。
这里也有两种方式,可以抠出完整的v对象,也可以单纯顺着finalize方法抠。这里就单纯从finalize方法开始抠。
不管v.init(E)。直接看finalize,进入finalize方法,打上断点,执行进去
可以看到这里实际执行了_append方法。进入_append方法。
同样的,接下来进入m.parse,再从m.parse进入h.parse,其中就是对v的处理,然后又创建一个d.init对象,d.init创建了一个包含v、y值的对象,对应word和sigBytes的对象。把这个方法单独写出来

到这里先把代码写一下,目前是梳理到_append()方法
function init(v, y) {
v = this.words = v || [],
y != undefined ? this.sigBytes = y : this.sigBytes = v.length * 4
}
function h_parse(v) {
for (var y = v.length, E = [], B = 0; B < y; B++)
E[B >>> 2] |= (v.

本文记录了学习JS逆向破解广东省公共资源交易平台的全过程。包括数据采集流程、cookie生成逻辑、headers生成关键函数的定位与逆向,还介绍了t1()函数的逆向方法,最后给出了完整的JS和Python代码,强调仅供学习,请勿滥用爬虫。
最低0.47元/天 解锁文章
1798

被折叠的 条评论
为什么被折叠?



