芝麻HTTP:JavaScript加密邏輯分析與Python模擬執行實現數據爬取

本節來講明一下 JavaScript 加密邏輯分析並利用 Python 模擬執行 JavaScript 實現數據爬取的過程。在這裏以中國空氣質量在線監測分析平臺爲例來進行分析,主要分析其加密邏輯及破解方法,並利用 PyExecJS 來實現 JavaScript 模擬執行來實現該網站的數據爬取。javascript

反混淆

JavaScript 混淆以後,實際上是有反混淆方法的,最簡單的方法即是搜索在線反混淆網站,這裏提供一個:http://www.bm8.com.cn/jsConfusion/,咱們將 jquery-1.8.0.min.js 中第二行 eval 開頭的混淆後的 JavaScript 代碼複製一下,而後粘貼到這個網站中進行反混淆,就能夠看到正常的 JavaScript 代碼了,搜索一下就能夠找到 getServerData() 方法了,能夠看到這個方法確實發出了一個 Ajax 請求,請求了剛纔咱們分析到的接口:php

那麼到這裏咱們又能夠發現一個很關鍵的方法,那就是 getParam(),它接受了 method 和 object 參數,而後返回獲得的 param 結果就做爲 POST Data 參數請求接口了,因此 param 就是加密後的 POST Data,一些加密邏輯都在 getParam() 方法裏面,其方法實現以下:java

var getParam = (function () {
        function ObjectSort(obj) {
            var newObject = {};
            Object.keys(obj).sort().map(function (key) {
                newObject[key] = obj[key]
            });
            return newObject
        }
        return function (method, obj) {
            var appId = '1a45f75b824b2dc628d5955356b5ef18';
            var clienttype = 'WEB';
            var timestamp = new Date().getTime();
            var param = {
                appId: appId,
                method: method,
                timestamp: timestamp,
                clienttype: clienttype,
                object: obj,
                secret: hex_md5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj)))
            };
            param = BASE64.encrypt(JSON.stringify(param));
            return AES.encrypt(param, aes_client_key, aes_client_iv)
        }
    })();

能夠看到這裏使用了 Base64 和 AES 加密。加密以後的字符串便做爲 POST Data 傳送給服務器了,而後服務器再進行解密處理,而後進行邏輯處理,而後再對處理後的數據進行加密,返回了加密後的數據,那麼 JavaScript 再接收到以後再進行一次解密,再渲染才能獲得正常的結果。node

因此這裏還須要分析服務器傳回的數據是怎樣解密的。順騰摸瓜,很容易就找到一個 decodeData() 方法,其定義以下:jquery

function decodeData(data) {
        data = AES.decrypt(data, aes_server_key, aes_server_iv);
        data = DES.decrypt(data, des_key, des_iv);
        data = BASE64.decrypt(data);
        return data
    }

嗯,這裏又通過了三層解密,才把正常的明文數據解析出來。api

因此一切都清晰了,咱們須要實現兩個過程才能正常使用這個接口,即實現 POST Data 的加密過程和 Response Data 的解密過程。其中 POST Data 的加密過程是 Base64 + AES 加密,Response Data 的解密是 AES + DES + Base64 解密。加密解密的 Key 也都在 JavaScript 文件裏能找到,咱們用 Python 實現這些加密解密過程就能夠了。服務器

因此接下來怎麼辦?接着剛啊!app

接着剛纔怪!ide

何須去費那些事去用 Python 重寫一遍 JavaScript,萬一兩者裏面有數據格式不統一或者兩者因爲語言不兼容問題致使計算結果誤差,上哪裏去 Debug?post

那怎麼辦?這裏咱們藉助於 PyExecJS 庫來實現 JavaScript 模擬就行了。

PyExecJS

PyExecJS 是一個可使用 Python 來模擬運行 JavaScript 的庫。你們可能據說過 PyV8,它也是用來模擬執行 JavaScript 的庫,但是因爲這個項目已經不維護了,並且對 Python3 的支持很差,並且安裝出現各類問題,因此這裏選用了 PyExecJS 庫來代替它。

首先咱們來安裝一下這個庫:

pip install PyExecJS

使用 pip 安裝便可。

在使用這個庫以前請確保你的機器上安裝瞭如下其中一個JS運行環境:

  • JScript
  • JavaScriptCore
  • Nashorn
  • Node
  • PhantomJS
  • PyV8
  • SlimerJS
  • SpiderMonkey

PyExecJS 庫會按照優先級調用這些引擎來實現 JavaScript 執行,這裏推薦安裝 Node.js 或 PhantomJS。

接着咱們運行代碼檢查一下運行環境:

import execjs
print(execjs.get().name)

運行以後,因爲我安裝了 Node.js,因此這裏會使用 Node.js 做爲渲染引擎,結果以下:

Node.js (V8)

接下來咱們將剛纔反混淆的 JavaScript 保存成一個文件,叫作 encryption.js,而後用 PyExecJS 模擬運行相關的方法便可。

首先咱們來實現加密過程,這裏 getServerData() 方法其實已經幫咱們實現好了,並實現了 Ajax 請求,但這個方法裏面有獲取 Storage 的方法,Node.js 不適用,因此這裏咱們直接改寫下,實現一個 getEncryptedData() 方法實現加密,在 encryption.js 裏面實現以下方法:

function getEncryptedData(method, city, type, startTime, endTime) {
    var param = {};
    param.city = city;
    param.type = type;
    param.startTime = startTime;
    param.endTime = endTime;
    return getParam(method, param);
}

