之前在分析某網站時也說過一些,python爬蟲 - js逆向之取巧秒解webpack打包的加密參數
不過,可能還是有些朋友不太理解怎么找的,你怎么就知道找到那個main.js文件呢?所以,肯定是有規律的,以下就是用實際的案例介紹規律
以下內容轉自公眾號 “k哥爬蟲”,原帖:點我
聲明
本文章中所有內容僅供學習交流,抓包內容、敏感網址、數據接口均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切后果均與作者無關,若有侵權,請聯系我立即刪除!
逆向目標
- 主頁:
aHR0cHM6Ly93d3cuZ205OS5jb20v
- 接口:
aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=
- 逆向參數:Query String Parameters:
password: kRtqfg41ogc8btwGlEw6nWLg8cHcCW6R8JaeM......
逆向過程
抓包分析
來到首頁,隨便輸入一個賬號密碼,點擊登陸,抓包定位到登錄接口為 aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=
,GET 請求,Query String Parameters 里,密碼 password 被加密處理了。
加密入口
直接搜索關鍵字 password 會發現結果太多不好定位,使用 XHR 斷點比較容易定位到加密入口,有關 XHR 斷點調試可以查看 K 哥往期的教程:【JS 逆向百例】XHR 斷點調試,Steam 登錄逆向,如下圖所示,在 home.min.js 里可以看到關鍵語句 a.encode(t.password, s)
,t.password
是明文密碼,s
是時間戳。
跟進 a.encode()
函數,此函數仍然在 home.min.js 里,觀察這部分代碼,可以發現使用了 JSEncrypt,並且有 setPublicKey 設置公鑰方法,由此可以看出應該是 RSA 加密,具體步驟是將明文密碼和時間戳組合成用 | 組合,經過 RSA 加密后再進行 URL 編碼得到最終結果,如下圖所示:
RSA 加密找到了公鑰,其實就可以直接使用 Python 的 Cryptodome 模塊來實現加密過程了,代碼如下所示:
import time import base64 from urllib import parse from Cryptodome.PublicKey import RSA from Cryptodome.Cipher import PKCS1_v1_5 password = "12345678" timestamp = str(int(time.time() * 1000)) encrypted_object = timestamp + "|" + password public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB" rsa_key = RSA.import_key(base64.b64decode(public_key)) # 導入讀取到的公鑰 cipher = PKCS1_v1_5.new(rsa_key) # 生成對象 encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8"))) encrypted_password = parse.quote(encrypted_password) print(encrypted_password)
即便是不使用 Python,我們同樣可以自己引用 JSEncrypt 模塊來實現這個加密過程(該模塊使用方法可參考 JSEncrypt GitHub[1]),如下所示:
/* 引用 jsencrypt 加密模塊,如果在 PyCharm 里直接使用 require 引用最新版 jsencrypt, 運行可能會提示 jsencrypt.js 里 window 未定義,直接在該文件定義 var window = this; 即可, 也可以使用和網站用的一樣的 2.3.1 版本:https://npmcdn.com/jsencrypt@2.3.1/bin/jsencrypt.js 也可以將 jsencrypt.js 直接粘貼到此腳本中使用,如果提示未定義,直接在該腳本中定義即可。 */ JSEncrypt = require("jsencrypt") function getEncryptedPassword(t, e) { var jsEncrypt = new JSEncrypt(); jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB'); var i = e ? e + "|" + t : t; return encodeURIComponent(jsEncrypt.encrypt(i)); } var password = "12345678"; var timestamp = (new Date).getTime(); console.log(getEncryptedPassword(password, timestamp));
webpack 改寫
本文的標題是 webpack 改寫實戰,所以很顯然本文的目的是為了練習 JavaScript 模塊化編程 webpack 代碼的改寫,現在大多數站點都使用了這種寫法,然而並不是所有站點都像本文遇到的站點一樣,可以很容易使用其他方法來實現的,往往大多數站點需要你自己扒下他的源碼來還原加密過程,有關 JavaScript 模塊化編程,即 webpack,在 K 哥往期的文章中有過詳細的介紹:爬蟲逆向基礎,理解 JavaScript 模塊化編程 webpack
一個標准的 webpack 整體是一個 IIFE 立即調用函數表達式,其中有一個模塊加載器,也就是調用模塊的函數,該函數中一般具有 function.call()
或者 function.apply()
方法,IIFE 傳遞的參數是一個列表或者字典,里面是一些需要調用的模塊,寫法類似於:
!function (allModule) { function useModule(whichModule) { allModule[whichModule].call(null, "hello world!"); } }([ function module0(param) {console.log("module0: " + param)}, function module1(param) {console.log("module1: " + param)}, function module2(param) {console.log("module2: " + param)}, ]);
觀察這次站點的加密代碼,會發現所有加密方法都在 home.min.js 里面,在此文件開頭可以看到整個是一個 IIFE 立即調用函數表達式,function e
里面有關鍵方法 .call()
,由此可以判斷該函數為模塊加載器,后面傳遞的參數是一個字典,里面是一個個的對象方法,也就是需要調用的模塊函數,這就是一個典型的 webpack 寫法,如下圖所示:
接下來我們通過 4 步完成對 webpack 代碼的改寫,將原始代碼扒下來實現加密的過程。
1、找到 IIFE
IIFE 立即調用函數表達式,也稱為立即執行函數,自執行函數,將源碼中的 IIFE 框架摳出來,后續將有用的代碼再往里面放:
!function (t) { }({ })
2、找到模塊加載器
前面我們已經講過,帶有 function.call()
或者 function.apply()
方法的就是模塊加載器,也就是調用模塊的方法,在本例中,function e
就是模塊加載器,將其摳下來即可,其他多余的代碼可以直接刪除,注意里面用到了 i
,所以定義 i
的語句也要摳下來:
!function (t) { function e(s) { if (i[s]) return i[s].exports; var n = i[s] = { exports: {}, id: s, loaded: !1 }; return t[s].call(n.exports, n, n.exports, e), n.loaded = !0, n.exports } var i = {}; }({ })
3、找到調用的模塊
重新來到加密的地方,第一個模塊是 3,n
里面的 encode
方法最終返回的就是加密后的結果,如下圖所示:
第二個模塊是 4,可以看到模塊 3 里面的 this.jsencrypt.encrypt(i)
方法實際上是調用的第 3340 行的方法,該方法在模塊 4 里面,這里定位在模塊 4 的方法,可以在瀏覽器開發者工具 source 頁面,將鼠標光標放到該函數前面,一直往上滑動,直到模塊開頭,也可以使用 VS Code 等編輯器,將整個 home.min.js 代碼粘貼過去,然后選擇折疊所有代碼,再搜索這個函數,即可快速定位在哪個模塊。
確定使用了 3 和 4 模塊后,將這兩個模塊的所有代碼扣下來即可,大致代碼架構如下(模塊 4 具體的代碼太長,已刪除):
!function (t) { function e(s) { if (i[s]) return i[s].exports; var n = i[s] = { exports: {}, id: s, loaded: !1 }; return t[s].call(n.exports, n, n.exports, e), n.loaded = !0, n.exports } var i = {}; }( { 4: function (t, e, i) {}, 3: function (t, e, i) { var s; s = function (t, e, s) { function n() { "undefined" != typeof r && (this.jsencrypt = new r.JSEncrypt, this.jsencrypt.setPublicKey("-----BEGIN PUBLIC KEY-----略-----END PUBLIC KEY-----")) } var r = i(4); n.prototype.encode = function (t, e) { var i = e ? e + "|" + t : t; return encodeURIComponent(this.jsencrypt.encrypt(i)) }, s.exports = n }.call(e, i, e, t), !(void 0 !== s && (t.exports = s)) } } )
這里需要我們理解一個地方,那就是模塊 3 的代碼里有一行 var r = i(4);
,這里的 i
是 3: function (t, e, i) {}
,傳遞過來的 i
,而模塊 3 又是由模塊加載器調用的,即 .call(n.exports, n, n.exports, e) 里面的某個參數就是 i
,前面在講解基礎的時候已經說過,.call
的第一個參數指定的是函數體內 this 對象的指向,並不代表真正參數,所以第一個 n.exports 並不是參數,從第二個參數即 n 開始算,那么 i 其實就是.call(n.exports, n, n.exports, e)
里面的 e
,所以 var r = i(4);
實際上就是模塊加載器 function e
調用了模塊 4,由於這里模塊 4 是個對象,所以這里最好寫成 var r = i("4");
,這里是數字,所以可以成功運行,如果模塊 4 名字變成 func4 或者其他名字,那么調用時就必須要加引號了。
4、導出加密函數
目前關鍵的加密代碼已經剝離完畢了,最后一步就是需要把加密函數導出來供我們調用了,首先定義一個全局變量,如 eFunc,然后在模塊加載器后面使用語句 eFunc = e
,把模塊加載器導出來:
var eFunc; !function (t) { function e(s) { if (i[s]) return i[s].exports; var n = i[s] = { exports: {}, id: s, loaded: !1 }; return t[s].call(n.exports, n, n.exports, e), n.loaded = !0, n.exports } var i = {}; eFunc = e }( { 4: function (t, e, i) {}, 3: function (t, e, i) {} } )
然后定義一個函數,傳入明文密碼,返回加密后的密碼:
function getEncryptedPassword(password) { var timestamp = (new Date).getTime(); var encryptFunc = eFunc("3"); var encrypt = new encryptFunc; return encrypt.encode(password, timestamp) }
其中 timestamp 為時間戳,因為我們最終需要調用的是模塊 3 里面的 n.prototype.encode
這個方法,所以首先調用模塊 3,返回的是模塊 3 里面的 n 函數(可以在瀏覽器運行代碼,一步一步查看結果),然后將其 new 出來,調用 n 的 encode 方法,返回加密后的結果。
自此,webpack 的加密代碼就剝離完畢了,最后調試會發現 navigator 和 window 未定義,定義一下即可:
var navigator = {}; var window = global;
這里擴展一下,在瀏覽器里面 window 其實就是 global,在 nodejs 里沒有 window,但是有個 global,與瀏覽器的 window 對象類型相似,是全局可訪問的對象,因此在 nodejs 環境中可以將 window 定義為 global,如果定義為空,可能會引起其他錯誤。
完整代碼
GitHub 關注 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !https://github.com/kgepachong/
以下只演示部分關鍵代碼,不能直接運行!完整代碼倉庫地址:https://github.com/kgepachong/crawler/
JavaScript 加密關鍵代碼架構
方法一:webpack 改寫源碼實現 RSA 加密:
var navigator = {}; var window = global; var eFunc; !function (t) { function e(s) { if (i[s]) return i[s].exports; var n = i[s] = { exports: {}, id: s, loaded: !1 }; return t[s].call(n.exports, n, n.exports, e), n.loaded = !0, n.exports } var i = {}; eFunc = e; }( { 4: function (t, e, i) {}, 3: function (t, e, i) {} } ) function getEncryptedPassword(password) { var timestamp = (new Date).getTime(); var encryptFunc = eFunc("3"); var encrypt = new encryptFunc; return encrypt.encode(password, timestamp) } // 測試樣例 // console.log(getEncryptedPassword("12345678"))
方法二:直接使用 JSEncrypt 模塊實現 RSA 加密:
/* 引用 jsencrypt 加密模塊,此腳適合在 nodejs 環境下運行。 1、使用 require 語句引用,前提是使用 npm 安裝過; 2、將 jsencrypt.js 直接粘貼到此腳本中使用,同時要將結尾 exports.JSEncrypt = JSEncrypt; 改為 je = JSEncrypt 導出方法。 PS:需要定義 var navigator = {}; var window = global;,否則提示未定義。 */ // ========================= 1、require 方式引用 ========================= // var je = require("jsencrypt") // =================== 2、直接將 jsencrypt.js 復制過來 =================== /*! JSEncrypt v2.3.1 | https://npmcdn.com/jsencrypt@2.3.1/LICENSE.txt */ var navigator = {}; var window = global; // 這里是 jsencrypt.js 代碼 function getEncryptedPassword(t) { var jsEncrypt = new je(); jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB'); var e = (new Date).getTime(); var i = e ? e + "|" + t : t; return encodeURIComponent(jsEncrypt.encrypt(i)); } // 測試樣例 // console.log(getEncryptedPassword("12345678"));
Python 登錄關鍵代碼
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import re import json import time import random import base64 from urllib import parse import execjs import requests from PIL import Image from Cryptodome.PublicKey import RSA from Cryptodome.Cipher import PKCS1_v1_5 login_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler' verify_image_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler' check_code_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } session = requests.session() def get_jquery(): jsonp = '' for _ in range(21): jsonp += str(random.randint(0, 9)) jquery = 'jQuery' + jsonp + '_' return jquery def get_dict_from_jquery(text): result = re.findall(r'\((.*?)\)', text)[0] return json.loads(result) def get_encrypted_password_by_javascript(password): # 兩個 JavaScript 腳本,兩種方法均可 with open('gm99_encrypt.js', 'r', encoding='utf-8') as f: # with open('gm99_encrypt_2.js', 'r', encoding='utf-8') as f: exec_js = f.read() encrypted_password = execjs.compile(exec_js).call('getEncryptedPassword', password) return encrypted_password def get_encrypted_password_by_python(password): timestamp = str(int(time.time() * 1000)) encrypted_object = timestamp + "|" + password public_key = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler" rsa_key = RSA.import_key(base64.b64decode(public_key)) # 導入讀取到的公鑰 cipher = PKCS1_v1_5.new(rsa_key) # 生成對象 encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8"))) encrypted_password = parse.quote(encrypted_password) return encrypted_password def get_verify_code(): response = session.get(url=verify_image_url, headers=headers) with open('code.png', 'wb') as f: f.write(response.content) image = Image.open('code.png') image.show() code = input('請輸入圖片驗證碼: ') return code def check_code(code): timestamp = str(int(time.time() * 1000)) params = { 'callback': get_jquery() + timestamp, 'ckcode': code, '_': timestamp, } response = session.get(url=check_code_url, params=params, headers=headers) result = get_dict_from_jquery(response.text) if result['result'] == 1: pass else: raise Exception('驗證碼輸入錯誤!') def login(username, encrypted_password, code): timestamp = str(int(time.time() * 1000)) params = { 'callback': get_jquery() + timestamp, 'encrypt': 1, 'uname': username, 'password': encrypted_password, 'remember': 'checked', 'ckcode': code, '_': timestamp } response = session.get(url=login_url, params=params, headers=headers) result = get_dict_from_jquery(response.text) print(result) def main(): # 測試賬號:15434947408,密碼:iXqC@aJt8fi@VwV username = input('請輸入登錄賬號: ') password = input('請輸入登錄密碼: ') # 獲取加密后的密碼,使用 Python 或者 JavaScript 實現均可 encrypted_password = get_encrypted_password_by_javascript(password) # encrypted_password = get_encrypted_password_by_python(password) # 獲取驗證碼 code = get_verify_code() # 校驗驗證碼 check_code(code) # 登錄 login(username, encrypted_password, code) if __name__ == '__main__': main()
參考資料
[1]JSEncrypt GitHub: https://github.com/travist/jsencrypt
------------------------------------------------------------------------------------------------------------------------------------------------------------------
以上為原文內容,圖片不清晰的(原文章就不太清晰),可以打開目標站對應着看,涉及相關的細節,可以關注"k哥爬蟲"咨詢作者
總結下,webpack解包導出流程:
1、找到 IIFE
2、找到模塊加載器
3、找到調用的模塊
4、導出加密函數
5、調用加密函數,得出值