爬蟲逆向基礎,理解 JavaScript 模組化程式設計 webpack

K哥爬蟲發表於2021-10-21

關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

簡介

在分析一些站點的 JavaScript 程式碼時,比較簡單的程式碼,函式通常都是一個一個的,例如:

function a() {console.log("a")}
function b() {console.log("a")}
function c() {console.log("a")}

但是稍微複雜一點的站點,通常會遇到類似如下的程式碼結構:

!function(i) {
    function n(t) {
        return i[t].call(a, b, c, d)
    }
}([
    function(t, e) {}, 
    function(t, e, n) {}, 
    function(t, e, r) {}, 
    function(t, e, o) {}
]);

這種寫法在 JavaScript 中很常見,對於熟悉 JavaScript 的人來說可能非常簡單,但是爬蟲工程師大多數都是用 Python 或者 Java 來寫程式碼的,看到這種語法就有可能懵了,由於在剝離 JS 加密程式碼時會經常遇到,所以理解這種語法對於爬蟲工程師來說是非常重要的。

這種寫法貌似沒有官方的名稱,相當於進行了模組化程式設計,因此大多數人稱其為 webpack,上面的示例看起來比較費勁,簡單優化一下:

!function (allModule) {
    function useModule(whichModule) {
        allModule[whichModule].call(null, "hello world!");
    }
    useModule(0)
}([
    function module0(param) {console.log("module0: " + param)},
    function module1(param) {console.log("module1: " + param)},
    function module2(param) {console.log("module2: " + param)},
]);

執行以上程式碼,會輸出 module0: hello world!,相信通過淺顯易懂的變數名和函式名,應該就可以看懂大致含義了,呼叫 useModule(0),從所有函式裡選擇第一個,將 hello world! 傳遞給 module0 並輸出。

仔細觀察以上程式碼,我們會發現主要用到了 !function(){}()function.call() 語法,接下來就一一介紹一下。

函式宣告與函式表示式

在 ECMAScript(JavaScript 的一個標準)中,有兩個最常用的建立函式物件的方法,即使用函式宣告或者函式表示式,ECMAScript 規範明確了一點,即函式宣告必須始終帶有一個識別符號,也就是我們所說的函式名,而函式表示式則可以省略。

函式宣告,會給函式指定一個名字,會在程式碼執行以前被載入到作用域中,所以呼叫函式在函式宣告之前或之後都是可以的

test("Hello World!")

function test(arg) {
    console.log(arg)
}

函式表示式,建立一個匿名函式,然後將這個匿名函式賦給一個變數,在程式碼執行到函式表示式的時候才會有定義,所以呼叫函式在函式表示式之後才能正確執行,否則是會報錯的:

var test = function (arg) {
    console.log(arg)
}

test("Hello World!")

IIFE 立即呼叫函式表示式

IIFE 全稱 Immediately-invoked Function Expressions,譯為立即呼叫函式表示式,也稱為自執行函式、立即執行函式、自執行匿名函式等,IIFE 是一種語法,這種模式本質上就是函式表示式(命名的或者匿名的)在建立後立即執行。當函式變成立即執行的函式表示式時,表示式中的變數不能從外部訪問。IIFE 主要用來隔離作用域,避免汙染。

IIFE 基本語法

IIFE 的寫法非常靈活,主要有以下幾種格式:

1、匿名函式前面加上一元操作符,後面加上 ()

!function () {
    console.log("I AM IIFE")
}();

-function () {
    console.log("I AM IIFE")
}();

+function () {
    console.log("I AM IIFE")
}();

~function () {
    console.log("I AM IIFE")
}();

2、匿名函式後面加上 (),然後再用 () 將整個括起來:

(function () {
    console.log("I AM IIFE")
}());

3、先用 () 將匿名函式括起來,再在後面加上 ()

(function () {
    console.log("I AM IIFE")
})();

4、使用箭頭函式表示式,先用 () 將箭頭函式表示式括起來,再在後面加上 ()

(() => {
  console.log("I AM IIFE")
})()

5、匿名函式前面加上 void 關鍵字,後面加上 ()void 指定要計算或執行一個表示式,但是不返回值:

void function () {
    console.log("I AM IIFE")
}();

有的時候,我們還有可能見到立即執行函式前面後分號的情況,例如:

;(function () {
    console.log("I AM IIFE")
}())

;!function () {
    console.log("I AM IIFE")
}()

這是因為立即執行函式通常作為一個單獨模組使用一般是沒有問題的,但是還是建議在立即執行函式前面或者後面加上分號,這樣可以有效地與前面或者後面的程式碼進行隔離,否則可能出現意想不到的錯誤。

IIFE 引數傳遞

將引數放在末尾的 () 裡即可實現引數傳遞:

var text = "I AM IIFE";

(function (param) {
    console.log(param)
})(text);

// I AM IIFE
var dict = {name: "Bob", age: "20"};

(function () {
    console.log(dict.name);
})(dict);

// Bob
var list = [1, 2, 3, 4, 5];

(function () {
    var sum = 0;
    for (var i = 0; i < list.length; i++) {
        sum += list[i];
    }
    console.log(sum);
})(list);

// 15

Function.prototype.call() / apply() / bind()

Function.prototype.call()Function.prototype.apply()Function.prototype.bind() 都是比較常用的方法。它們的作用一模一樣,即改變函式中的 this 指向,它們的區別如下:

  • call() 方法會立即執行這個函式,接受一個多個引數,引數之間用逗號隔開;
  • apply() 方法會立即執行這個函式,接受一個包含多個引數的陣列;
  • bind() 方法不會立即執行這個函式,返回的是一個修改過後的函式,便於稍後呼叫,接受的引數和 call() 一樣。

