WEB前端逆向在nodejs环境中复用webpack代码

admin 2026-04-04 05:18:35 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细介绍WEB前端逆向中复用webpack打包代码的技术方法,重点讲解如何将浏览器环境运行的webpack代码移植到Node.js环境。作者通过完整实验用例演示从编写测试代码、webpack打包配置到逆向抠取关键函数的全过程,提供具体目录结构和配置文件示例,涵盖静态导入、动态导入及加密函数调用等实际场景。 综合评分: 85 文章分类: WEB安全,逆向分析,代码审计,安全工具,安全开发


cover_image

WEB前端逆向在nodejs环境中复用webpack代码

scz scz

逆向有你

2026年4月3日 14:17 河南

背景介绍

WEB前端逆向中会遭遇webpack处理过的js代码,这种代码不是给人类阅读的,所以没什么可读性。有些生成关键字段的函数位于其中,不想调试分析算法逻辑,想视之为黑盒函数,给in,返回out;想在nodejs环境中复用本来在browser环境中运行的webpack代码;此过程俗称「webpack代码抠取」。

这事没有真正意义上的技术门槛,但绝大多数讲抠取的文章写得不合我意,故有此篇科普。

简单点说,webpack是一种工具,可将本来在nodejs环境中运行的js处理一下,生成可在browser环境中运行的js。对WEB前端逆向人员,没必要往更复杂理解。学习思路如下

 复制代码 隐藏代码
a. 写一套nodejs中可用的测试用例
b. 正向应用webpack技术,从a中js得到新的带符号信息的js
c. 编写HTML,在browser中使用b中js
d. F12调试,了解webpack框架流程
e. 正向应用webpack技术,从a中js得到新的strip过的js,接近现实世界案例
f. 针对e中js进行逆向工程,从中抠取webpack代码,得到新的js
g. 在nodejs中加载f中js,调用其中感兴趣的函数,得到返回值

按此思路学习,可从原理上真正理解「webpack代码抠取」。

☆ nodejs环境完整实验用例

假设目录结构如下

 复制代码 隐藏代码
/home/scz/src/js/webpack/hello/
|
\---src/                    // 原始js所在
        foo_0.js
        foo_1.js
        foo_2.js
        foo_3.js
        bar_0.js
        bar_1.js
        bar_2.js
        bar_3.js
        hello_node.js       //for nodejs
        crypto-js.min.js    // https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/crypto-js/4.1.1/crypto-js.min.js

a中js位于”hello/src”子目录,这是一组nodejs环境完整实验用例。

 复制代码 隐藏代码
// foo_0.js
functionfunc_foo_0 ( sth ) {
    console.log( sth );
}

module.exports  = {
    func_foo_0  : func_foo_0,
};

 复制代码 隐藏代码
// foo_1.js
function func_foo_1( a, b ) {
    return a + b;
}

module.exports  = {
    func_foo_1  : func_foo_1,
};

 复制代码 隐藏代码
// foo_2.js
letCryptoJS    = require( './crypto-js.min.js' );

functionfunc_foo_2 ( sth ) {
    let hash    = CryptoJS.MD5( sth );
    return hash.toString( CryptoJS.enc.Hex );
}

module.exports  = {
    func_foo_2  : func_foo_2,
};

 复制代码 隐藏代码
// foo_3.js
letCryptoJS    = require( './crypto-js.min.js' );

functionfunc_foo_3 ( sth ) {
    let hash    = CryptoJS.SHA256( sth );
    return hash.toString( CryptoJS.enc.Hex );
}

module.exports  = {
    func_foo_3  : func_foo_3,
};

 复制代码 隐藏代码
// bar_0.js
let foo_0   = require( './foo_0.js' );

functionfunc_bar_0 ( sth ) {
    foo_0.func_foo_0( sth );
}

module.exports  = {
    func_bar_0  : func_bar_0,
};

 复制代码 隐藏代码
// bar_1.js
let foo_1   = require( './foo_1.js' );

functionfunc_bar_1 ( a, b ) {
    return foo_1.func_foo_1( a, b );
}

module.exports  = {
    func_bar_1  : func_bar_1,
};

 复制代码 隐藏代码
// bar_2.js
let foo_2   = require( './foo_2.js' );

functionfunc_bar_2 ( sth ) {
    return foo_2.func_foo_2( sth );
}

module.exports  = {
    func_bar_2  : func_bar_2,
};

 复制代码 隐藏代码
// bar_3.js
asyncfunctionfunc_bar_3 ( sth ) {
    let foo_3   = awaitimport( './foo_3.js' );
    return foo_3.func_foo_3( sth );
}

module.exports  = {
    func_bar_3  : func_bar_3,
};

 复制代码 隐藏代码
// hello_node.js
(async() => {

let bar_1   = require( './bar_1.js' );
let bar_3   = require( './bar_3.js' );
let foo_3   = require( './foo_3.js' );

asyncfunctiondosth ( sth, a, b ) {
    let bar_0   = awaitimport( './bar_0.js' );
    let bar_2   = awaitimport( './bar_2.js' );
    let foo_2   = awaitimport( './foo_2.js' );

    sth         = sth + ' ' + bar_1.func_bar_1( a, b );
    bar_0.func_bar_0( sth );

    let ret_2   = bar_2.func_bar_2( sth );
    bar_0.func_bar_0( ret_2 );
    ret_2       = foo_2.func_foo_2( sth );
    bar_0.func_bar_0( ret_2 );

    let ret_3   = await bar_3.func_bar_3( sth );
    bar_0.func_bar_0( ret_3 );
    ret_3       = foo_3.func_foo_3( sth );
    bar_0.func_bar_0( ret_3 );
}

awaitdosth( 'Hello World', 5120, 1314 );
awaitdosth( 'Webpack Test', 1234, 5678 );

})();

