1.通过Sign加密的请求URL案例
不少网站做数据反爬虫会做一系列的的措施,就包括这里要说的url加密,例如如下url是请求b站游戏id为109905的游戏的评分数据 (无意冒犯,仅首页随机选取做学习案例使用):
https://line1-h5-pc-api.biligame.com/game/comment/summary?game_base_id=109905&ts=1679988261931&request_id=YKceEELUnX5m4aELQACuqX2mG48wv13B&appkey=h9Ejat5tFh81cq8V&sign=00d3c6791f798be86bac45576b31dc32
大家能看出, 这个请求包含了5个参数:
game_base_id: 109905
ts: 1679988261931
request_id: YKceEELUnX5m4aELQACuqX2mG48wv13B
appkey: h9Ejat5tFh81cq8V
sign: 00d3c6791f798be86bac45576b31dc32
通过此请求可以得到正常的返回数据:
{“code”:0,“message”:“成功”,“request_id”:“9dbfc9cac59045709dcda62e32cac670”,“ts”:1679988274628,“data”:{“grade”:8.3,“comment_number”:1709,“valid_comment_number”:1704,“star_number_list”:[246,59,69,41,951],“state”:3}}
回头分析那5个参数,明显对于用户和实际申请数据来说, 只需要game_baseid这一个参数就足够了.
ts明显是时间戳,而request_id翻译过来应该是随机生成的请求ID.
根据经验, 参数sign放请求参数里就大多是加密了. 那么我们假设其他4个数据都是作为加密使用的.
为了验证我们的想法, 我们可以任意修改这5个参数中的一个.
发现无论修改五个中的哪一个, 都会得到错误的返回:
{“code”:-1203,“message”:“invalid request”}
说明五个参数都参与了加密或者验证.
当其他人想通过复制这个URL, 然后仅修改game_base_id参数来获取各游戏的数据, 就都会遇到 invalid request
的错误返回.
2.解密, 考察sign加密方式
这是个不错的防爬虫方案, 那么这个sign具体是如何生成的呢. 我们可以去测试更多的其他请求,来对比所有请求的参数构造.
如appid分别为107681和141的请求的参数:
game_base_id: 107681
ts: 1679989802458
request_id: WjSH6StUYwo0LcFi5yhvfoopzWXKdNTU
appkey: h9Ejat5tFh81cq8V
sign: 528c7740a4e62d01e6f208db4f1409ac
game_base_id: 141
ts: 1679989855423
request_id: vYUoWMZTDHAx6wnFYh6akUXzCmcNd060
appkey: h9Ejat5tFh81cq8V
sign: 1687d53da12c13fb8c3a2d207d434fc4
通过比较多个请求,我们发现
- sign都是32位, 而常用的md5加密也有32位
- appkey都没变
- request_id通常都是随机的,只是参与加密验证,而不会直接验证真伪
之后的解密稍微走了些弯路: 暴力尝试. 这个在踩坑环节再说. 下面继续说正确的解密姿势
既然目前最大的可能是通过sign加密,那么我们就开始下一步,找源码.
我这里是chrome浏览器, 按F12进入调试模式, F5刷新请求数据的页面. Ctrl+Shift+F打开全局搜索, 搜 sign, 会搜到很多, 快速过滤掉不对的. 就找到了这一段代码:
i[n(137)] = "h9Ejat5tFh81cq8V",
n(157));
return i.sign = h(""[n(130)](d[n(122)](i, {
sort: function(t, e) {
return t[n(152)](e)
}
}))[n(130)](e))
注意到这里甚至有我们的appkey, 所以基本可以确定这里就是我们要找到JS代码. 这里sign应该就是我们想找的算法, 但是这段JS被混淆处理了.
这里又走了些弯路–反混淆化, 也在后面踩坑环节说, 先继续正确的解密方法
我们在这里打断点,刷新页面
首先看到这最外围的h()函数还是一个未知函数,我们先不管. 然后复制函数里所有东西,到控制台输出
一下就明朗了, 里面就是被加密的字符串. 当然我们可以继续细致的看各个部分的含义,比如出现了2次的n(130)函数是concat() 用concat连接的这段函数,得到的是拼起来的parameters. 包含了sign以外的全部4个参数. 拼接格式也得到了, 用’&'拼接, 而最后还拼接了一个字符串, 可以知道这个字符串是变量e.
这个e是不属于参数的, 我们称呼它为secret. 至于这个值具体如何取的. 我们先验证这个值是否固定, 如果不固定, 那么就要继续逆推JS.
我们再去打开其他appid的网站.重复以上调试方式.发现这个e值固定不变.
另外我们注意到代码里有个sort关键字. 因此sign的计算方式已经呼之欲出了:
将请求中所有明参数按key的字母排序, 以key=value格式化, 再用&拼接起来, 最后再拼接上secret得到原始字符, 对此字符进行md5加密, 得到最后的sign
然后我们来验证我们的想法. 下面是根据此猜想写的一个python自动生成带sign加密参数的URL的代码
def taskMd5():
import requests
url = getURL(107681)
res = requests.get(url)
if res.status_code == 200:
print(res.text)
def getURL(gameid):
appkey = 'h9Ejat5tFh81cq8V'
secret = #上文提到的e值,这里按网站要求不暴露实际值#
params = {'appkey': appkey,
'game_base_id': gameid,
'request_id': 'cpdThHlVU7jZMfFzn33tBy7AvKMypvP5',
'ts': math.ceil(time.time()*1000)}
str_params = []
for k in params.keys():
str_params.append(f'{k}={params[k]}')
org = '&'.join(str_params) + secret
#print(org)
sign = getMD5(org)
#print(sign)
params['sign'] = sign
str_params.append(f'sign={sign}')
url = 'https://line1-h5-pc-api.biligame.com/game/comment/summary?' + '&'.join(str_params)
#print(url)
return url
任意测试一个实际存在的gameid, 返回成功
{“code”:0,“message”:“成功”,“request_id”:“c0bdf44236bf428ebc968813d8aad1ea”,“ts”:1679993440018,“data”:{“grade”:8.7,“comment_number”:18167,“valid_comment_number”:17708,“star_number_list”:[1412,394,744,1158,9619],“state”:3}}
验证成功!
至此我们通过此案例介绍了通过sign加密请求的常用方式, 即:
将参与加密的参数(不一定是所有参数), 按一定方式排序, 一定格式拼接, 再加上一个秘xx钥组合成原始字符串, 然后使用MD5或者SHA等加密, 得到sign, 在响应时进行验证
3.踩坑
最后说下坑
- 暴力反解MD5误区.
本以为这个sign加密可能appkey就是秘oo钥. sign可以只通过这4个参数计算, 就用了暴力反解, 尝试各种格式拼接计算出加密结果与实际sign值比较:
def md5_crack():
format_param = ['{0}={1}', '{1}']
str_splits = [ '|', '&', '']
key_format = ['appkey={}', "{}"]
appkey = 'h9Ejat5tFh81cq8V'
param = {'game_base_id': '109905', 'request_id': 'ApHV8L2fNmxD5d2dd2mIPsuRo8YJvYhI', 'ts': '1679884518004'}
sign = "cedaac200fd97738677b4beb3a2c50bd"
for f in format_param:
for kf in key_format:
for sp in str_splits:
str_params = []
for key in param.keys():
concat = f.format(key, param[key])
str_params.append(concat)
alls = [kf.format(appkey)] + str_params
org = sp.join(alls)
print(org)
print(sign==getMD5(org), getMD5(org))
print()
alls = str_params + [kf.format(appkey)]
org = sp.join(alls)
print(org)
print(sign==getMD5(org), getMD5(org))
print()
- 反混淆误区. 发现加密代码被混淆化处理之后, 做反混淆的处理, 发现该代码混淆后足足有8W行, 很难完成反混淆处理.