js逆向:pyhon爬虫空气质量,无线debugger,AES,DES,MD5加密
前言
(大家可以结合最后完整代码的注释看文章更佳哦!)
目标网址:空气质量在线
进入目标网址,抓取如下图表格数据。
解决无限debugger
第一次debugger
进入网址,F12与右键均被禁止,采取:设置—更多工具—开发者工具可以打开浏览器调试工具,但会被debugger。
进入如下txsdefwsw调用栈分析,发现左上方定义了3个变量:r,h,e,及一个o函数(看不懂没事),然后下面try里面声明了一个数组a:[“r”, o(“갯”), “g”, o(“갭”), function(t), “b”, “e”, “d”].reverse().join(“”)里面调用了o函数,如下图,通过控制台打印o函数,及function(t),可以发现其实27行上方的代码的作用就是声明了a=‘debugger’。
然后根据如下图可知,生成了一个function(){debugger;}自运行函数,并无限循环,这就是我们卡住的原因。
可在comsol输入下方js代码并运行就能过掉这个断点了;
var A = Function.prototype.constructor;
Function.prototype.constructor = function() {
if (arguments[0] = 'debugger') {
return function(){};
}
else{
return A;
}
}
第二次debugger
但发现我们又进入新的debugger:
阅读代码发现,自运行函数里面有个定时器,定时器里面有个检测屏幕的反调试,和一个function(){debugger;},故setInterval里面并没有正常的逻辑,我们可以在20行下个断点,直接将定时器置空就行,故在console输入如下代码。
注意:打上断点后,需刷新网页,按上述步骤重新过第一次的debugger,然后程序就会停在断点处,再在console执行下方代码就可过第二个debugger。
setInterval = function(){};
到这,发现就可以正常使用浏览器调试工具了。
加密解密流程分析
请求数据加密
选择XHR,改变左边输入框的城市名称,抓包,如下的接口https://www.aqistudy.cn/apinew/aqistudyapi.php为返回所需数据的接口,可以看出请求数据进行了加密。接下来就需要找到h92Fy7f02的生成过程。
通过调用栈分析,进入如下图第三个调用栈
可以找到h92Fy7f02,打上断点调试,发现h92Fy7f02由pDkNw65得到,而pDkNw65由ppz1QG2WgSi(“GETDATA”,{city: “苏州”})得到,故ppz1QG2WgSi为我们需要还原的加密函数。
进入ppz1QG2WgSi加密函数,如下图所示,其中的hex_md5和BASE64均为正常的MD5和BASE64,而AES加密,进行了修改,如下图,传入AES的key与iv进行了md5运算,并截取的不同的部分。
到这里是不是觉得请求所需的加密参数搞定了,并不是,这里还存在两个问题
- 这里所需appId,及AES所需的key和iv都是动态的,这里放到加解密所需的参数获取里一起解决。
- 上图中最后一步是对param的AES加密,并不一定,还可能是DES,也可能直接经过base64加密就可以了,既没有AES也没有DES,如下图是我抓的两次对比,可以发现不一样,这里的解决方式也放到加解密所需的参数获取里一起解决。
故此请求所需加密的参数告一段落。
返回数据解密
可以看出返回数据进行了加密,故需解密。
同样,调用栈分析,在第三个调用栈发现返回的加密数据
打下断点,如下图dDzwEjOYrl9Zu即为解密函数
进入dDzwEjOYrl9Zu解密函数:
解密函数简单:分别经过AES解密—DES解密—base解密
同之前请求参数加密一样AES,DES的key与iv经过MD5处理了,且是动态变化的,在加解密所需的参数分析。
加解密所需的参数获取
思路分析
我的第一个思路就是分析主网页:https://www.aqistudy.cn/的网页源代码:没有直接显示我们需要的参数,然后分析里面的script标签引入的js文件:如下图红框的src里面发现可疑
然后分析对应上图src:js/jquery.min.js?v=1.1的包,如下图为返回的内容,里面五个eval,但找了找都没有用,只有最后一个有点用。
最后一个eval执行出的js代码如下:是一个dswejwehxt命名的Base64解密函数,大家可以先记住这个函数名,后面会出现。
function dswejwehxt(tksl) {
var b = new Base64();
return b.decode(tksl)
}
现在卡住了,主页面居然没有,这里返回我们请求数据的url的包:https://www.aqistudy.cn/apinew/aqistudyapi.php仔细查看一些信息,它的referer并不是主页面https://www.aqistudy.cn/,而是下图中的v=2.3的页面。
故接下来分析https://www.aqistudy.cn/html/city_realtime.php?v=2.3这个请求包,同样发现返回的页面信息并没有直接的我们需要的key啊,iv啊,appID啊,继续分析script标签引入的js,如下框的src=/js/encrypt_的链接发现可疑。(注意,这个src后面的v=的数字是变化的,故需从此处页面源代码正则匹配获得)
如下图是一个src=/js/encrypt_的链接的返回的内容,又是eval
在浏览器执行eval后,如下图(代码太长,仅仅截取的部分),发现还有eval,但是eval(dswejwehxt(…))的js代码,是不是眼熟,dswejwehxt是之前分析的一个Base64解密函数。
故在本地将上述代码Base64解密,并eval后的部分js代码如下:
终于找到我们所需的KEY,iv,appid了
流程
故此,所需的均分析完毕,这里再梳理一遍流程:
- 通过这个https://www.aqistudy.cn/html/city_realtime.php?v=2.3链接,正则匹配得到js/encrypt_的动态url。
- 请求上面得到的动态url,获得响应数据,响应数据类似于这样eval(dswejwehxt(…))的字符串。
- 解密eval(dswejwehxt(…))字符串,得到明文js代码,并从中解析得到加解密所需的参数,包括aes的key,iv;des的key,iv;appid
- 加密请求数据,并封装成需要的post请求格式
- 带着第4步的加密数据,对返回所需接口url:https://www.aqistudy.cn/apinew/aqistudyapi.php发送请求,得到返回的密文数据
- 解密返回的密文数据得到明文。
完整代码
代码
感谢看到这里的小伙伴。
import time
import json
import requests
import re
import execjs
import base64
from Crypto.Cipher import AES, DES
from hashlib import md5
def get_miwen_md5(miwen):
obj = md5()
obj.update(miwen.encode("utf-8"))
miwen = obj.hexdigest()
return miwen
def AES_encrypt(key, iv, miwen):
'''
AES加密:对key,iv进行了MD5加密
:param key AES加密用到的key:
:param iv AES加密用到的iv:
:param miwen: 待加密的数据
:return: 加密后的数据
'''
key = get_miwen_md5(key)[16:32].encode('utf-8')
iv = get_miwen_md5(iv)[:16].encode('utf-8')
aes = AES.new(key, mode=AES.MODE_CBC, IV=iv)
bs = miwen.encode("utf-8")
que = 16 - len(bs) % 16 # 缺少字节的个数
bs += (que * chr(que)).encode("utf-8")
result = aes.encrypt(bs) # 要求加密的内容必须是字节
b64 = base64.b64encode(result).decode()
return b64
def DES_encrypt(key, iv, miwen):
'''
DES加密:对key,iv进行了MD5加密
:param key DES加密用到的key:
:param iv DES加密用到的iv:
:param miwen: 待加密的数据
:return: 加密后的数据
'''
key = get_miwen_md5(key)[:8].encode('utf-8')
iv = get_miwen_md5(iv)[24:].encode('utf-8')
des = DES.new(key=key, mode=DES.MODE_CBC, IV=iv)
bs = miwen.encode("utf-8")
que = 8 - len(bs) % 8 # 缺少字节的个数
bs += (que * chr(que)).encode("utf-8")
result = des.encrypt(bs)
b64 = base64.b64encode(result).decode()
return b64
def get_data(data):
'''
解析得到一个包含key,iv,appId等信息的字典,方便后续使用
:param data:包含key,iv,appId的js明文代码:
:return: 封装好的key,iv,appId的一个字典
'''
dic_data = {}
key_iv_list = re.findall('const\s*(.*?)\s*=\s*"(.*?)";', data)
appId = re.findall("var appId\s*=\s*'(.*?)';", data)[0]
dic_data['appId'] = appId
for key, iv in key_iv_list:
dic_data[key] = iv
print(data.count('AES.encrypt(param'))
if data.count('AES.encrypt(param'):
dic_data['Params_AES_key'], dic_data['Params_AES_iv'] = re.findall('AES.encrypt\(param,\s*(.*?),\s*(.*?)\);', data)[0]
dic_data['Params_AES_key'], dic_data['Params_AES_iv'] = dic_data[dic_data['Params_AES_key']], dic_data[dic_data['Params_AES_iv']]
if data.count('DES.encrypt(param'):
dic_data['Params_DES_key'], dic_data['Params_DES_iv'] = re.findall('DES.encrypt\(param,\s*(.*?),\s*(.*?)\);', data)[0]
dic_data['Params_DES_key'], dic_data['Params_DES_iv'] = dic_data[dic_data['Params_DES_key']], dic_data[dic_data['Params_DES_iv']]
return dic_data
def get_res(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36'
}
resp = requests.get(url, headers=headers)
resp.encoding = 'utf-8'
return resp.text
def de_eval(code):
'''
用来处理eval()的js代码
:param code: 开头eval()的代码
:return: 执行eval后的js代码
'''
js_eval = '''
function decode(code) {
code2 = code.replace(/^eval/, '');
return eval(code2);
}
'''
jsde = execjs.compile(js_eval)
de_eval_code = jsde.call("decode", code)
return de_eval_code
def get_requests_data(city):
'''
:param city:需请求的城市
:return :返回加密后的请求数据
'''
appId = data_dic['appId']
obj = {'city': city}
clienttype = 'WEB'
method = "GETDATA"
timestamp = int(time.time()*1000)
param = {
'appId': appId,
'method': "GETDATA",
'timestamp': timestamp,
'clienttype': clienttype,
'object': obj,
'secret': get_miwen_md5(appId + method + str(timestamp) + clienttype + json.dumps(obj, ensure_ascii=False).replace(" ", ""))
}
# print(param['secret'])
param = base64.b64encode(str.encode(json.dumps(param, ensure_ascii=False).replace(" ", ""), 'utf-8')).decode("utf-8")
# print(param=='eyJhcHBJZCI6IjliZjg4NDFlNWE5MDEwYmY5ZjMyYjNmODhmMDJiYjUyIiwibWV0aG9kIjoiR0VUREFUQSIsInRpbWVzdGFtcCI6MTY1OTYwODI2ODU1NSwiY2xpZW50dHlwZSI6IldFQiIsIm9iamVjdCI6eyJjaXR5Ijoi5YyX5LqsIn0sInNlY3JldCI6ImFiM2E2MDNhZWZkOWFlNTcxZGViYjRjZTAzYWEyMzM3In0=')
# print()
if 'Params_AES_key' in data_dic.keys():
param = AES_encrypt(data_dic['Params_AES_key'], data_dic['Params_AES_iv'], param)
elif 'Params_DES_key' in data_dic.keys():
param = DES_encrypt(data_dic['Params_DES_key'], data_dic['Params_DES_iv'], param)
# print(param)
return param
def decrypt_data(params):
'''
返回数据解密 1. AES解密 2. DES解密 3. B64解密
:param params: 加密数据
:return: 明文数据
'''
# AES解密
AES_key_id, AES_iv_id = re.findall('AES.decrypt\(data,\s*(.*?),\s*(.*?)\);', data)[0]
AES_key, AES_iv = data_dic[AES_key_id], data_dic[AES_iv_id]
AES_key, AES_iv = get_miwen_md5(AES_key)[16:32], get_miwen_md5(AES_iv)[:16]
aes = AES.new(AES_key.encode(), mode=AES.MODE_CBC, IV=AES_iv.encode())
bs = base64.b64decode(params.encode())
params = aes.decrypt(bs).decode().strip("")
# DES解密
DES_key_id, DES_iv_id = re.findall('DES.decrypt\(data,\s*(.*?),\s*(.*?)\);', data)[0]
DES_key, DES_iv = data_dic[DES_key_id], data_dic[DES_iv_id]
DES_key, DES_iv = get_miwen_md5(DES_key)[:8], get_miwen_md5(DES_iv)[24:]
aes = DES.new(DES_key.encode(), mode=DES.MODE_CBC, IV=DES_iv.encode())
bs = base64.b64decode(params.encode())
params = aes.decrypt(bs).decode().strip("")
# B64解密
params = base64.b64decode(params)
params = params.decode("utf-8")
return json.loads(params)
if __name__ == '__main__':
'''
找到返回信息包含加解密所需参数的url:密钥,iv,appId 的url(即为如下的data_url)
'''
url = 'https://www.aqistudy.cn/html/city_realtime.php?v=2.3'
page_source = get_res(url)
data_url = 'https://www.aqistudy.cn/js/encrypt_' + re.findall('<script.*?/js/encrypt_(.*?)"></script>', page_source)[0]
# print(data_url)
'''
请求data_url,返回文本的是eval(dswejwehxt(...)),其中dswejwehxt的次数不确定,dswejwehxt为base64解密
如下代码即解决eval(dswejwehxt(...))文本,得到明文数据data
'''
page_source = get_res(data_url).strip()
data = de_eval(page_source)
while True:
for i in range(data.count("dswejwehxt")):
if i == 0:
data = re.findall("dswejwehxt\('(.*?)'\)", data)[0]
data = base64.b64decode(data.encode()).decode("utf-8")
if data.startswith('eval'):
data = de_eval(data)
if data.count("dswejwehxt") == 0 and not data.startswith('eval'):
break
'''
从data解析得到加解密需要的key,iv,appId等,封装进了data_dic字典
这里注意返回的data,可能是压缩的,也可能是格式化的,故写对两种情况下通用的正则表达式
'''
with open('temp2.js', 'w') as fp:
fp.write(data)
data_dic = get_data(data)
# print(data_dic)
'''
加密请求数据,并封装成需要的字典格式
'''
post_data = get_requests_data('北京')
postdatakey = re.findall('data:\s*\{\s*(.*?):', data)[0]
post_data = { postdatakey: post_data }
# 对返回所需数据的接口发送请求
url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36'
}
params = requests.post(url=url, headers=headers, data=post_data).text
# 将返回的密文数据解密
decrypt_data = decrypt_data(params)
print(decrypt_data)