
關注微信公眾號: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 = https://www.cnblogs.com/ikdl/archive/2021/10/21/{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 = https://www.cnblogs.com/ikdl/archive/2021/10/21/{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 = https://www.cnblogs.com/ikdl/archive/2021/10/21/{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 立即呼叫函式運算式,傳遞的引數是一個陣列,里面包含三個方法,分別是 module0、module1 和 module2,可以將其視為三個模塊,那么 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 = https://www.cnblogs.com/ikdl/archive/2021/10/21/{
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!敬請關注!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/330013.html
標籤:其他