上例涉及静态导入、动态导入、静态导出,但未涉及动态导出,求MD5、SHA256。

 复制代码 隐藏代码
$ node hello_node.js

Hello World 6434
2ab71d221778bf89844546711dab751d
2ab71d221778bf89844546711dab751d
f9b5771e0c341ceec6546e44b4d4212930413f185396542628d544618a45149a
f9b5771e0c341ceec6546e44b4d4212930413f185396542628d544618a45149a
Webpack Test6912
1c4a039a5443cdee3fc425220a46a576
1c4a039a5443cdee3fc425220a46a576
f00ac1bc3e8d90718658228e0c5657c24f55537583fee7527a5e10933657ff27
f00ac1bc3e8d90718658228e0c5657c24f55537583fee7527a5e10933657ff27

☆ 安装webpack开发环境

 复制代码 隐藏代码
cd /home/scz/src/js/webpack/hello
npm init-y (将在当前目录生成package.json)
npm install --save-dev webpack webpack-cli (不要指定-g,将使用当前目录)
npm list
npm uninstall webpack webpack-cli

当前测试版本如下

 复制代码 隐藏代码
Node.js v20.11.1
[email protected]
[email protected]

☆ 用webpack打包成browser可用代码

1) hello.js

 复制代码 隐藏代码
// /home/scz/src/js/webpack/hello/src/hello.js
(async() => {

let bar_1   = require( './bar_1.js' );
let bar_3   = require( './bar_3.js' );
let foo_3   = require( './foo_3.js' );

asyncfunctiondosth ( sth, a, b ) {
    let bar_0   = awaitimport( './bar_0.js' );
    let bar_2   = awaitimport( './bar_2.js' );
    let foo_2   = awaitimport( './foo_2.js' );

    sth         = sth + ' ' + bar_1.func_bar_1( a, b );
    bar_0.func_bar_0( sth );

    let ret_2   = bar_2.func_bar_2( sth );
    bar_0.func_bar_0( ret_2 );
    ret_2       = foo_2.func_foo_2( sth );
    bar_0.func_bar_0( ret_2 );

    let ret_3   = await bar_3.func_bar_3( sth );
    bar_0.func_bar_0( ret_3 );
    ret_3       = foo_3.func_foo_3( sth );
    bar_0.func_bar_0( ret_3 );
}

window.dosth = dosth;

})();

从hello_node.js修改出hello.js,主要区别是,全局导出dosth函数,不在IIFE (Immediately Invoked Function Expression/立即执行的函数表达式)中直接调用dosth函数,留待hello.html交互式调用,见后。

2) webpack配置文件

webpack打包时,需要原始js,还需要配置文件,一般名为webpack.config.js

 复制代码 隐藏代码
// /home/scz/src/js/webpack/hello/webpack.config.js
let path        = require( 'path' );

module.exports  = {
    mode: 'development',
    entry: './src/hello.js',
    output: {
        filename: 'hello.bundle.js',
        path: path.resolve( __dirname, 'dist' ),
        publicPath: 'dist/',
        chunkFilename: '[name].chunk.js',
    },
    resolve: {
        fallback: {
            "crypto": false,
        },
    },
    experiments: {
        topLevelAwait: false,
    },
    optimization: {
        minimize: false,
    },
    devtool: 'source-map',
};

 复制代码 隐藏代码
///home/scz/src/js/webpack/hello/webpack.config_1.js
letpath        =require('path');

module.exports= {
    mode:'production',
    entry:'./src/hello.js',
    output: {
        filename:'hello.bundle.js',
        path:path.resolve(__dirname, 'dist_1'),
        publicPath:'dist_1/',
        chunkFilename:'[name].chunk.js',
    },
    resolve: {
        fallback: {
            "crypto":false,
        },
    },
    experiments: {
        topLevelAwait:false,
    },
    optimization: {
        minimize:true,
    },
    devtool:false,
};

上面给了两个配置文件;webpack.config.js用于开发者模式,打包结果包含丰富的原始信息,函数名人类可读;webpack.config_1.js用于生产模式,打包结果相当于strip过。大部分选项都是自解释的,还可用AI辅助理解。entry指定入口js,output指定打包结果所在。

“resolve fallback”非通用配置。测试用例用到crypto-js.min.js,webpack打包该文件时有个错,对本例无影响,通过指定”resolve fallback”规避之。

3) webpack打包

假设目录结构如下

 复制代码 隐藏代码
/home/scz/src/js/webpack/hello/
|   webpack.config.js       // development mode
|   webpack.config_1.js     // production mode
|   package.json            // npm init -y
|
+---node_modules/           // npm install --save-dev webpack webpack-cli
|
+---dist/                   // 手工创建,存放development mode打包结果
|
+---dist_1/                 // 手工创建,存放production mode打包结果
|
\---src/                    // 原始js所在
        hello.js            // 与webpack.config.js中entry相符
        ...

 复制代码 隐藏代码
