爬蟲逆向基礎,理解 JavaScript 模組化編程 webpack

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

簡介

在分析一些站點的 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!敬請關注!