js逆向-webpack
前言
TIP
Webpack是一个现代JavaScript应用程序的静态模块打包工具,它能够将多个模块(包括JavaScript、CSS、图片等)进行打包,生成优化后的静态资源文件。
在JS逆向工程中,Webpack打包后的代码具有明显的特征,通常表现为自执行函数形式,包含模块加载器和模块数组/字典。
核心特征
Webpack打包后的代码通常以以下形式出现:
1. 代码结构特征
!function (e) {
// 加载器
function n(r) {
if (t[r]) return t[r].exports;
var o = t[r] = {i: r, l: !1, exports: {}};
return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
}
// 其他方法...
}([模块1, 模块2, ...])2. 模块加载机制
Webpack通过模块加载器管理所有模块,每个模块都有一个唯一的标识符(通常是数字或字符串),通过n(模块ID) 的方式调用。模块之间通过exports对象暴露接口,通过require或import的方式引用依赖。
webpack的基本使用
基础知识
1. 安装webpack
npm install webpack -g
npm install webpack-cli -g2. webpack的使用
js的导包:
导入:var xxx = require("xxx");-> 一种 commonjs -> 不用改任何东西
导出:module.exports = 导出的东西 这边导出的是什么,那边收到的就是什么
webpack开始打包 完成你的代码开发(正常的业务逻辑开发) 完成webpack.config.js文件的编写 -> 配置文件
模拟实现
1. 模拟检测环境
env.js 模拟检测环境
function checkEnv() {
// 检测是否在浏览器
return typeof window == "undefined" ? "error_env" : "ok_env"
}
/**
* 1.必须要导出,才可以被其他包require
*/
module.exports = {
checkenv: checkEnv
}2. 对环境参数加密
m_encrypt.js 对环境参数加密
/**
* 核心加密函数
*/
var CryptoJs = require('crypto-js');
function encrypt(plainText) {
const iv = CryptoJs.lib.WordArray.random(16); // 生成16字节的随机IV
const key = CryptoJs.enc.Utf8.parse('yunchengyu'); // 密钥也需为16/24/32字节
return CryptoJs.AES.encrypt(plainText, key, {
iv: iv,
mode: CryptoJs.mode.CBC,
padding: CryptoJs.pad.Pkcs7
}).toString()
}
module.exports = {
encrypt: {
AES: encrypt
}
}3. 模拟发送请求
http.js 模拟发送请求
/**
* 负责发送网络请求
*/
/*
var env_obj = require("env.js") // 不用使用后缀
var env_obj = require("env") // 默认会在 node_modules中寻找
var env_obj = require("./env") // 在当前目录下寻找
*/
var env_obj = require("./env")
var crypt = require("./m_encrypt")
function send() {
// 检测环境
var env_text = env_obj.checkenv()
// 对环境加密
var encrypt_env = crypt.encrypt.AES(env_text)
// 发送请求
console.log("我发送请求了....")
console.log(`环境参数: ${encrypt_env}`)
}
send()4.打包
创建html引入这个http.js(注意:require是node中的,所以不能能直接引入)
需要对整个代码进行打包!!!
1. 创建webpack.config.js配置文件:
const path = require('path');
module.exports = {
entry: './http.js', // 入口文件
output: {
filename: './js/app.js', // 输出文件名
path: path.resolve(__dirname, 'dist') // 输出路径
},
mode: 'development' // 开发模式
};2. 配置打包命令
package.json中添加
{
"scripts": {
"build": "webpack"
}
}3. 创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script></script>
</body>
</html>4. 引入打包好的app.js
<script src="./dist/js/app.js"></script>5.效果
打包代码
/*! For license information please see app.js.LICENSE.txt */
(() => {
var t = [...内容太多,就不放啦
]
,
e = {};
function r(i) {
var n = e[i];
if (void 0 !== n)
return n.exports;
var o = e[i] = {
exports: {}
};
return t[i].call(o.exports, o, o.exports, r),
o.exports
}
r.g = function () {
if ("object" == typeof globalThis)
return globalThis;
try {
return this || new Function("return this")()
} catch (t) {
if ("object" == typeof window)
return window
}
}();
var i, n, o = r(26), s = r(712);
i = o.checkenv(),
n = s.encrypt.AES(i),
console.log("我发送请求了...."),
console.log(`环境参数: ${n}`)
}
)();这个时候使用checkenv、send方法就会失败!
var window = {}
var i, n;
var o = r(26);
var s = r(712);
i = o.checkenv(),
n = s.encrypt.AES(i),
console.log("我发送请求了...."),
console.log(`环境参数: ${n}`)
window.checkenv = o.checkenv
window.encrypt = s.encrypt.AES那怎么调用打包后的指定函数呢?
使用全局对象把它拿出来
var window = {}
var i, n;
var o = r(26);
var s = r(712);
i = o.checkenv();
n = s.encrypt.AES(i);
window.checkenv = o.checkenv
window.encrypt = s.encrypt.AES6. 核心代码分析
1. 加载器
如: s = r(712);
function r(i) {
var n = e[i];
if (void 0 !== n)
return n.exports;
var o = e[i] = {
exports: {}
};
t[i].call(o.exports, o, o.exports, r);
return o.exports; // 返回了序号对应具体函数
}核心: t[i].call(o.exports, o, o.exports, r);
i: 当前调用第几个函数(函数序号)
call 方法: func.call(谁[哪个对象]在调用,参数1,参数2,参数3)
相当于函数执行:t[i]( o, o.exports, r) => 函数中this: o.exports
2. 具体调用
开发时
function encrypt(plainText) {
const iv = CryptoJs.lib.WordArray.random(16); // 生成16字节的随机IV
const key = CryptoJs.enc.Utf8.parse('yunchengyu'); // 密钥也需为16/24/32字节
return CryptoJs.AES.encrypt(plainText, key, {
iv: iv,
mode: CryptoJs.mode.CBC,
padding: CryptoJs.pad.Pkcs7
}).toString()
}
module.exports = {
encrypt: {
AES: encrypt
}
}打包后
t = [
712(t, e, r) {
var i = r(396);
t.exports = {
encrypt: {
AES: function (t) {
const e = i.lib.WordArray.random(16)
, r = i.enc.Utf8.parse("yunchengyu");
return i.AES.encrypt(t, r, {
iv: e,
mode: i.mode.CBC,
padding: i.pad.Pkcs7
}).toString()
}
}
}
},
]参数:
t: {exports : { }}
e: t.exports
r: 加载器
做了两件事
- 使用加载器先加载了模块396为i
- 修改t.exports 的内容,导出当前序号下的具体方法
3.结论
- t 存储所有需要的模块
- e 主要起到缓存作用,调用过的可以直接返回exports
- "712" 就是一个单独的模块(闭包),不能和其他人起冲突!
r(712)是把712这个序号对应的具体函数导出
逆向的核心思路
1. 定位加载器
加载器是Webpack逆向的关键,通常可以通过以下方式定位:
- 在加密函数处下断点,通过调用堆栈找到加载器位置
- 搜索exports、call等关键字
- 查找n(数字)或n("模块名")的调用语句
2. 模块定位方法
手动定位:在加载器代码中打上断点,通过控制台输出e[模块ID]获取模块函数,然后逐个复制到本地。
自动导出:修改加载器代码,在模块加载时自动记录所有模块:
window.code = '';
function n(r) {
if (t[r]) return t[r].exports;
var o = t[r] = {i: r, l: !1, exports: {}};
window.code += '"' + r + '":' + e[r].toString() + ',\r\n';
return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
}3. 模块改写方式
传数组:将模块按数组形式传入自执行函数,模块ID对应数组下标:
var loader;
!function (e) {
// 加载器代码
loader = n
}([模块0, 模块1, 模块2])传字典:将模块按对象形式传入,键名为模块ID:
var loader;
!function (e) {
// 加载器代码
loader = n
}({"109": 模块109, "202": 模块202})实战逆向步骤
步骤1:定位加密位置
通过抓包分析找到需要逆向的加密参数,在浏览器开发者工具中搜索相关关键字,找到加密函数的位置。
步骤2:找到加载器
在加密函数处下断点,通过调用堆栈找到加载器函数,通常是一个自执行函数,内部包含模块加载逻辑。
步骤3:导出加载器
将加载器代码复制到本地,修改为可独立运行的形式: var myLoader; !function(e) { // 加载器代码 myLoader = n }({})
步骤4:定位所需模块
通过断点调试,找到加密函数调用的所有模块,包括直接依赖和间接依赖。可以使用控制台输出e[模块ID]的方式逐个定位。
步骤5:补全模块
将找到的模块代码复制到本地,按数组或字典形式传入加载器。如果模块还依赖其他模块,需要继续查找并补全。
步骤6:导出加密函数
在本地环境中,通过加载器调用加密模块,将加密函数导出到全局,供外部调用: var encryptFunc = myLoader(模块ID); window.encrypt = encryptFunc;
五、常见问题与解决方案
- 模块依赖过多
当模块依赖链过长时,手动补全效率低下。可以使用自动化工具如webpack_ast进行批量导出,或者采用"缺什么补什么"的方式,根据报错信息逐个补全。
- 环境检测
某些Webpack代码会检测运行环境,需要补全相关环境变量:
window = global;
document = {};
navigator = {userAgent: 'Mozilla/5.0...'};- 代码混淆
对于混淆严重的代码,可以使用反混淆工具如JSNice.org进行美化,或者使用AST工具进行代码还原。