cd /home/scz/src/js/webpack/hello

npx webpack
npx webpack -c webpack.config_1.js

第一条命令默认使用webpack.config.js,第二条命令指定任意配置文件。之后dist、dist_1分别出现不同模式的打包结果,重点查看其中的hello.bundle.js。

 复制代码 隐藏代码
/home/scz/src/js/webpack/hello/
|
+---dist                    // development mode
|       hello.bundle.js     // webpack打包生成
|       hello.bundle.js.map
|       src_bar_0_js.chunk.js
|       src_bar_0_js.chunk.js.map
|       src_bar_2_js.chunk.js
|       src_bar_2_js.chunk.js.map
|       src_foo_2_js.chunk.js
|       src_foo_2_js.chunk.js.map
|
\---dist_1                  // production mode
        245.chunk.js
        808.chunk.js
        954.chunk.js
        hello.bundle.js     // webpack打包生成

4) hello.html

webpack打包结果应该在浏览器中测试,配套的hello.html、hello_1.html如下

 复制代码 隐藏代码
<!DOCTYPE&nbsp;html>
<html>
&nbsp; &nbsp;&nbsp;<head>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<metacharset="utf-8">
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<title>Hello World</title>
&nbsp; &nbsp;&nbsp;</head>
&nbsp; &nbsp;&nbsp;<body>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<h1>Hello World</h1>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<buttononclick="(async() => { await dosth( 'Hello World', 5120, 1314 ); })();">Test 0</button>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<p>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<buttononclick="(async() => { await dosth( 'Webpack Test', 1234, 5678 ); })();">Test 1</button>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<scriptsrc="./dist/hello.bundle.js"></script>
&nbsp; &nbsp;&nbsp;</body>
</html>

&nbsp;复制代码&nbsp;隐藏代码
<!DOCTYPE&nbsp;html>
<html>
&nbsp; &nbsp;&nbsp;<head>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<metacharset="utf-8">
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<title>Hello World</title>
&nbsp; &nbsp;&nbsp;</head>
&nbsp; &nbsp;&nbsp;<body>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<h1>Hello World</h1>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<buttononclick="(async() => { await dosth( 'Hello World', 5120, 1314 ); })();">Test 0</button>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<p>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<buttononclick="(async() => { await dosth( 'Webpack Test', 1234, 5678 ); })();">Test 1</button>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<scriptsrc="./dist_1/hello.bundle.js"></script>
&nbsp; &nbsp;&nbsp;</body>
</html>

&nbsp;复制代码&nbsp;隐藏代码
cd&nbsp;/home/scz/src/js/webpack/hello
python3 -m http.server -b&nbsp;192.168.65.258080
http://192.168.65.25:8080/hello.html
http://192.168.65.25:8080/hello_1.html

可用F12调试,辅助理解webpack打包结果的框架流程。

☆ 用AI辅助理解webpack打包结果框架流程

参看

《让AI协助阅读代码》

&nbsp;复制代码&nbsp;隐藏代码
https://scz.617.cn/misc/202503101024.txt

&nbsp;复制代码&nbsp;隐藏代码
python3 MergeFile.py <path>/dist dist.merge.txt js

写作本节时用的是”Gemini 2.5 Flash Preview 04-17″,上传dist.merge.txt,依次提问

&nbsp;复制代码&nbsp;隐藏代码
「这是webpack结果,请介绍每个文件的主要功能,请详细解释hello.bundle.js」
「chunk与module的区别」
「想从webpack生成的js中抽取代码,在nodejs环境中直接调用func_foo_2()」
「解释webpack异步chunk加载机制」

可根据自身水平提问,比如我还问过

&nbsp;复制代码&nbsp;隐藏代码
Q:

let foo_2 &nbsp; = await __webpack_require__.e(/*! import() */"src_foo_2_js").then(__webpack_require__.t.bind(__webpack_require__,&nbsp;/*! ./foo_2.js */"./src/foo_2.js",&nbsp;23));

这句怎么知道要去找src_foo_2_js.chunk.js?后者有句

