js逆向安全指南(3)--webpack解包指南
之前在分析某⽹站时也说过⼀些,
不过,可能还是有些朋友不太理解怎么的,你怎么就知道到那个main.js⽂件呢?所以,肯定是有规律的,以下就是⽤实际的案例介绍规律
以下内容转⾃ “k哥爬⾍”,原帖:
声明
本⽂章中所有内容仅供学习交流,抓包内容、敏感⽹址、数据接⼝均已做脱敏处理,严禁⽤于商业⽤途和⾮法⽤途,否则由此产⽣的⼀切后果均与作者⽆关,若有侵权,请联系
我⽴即删除!
逆向⽬标
主页:aHR0cHM6Ly93d3cuZ205OS5jb20v
接⼝:aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=
逆向参数:Query String Parameters:
password:
逆向过程
> 抓包分析
来到⾸页,随便输⼊⼀个账号密码,点击登陆,抓包定位到登录接⼝为aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=,GET 请求,Query String Parameters ⾥,密码
password 被加密处理了。
> 加密⼊⼝
直接搜索关键字 password 会发现结果太多不好定位,使⽤ XHR 断点⽐较容易定位到加密⼊⼝,有关 XHR 断点调试可以查看 K 哥往期的教程:,如下图所⽰,在 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//MZrixam7lCYpNOja rsa_key = RSA.import_key(base64.b64decode(public_key))  # 导⼊读取到的公钥
cipher = PKCS1_w(rsa_key)                        # ⽣成对象
encrypted_password = base64.pt(de(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 版本:npmcdn/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//MZrixa    var i = e ? e + "|" + t : t;
return pt(i));
}
var password = "12345678";
var timestamp = (new Date).getTime();
console.log(getEncryptedPassword(password, timestamp));
改写
本⽂的标题是 webpack 改写实战,所以很显然本⽂的⽬的是为了练习 JavaScript 模块化编程 webpack 代码的改写,现在⼤多数站点都使⽤了这种写法,然⽽并不是所有站点都像本⽂遇到的站点⼀样,可以很容易使⽤其他⽅法来实现的,往往⼤多数站点需要你⾃⼰扒下他的源码来还原加密过程,有关 JavaScript 模块化编程,即 webpack,在 K 哥往期的⽂章中有过详细的介绍:
⼀个标准的 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].ports, n, n.exports, e),
n.loaded = !0,
}
var i = {};
}({
})
>> 3、到调⽤的模块
重新来到加密的地⽅,第⼀个模块是 3,n⾥⾯的encode⽅法最终返回的就是加密后的结果,如下图所⽰:
第⼆个模块是 4,可以看到模块 3 ⾥⾯的pt(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].ports, n, n.exports, e),
n.loaded = !0,
}
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);
de = function (t, e) {
var i = e ? e + "|" + t : t;
return encodeURIComponent(pt(i))
},
}.call(e, i, e, t),
!(void 0 !== s && (t.exports = s))
}
}
)
这⾥需要我们理解⼀个地⽅,那就是模块 3 的代码⾥有⼀⾏var r = i(4);,这⾥的i是3: function (t, e, i) {},传递过来的i,⽽模块 3 ⼜是由模块加载器调⽤的,即 .ports, n, n.exports, e) ⾥⾯的某个参数就是i,前⾯在讲解基础的时候已经说过,.call的第⼀个参数指定的是函数体内 this 对象的指向,并不代表真正参数,所以第⼀个 n.exports 并不是参数,从第⼆个参数即 n 开始算,那么 i 其实就是.ports, 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].ports, n, n.exports, e),
n.loaded = !0,
}
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;
de(password, timestamp)
}
其中 timestamp 为时间戳,因为我们最终需要调⽤的是模块 3 ⾥⾯的de这个⽅法,所以⾸先调⽤模块 3,返回的是模块 3 ⾥⾯的 n 函数(可以在浏览器运⾏代码,⼀步⼀步查看结果),然后将其 new 出来,调⽤ n 的 encode ⽅法,返回加密后的结果。
⾃此,webpack 的加密代码就剥离完毕了,最后调试会发现 navigator 和 window 未定义,定义⼀下即可:
var navigator = {};
var window = global;
js以6结尾的数字表达式
这⾥扩展⼀下,在浏览器⾥⾯ window 其实就是 global,在 nodejs ⾥没有 window,但是有个 global,与浏览器的 window 对象类型相似,是全局可访问的对象,因此在 nodejs 环境中可以将 window 定义为 global,如果定义为空,可能会引起其他错误。
完整代码
加密关键代码架构
⽅法⼀: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].ports, n, n.exports, e),
n.loaded = !0,
}
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;
de(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 | npmcdn/jsencrypt@2.3. */
var navigator = {};
var window = global;
// 这⾥是 jsencrypt.js 代码
function getEncryptedPassword(t) {
var jsEncrypt = new je();
jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixa    var e = (new Date).getTime();
var i = e ? e + "|" + t : t;
return pt(i));
}
// 测试样例
// console.log(getEncryptedPassword("12345678"));
登录关键代码
#!/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:github/kgepachong/crawler'
verify_image_url = '脱敏处理,完整代码关注 GitHub:github/kgepachong/crawler'
check_code_url = '脱敏处理,完整代码关注 GitHub:github/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 = execjspile(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:github/kgepachong/crawler"
rsa_key = RSA.import_key(base64.b64decode(public_key))  # 导⼊读取到的公钥
cipher = PKCS1_w(rsa_key)                        # ⽣成对象
encrypted_password = base64.pt(de(encoding="utf-8")))
encrypted_password = parse.quote(encrypted_password)
return encrypted_password
def get_verify_code():
response = (url=verify_image_url, headers=headers)
with open('code.png', 'wb') as f:
f.t)
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 = (url=check_code_url, params=params, headers=headers)
result = get_dict_from_)
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 = (url=login_url, params=params, headers=headers)
result = get_dict_from_)
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()
参考资料
------------------------------------------------------------------------------------------------------------------------------------------------------------------
以上为原⽂内容,图⽚不清晰的(原⽂章就不太清晰),可以打开⽬标站对应着看,涉及相关的细节,可以关注"k哥爬⾍"咨询作者> 总结下,webpack解包导出流程:
>> 1、到 IIFE
>> 2、到模块加载器
>> 3、到调⽤的模块
>> 4、导出加密函数
>> 5、调⽤加密函数,得出值