【python爬虫】js逆向:空气质量在线平台,解决反调试,加密

前言

(大家可以结合最后完整代码的注释看文章更佳哦!)
目标网址:空气质量在线
进入目标网址,抓取如下图表格数据。
在这里插入图片描述

解决无限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,打上断点调试,发现h92Fy7f02pDkNw65得到,而pDkNw65由ppz1QG2WgSi(“GETDATA”,{city: “苏州”})得到,故ppz1QG2WgSi为我们需要还原的加密函数。
在这里插入图片描述
进入ppz1QG2WgSi加密函数,如下图所示,其中的hex_md5和BASE64均为正常的MD5和BASE64,而AES加密,进行了修改,如下图,传入AES的key与iv进行了md5运算,并截取的不同的部分。

在这里插入图片描述
在这里插入图片描述

到这里是不是觉得请求所需的加密参数搞定了,并不是,这里还存在两个问题

  1. 这里所需appId,及AES所需的key和iv都是动态的,这里放到加解密所需的参数获取里一起解决。
  2. 上图中最后一步是对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了

流程

故此,所需的均分析完毕,这里再梳理一遍流程:

  1. 通过这个https://www.aqistudy.cn/html/city_realtime.php?v=2.3链接,正则匹配得到js/encrypt_的动态url。
  2. 请求上面得到的动态url,获得响应数据,响应数据类似于这样eval(dswejwehxt(…))的字符串。
  3. 解密eval(dswejwehxt(…))字符串,得到明文js代码,并从中解析得到加解密所需的参数,包括aes的key,iv;des的key,iv;appid
  4. 加密请求数据,并封装成需要的post请求格式
  5. 带着第4步的加密数据,对返回所需接口url:https://www.aqistudy.cn/apinew/aqistudyapi.php发送请求,得到返回的密文数据
  6. 解密返回的密文数据得到明文。

完整代码

代码

感谢看到这里的小伙伴。

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)

运行结果

在这里插入图片描述

爬取空气质量检测网的部分城市的历年每天质量数据 思路----------------------------------------- 从某城市的空气质量网页获取某市每月的链接,再爬取每个月的表格数据。连云港市:https://www.aqistudy.cn/historydata/daydata.php?city=连云港 连云港2014年5月的空气质量:https://www.aqistudy.cn/historydata/daydata.php?city=连云港&month=2014-05 遇到的问题----------------------------------------- 获取的网页中的表格数据隐藏,尝试requests无法获取。判断可能是动态加载的网页 尝试----------------------------------------- 1. 通过XHR,js查找隐藏数据的加载网页,没有找到。 2. 使用phantomjs.get() result=pd.read_html ,可以获得隐藏的表格数据,但是并不稳定,只是偶尔出现加载的表格数据,无法大规模的获取 解决方法----------------------------------------- 查找资料得知这个网站的表格数据在Console里的items中, 使用selenium的webdriver.firefox(),driver.execute_script("return items") 数据可获得。 仍遇到的问题:----------------------------------------- 爬取一个网页可获得数据,但是连续的获取网页,会出现两个错误。 1.Message: ReferenceError: items is not defined 2.connection refused 解决方法: 1.connection refused问题,可能是网页开太多,使用driver.quit() 2. 如果 execute_script 还是出错,可尝试pd.read_html获取信息。之前用phantomjs获取的时候输出空的表格,可能由于加载不够,用 Waite直到table出现之后再获取网页 Element=wait.until(EC.element_to_be_clickable((By.XPATH,"/html/body/div[3]/div[1]/div[1]/table/tbody"))) 3.之后出现偶尔出现输出为空,使用循环,如果输出表格为空,再重新获取。 if len(result)>1: filename = str(month) + '.xls' result.to_excel('E:\python\案例程序\data\\' + filename) print('成功存入'+filename) driver.quit() else: driver.quit() return getdata(monthhref,month)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值