push([["src_foo_2_js"],{

但这个push怎么与文件名src_foo_2_js.chunk.js产生关联的,哪段代码负责这种关
联?

&nbsp;复制代码&nbsp;隐藏代码
Q:

为什么

__webpack_require__.t.bind(__webpack_require__,&nbsp;"./src/foo_2.js",&nbsp;23)

而非

__webpack_require__("./src/foo_2.js")

这些问题都得到AI的良好回答,节省篇幅,略。了解webpack框架流程后,从逆向工程角度看,全局导出__webpack_require__是关键;只需关心moduleId,无需关心chunkId。

此节内容应亲自实践,不要跳过,后续内容假设已了解webpack框架流程。

☆ webpack动态导出

在hello.bundle.js中有

&nbsp;复制代码&nbsp;隐藏代码
/* webpack/runtime/define property getters */
(() =>&nbsp;{
&nbsp; &nbsp;&nbsp;// define getter functions for harmony exports
&nbsp; &nbsp; __webpack_require__.d&nbsp;=&nbsp;(exports, definition) =>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for(var&nbsp;key&nbsp;in&nbsp;definition) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object.defineProperty(exports, key, {&nbsp;enumerable:&nbsp;true,&nbsp;get: definition[key] });
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; };
})();

/* webpack/runtime/hasOwnProperty shorthand */
(() =>&nbsp;{
&nbsp; &nbsp; __webpack_require__.o&nbsp;=&nbsp;(obj, prop) =>&nbsp;(Object.prototype.hasOwnProperty.call(obj, prop))
})();

__webpack_require__.d用于动态导出。src_foo_2_js.chunk.js中有

&nbsp;复制代码&nbsp;隐藏代码
module.exports&nbsp; = {
&nbsp; &nbsp; func_foo_2 &nbsp;: func_foo_2,
};

这种姑且称为静态导出,与之对应的动态导出形如

&nbsp;复制代码&nbsp;隐藏代码
__webpack_require__.d(
&nbsp; &nbsp; __unused_webpack_exports,
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"func_foo_2": (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;function&nbsp;() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;func_foo_2;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; ),
&nbsp; &nbsp; }
);

__webpack_require__.d()的第一形参需要对应模块函数的第二形参,本例模块函数形参如下

&nbsp;复制代码&nbsp;隐藏代码
(module, __unused_webpack_exports, __webpack_require__)

本例webpack结果保持静态导出,未使用动态导出,所以模块函数第二形参名字中出现”unused”,但这不影响”静态导出”转”动态导出”。

现实世界中多次遭遇动态导出。

☆ 在nodejs环境中使用browser环境代码

假设目录结构如下

&nbsp;复制代码&nbsp;隐藏代码
/home/scz/src/js/webpack/hello/
|
+---browser2node &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//&nbsp;development mode
| &nbsp; &nbsp; &nbsp; hello.bundle_1.js &nbsp;&nbsp;//&nbsp;从"dist/hello.bundle.js"修改而来
| &nbsp; &nbsp; &nbsp; src_foo_2_js.chunk.js
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;从"dist/src_foo_2_js.chunk.js"复制而来
| &nbsp; &nbsp; &nbsp; run_webpack_code_1.js
| &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;在nodejs环境中使用browser环境代码
|
\---browser2node_1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//&nbsp;production mode
&nbsp; &nbsp; &nbsp; &nbsp; hello.bundle_1.js &nbsp;&nbsp;//&nbsp;从"dist_1/hello.bundle.js"修改而来
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;808.chunk.js &nbsp; &nbsp; &nbsp; &nbsp;//&nbsp;从"dist_1/808.chunk.js"复制而来
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;此即strip过的src_foo_2_js.chunk.js
&nbsp; &nbsp; &nbsp; &nbsp; run_webpack_code_1.js
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;在nodejs环境中使用browser环境代码

1) 全局导出__webpack_require__函数

从dist目录复制hello.bundle.js、src_foo_2_js.chunk.js到browser2node目录,对hello.bundle_1.js进行改造。

&nbsp;复制代码&nbsp;隐藏代码
function&nbsp;__webpack_require__(moduleId)&nbsp;{
&nbsp; &nbsp;&nbsp;// Check if module is in cache
&nbsp; &nbsp;&nbsp;varcachedModule=&nbsp;__webpack_module_cache__[moduleId];
&nbsp; &nbsp;&nbsp;if&nbsp;(cachedModule !== undefined) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;cachedModule.exports;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// Create a new module (and put it into the cache)
&nbsp; &nbsp;&nbsp;varmodule=&nbsp;__webpack_module_cache__[moduleId] = {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; };

&nbsp; &nbsp;&nbsp;// Execute the module function
&nbsp; &nbsp; __webpack_modules__[moduleId].call(module.exports,&nbsp;module,&nbsp;module.exports, __webpack_require__);

&nbsp; &nbsp;&nbsp;// Return the exports of the module
&nbsp; &nbsp;&nbsp;returnmodule.exports;
}
//
// 在__webpack_require__函数体后增加如下代码,全局导出该函数
//
globalThis.__exposedWebpackRequire &nbsp;= __webpack_require__;

主要就是全局导出__webpack_require__函数,熟悉框架后可精简hello.bundle_1.js,初学者不建议精简。

2) run_webpack_code_1.js

&nbsp;复制代码&nbsp;隐藏代码
// /home/scz/src/js/webpack/hello/browser2node/run_webpack_code_1.js
global.self&nbsp; &nbsp; &nbsp;=&nbsp;global;

require(&nbsp;'./hello.bundle_1.js'&nbsp;);
require(&nbsp;'./src_foo_2_js.chunk.js'&nbsp;);

let&nbsp;__webpack_require__
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = globalThis.__exposedWebpackRequire;
let&nbsp;foo_2 &nbsp; &nbsp; &nbsp; =&nbsp;__webpack_require__(&nbsp;"./src/foo_2.js"&nbsp;);
let&nbsp;sth &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;"Webpack Test 6912";
console.log( sth );
let&nbsp;ret &nbsp; &nbsp; &nbsp; &nbsp; = foo_2.func_foo_2( sth );
console.log( ret );

为什么要补个全局self?本例加载src_foo_2_js.chunk.js时,第一句就是

