【JS 逆向百例】層層巢狀!某加速商城 RSA 加密
宣告
本文章中所有內容僅供學習交流,敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯絡我立即刪除!
逆向目標
-
目標:某加速商城登入介面
-
主頁:aHR0cDovL3d3dy4xNXl1bm1hbGwuY29tL3BjL2xvZ2luL2luZGV4
-
介面:aHR0cDovL3d3dy4xNXl1bm1hbGwuY29tL3BjL2xvZ2luL2NoZWNr
-
逆向引數:
Cookie:PHPSESSID=g9jp7sfpukg99v03gj69nr9r56
Form Data:
u[password]: 7aad70a547b016a07f2e20bee7b4936111e650aa5c419fafdfb28322...... _csrfToken: 4bea63330c5ccdd37588321d027f4c40129687b0
逆向過程
抓包分析
在首頁點選登陸,來到登入頁面,隨便輸入一個賬號密碼登陸,抓包定位到登入介面為 aHR0cDovL3d3dy4xNXl1bm1hbGwuY29tL3BjL2xvZ2luL2NoZWNr ,POST 請求,Form Data 裡,密碼 u[password]
被加密處理了,此外還有一個 _csrfToken
也是需要我們解決的,cookie 裡面有一個 PHPSESSID
,經過測試,如果不帶此引數,最終的請求也是失敗的。
引數逆向
首先看看 _csrfToken
,先嚐試直接搜尋一下它的值,可以發現其實在首頁的原始碼裡面就有,直接匹配拿過來即可:
再看一下 cookie 裡面的 PHPSESSID
最後一個密碼引數 u[password]
,肯定是通過 JS 加密得到的,直接 Ctrl+Shift+F 全域性搜尋,可以直接在 index 首頁找到 RSA 加密的地方,埋下斷點進行除錯,最後的 res 正是加密後的密碼:
我們將這段關鍵程式碼進行改寫,封裝成一個函式:
function getEncryptedPassword(password) { var public_key = "00bdf3db924714b9c4ddd144910071c282e235ac51371037cf89fa08f28b9105b6326338ed211280154c645bf81bae4184c2b52e2b02b0953e7aa8b25a8e212a0b"; var public_length = "10001"; var rsa = new RSAKey(); rsa.setPublic(public_key, public_length); return rsa.encrypt(password); }
這裡主要用到的三個函式 RSAKey()
、setPublic()
、encrypt()
,在開發者工具中,滑鼠放到函式上,可以看到這裡都是呼叫的 rsa.js 裡面的方法,我們直接將整個檔案剝離下來進行本地除錯:
本地除錯會發現提示 BigInteger
未定義,滑鼠移到這個函式上面,可以發現是呼叫了 jsbn.js 裡面的方法,同樣的,直接將整個 jsbn.js 檔案剝離下來進行本地除錯。
這裡其實在 rsa.js 檔案的第一行有一句註釋:// Depends on jsbn.js and rng.js
,我們可以猜測 rsa.js 是可能依賴 jsbn.js 和 rng.js 這兩個檔案的。
有了 jsbn.js 的程式碼,再次進行除錯,會發現又提示 navigator
和 SecureRandom
未定義,navigator
我們已經非常熟悉了,是瀏覽器的相關資訊,一般情況下直接定義為空即可(navigator = {};
);將滑鼠移到 SecureRandom
函式上面,可以發現是呼叫了 rng.js 裡面的方法,同樣的,直接將整個 rng.js 檔案剝離下來進行本地除錯。這裡就證實了前面我們的猜想,rsa.js 確實是依賴 jsbn.js 和 rng.js 的。
我們注意到,這裡在 rng.js 檔案的第一行,同樣有一句註釋:// Random number generator - requires a PRNG backend, e.g. prng4.js
,表明 rng.js 是隨機數生成器,需要 PRNG 後端,例如 prng4.js,在密碼學中,PRNG 全稱是 pseudorandom number generator,即偽隨機數生成器,是指通過特定演算法生成一系列的數字,使得這一系列的數字看起來是隨機的,但是實際是確定的,所以叫偽隨機數,感興趣的朋友可以深入研究一下,在這裡我們知道 rng.js 可能還依賴於 prng4.js,需要進一步除錯才清楚。
rsa.js、jsbn.js、rng.js 都齊全了,再次本地除錯,會發現 rng.js 裡面的 rng_psize
未定義,滑鼠放上去看到 rng_psize
就是一個定值 256,在右邊的 Global 全域性變數裡也可以看到值為 256,嘗試搜尋一下 rng_psize
,可以發現在 prng4.js 裡面有定義 var rng_psize = 256;
,果然和註釋說得一樣,rng.js 是依賴 prng4.js 的,但是這裡似乎直接定義一下 rng_psize
就行了。
直接在原生代碼定義一下 var rng_psize = 256;
,再次進行除錯,此時又會提示 rng.js 裡缺少 prng_newstate()
物件,再次回到開發者工具,可以看到 prng_newstate()
是 prng4.js 裡面的方法,果然 rng.js 和 prng4.js 的關係並不簡單,同樣的,我們也直接將整個 prng4.js 檔案剝離下來進行本地除錯。
再次除錯,執行無誤,可以成功拿到加密後的密碼了:
邏輯總結
-
加密入口可以在 index 首頁找到,用到了 rsa.js 裡面的三個加密函式
RSAKey()
、setPublic()
、encrypt()
; -
rsa.js 裡的
BigInteger()
函式依賴 jsbn.js,SecureRandom()
函式依賴 rng.js; -
rng.js 裡的變數
rng_psize
在 prng4.js 中定義,prng_newstate()
函式也依賴 prng4.js;
要將 rsa.js、jsbn.js、rng.js、prng4.js 這四個 JS 加密檔案完整的剝離下來才能還原整個加密過程。
完整程式碼
GitHub 關注 K 哥爬蟲:https://github.com/kuaidaili,持續分享爬蟲相關程式碼!歡迎 star !
以下只演示部分關鍵程式碼,完整程式碼倉庫地址:https://github.com/kuaidaili/crawler/
引數 JS 加密關鍵程式碼
navigator = {};
// ================== prng4.js begin ================== //
function Arcfour() {}
function ARC4init(key) {}
function ARC4next() {}
// 此處省略 N 個函式
var rng_psize = 256;
// ================== prng4.js end ================== //
// ================== rng.js begin ================== //
var rng_state;
var rng_pool;
var rng_pptr;
function rng_seed_int(x) {}
function rng_seed_time() {}
// 此處省略 N 個函式
function SecureRandom() {}
SecureRandom.prototype.nextBytes = rng_get_bytes;
// ================== rng.js end ================== //
// ================== jsbn.js begin ================== //
var dbits;
var canary = 0xdeadbeefcafe;
var j_lm = ((canary & 0xffffff) == 0xefcafe);
function BigInteger(a, b, c) {}
function nbi() {}
// 此處省略 N 個函式
// protected
BigInteger.prototype.copyTo = bnpCopyTo;
BigInteger.prototype.fromInt = bnpFromInt;
BigInteger.prototype.fromString = bnpFromString;
BigInteger.prototype.clamp = bnpClamp;
BigInteger.prototype.dlShiftTo = bnpDLShiftTo;
BigInteger.prototype.drShiftTo = bnpDRShiftTo;
BigInteger.prototype.lShiftTo = bnpLShiftTo;
BigInteger.prototype.rShiftTo = bnpRShiftTo;
BigInteger.prototype.subTo = bnpSubTo;
BigInteger.prototype.multiplyTo = bnpMultiplyTo;
BigInteger.prototype.squareTo = bnpSquareTo;
BigInteger.prototype.divRemTo = bnpDivRemTo;
BigInteger.prototype.invDigit = bnpInvDigit;
BigInteger.prototype.isEven = bnpIsEven;
BigInteger.prototype.exp = bnpExp;
// public
BigInteger.prototype.toString = bnToString;
BigInteger.prototype.negate = bnNegate;
BigInteger.prototype.abs = bnAbs;
BigInteger.prototype.compareTo = bnCompareTo;
BigInteger.prototype.bitLength = bnBitLength;
BigInteger.prototype.mod = bnMod;
BigInteger.prototype.modPowInt = bnModPowInt;
// "constants"
BigInteger.ZERO = nbv(0);
BigInteger.ONE = nbv(1);
// ================== jsbn.js end ================== //
// ================== rsa.js begin ================== //
function parseBigInt(str, r) {}
function linebrk(s, n) {}
function byte2Hex(b) {}
function pkcs1pad2(s, n) {}
function RSAKey() {}
function RSASetPublic(N, E) {}
function RSADoPublic(x) {}
function RSAEncrypt(text) {}
// protected
RSAKey.prototype.doPublic = RSADoPublic;
// public
RSAKey.prototype.setPublic = RSASetPublic;
RSAKey.prototype.encrypt = RSAEncrypt;
//RSAKey.prototype.encrypt_b64 = RSAEncryptB64;
// ================== rsa.js end ================== //
function getEncryptedPassword(password) {
var public_key = "00bdf3db924714b9c4ddd144910071c282e235ac51371037cf89fa08f28b9105b6326338ed211280154c645bf81bae4184c2b52e2b02b0953e7aa8b25a8e212a0b";
var public_length = "10001";
var rsa = new RSAKey();
rsa.setPublic(public_key, public_length);
return rsa.encrypt(password);
}
// 測試樣例
console.log(getEncryptedPassword("123456"))
Python 登入關鍵程式碼
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import execjs
import requests
from lxml import etree
from PIL import Image
index_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/'
login_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/'
code_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/'
headers = {
'Host': '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/',
'Referer': '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/',
'Origin': '脫敏處理,完整程式碼關注 GitHub:https://github.com/kuaidaili/crawler/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
}
session = requests.session()
def get_encrypted_password(password):
with open('encrypt.js', 'r', encoding='utf-8') as f:
yunmall_js = f.read()
encrypted_password = execjs.compile(yunmall_js).call('getEncryptedPassword', password)
return encrypted_password
def get_csrf_token_cookie():
response = session.get(url=index_url, headers=headers)
tree = etree.HTML(response.text)
csrf_token = tree.xpath("//input[@name='_csrfToken']/@value")[0]
cookies = response.cookies.get_dict()
# print(csrf_token, cookies)
return csrf_token, cookies
def get_very_code(cookies):
response = session.get(url=code_url, cookies=cookies, headers=headers)
with open('code.png', 'wb') as f:
f.write(response.content)
image = Image.open('code.png')
image.show()
very_code = input('請輸入驗證碼: ')
return very_code
def login(csrf_token, very_code, cookies, username, encrypted_password):
data = {
'u[loginType]': 'name',
'u[phone]': username,
'u[password]': encrypted_password,
'u[veryCode]': very_code,
'u[token]': '',
'_csrfToken': csrf_token
}
header_info = {
'X-Requested-With': 'XMLHttpRequest',
}
headers.update(header_info)
response = session.post(url=login_url, data=data, cookies=cookies, headers=headers)
response.encoding = 'utf-8-sig'
response_code = response.text
# print(response_code)
status_code = {
'31': '恭喜,登陸成功。',
'32': '登陸失敗。',
'33': '使用者名稱或密碼錯誤。',
'35': '該使用者已被管理員鎖定。',
'311': '該賬號已繫結裝置,請在繫結的裝置登陸。',
'20001': '驗證碼填寫錯誤!'
}
try:
print(status_code[response_code])
except KeyError:
print('請求超時!')
def main():
username = input('請輸入登入賬號: ')
password = input('請輸入登入密碼: ')
if len(password) > 32:
raise Exception('請輸入正確的密碼!')
encrypted_password = get_encrypted_password(password)
csrf_token, cookies = get_csrf_token_cookie()
very_code = get_very_code(cookies)
login(csrf_token, very_code, cookies, username, encrypted_password)
if __name__ == '__main__':
main()