Skip to content

js逆向-webpack

前言

TIP

Webpack是一个现代JavaScript应用程序的静态模块打包工具,它能够将多个模块(包括JavaScript、CSS、图片等)进行打包,生成优化后的静态资源文件。

在JS逆向工程中,Webpack打包后的代码具有明显的特征,通常表现为自执行函数形式,包含模块加载器和模块数组/字典。

核心特征

Webpack打包后的代码通常以以下形式出现:

1. 代码结构特征

js
!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

cmd
npm install webpack -g
npm install webpack-cli -g

2. webpack的使用

js的导包:

  • 导入:var xxx = require("xxx");-> 一种 commonjs -> 不用改任何东西

  • 导出:module.exports = 导出的东西 这边导出的是什么,那边收到的就是什么

webpack开始打包 完成你的代码开发(正常的业务逻辑开发) 完成webpack.config.js文件的编写 -> 配置文件

模拟实现

1. 模拟检测环境

env.js 模拟检测环境

js
function checkEnv() {
    // 检测是否在浏览器
    return typeof window == "undefined" ? "error_env" : "ok_env"
}

/**
 * 1.必须要导出,才可以被其他包require
 */
module.exports = {
    checkenv: checkEnv
}

2. 对环境参数加密

m_encrypt.js 对环境参数加密

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 模拟发送请求

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配置文件:

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中添加

json
{
  "scripts": {
    "build": "webpack"
  }
}

3. 创建index.html

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script></script>
</body>
</html>

4. 引入打包好的app.js

html

<script src="./dist/js/app.js"></script>

5.效果

打包代码

js
/*! 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方法就会失败!

js
    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

那怎么调用打包后的指定函数呢?

使用全局对象把它拿出来

js
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.AES

6. 核心代码分析

1. 加载器

如: s = r(712);

js
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. 具体调用

开发时

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
    }
}

打包后

js
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.结论
  1. t 存储所有需要的模块
  2. e 主要起到缓存作用,调用过的可以直接返回exports
  3. "712" 就是一个单独的模块(闭包),不能和其他人起冲突!
  4. r(712) 是把712这个序号对应的具体函数导出

逆向的核心思路

1. 定位加载器

加载器是Webpack逆向的关键,通常可以通过以下方式定位:

  • 在加密函数处下断点,通过调用堆栈找到加载器位置
  • 搜索exports、call等关键字
  • 查找n(数字)或n("模块名")的调用语句

2. 模块定位方法

手动定位:在加载器代码中打上断点,通过控制台输出e[模块ID]获取模块函数,然后逐个复制到本地。

自动导出:修改加载器代码,在模块加载时自动记录所有模块:

js
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对应数组下标:

js
var loader;
!function (e) {
// 加载器代码
    loader = n
}([模块0, 模块1, 模块2])

传字典:将模块按对象形式传入,键名为模块ID:

js
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;

五、常见问题与解决方案

  1. 模块依赖过多

当模块依赖链过长时,手动补全效率低下。可以使用自动化工具如webpack_ast进行批量导出,或者采用"缺什么补什么"的方式,根据报错信息逐个补全。

  1. 环境检测

某些Webpack代码会检测运行环境,需要补全相关环境变量:

js
window = global;
document = {};
navigator = {userAgent: 'Mozilla/5.0...'};
  1. 代码混淆

对于混淆严重的代码,可以使用反混淆工具如JSNice.org进行美化,或者使用AST工具进行代码还原。