&nbsp;复制代码&nbsp;隐藏代码
(self["webpackChunkhello"] =&nbsp;self["webpackChunkhello"] || []).push([["src_foo_2_js"],{

nodejs环境缺省没有self变量。

本例比较简单,复用webpack打包结果时,补环境时只需补全局self变量;实战时缺啥补啥。

&nbsp;复制代码&nbsp;隐藏代码
$ node run_webpack_code_1_test.js
Webpack&nbsp;Test6912
1c4a039a5443cdee3fc425220a46a576

3) 生产模式webpack打包结果复用

前面是开发者模式webpack打包结果复用,但逆向工程遇上的是生产模式webpack打包结果复用。实战时,假设F12调试定位到某函数,需要抠取到nodejs环境中调用之。F12调试hello_1.html时,注意到808.chunk.js中含有MD5相关代码。从dist_1目录复制hello.bundle.js、808.chunk.js到browser2node_1目录,改成hello.bundle_1.js。

3.1) 全局导出__webpack_require__函数

生产模式strip过,hello.bundle_1.js中没有__webpack_require__这个符号,已变成某个短名字,可F12动态调试定位。

比如808.chunk.js内容如下:

&nbsp;复制代码&nbsp;隐藏代码
(self.webpackChunkhello&nbsp;= self.webpackChunkhello&nbsp;|| []).push([[245,&nbsp;808], {
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 245模块入口,第三形参o即__webpack_require__
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;245:&nbsp;(e, n, o) =>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// F12 Console中查看o,即可定位__webpack_require__函数
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;t =&nbsp;o(633);
&nbsp; &nbsp; &nbsp; &nbsp; e.exports&nbsp;= {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;func_foo_2:&nbsp;function(e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;t.MD5(e).toString(t.enc.Hex)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 245模块出口
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; }
&nbsp; &nbsp; ,
&nbsp; &nbsp;&nbsp;808:&nbsp;(e, n, o) =>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;t =&nbsp;o(245);
&nbsp; &nbsp; &nbsp; &nbsp; e.exports&nbsp;= {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;func_bar_2:&nbsp;function(e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;t.func_foo_2(e)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}]);

参看注释,可断在245模块入口,查看模块函数第三形参o,跳过去。

本例__webpack_require__函数实际是这个:

&nbsp;复制代码&nbsp;隐藏代码
//
// 此即__webpack_require__函数,只是本例中名为s,t即moduleId
//
function&nbsp;s(t)&nbsp;{
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// o[t]实际是__webpack_module_cache__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;vare=&nbsp;o[t];
&nbsp; &nbsp;&nbsp;if&nbsp;(void0&nbsp;!== e)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;e.exports;
&nbsp; &nbsp;&nbsp;varr=&nbsp;o[t] = {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; };
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// n[t]实际是__webpack_modules__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;return&nbsp;n[t].call(r.exports, r, r.exports, s),
&nbsp; &nbsp; r.exports
}
//
// 在__webpack_require__函数体后增加如下代码,全局导出该函数
//
globalThis.__exposedWebpackRequire &nbsp;= s;

无论strip与否,webpack_require__函数框架样子差不多都这样,熟悉后亦可静态定位之。同样对hello.bundle_1.js进行改造,全局导出webpack_require__函数。

全局导出时用什么名字无所谓,上面只是我的习惯用法。

3.2) run_webpack_code_1.js

&nbsp;复制代码&nbsp;隐藏代码
// /home/scz/src/js/webpack/hello/browser2node_1/run_webpack_code_1.js
global.self&nbsp; &nbsp; &nbsp;=&nbsp;global;

require(&nbsp;'./hello.bundle_1.js'&nbsp;);
require(&nbsp;'./808.chunk.js'&nbsp;);

let&nbsp;__webpack_require__
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = globalThis.__exposedWebpackRequire;
let&nbsp;module_245 &nbsp;=&nbsp;__webpack_require__(&nbsp;245&nbsp;);
let&nbsp;module_808 &nbsp;=&nbsp;__webpack_require__(&nbsp;808&nbsp;);
let&nbsp;module_54 &nbsp; =&nbsp;__webpack_require__(&nbsp;54&nbsp;);

let&nbsp;sth &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;"Webpack Test 6912";
console.log( sth );

let&nbsp;ret;
ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = module_245.func_foo_2( sth );
console.log( ret );
ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = module_808.func_bar_2( sth );
console.log( ret );
ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = module_54.func_foo_3( sth );
console.log( ret );

&nbsp;复制代码&nbsp;隐藏代码
$ node run_webpack_code_1.js
Webpack&nbsp;Test6912
1c4a039a5443cdee3fc425220a46a576
1c4a039a5443cdee3fc425220a46a576
f00ac1bc3e8d90718658228e0c5657c24f55537583fee7527a5e10933657ff27

3.3) 半自动收集245模块所有依赖模块

假设245模块内部依赖很多其他模块,分散在不同chunk文件中,想收集这些被依赖的模块,本小节介绍如何达此目的。

在245模块入口、出口分别设断点。待245模块入口断点命中后,Console中查看第三形参(本例为o),此即__webpack_require__函数;点击跳转,即hello.bundle_1.js中”function s(t) {“所在。

&nbsp;复制代码&nbsp;隐藏代码
//
// 此即__webpack_require__函数,只是本例中名为s,t即moduleId
//
function&nbsp;s(t)&nbsp;{
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// o[t]实际是__webpack_module_cache__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;vare=&nbsp;o[t];
&nbsp; &nbsp;&nbsp;if&nbsp;(void0&nbsp;!== e)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;e.exports;
&nbsp; &nbsp;&nbsp;varr=&nbsp;o[t] = {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; };
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 可在此设日志断点
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 'Need module ' + moduleId
&nbsp; &nbsp;&nbsp;// 'Need module ' + t
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// window.module_array += moduleId + ':' + __webpack_modules__[moduleId] + ',\n'
&nbsp; &nbsp;&nbsp;// window.module_array += t + ':' + n[t] + ',\n'
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// n[t]实际是__webpack_modules__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;return&nbsp;n[t].call(r.exports, r, r.exports, s),
&nbsp; &nbsp; r.exports
}

在__webpack_require()入口设断点,F8继续,断点命中后立即删除该断点。先在Console中清空webpack_module_cache__,如下

&nbsp;复制代码&nbsp;隐藏代码
__webpack_module_cache__&nbsp;= {} &nbsp; // 原理
o&nbsp;= {} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 本例

这步是为了后面的__webpack_modules[moduleId].call(本例对应n[t].call)触发更多的依赖。考虑这种情况,245模块依赖633模块,633模块在245模块之前因为其他原因已加载过,webpack_module_cache中有633模块对应项;633模块依赖某个假想中的xxx模块;现在加载245模块,n[t].call不会再次触发对633、xxx模块的加载,很可能导致收集不到xxx模块信息。预清空webpack_module_cache__,可收集245模块更多的依赖。

即使这样,也不能保证一次性收集245模块所有依赖。比如加载245模块时触发的依赖已收集到位,但调用245模块中某函数时触发的依赖在加载245模块时收集不到,只能等报错后用同样技术继续补充模块,此时可能出现模块重复;现实世界遭遇过。

假设正断在__webpack_require__()入口,立即在Console中定义一个全局变量

&nbsp;复制代码&nbsp;隐藏代码
window.module_array&nbsp;=&nbsp;''

后面靠它收集结果。

webpack_require()中必有webpack_modules[moduleId].call()。即使strip过,也能找到,只要找唯一的call()即可,本例中是n[t].call(),此n[t]即__webpack_modules__[moduleId]。

在call所在行设日志断点,比如

&nbsp;复制代码&nbsp;隐藏代码
window.module_array += moduleId +&nbsp;':'&nbsp;+ __webpack_modules__[moduleId] +&nbsp;',\n'
window.module_array +=&nbsp;'"'&nbsp;+ moduleId +&nbsp;'":'&nbsp;+ __webpack_modules__[moduleId] +&nbsp;',\n'

本例应为

&nbsp;复制代码&nbsp;隐藏代码
window.module_array += t +&nbsp;':'&nbsp;+ n[t] +&nbsp;',\n'

日志断点中作为key出现的moduleId两侧加双引号与否,视当前case而定,两种情况都在现实世界遭遇过。

为什么在call所在行设日志断点,而非在__webpack_require()入口设日志断点?即使前面已经预清空webpack_module_cache,但在加载245模块过程中,仍可能多次命中webpack_module_cache__,cache的目的就是这个。若在入口设前述日志断点,可能导致module_array重复收集模块,体积不必要地增大。

测试观察到意外现象,dict的key可以重复,不会报错。若真碰上模块重复,可不消重,有洁癖时再说。

F8继续执行,会断在245模块出口断点,Console中查看window.module_array,即245模块所有依赖模块。右键”Copy string contents”,用美化网站查看,比如:

&nbsp;复制代码&nbsp;隐藏代码
https://beautifier.io/

理论上可将window.module_array的内容全部放入one_bundle.js,本例过于简单,看不出效果。此法在超级复杂的现实webpack中半自动抠取成功,呈深度优先遍历。

为什么在245模块入口、出口同时设断点?因为只想收集245模块的依赖模块,出口断点可避免污染window.module_array。

前述操作初看较繁琐,熟悉后并不复杂。亦可考虑Local Overrides,而非日志断点。

☆ 实战讨论

1) 识别webpack

特征比较多,看几个现实世界示例,F12调试时注意到

&nbsp;复制代码&nbsp;隐藏代码
(window.webpackJsonp = window.webpackJsonp || []).push([[242], {
&nbsp; &nbsp;&nbsp;2368: function(t, e, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(42),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(43),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(7),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(4176),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(4177),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(34),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;n(54);

&nbsp;复制代码&nbsp;隐藏代码
!function(e) {
&nbsp; &nbsp; ...
}({
&nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;"7d92":&nbsp;function(e, t, n) {

上面的2368、7d92就是moduleId,后面的三参数函数就是模块函数。模块函数的第三形参,别管它名字是什么,都对应__webpack_require__函数。moduleId与模块函数呈”key:value”形式。

2) webpack_require/webpack_module_cache/webpack_modules

看几个现实世界示例

&nbsp;复制代码&nbsp;隐藏代码
//
// 此即__webpack_require__函数
//
function&nbsp;c(e)&nbsp;{
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// l[e]即__webpack_module_cache__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;if&nbsp;(l[e])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;l[e].exports;
&nbsp; &nbsp;&nbsp;vard=&nbsp;l[e] = {
&nbsp; &nbsp; &nbsp; &nbsp; i: e,
&nbsp; &nbsp; &nbsp; &nbsp; l: !1,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; };
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// o[e]即__webpack_modules__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;return&nbsp;o[e].call(d.exports, d, d.exports, c),
&nbsp; &nbsp; d.l = !0,
&nbsp; &nbsp; d.exports
}

&nbsp;复制代码&nbsp;隐藏代码
//
// 此即__webpack_require__函数
//
function&nbsp;o(t)&nbsp;{
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// n[t]即__webpack_module_cache__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;if&nbsp;(n[t])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;n[t].exports;
&nbsp; &nbsp;&nbsp;vari=&nbsp;n[t] = {
&nbsp; &nbsp; &nbsp; &nbsp; i: t,
&nbsp; &nbsp; &nbsp; &nbsp; l: !1,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; };
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// e[t]即__webpack_modules__[moduleId]
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;return&nbsp;e[t].call(i.exports, i, i.exports, o),
&nbsp; &nbsp; i.l = !0,
&nbsp; &nbsp; i.exports
}

webpack半自动抠取时需要找到这三个对象

&nbsp;复制代码&nbsp;隐藏代码
__webpack_require__/__webpack_module_cache__/__webpack_modules__

strip过的,都是短名字,框架结构不变。

3) webpack运行时

__webpack_require__()所在js含有完整的webpack运行时,现实世界中,运行时可能单独位于某个runtime.js中,可能位于某个chunk文件中。初学者建议将webpack运行时完整抠取,减少麻烦,熟悉之后另说。

4) webpack抠取结果的集成形式

我习惯于编写run_webpack_code.js,框架如下

&nbsp;复制代码&nbsp;隐藏代码
//
//&nbsp;补环境
//
global.self &nbsp; &nbsp; &nbsp; &nbsp; = global;
global.window &nbsp; &nbsp; &nbsp; = global;
window.navigator &nbsp; &nbsp;= {
&nbsp; &nbsp; userAgent:&nbsp;'...',
};
...

//////////////////////////////////////////////////////////////////////////

//
//&nbsp;webpack运行时,含改名过的__webpack_require__()
//
require(&nbsp;'./runtime.js'&nbsp;);

//
//&nbsp;目标模块所在chunk,已补完所有依赖模块
//
require(&nbsp;'./one_bundle.js'&nbsp;);

//////////////////////////////////////////////////////////////////////////

let __webpack_require__
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = globalThis.__exposedWebpackRequire;
let module_xxx &nbsp;= __webpack_require__( xxx );

//////////////////////////////////////////////////////////////////////////

let param &nbsp; &nbsp; &nbsp; = ...;

let ret &nbsp; &nbsp; &nbsp; &nbsp; = module_xxx.func_yyy( param );
console.log( ret );

用require()加载chunk,用__webpack_require__()进一步加载module。

runtime.js、one_bundle.js有可能分开,有可能在一个js中。无论哪种情况,都需全局导出__webpack_require__函数,以便run_webpack_code.js调用之。无论哪种情况,都需补全目标模块所有依赖模块。

看几个现实世界one_bundle.js示例

&nbsp;复制代码&nbsp;隐藏代码
(window.webpackJsonp&nbsp;=&nbsp;window.webpackJsonp&nbsp;|| []).push([[242], {
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 2368模块入口,第三形参n即__webpack_require__
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;2368:&nbsp;function(t, e, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 2368模块出口
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; },

&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// For 2368
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 此部分用半自动抠取技术获取
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;120:...,
&nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;2280:...,
}, [[...]]]);

&nbsp;复制代码&nbsp;隐藏代码
!function(e) {
&nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 此即__webpack_require__函数
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;functiono(t) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(n[t])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;n[t].exports;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;i = n[t] = {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;i: t,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;l: !1,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exports: {}
&nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;e[t].call(i.exports, i, i.exports, o),
&nbsp; &nbsp; &nbsp; &nbsp; i.l&nbsp;= !0,
&nbsp; &nbsp; &nbsp; &nbsp; i.exports
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 在__webpack_require__函数体后增加如下代码,全局导出该函数
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; globalThis.__exposedWebpackRequire&nbsp; = o;
&nbsp; &nbsp; ...
}({
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 7d92模块入口,第三形参n即__webpack_require__
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;"7d92":&nbsp;function(e, t, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 7d92模块出口
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; },

&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// For 7d92
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 此部分用半自动抠取技术获取
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;"b639":...,
&nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;"21bf":...,

&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 6c27模块入口,第三形参即__webpack_require__
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;"6c27":&nbsp;function(module,&nbsp;exports, __webpack_require__) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 6c27模块出口
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; },

&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// For 6c27
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;// 此部分用半自动抠取技术获取
&nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp;&nbsp;"f28c":...,
&nbsp; &nbsp;&nbsp;"3c35":...,
});

5) 补环境

逆向工程一般关注算法函数,其不涉及DOM操作。用半自动抠取技术后,一般补环境不会很复杂,主要是self、window、navigator、document这几个。没补全时,执行run_webpack_code.js会报错,根据错误提示再补。

若真碰上补document,可能比较复杂,现实世界让AI补过一次

&nbsp;复制代码&nbsp;隐藏代码
global.document&nbsp; &nbsp; &nbsp;= {
&nbsp; &nbsp;&nbsp;createEvent:&nbsp;function(&nbsp;type&nbsp;) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;timeStamp:&nbsp;Number( process.hrtime.bigint() /&nbsp;1_000_000n&nbsp;),
&nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; },
};

6) 导出module_xxx.func_yyy

若module_xxx.func_yyy已静态导出,这种易认别,模块函数中会有module.exports之类的代码。

现实世界碰上过动态导出

&nbsp;复制代码&nbsp;隐藏代码
(window.webpackJsonp&nbsp;=&nbsp;window.webpackJsonp&nbsp;|| []).push([[242], {
&nbsp; &nbsp;&nbsp;2368:&nbsp;function(t, e, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; (function(t) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/*
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;* 将来有个module_2368.a,对应h(),相当于导出h()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; n.d(e,&nbsp;"a", (function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;h
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ));

&nbsp;复制代码&nbsp;隐藏代码
!function(e) {
&nbsp; &nbsp; ...
}({
&nbsp; &nbsp;&nbsp;"7d92":&nbsp;function(e, t, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; (function(e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/*
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;* 将来有个module_7d92.a,对应f(),相当于导出f()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; n.d(t,&nbsp;"a", (function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;f
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )),

此二例的__webpack_require__.d()定义如下

&nbsp;复制代码&nbsp;隐藏代码
c.d =&nbsp;function(o, e, d)&nbsp;{
&nbsp; &nbsp; c.o(o, e) || Object.defineProperty(o, e, {
&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;enumerable: !0,
&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;get: d
&nbsp; &nbsp; })
}

&nbsp;复制代码&nbsp;隐藏代码
o.d =&nbsp;function(e, t, n)&nbsp;{
&nbsp; &nbsp; o.o(e, t) || Object.defineProperty(e, t, {
&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;enumerable: !0,
&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;get: n
&nbsp; &nbsp; })
}

比较弱,只支持一组”key:value”;hello.bundle.js中的d()支持多组”key:value”。

F12调试,断在func_yyy入口,查看调用栈回溯,可看出func_yyy有否导出。若module_xxx未导出func_yyy,既无静态导出,亦无动态导出,可修改module_xxx的模块函数,手工增加导出。试举一例

&nbsp;复制代码&nbsp;隐藏代码
(window.webpackJsonp&nbsp;=&nbsp;window.webpackJsonp&nbsp;|| []).push([[242], {
&nbsp; &nbsp;&nbsp;2368:&nbsp;function(t, e, n) {
&nbsp; &nbsp; &nbsp; &nbsp; ...

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 保存2368模块对象
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;module_2368 = t;

&nbsp; &nbsp; &nbsp; &nbsp; (function(t) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionh(e, path, n, r) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 让2368模块导出h()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; module_2368.exports&nbsp; = {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; h &nbsp; : h,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; ).call(this,&nbsp;n(120).Buffer)
&nbsp; &nbsp; },

2368模块有个动态导出,上例只是用它演示冗余的静态导出,二者可并存。

7) 怎么找模块

不用刻意找webpack运行时,通过目标模块函数第三形参可快速找到webpack运行时。一般也不用刻意找模块,因为想抠代码时,肯定是F12调试到目标模块的目标函数了。

但有时想静态找其他模块,怎么找?可Ctrl-Shift-F全局搜索

&nbsp;复制代码&nbsp;隐藏代码
120:function&nbsp; &nbsp;&nbsp;// 中间无空格
,42:function&nbsp; &nbsp;&nbsp;// 前面有逗号
{3665:function&nbsp;&nbsp;// 前面有左花括号

现实世界webpack结果非常紧凑,搜索”key:value”时,不要乱加空格。有些key太短,可在key前面加逗号、左花括号,减少误命中,但可能导致漏命中。

可F12断在webpack_require()中,用webpack_modules[moduleId]快速定位模块。现实世界strip过,需将原理性符号换成实际变量名。若不想每次F12断在webpack_require()中时才能用此法,可全局导出webpack_modules,比如用某种日志断点持续全局导出。

☆ 小结

「webpack代码抠取」基本过程

&nbsp;复制代码&nbsp;隐藏代码
a. F12调试跟踪至目标模块中某函数,欲抠取
b. 识别出目标模块的模块函数,识别模块入口、出口
c. 通过模块入口第三形参定位__webpack_require__()
d. 下载webpack运行时所在js,修改,全局导出__webpack_require__()
e. 下载目标模块所在chunk,精简,只留目标模块
f. 半自动抠取目标模块的依赖模块,补充到e中chunk
g. 编写run_webpack_code.js,加载webpack运行时,加载f中chunk,调用目标函数

步骤d、e可能是同一个js。步骤g可能需要二次补依赖模块。

有一些傻瓜化工具用于webpack抠取,不好这口,略过。

webpack抠取多用于黑灰产,实在想不出正经人为什么有这种需求?本文仅为逆向工程技术探讨,无意招麻烦,无现实世界完整示例,但相关技术全部提供,无保留,照猫画虎可实操。

完整测试用例打包

&nbsp;复制代码&nbsp;隐藏代码
https://scz.617.cn/web/202505130933.txt
https://scz.617.cn/web/202505130933.7z

☆ 其它

参看

《WEB前端逆向JS RPC简介》

&nbsp;复制代码&nbsp;隐藏代码
https://scz.617.cn/web/202408281708.txt

「JS RPC」是「webpack代码抠取」的反面,不抠代码,直接在nodejs与browser之间搭一个通道,nodejs可调用browser中的函数。

附件下载链接:https://pan.quark.cn/s/e08353802ec1

·今 日 推 荐·

本文内容来自网络,如有侵权请联系删除


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:逆向有你 scz scz《WEB前端逆向在nodejs环境中复用webpack代码》

评论:0   参与:  0