鐵人賽 Day 19 Webpack 編譯過的程式碼是如何運作的?解析打包後的結構

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
在前端資安逆向工程中遇到 Webpack 打包編譯過的程式碼是非常常見的,所以理解 Webpack 的內部機制,對於資安逆向工程來說,從打包編譯過的程式碼找到關鍵邏輯是非常之重要的。
Webpack 是什麼?
原由
Webpack 是前端模組打包工具,在 Webpack 出現之前可能會使用 <script> 多個標籤引入 JavaScript 檔案。
這會導致全域變數污染、載入順序問題以及請求過多等眾多問題。
Webpack 作為一個模組打包工具將這些分散的 JavaScript 模組打包成一個或多個檔案。
核心概念
Webpack 的核心概念是把專案中所有的資源(JavaScript、CSS、圖片、字體等)都視為模組,透過分析依賴關係,把它們打包成一個或多個「bundle 檔案」,方便在瀏覽器中載入與使用。
分析Webpack打包編譯後的程式
這是一個簡單的範例
(() => {
var n = {
123: (module, exports, t) => {
const Dog = t(999);
const d = new Dog();
d.eat();
},
153: (module, exports, t) => {
class Person{
constructor(n){
this.name = n;
}
sayHello(){
console.log(`Hello, ${this.name}!`);
}
}
module.exports = new Person("Nick")
},
999: (module, exports, t) => {
class Dog {
eat() {
console.log(`The dog is eating food`);
}
}
module.exports = Dog;
}
},
r = {};
function t(u) {
var e = r[u];
if (void 0 !== e)
return e.exports;
var i = r[u] = { exports: {} };
n[u].call(i.exports, i, i.exports, t);
return i.exports;
}
t.g = function() {
if ("object" == typeof globalThis)
return globalThis;
try {
return this || new Function("return this")()
} catch (n) {
if ("object" == typeof window)
return window
}
}(),
t.o = (n, r) => Object.prototype.hasOwnProperty.call(n, r),
t(123)
})();
立即執行函式
整包程式的本體使用立即執行函式包裝起來,起到一個封閉的作用域執行程式,也避免全域污染。
模組定義
這裡的 n 物件就像是 Webpack 打包後的模組表。
key 是模組 ID(通常是壓縮過或編號的數字)。
value 是一個函式,代表模組的程式碼。
var n = {
123: (module, exports, t) => {
const Dog = t(999);
const d = new Dog();
d.eat();
},
153: (module, exports, t) => {
class Person{
constructor(n){
this.name = n;
}
sayHello(){
console.log(`Hello, ${this.name}!`);
}
}
module.exports = new Person("Nick")
},
999: (module, exports, t) => {
class Dog {
eat() {
console.log(`The dog is eating food`);
}
}
module.exports = Dog;
}
}
模組快取
r = {};
這是快取物件,避免同一個模組重複執行。
模組載入器 (t)
function t(u) {
var e = r[u];
if(void 0 !== e)
return e.exports;
var i = r[u] = { exports: {} };
n[u].call(i.exports, i, i.exports, t);
return i.exports
}
- 先檢查快取 r[u] 裡有沒有資料,如果有就是有載入過。
- 沒有的話,建立一個新的模組物件 { exports: {} }。
- 執行對應的模組函式 n[u],並傳入 (模組本身, exports 物件, 載入器函式)。
- 回傳 exports。
輔助工具
t.g = function() {
if("object" == typeof globalThis)
return globalThis;
try{
return this || new Function("return this")()
}catch(n){
if("object" == typeof window)
return window
}
}(),
t.o = (n, r) => Object.prototype.hasOwnProperty.call(n, r),
- t.g:取得全域物件(不管是 globalThis, window, global 都能取得)。
- t.o:檢查物件是否有某個屬性(封裝 Object.prototype.hasOwnProperty)。
啟動程式
t(123)
外拋載入器
我們已經知道t函式是載入器,n是所有的模組,那我們可以將t外拋給window或是global這時我們可以隨心所欲的調用所有的模組。

實際的執行結果如下

圖片中「1」為原先t(123)的執行結果。
圖片中「2」為外部調用t(999)的執行結果。
這時可以方便的調用所有模組對於逆向分析來說有極大的幫助。