call()

call() 方法接受多個引數,第一個引數 thisArg 指定了函式體內 this 物件的指向,如果這個函式處於非嚴格模式下,指定為 null 或 undefined 時會自動替換為指向全域性物件(瀏覽器中就是 window 物件),在嚴格模式下,函式體內的 this 還是為 null。從第二個引數開始往後,每個引數被依次傳入函式,基本語法如下:

function.call(thisArg, arg1, arg2, ...)

示例:

function test(a, b, c) {
    console.log(a + b + c)
}

test.call(null, 1, 2, 3)  // 6
function test() {
    console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.call(data)  // John Doe

apply()

apply() 方法接受兩個引數,第一個引數 thisArg 與 call() 方法一致,第二個引數為一個帶下標的集合,從 ECMAScript 第5版開始,這個集合可以為陣列,也可以為類陣列,apply() 方法把這個集合中的元素作為引數傳遞給被呼叫的函式,基本語法如下:

function.apply(thisArg, [arg1, arg2, ...])

示例:

function test(a, b, c) {
    console.log(a + b + c)
}

test.apply(null, [1, 2, 3])  // 6
function test() {
    console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.apply(data)  // John Doe

bind()

bind() 方法和 call() 接受的引數是相同的,只不過 bind() 返回的是一個函式,基本語法如下:

function.bind(thisArg, arg1, arg2, ...)

示例:

function test(a, b, c) {
    console.log(a + b + c)
}

test.bind(null, 1, 2, 3)()  // 6
function test() {
    console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.bind(data)()  // John Doe

理解 webpack

有了以上知識後,我們再來理解一下模組化程式設計,也就是前面所說的 webpack 寫法:

!function (allModule) {
    function useModule(whichModule) {
        allModule[whichModule].call(null, "hello world!");
    }
    useModule(0)
}([
    function module0(param) {console.log("module0: " + param)},
    function module1(param) {console.log("module1: " + param)},
    function module2(param) {console.log("module2: " + param)},
]);

首先,這整個程式碼是一個 IIFE 立即呼叫函式表示式,傳遞的引數是一個陣列,裡面包含三個方法,分別是 module0module1module2,可以將其視為三個模組,那麼 IIFE 接受的引數 allModule 就包含這三個模組,IIFE 裡面還包含一個函式 useModule(),可以將其視為模組載入器,即要使用哪個模組,示例中 useModule(0) 即表示呼叫第一個模組,函式裡面使用 call() 方法改變函式中的 this 指向並傳遞引數,呼叫相應的模組進行輸出。

改寫 webpack

對於我們爬蟲逆向當中經常遇到的 webpack 模組化的寫法,可以很容易對其進行改寫,以下以一段加密程式碼為例:

CryptoJS = require("crypto-js")

!function (func) {
    function acvs() {
        var kk = func[1].call(null, 1e3);
        var data = {
            r: "I LOVE PYTHON",
            e: kk,
            i: "62bs819idl00oac2",
            k: "0123456789abcdef"
        }
        return func[0].call(data);
    }

    console.log("加密文字:" + acvs())

    function odsc(account) {
        var cr = false;
        var regExp = /(^\d{7,8}$)|(^0\d{10,12}$)/;
        if (regExp.test(account)) {
            cr = true;
        }
        return cr;
    }

    function mkle(account) {
        var cr = false;
        var regExp = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
        if (regExp.test(account)) {
            cr = true;
        }
        return cr;
    }

}([
    function () {
        for (var n = "", t = 0; t < this.r.length; t++) {
            var o = this.e ^ this.r.charCodeAt(t);
            n += String.fromCharCode(o)
        }
        return encodeURIComponent(n)
    },
    function (x) {
        return Math.ceil(x * Math.random())
    },
    function (e) {
        var a = CryptoJS.MD5(this.k);
        var c = CryptoJS.enc.Utf8.parse(a);
        var d = CryptoJS.AES.encrypt(e, c, {
            iv: this.i
        });
        return d + ""
    },
    function (e) {
        var b = CryptoJS.MD5(this.k);
        var d = CryptoJS.enc.Utf8.parse(b);
        var a = CryptoJS.AES.decrypt(e, d, {
            iv: this.i
        }).toString(CryptoJS.enc.Utf8);
        return a
    }
]);

可以看到關鍵的加密入口函式是 acvs()acvs() 裡面又呼叫了 IIFE 引數列表裡面的第一個和第二個函式,剩下的其他函式都是干擾項,而第一個函式中用到了 r 和 e 引數,將其直接傳入即可,最終改寫如下:

function a(r, e) {
    for (var n = "", t = 0; t < r.length; t++) {
        var o = e ^ r.charCodeAt(t);
        n += String.fromCharCode(o)
    }
    return encodeURIComponent(n)
}

function b(x) {
    return Math.ceil(x * Math.random())
}

function acvs() {
    var kk = b(1e3);
    var r = "I LOVE PYTHON";
    return a(r, kk);
}

console.log("加密文字:" + acvs())

總結

看完本文後,你可能會覺得 webpack 也不過如此,看起來確實比較簡單,但實際上我們在分析具體站點時往往不會像上述例子這麼簡單,本文旨在讓大家簡單理解一下模組化程式設計 webpack 的原理,後續 K 哥將會帶領大家實戰分析比較複雜的 webpack!敬請關注!

相關文章