接着咱們模擬執行這些方法便可:

import execjs

# Init environment
node = execjs.get()

# Params
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'

# Compile javascript
file = 'encryption.js'
ctx = node.compile(open(file).read())

# Get params
js = 'getEncryptedData("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
params = ctx.eval(js)

這裏咱們首先定義一些參數,如 method、city、start_time 等,這些均可以經過分析 JavaScript 很容易得出其規則。

而後這裏首先經過 execjs(即 PyExecJS)的 get() 方法聲明一個運行環境,而後調用 compile() 方法來執行剛纔保存下來的加密庫 encryption.js,由於這裏麪包含了一些加密方法和自定義方法,因此只有執行一遍才能調用。

接着咱們再構造一個 js 字符串,傳遞這些參數,而後經過 eval() 方法來模擬執行,獲得的結果賦值爲 params,這個就是 POST Data 的加密數據。

接着咱們直接用 requests 庫來模擬 POST 請求就行了,也不必用 jQuery 自帶的 Ajax 了,固然後者也是可行的,只不過須要加載一下 jQuery 庫。

接着咱們用 requests 庫來模擬 POST 請求:

# Get encrypted response text
api = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
response = requests.post(api, data={'d': params})

這樣 response 的內容就是服務器返回的加密的內容了。

接下來咱們再調用一下 JavaScript 中的 decodeData() 方法便可實現解密:

# Decode data
js = 'decodeData("{0}")'.format(response.text)
decrypted_data = ctx.eval(js)

這樣 decrypted_data 就是解密後的字符串了,解密以後,其實是一個 JSON 字符串:

{'success': True, 'errcode': 0, 'errmsg': 'success', 'result': {'success': True, 'data': {'total': 22, 'rows': [{'time': '2018-01-25 00:00:00', 'temp': '-7', 'humi': '35', 'wse': '1', 'wd': '東北風', 'tq': '晴'}, {'time': '2018-01-25 01:00:00', 'temp': '-9', 'humi': '38', 'wse': '1', 'wd': '西風', 'tq': '晴'}, {'time': '2018-01-25 02:00:00', 'temp': '-10', 'humi': '40', 'wse': '1', 'wd': '東北風', 'tq': '晴'}, {'time': '2018-01-25 03:00:00', 'temp': '-8', 'humi': '27', 'wse': '2', 'wd': '東北風', 'tq': '晴'}, {'time': '2018-01-25 04:00:00', 'temp': '-8', 'humi': '26', 'wse': '2', 'wd': '東風', 'tq': '晴'}, {'time': '2018-01-25 05:00:00', 'temp': '-8', 'humi': '23', 'wse': '2', 'wd': '東北風', 'tq': '晴'}, {'time': '2018-01-25 06:00:00', 'temp': '-9', 'humi': '27', 'wse': '2', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 07:00:00', 'temp': '-9', 'humi': '24', 'wse': '2', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 08:00:00', 'temp': '-9', 'humi': '25', 'wse': '2', 'wd': '東風', 'tq': '晴轉多雲轉多雲間晴'}, {'time': '2018-01-25 09:00:00', 'temp': '-8', 'humi': '21', 'wse': '3', 'wd': '東北風', 'tq': '晴轉多雲轉多雲間晴'}, {'time': '2018-01-25 10:00:00', 'temp': '-7', 'humi': '19', 'wse': '3', 'wd': '東北風', 'tq': '晴轉多雲轉多雲間晴'}, {'time': '2018-01-25 11:00:00', 'temp': '-6', 'humi': '18', 'wse': '3', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 12:00:00', 'temp': '-6', 'humi': '17', 'wse': '3', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 13:00:00', 'temp': '-5', 'humi': '17', 'wse': '2', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 14:00:00', 'temp': '-5', 'humi': '16', 'wse': '2', 'wd': '東風', 'tq': '多雲'}, {'time': '2018-01-25 15:00:00', 'temp': '-5', 'humi': '15', 'wse': '2', 'wd': '北風', 'tq': '多雲'}, {'time': '2018-01-25 16:00:00', 'temp': '-5', 'humi': '16', 'wse': '2', 'wd': '東北風', 'tq': '多雲'}, {'time': '2018-01-25 17:00:00', 'temp': '-5', 'humi': '16', 'wse': '2', 'wd': '東風', 'tq': '多雲'}, {'time': '2018-01-25 18:00:00', 'temp': '-6', 'humi': '18', 'wse': '2', 'wd': '東風', 'tq': '晴間多雲'}, {'time': '2018-01-25 19:00:00', 'temp': '-7', 'humi': '19', 'wse': '2', 'wd': '東風', 'tq': '晴間多雲'}, {'time': '2018-01-25 20:00:00', 'temp': '-7', 'humi': '19', 'wse': '1', 'wd': '東風', 'tq': '晴間多雲'}, {'time': '2018-01-25 21:00:00', 'temp': '-7', 'humi': '19', 'wse': '0', 'wd': '南風', 'tq': '晴間多雲'}]}}}

大功告成!

這樣咱們就能夠成功獲取溫度、溼度、風力、天氣等信息了。

另外這部分數據其實不全,還有 PM 2.五、AQI 等數據須要用另一個 method 參數 GETDETAIL,修改一下便可獲取這部分數據了。

再日後的數據就是解析和存儲了,這裏再也不贅述。

相關文章
相關標籤/搜索