Skip to content

JavaScript 代码混淆和逆向

文章:


全局使用的代码:demoNew.js

js
Date.prototype.format = function (formatstr) {
	eval(
		String.fromcharCode(
			118,
			97,
			114,
			32,
			115,
			116,
			114,
			32,
			61,
			32,
			102,
			111,
			114,
			109,
			97,
			116,
			83,
			116,
			114,
			59
		)
	);
	// ASCIIEncrypt
	var week = ["日", "一", "二", "三", "四", "五", "六"];

	eval(
		atob("c3RyID0gc3RyLnJlcGxhY2UoL315eXl8WVlZWS8sIHRoaXMuZ2V0RnVsbFllYXIoKSk7")
	);
	// Base64Encrypt
	str = str.replace(
		/MM/,
		this.getMonth() + 1 > 9
			? (this.getMonth() + 1).toString()
			: "0" + (this.getMonth() + 1)
	);
	str = str.replace(
		/dd|DD/,
		this.getDate() > 9 ? this.getDate().toString() : "0" + this.getDate()
	);
	return str;
};

console.log(new Date().format("yyyy-MM-dd"));

JavaScript 的代码混淆:

使程序不易破解 ↓

先看一段常用的简单防御 sql 注入,xss 等攻击,转义常见危险字符,格式化字符串的 JS 代码

js
/**
 * 处理问题字符串
 */
String.prototype.format = function formatStr(str) {
	// String是JS的内置对象,可以new一个字符串对象,prototype是String对象的原型对象
	// 在原型对象上增加了format,然后赋值给一个匿名函数
	str = str.replace(/^\s+|\s+$/g, ""); //去掉前后空格
	str = str.replace(/\s+/g, ""); // 去掉空格
	str = str.replace(/^\s/, ""); // 去掉左空格
	str = str.replace(/(\s$)/g, ""); // 去掉右空格
	let div = document.createElement("div");
	div.textContent = str; // 利用textContent属性转化"<",">","&","'"等字符
	let formatString = div.innerHTML;
	return formatString;
};
console.log(new String().format("&&My name is xiao ming&&"));
// node环境会报错,请复制进浏览器console下运行
// 输出结果 &amp;&amp;Mynameisxiaoming&amp;&amp;

以上代码没有经过任何处理,能让人一眼看出代码的作用,复制下来即可用在自己的网站上.

有人说:Javascript 代码不就是明文放在前端给别人看的吗?暴露这些无关紧要的代码又有什么关系?

这仅仅是一个例子,假如这段代码是网站中一段比较关键的业务逻辑代码,抑或是暴露了一些敏感的接口信息。

如果就这么把这段代码直接嵌入在网页中,那么只要有心之人按下 F12 开发者工具即可一览无余,给偷盗源码者,

恶意爬虫编写者,代码剽窃者以可乘之机,所以你肯定不希望自己写的代码被其他人看的一清二楚.

因此,设计了一套基于抽象语法树(AST)的自动化 Javascript 防护方案,通过这套加密混淆方案进行一系列混淆之

后,例子中的代码变为了:

js
var ooOoOo = [
	"ZGl2",
	"cmVwbGFjZQ==",
	"",
	"ICAgIGRzYWRhcyYmICAg",
	"dGV4dENvbnRlbnQ=",
	"bG9n",
	"aW5uZXJIVE1M",
	"U3RyaW5n",
	"cHJvdG90eXBl",
	"Zm9ybWF0",
	"NnwzfDR8OHw3fDF8NXwwfDJ8OXwxMA==",
	"fA==",
	"Y3JlYXRlRWxlbWVudA==",
];

(function (ooOoOo, oOoOoo) {
	var oOoOo0 = function (oOoOoo) {
		while (--oOoOoo) {
			ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
		}
	};

	oOoOo0(++oOoOoo);

	for (
		let i = ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (85477840 ^ 85477841);
		i > -(35660943 ^ 35660942);
		i--
	) {
		let temp =
			ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (65273944 ^ 65273947)];
		ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (69264744 ^ 69264747)] =
			ooOoOo[i];
		ooOoOo[i] = temp;
	}
})(ooOoOo, 61064427 ^ 61064395);

window[atob(ooOoOo[40917713 ^ 40917713])][atob(ooOoOo[52279225 ^ 52279224])][
	atob(ooOoOo[79534083 ^ 79534081])
] = function oOoOoo(oOoOo0, OoOooO, OoOooo) {
	let OoOoo0 = atob(ooOoOo[69258673 ^ 69258674]).split(
			atob(ooOoOo[70677059 ^ 70677063])
		),
		OoOo0O = 70497518 ^ 70497518;

	while (!![]) {
		switch (+OoOoo0[OoOo0O++]) {
			case 64264984 ^ 64264984:
				OoOooO = document[atob(ooOoOo[63781739 ^ 63781742])](
					atob(ooOoOo[32906682 ^ 32906684])
				);
				continue;

			case 37318317 ^ 37318316:
				oOoOo0 = oOoOo0[atob(ooOoOo[36465873 ^ 36465878])](
					/(\s$)/g,
					atob(ooOoOo[33386246 ^ 33386254])
				);
				continue;

			case 68561252 ^ 68561254:
				OoOooO[atob(ooOoOo[35571111 ^ 35571118])] = oOoOo0;
				continue;

			case 81239349 ^ 81239350:
				function _ooOoOo(ooOoOo, oOoOoo) {
					return ooOoOo + oOoOoo;
				}

				continue;

			case 78524242 ^ 78524246:
				oOoOo0 = oOoOo0[atob(ooOoOo[54436180 ^ 54436179])](
					/^\s+|\s+$/g,
					atob(ooOoOo[85047616 ^ 85047624])
				);
				continue;

			case 49869939 ^ 49869942:
				oOoOo0 = oOoOo0[atob(ooOoOo[70551548 ^ 70551547])](
					_ooOoOo(78636218 ^ 77524147, 84113672 ^ 84127041),
					_ooOoOo2(85067497 ^ 89390435, 84139554 ^ 86800785)
				);
				continue;

			case 36957945 ^ 36957951:
				function _ooOoOo2(ooOoOo, oOoOoo) {
					return ooOoOo + oOoOoo;
				}

				continue;

			case 55933985 ^ 55933990:
				oOoOo0 = oOoOo0[atob(ooOoOo[52559760 ^ 52559767])](
					/^\s/,
					atob(ooOoOo[58408120 ^ 58408112])
				);
				continue;

			case 55744782 ^ 55744774:
				oOoOo0 = oOoOo0[atob(ooOoOo[41003852 ^ 41003851])](
					/\s+/g,
					atob(ooOoOo[70407872 ^ 70407880])
				);
				continue;

			case 75552957 ^ 75552948:
				OoOooo = OoOooO[atob(ooOoOo[36669448 ^ 36669442])];
				continue;

			case 57539216 ^ 57539226:
				return OoOooo;
				continue;
		}

		break;
	}
};

console[atob(ooOoOo[80858441 ^ 80858434])](
	new window[atob(ooOoOo[62151775 ^ 62151775])]()[
		atob(ooOoOo[35109869 ^ 35109871])
	](atob(ooOoOo[87556368 ^ 87556380]))
);

由此可见,可以在彻底破坏掉代码可读性的同时不影响正常业务逻辑,从而增加 Javascript 的保密性,大幅提高恶意黑客信息收集,恶意爬虫收集信息接口,攻击难度和攻击成本。

接下来我们将深入浅出的介绍一套 JavaScript 代码防护方案以对抗前端 JS 文件泄露信息的安全风险.

JavaScript 代码安全防护原理

常量的代码混淆原理

1.修改对象属性的两种访问方式

定义一个名为People的构造函数,并在其原型对象上定义一个名为 haveLunch 的方法。

使用 p['name'] 属性选中方法

javascript
function People(name) {
	this.name = name;
}
People.prototype.haveLunch = function () {
	console.log("is having lunch");
};
var p = new People("xiaowang");

// ***** 两种方法访问构造函数实例对象的属性 *****
console.log(p.name); // xiaowang
p.haveLunch(); // is having lunch
// 要使用该方法
console.log(p["name"]); // xiaowang
p["haveLunch"](); // is having lunch

修改对象属性的访问方式后.js

javascript
/**
 * 处理问题字符串
 */
String.prototype.format = function formatStr(str) {
	// String是JS的内置对象,可以new一个字符串对象,prototype是String对象的原型对象
	// 在原型对象上增加了format,然后赋值给一个匿名函数
	str = str["replace"](/^\s+|\s+$/g, ""); // 去掉前后空格
	str = str["replace"](/\s+/g, ""); // 去掉空格
	str = str["replace"](/^\s/, ""); // 去掉左空格
	str = str["replace"](/(\s$)/g, ""); // 去掉右空格
	let div = document["createElement"]("div");
	div["textContent"] = str; // 利用textContent属性转化"<",">","&","'"等字符
	let formatString = div["innerHTML"];
	return formatString;
};
console.log(new window["String"]()["format"]("&&My name is xiao ming&&"));
// node环境无window属性,会报错,请复制进浏览器console下运行
// 输出结果 &amp;&amp;Mynameisxiaoming&amp;&amp;

代码中定义的全局变量都是全局对象 window 的属性,代码中定义的全局函数都是全局对象 window 的方法.

全局对象的属性或方法在调用的时候,可以省略全局对象名。比如new window.Date()等同于new Date()

举例:

javascript
var a = 123;
function test() {
	console.log(6666);
}

console.log(a); // 输出结果 123
console.log(window.a); // 输出结果 123

test(); // 输出结果 6666
window.test(); // 输出结果 6666

由于我们在混淆中需要将 Date 变为字符串。

因此我们需要把 new Date() 变成 new window['Date']() 从而达到对字符串进行拼接,加密等操作的目的

2.十六进制字符串转换混淆

仅通过修改对象访问属性的方法降低可读性的效果显然还不够理想,所以需要配合其他方法来进行一个简单的加密。

由于 JS 中字符串支持以十六进制的形式表示,所以我们用字符串的十六进制形式来代替原有的字符串,比如

bash
`My name is xiao ming` 用二进制字符串来表示就是
`\x7ea2\x9ca4\x9c7c\x4e0e\x7eff\x9ca4\x9c7c\x4e0e\x9a74`

使用 JS 代码实现该种转换 --> 字符串转十六进制.js

1.charAt 方法是用来取出字符串中对应索引的字符

2.charCodeAt 方法是用来取出字符串中对应索引的字符的 ASCII 码

3.然后用 toString(16) 转换为十六进制

4.再与 \x 拼接之后变为 JS 可识别的十六进制形式的字符串

javascript
function hexEn(code) {
	for (var hexString = [], i = 0, s; i < code.length; i++) {
		s = code.charCodeAt(i).toString(16); // 通过此方法转换成16进制
		hexString += "\\x" + s; // 与\x 拼接
	}
	return hexString;
}
console.log(hexEn("&&My name is xiao ming&&"));
//输出结果 \x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26

字符串转十六进制混淆后.js:在 String 上添加一个原型属性方法,用作正则替换

这种方式虽然表面上使传入的字符串不可读,但十分容易恢复还原:只需要复制这串十六进制代码,加上单引号放入浏览器控制台回车即可还原

javascript
String.prototype.format = function formatStr(str) {
	// String是JS的内置对象,可以new一个字符串对象,prototype是String对象的原型对象
	// 在原型对象上增加了format,然后赋值给一个匿名函数
	str = str["replace"](/^\s+|\s+$/g, ""); // 去掉前后空格
	str = str["replace"](/\s+/g, ""); // 去掉空格
	str = str["replace"](/^\s/, ""); // 去掉左空格
	str = str["replace"](/(\s$)/g, ""); // 去掉右空格
	let div = document["createElement"]("div");
	div["textContent"] = str; // 利用textContent属性转化"<",">","&","'"等字符
	let formatString = div["innerHTML"];
	return formatString;
};
console.log(
	new window["String"]()["format"](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

3.unicode 字符串编码混淆

在 JS 中字符串还可以以 unicode 形式表示。

bash
 `var a = ['我','叫','小','明']`为例
可以表示为↓
`var a = ['\u6211','\u53eb','\u5c0f','\u660e']`

字符串转 unicode.js

JS 中的标识符也支持 unicode 形式表示。因此,代码中的formatformatStrstrwindow等都支持 unicode 形式表示。将前面代码处理过后如下

javascript
function unicodeEn(string) {
	var value = "";
	for (var i = 0; i < string.length; i++) {
		value +=
			"\\u" + ("0000" + parseInt(string.charCodeAt(i)).toString(16)).substr(-4);
		return value;
	}
}
console.log(unicodeEn("&&My name is xiao ming&&"));

Unicode 混淆后.js

在使用\u0073\u0074\u0072定义变量后,依然可以使用对应的str来还原变量。

而在实际的 JS 混淆运用中一般不会替换成 Unicode 的形式,原因和十六进制一样,还原起来太容易。

还原方法也同十六进制一样,浏览器控制台直接输出即可还原

javascript
String.prototype.format = function formatString(str) {
	str = str["replace"](/^\s+|\s+$/g, "");
	str = str["replace"](/\s+/g, "");
	str = str["replace"](/^\s/, "");
	str = str["replace"](/(\s$)/g, "");
	let div = document["createElement"]("div");
	div["textContent"] = str;
	let formatString = div["innerHTML"];
	return formatString;
};
console.log(
	new window["String"]()["\u0066\u006f\u0072\u006d\u0061\u0074"](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

4.字符串的 ASCII 码混淆

需要使用两个函数来完成对字符串的 ASCII 码混淆。

一个是 String 字符串对象下的 charCodeAt 方法,另一个是 String 字符串类下的 fromCharCode 方法

javascript
console.log("a".charCodeAt(0)); //97,也就是小写字母a的ascii码
console.log("b".charCodeAt(0)); //98,也就是小写字母b的ascii码
console.log(String.fromCharCode(97, 98)); //ab,也就是ascii码为97,98对应的字符

字符串转 ascii 码.js

javascript
function stringToByte(str) {
	var byteArr = [];
	for (var i = 0; i < str.length; i++) {
		byteArr.push(str.charCodeAt(i));
	}
	return byteArr;
}
console.log(stringToByte("xiaoming"));
// 输出结果: [120, 105, 97, 111, 109, 105, 110, 103]

对字符串进行 ASCII 码混淆:

format为例,format转换为字节数组后为[ 102, 111, 114, 109, 97, 116 ],因此代码中的format字符串可以表示为String.fromCharCode(102, 111, 114, 109, 97, 116)

但需要注意的一点,fromCharCode 接收的参数类型不是数组,是可变长度参数,如果一定要传入一个数组,可以这么写:需要使用 apply 方法,第一个参数为 null,函数的 this 指向全局,也就是 window

js
String.formCharCode.apply(null, [102, 111, 114, 109, 97, 116]);

ASCII 混淆不仅可以用于字符串,还可以用于混淆代码

以下面这段代码为例:将其变为字符串,再转成字节数组 ↓

javascript
// str = str['replace'](/^\s+|\s+$/g, "");
function stringToByte(str) {
	var byteArr = [];
	for (var i = 0; i < str.length; i++) {
		byteArr.push(str.charCodeAt(i));
	}
	return byteArr;
}

console.log(stringToByte("str = str['replace']('/^s+|s+$/g', '');"));
// 输出结果: [115, 116, 114,  32,  61,  32, 115, 116, 114,91,  39, 114, 101, 112, 108,  97,  99, 101,39,  93,  40,  39,  47,  94, 115,  43, 124,115,  43,  36,  47, 103,  39,  44,  32,  39,39,  41,  59]

上面这段数组可以通过String.fromCharCode转换成字符串;转换成字符串后,再通过引用eval方法或Function来执行这段字符串

其中eval用来执行一段代码,Function用来生成一个函数。比如:在控制台执行eval('var a = 10;console.log(a)')会输出 10。

这里选择使用eval把字符串当做正常代码执行,处理后的代码为:

ASCII 码混淆后.js

javascript
String.prototype.format = function formatStr(str) {
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 47, 94, 115, 43, 124, 115, 43, 36, 47, 103, 44,
				32, 39, 39, 41, 59,
			]
		)
	);
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 47, 115, 43, 47, 103, 44, 39, 39, 41, 59,
			]
		)
	);
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 32, 47, 94, 115, 47, 44, 32, 39, 39, 41, 59,
			]
		)
	);
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 47, 40, 115, 36, 41, 47, 103, 44, 32, 39, 39,
				41, 59,
			]
		)
	);
	let div = document["createElement"]("div");
	div["textContent"] = str; //利用textContent属性转化"<",">","&","'"等字符
	let formatString = div["innerHTML"];
	return formatString;
};
console.log(
	new window["String"]()["format"](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

5.字符串常量加密

字符串加密的核心是先把字符串加密得到密文,然后在使用之前,调用对应的解密函数去解密,得到明文

代码中仅出现解密函数和密文。当然,也可以使用不同的加密方法去加密字符串,然后再调用不同的解密函数去解密.

这里采用最简单最常见的 Base64 编码进行加密.

bash
replace             // base64编码后为 cmVwbGFjZQ==
createElement       // base64编码后为 Y3JlYXRlRWxlbWVudA==
textContent         // base64编码后为 dGV4dENvbnRlbnQ=
innerHTML           // base64编码后为 aW5uZXJIVE1M

浏览器自带解密函数atob()可以将 base64 编码的字符串转换回原字符,所以通过嵌套可以进行如下的转换

javascript
str = str["replace"](/^\s+|\s+$/g, "");
// 等价于↓
str = str[atob("cmVwbGFjZQ==")](/^\s+|\s+$/g, "");

经过处理后,原例子可表示为

base64 混淆后.js

javascript
String.prototype.format = function formatStr(str) {
	str = str[atob("cmVwbGFjZQ==")](/^\s+|\s+$/g, "");
	str = str[atob("cmVwbGFjZQ==")](/\s+/g, "");
	str = str[atob("cmVwbGFjZQ==")](/^\s/, "");
	str = str[atob("cmVwbGFjZQ==")](/(\s$)/g, "");
	let div = document[atob("Y3JlYXRlRWxlbWVudA==")]("div");
	div[atob("dGV4dENvbnRlbnQ=")] = str;
	let formatString = div[atob("aW5uZXJIVE1M")];
	return formatString;
};
console.log(
	new window["String"]()["format"](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

但在实际的混淆应用中,标识符一定要进行非语义化处理,否则很容易被定位到关键代码。

其次是尽可能不要使用系统自带的函数,尽可能自己进行实现。

因为不论如何混淆,最终执行的过程中系统函数的名称是固定的,容易被攻击者 Hook 定位到关键代码

6.数值常量加密

算法加密过程中,会使用到一些固定的数值常量。比如:MD5 中的常量 0x674523010xefcdab890x98badcfe0x10325476 SHA1 中的常量 0x674523010xEFCDAB890x98badcfe 等.

当攻击者进行标准算法逆向时,通常会通过搜索这些数值常量来定为关键代码的位置,或者用于确定算法类型。比如0x67452301,在代码中可能会写成十进制的1732584193。为了安全起见,可以简单加密一下这些数值常量。

可以使用位异或特性来加密,比如:a ^ b = c,那么c ^ b = a

以 MD5 算法中的 0x98badcfe 为例,0x98badcfe ^ 0xf59f551f = 0x6d2589e1 那么在代码中可以使用 0x6d2589e1 ^ 0xf59f551f 来代替 0x98badcfe ,其中 0xf59f551f 可以理解为秘钥,可以使用随机数生成

混淆方案未必只使用一种,各种方案之间也可结合使用。

经过以上环节的处理,原本肉眼可以读懂的代码在不进行动态调试或 AST 的情况下已经几乎不具备可读性了,但因为其加密过程简单且流于表面,经过简单的调试和逆向工程依然会被还原成可读代码。

常量的代码混淆原理-小结

使用上这一小节一系列混淆方法后,我们将一段可读代码转换为了可读性极差的代码,如下所示

js
String.prototype.format = function formatString(str) {
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 47, 94, 115, 43, 124, 115, 43, 36, 47, 103, 44,
				32, 39, 39, 41, 59,
			]
		)
	);
	str = str[atob("cmVwbGFjZQ==")](/\s+/g, "");
	eval(
		String.fromCharCode.apply(
			null,
			[
				115, 116, 114, 32, 61, 32, 115, 116, 114, 91, 39, 114, 101, 112, 108,
				97, 99, 101, 39, 93, 40, 32, 47, 94, 115, 47, 44, 32, 39, 39, 41, 59,
			]
		)
	);
	str = str[atob("cmVwbGFjZQ==")](/(\s$)/g, "");
	let div = document[atob("Y3JlYXRlRWxlbWVudA==")]("\x64\x69\x76");
	div[atob("dGV4dENvbnRlbnQ=")] = str;
	let formatString =
		div["\u0069\u006e\u006e\u0065\u0072\u0048\u0054\u004d\u004c"];
	return formatString;
};
console.log(
	new window["\u0053\u0074\u0072\u0069\u006e\u0067"]()[
		"\u0066\u006f\u0072\u006d\u0061\u0074"
	](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

但上述代码的保密性依然不够,因为这段代码依然可以比较清晰的看出这里大量使用了解密函数+密文这种形式

具有逆向经验的攻击者依然可以看出哪里使用了 ascii 码混淆,哪里使用了 Unicode 混淆,进行简单的逆向工程后就可以还原为原来的文本.

增加 JavaScript 逆向难度复杂度

1.数组混淆

通过数组混淆的方式将所有字符串提取到一个数组中,然后通过数组下标进行引用。请看以下示例

数组引用示例.js

javascript
var array = ["log", "String", "format"];
// 定义函数略
console[arrary[0]](
	new window[arrary[1]]()[arrary[2]]("&&My name is xiao ming&&")
);
// console.log(new String().format('&&My name is xiao ming&&'));

通过这种引用数组下标的方法来代替直接使用字符串,那么几百上千个方法和字符串名称全部都在一个数组中时。

代码中的方法全都会以array[812]array[423]等形式出现,这时想直接在脑海中建立几千个对应关系来阅读代码几乎不可能。

与此同时,JS 由于其语法灵活的特性,使得一个数组内可以存放很多种类型的数据,布尔值,数组,对象,字符串等都可以放入。进而在大大减少了混淆工作量的同时大大增加了逆向的难度。

有时我们会把代码中的一部分函数也放入大数组中去。一般而言,我们需要将放入数组的内容进行加密。但在这个时候又会遇到问题,例如:用例中的String.fromCharCode不是一个字符串,无法进行加密。进行修改对象访问方式的方法改为String['fromCharCode']的,String是明文,改为window ['String']['fromCharCode']的话,window又是明文,所以需要一点奇技淫巧来完成函数的完全字符串化,如下所示:

javascript
console.log(""["constructor"]["fromCharCode"](120)); // 输出 x

最前面是任意的字符串对象,所以我们引入一个空字符串。constructor代表构造函数,因此''['constructor']等价于内置字符串对象String,通过这种方法就可以将函数完全变成字符串进行加密了。

数组引用处理后.js

javascript
var array = [
	"cmVwbGFjZQ==",
	"dGV4dENvbnRlbnQ=",
	""[atob("Y29uc3RydWN0b3I=")][
		"\u0066\u0072\u006f\u006d\u0043\u0068\u0061\u0072\u0043\u006f\u0064\u0065"
	],
	"Y3JlYXRlRWxlbWVudA==",
	"\u0069\u006e\u006e\u0065\u0072\u0048\u0054\u004d\u004c",
	"\u0053\u0074\u0072\u0069\u006e\u0067",
	"\x64\x69\x76",
];
String.prototype.format = function formatString(str) {
	eval(
		array[2](
			115,
			116,
			114,
			32,
			61,
			32,
			115,
			116,
			114,
			91,
			39,
			114,
			101,
			112,
			108,
			97,
			99,
			101,
			39,
			93,
			40,
			47,
			94,
			115,
			43,
			124,
			115,
			43,
			36,
			47,
			103,
			44,
			32,
			39,
			39,
			41,
			59
		)
	);
	str = str[atob(array[0])](/\s+/g, "");
	eval(
		array[2](
			115,
			116,
			114,
			32,
			61,
			32,
			115,
			116,
			114,
			91,
			39,
			114,
			101,
			112,
			108,
			97,
			99,
			101,
			39,
			93,
			40,
			32,
			47,
			94,
			115,
			47,
			44,
			32,
			39,
			39,
			41,
			59
		)
	);
	str = str[atob(array[0])](/(\s$)/g, "");
	let div = document[atob(array[3])](array[6]);
	div[atob(array[1])] = str;
	let formatString = div[array[4]];
	return formatString;
};
console.log(
	new window[array[5]]()["\u0066\u006f\u0072\u006d\u0061\u0074"](
		"\x26\x26\x4d\x79\x20\x6e\x61\x6d\x65\x20\x69\x73\x20\x78\x69\x61\x6f\x20\x6d\x69\x6e\x67\x26\x26"
	)
);

2.数组乱序

在经过数组引用处理后,虽然没法直接看懂了,但是由于数组下标与源代码内引用的部分一一对应。攻击者花些时间通过查找替换功能依然可以还原代码。

因此我们需要两个步骤来防止这一点

  1. 打乱数组
  2. 在代码中使用被打乱的数组,然后调用数组还原函数

通过这两步可以让源代码中的下标引用无法与他看到的数组一一对应,进而防止攻击者通过查找替换直接还原。

打乱数组.js

javascript
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9];

(function (array, num) {
	for (let i = 0; i < array.length; i++) {
		let temp = array[i];
		array[i] = array[array.length - 3];
		array[array.length - 3] = temp;
	}
	var shuffer = function (nums) {
		// 打乱数组
		while (--nums) {
			array.unshift(array.pop());
		}
	};
	shuffer(++num);
})(array, 0x20);
// 此处的循环数0x20类似于一个秘钥,只有加密和解密的循环数相同才可以解密
console.log(array);
// 输出结果 [4, 5, 9, 6, 8, 7, 1, 2, 3]

还原数组.js

javascript
var array = [4, 5, 9, 6, 8, 7, 1, 2, 3];

(function (array, num) {
	var shuffer = function (nums) {
		// 还原数组
		while (--nums) {
			array["push"](array["shift"]());
		}
	};
	shuffer(++num);
	for (let i = array.length - 1; i > -1; i--) {
		let temp = array[array.length - 3];
		array[array.length - 3] = array[i];
		array[i] = temp;
	}
})(array, 0x20);
//此处的循环数0x20类似于一个秘钥,只有加密和解密的循环数相同才可以解密
console.log(array);

我们将数组引用后.js里开头用于存放字符串的array(控制台打印无法呈现原文,Unicode 和十六进制会被翻译,函数内容会被省略,因此写了一个数组混淆.html页面进行处理)经过打乱数组后得到了

javascript
array = [
	""[atob("Y29uc3RydWN0b3I=")][
		"\u0066\u0072\u006f\u006d\u0043\u0068\u0061\u0072\u0043\u006f\u0064\u0065"
	],
	"\x64\x69\x76",
	"Y3JlYXRlRWxlbWVudA==",
	"\u0053\u0074\u0072\u0069\u006e\u0067",
	"\u0069\u006e\u006e\u0065\u0072\u0048\u0054\u004d\u004c",
	"cmVwbGFjZQ==",
	"dGV4dENvbnRlbnQ=",
];

然后我们将已被混淆完的数组和还原函数带入混淆代码中去:

javascript
array = [''[atob('Y29uc3RydWN0b3I=')]['\u0066\u0072\u006f\u006d\u0043\u0068\u0061\u0072\u0043\u006f\u0064\u0065'],'\x64\x69\x76','Y3JlYXRlRWxlbWVudA==','\u0053\u0074\u0072\u0069\u006e\u0067','\u0069\u006e\u006e\u0065\u0072\u0048\u0054\u004d\u004c','cmVwbGFjZQ==','dGV4dENvbnRlbnQ='];
(function(arr, num){
    var shuffer = function(nums){
        while(--nums){
            arr['push'](arr['shift']());
        }
    };
    shuffer(++num);
}(array, 0x20));
for (let i = array.length - 1; i > -1; i--) {
    let temp = array[array.length - 3];
    array[array.length - 3] = array[i];
    array[i] = temp;
};
String.prototype.\u0066\u006f\u0072\u006d\u0061\u0074 = function \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072\u0069\u006e\u0067(\u0073\u0074\u0072) {
    eval(array[2] (115, 116, 114,  32,  61,  32, 115, 116, 114,91,  39, 114, 101, 112, 108,  97,  99, 101,39,  93,  40,  47,  94, 115,  43, 124, 115,43,  36,  47, 103,  44,  32,  39,  39,  41,59));
    str = str[atob(array[0])](/\s+/g,'');
    eval( array[2](115, 116, 114,  32,  61,  32, 115, 116,114,  91,  39, 114, 101, 112, 108,  97,99, 101,  39,  93,  40,  32,  47,  94,115,  47,  44,  32,  39,  39,  41,  59));
    str = str[atob(array[0])](/(\s$)/g, '');
    let div = document[atob(array[3])](array[6]);
    div[atob(array[1])] = str;
    let \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072\u0069\u006e\u0067 = div[array[4]];
    return \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072\u0069\u006e\u0067;
}
console.log(new \u0077\u0069\u006e\u0064\u006f\u0077 [array[5]]()['\u0066\u006f\u0072\u006d\u0061\u0074'

3.花指令

所谓花指令,就是在正常代码中添加进大量无意义的代码,将有实际意义的代码混进花指令当中,进而增加逆向者的分析难度。我们以下面这个例子为例:

花指令例子.js

javascript
function People(name) {
	this.name = name;
}

People.prototype.eatting = function (food) {
	return p.name + " is eatting " + food;
};

var p = new People("xiaowang");

console.log(p.eatting("dumplings" + ".")); // 字符串拼接的二项式
//xiaowang is eatting dumplings.

我们在上面这个例子中可以看到一个字符串拼接的二项式 'dumplings' + '.' 这个二项式我们可以定义一个返回的结果为函数两个形参相加的和的函数,然后将两个字符串作为参数嵌套进去得到:

花指令处理后 1.js

javascript
function People(name) {
	this.name = name;
}

People.prototype.eatting = function (food) {
	return p.name + " is eatting " + food;
};

var p = new People("xiaowang");

function _0x64ed4dde10(a, b) {
	return a + b;
}

console.log(_0x64ed4dde10(p.eatting("dumplings"), "."));
//xiaowang is eatting dumplings.

我们不光可以通过将二项式转换为函数进行花指令混淆,还可以通过增加花指令深度,把函数拆解成花指令等方法来增加代码量

花指令处理后 2.js

JavaScript
function People(name){
    this.name = name;
}

People.prototype.eatting = function(food){
    return p.name + ' is eatting ' + food ;
}

var p = new People('xiaowang');

function _0x64ed4dde10(a,b){
    return a + b;
}

function _0xe230d68a85(a, b, c, d){
    return a[b](c,d)
}

function _0x038704d2c9(a, b, c, d){
    return _0xe230d68a85(a,b,c,d)
}

console.log(_0x038704d2c9(p,'eatting','dumplings' , '.'));
//xiaowang is eatting dumplings.

这里并没有直接引用定义的函数本身,而是通过多次传参嵌套,并把函数封装进另一个函数的方法来进行混淆,攻击者只能一眼看到所有参数,却要从众多函数中进行分析并找出真正有用的函数进行逆向,从而达到了增加逆向工作量的目的

4.jsfuck

GitHub 的 jsfuck 语法aemkei/jsfuck

jsfuck 官网工具JSFuck

上面的工具镜像:JSFuck - 在线加解密 (bugku.com)

JSFuck 加密解密:JSFuck 解密_javascript 在线混淆解密-利民吧 (liminba.com)


jsfcuk 是一种比较特殊的代码编码形式。

他能把任何 JS 代码转换为仅用[ ( + ! ) ]这六个字符表示的代码且可以正常执行。转换之后的代码可读性被彻底破坏,可以用来做一些简单的 JS 加密。

例如下面的代码可以执行console.log('My name is xiaoming')

JsFuck 打印.js

js
[][
	(![] + [])[+[]] +
		(![] + [])[!+[] + !+[]] +
		(![] + [])[+!+[]] +
		(!![] + [])[+[]]
][
	([][
		(![] + [])[+[]] +
			(![] + [])[!+[] + !+[]] +
			(![] + [])[+!+[]] +
			(!![] + [])[+[]]
	] + [])[!+[] + !+[] + !+[]] +
		(!![] +
			[][
				(![] + [])[+[]] +
					(![] + [])[!+[] + !+[]] +
					(![] + [])[+!+[]] +
					(!![] + [])[+[]]
			])[+!+[] + [+[]]] +
		([][[]] + [])[+!+[]] +
		(![] + [])[!+[] + !+[] + !+[]] +
		(!![] + [])[+[]] +
		(!![] + [])[+!+[]] +
		([][[]] + [])[+[]] +
		([][
			(![] + [])[+[]] +
				(![] + [])[!+[] + !+[]] +
				(![] + [])[+!+[]] +
				(!![] + [])[+[]]
		] + [])[!+[] + !+[] + !+[]] +
		(!![] + [])[+[]] +
		(!![] +
			[][
				(![] + [])[+[]] +
					(![] + [])[!+[] + !+[]] +
					(![] + [])[+!+[]] +
					(!![] + [])[+[]]
			])[+!+[] + [+[]]] +
		(!![] + [])[+!+[]]
](
	(!![] + [])[+!+[]] +
		(!![] + [])[!+[] + !+[] + !+[]] +
		(!![] + [])[+[]] +
		([][[]] + [])[+[]] +
		(!![] + [])[+!+[]] +
		([][[]] + [])[+!+[]] +
		(+[![]] +
			[][
				(![] + [])[+[]] +
					(![] + [])[!+[] + !+[]] +
					(![] + [])[+!+[]] +
					(!![] + [])[+[]]
			])[+!+[] + [+!+[]]] +
		(!![] + [])[!+[] + !+[] + !+[]] +
		(+(!+[] + !+[] + !+[] + [+!+[]]))[
			(!![] + [])[+[]] +
				(!![] +
					[][
						(![] + [])[+[]] +
							(![] + [])[!+[] + !+[]] +
							(![] + [])[+!+[]] +
							(!![] + [])[+[]]
					])[+!+[] + [+[]]] +
				([] + [])[
					([][
						(![] + [])[+[]] +
							(![] + [])[!+[] + !+[]] +
							(![] + [])[+!+[]] +
							(!![] + [])[+[]]
					] + [])[!+[] + !+[] + !+[]] +
						(!![] +
							[][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							])[+!+[] + [+[]]] +
						([][[]] + [])[+!+[]] +
						(![] + [])[!+[] + !+[] + !+[]] +
						(!![] + [])[+[]] +
						(!![] + [])[+!+[]] +
						([][[]] + [])[+[]] +
						([][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						] + [])[!+[] + !+[] + !+[]] +
						(!![] + [])[+[]] +
						(!![] +
							[][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							])[+!+[] + [+[]]] +
						(!![] + [])[+!+[]]
				][
					([][[]] + [])[+!+[]] +
						(![] + [])[+!+[]] +
						((+[])[
							([][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							] + [])[!+[] + !+[] + !+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								([][[]] + [])[+!+[]] +
								(![] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] + [])[+!+[]] +
								([][[]] + [])[+[]] +
								([][
									(![] + [])[+[]] +
										(![] + [])[!+[] + !+[]] +
										(![] + [])[+!+[]] +
										(!![] + [])[+[]]
								] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								(!![] + [])[+!+[]]
						] + [])[+!+[] + [+!+[]]] +
						(!![] + [])[!+[] + !+[] + !+[]]
				]
		](!+[] + !+[] + !+[] + [!+[] + !+[]]) +
		(![] + [])[+!+[]] +
		(![] + [])[!+[] + !+[]]
)()(
	[][
		(![] + [])[+[]] +
			(![] + [])[!+[] + !+[]] +
			(![] + [])[+!+[]] +
			(!![] + [])[+[]]
	][
		([][
			(![] + [])[+[]] +
				(![] + [])[!+[] + !+[]] +
				(![] + [])[+!+[]] +
				(!![] + [])[+[]]
		] + [])[!+[] + !+[] + !+[]] +
			(!![] +
				[][
					(![] + [])[+[]] +
						(![] + [])[!+[] + !+[]] +
						(![] + [])[+!+[]] +
						(!![] + [])[+[]]
				])[+!+[] + [+[]]] +
			([][[]] + [])[+!+[]] +
			(![] + [])[!+[] + !+[] + !+[]] +
			(!![] + [])[+[]] +
			(!![] + [])[+!+[]] +
			([][[]] + [])[+[]] +
			([][
				(![] + [])[+[]] +
					(![] + [])[!+[] + !+[]] +
					(![] + [])[+!+[]] +
					(!![] + [])[+[]]
			] + [])[!+[] + !+[] + !+[]] +
			(!![] + [])[+[]] +
			(!![] +
				[][
					(![] + [])[+[]] +
						(![] + [])[!+[] + !+[]] +
						(![] + [])[+!+[]] +
						(!![] + [])[+[]]
				])[+!+[] + [+[]]] +
			(!![] + [])[+!+[]]
	](
		(!![] + [])[+!+[]] +
			(!![] + [])[!+[] + !+[] + !+[]] +
			(!![] + [])[+[]] +
			([][[]] + [])[+[]] +
			(!![] + [])[+!+[]] +
			([][[]] + [])[+!+[]] +
			([] + [])[
				(![] + [])[+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					([][[]] + [])[+!+[]] +
					(!![] + [])[+[]] +
					([][
						(![] + [])[+[]] +
							(![] + [])[!+[] + !+[]] +
							(![] + [])[+!+[]] +
							(!![] + [])[+[]]
					] + [])[!+[] + !+[] + !+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					(![] + [])[!+[] + !+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					(!![] + [])[+!+[]]
			]()[+!+[] + [!+[] + !+[]]] +
			((!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				([][[]] + [])[+!+[]] +
				(![] + [])[!+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(![] + [])[!+[] + !+[]] +
				(!![] + [])[!+[] + !+[] + !+[]] +
				(+(
					+!+[] +
					[+!+[]] +
					(!![] + [])[!+[] + !+[] + !+[]] +
					[!+[] + !+[]] +
					[+[]]
				) + [])[+!+[]] +
				(![] + [])[!+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				[+!+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[+[]] +
				([][[]] + [])[+!+[]] +
				(![] + [])[+!+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[!+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[+[]] +
				([![]] + [][[]])[+!+[] + [+[]]] +
				(![] + [])[!+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				[+[]] +
				([![]] + [][[]])[+!+[] + [+[]]] +
				(![] + [])[+!+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				([![]] + [][[]])[+!+[] + [+[]]] +
				([][[]] + [])[+!+[]] +
				(!![] + [])[+[]] +
				[+!+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[] + !+[] + !+[]] +
				(!![] + [])[+[]] +
				[!+[] + !+[] + !+[] + !+[] + !+[]] +
				[+!+[]])
				[
					(![] + [])[!+[] + !+[] + !+[]] +
						(+(!+[] + !+[] + [+!+[]] + [+!+[]]))[
							(!![] + [])[+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								([] + [])[
									([][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									] + [])[!+[] + !+[] + !+[]] +
										(!![] +
											[][
												(![] + [])[+[]] +
													(![] + [])[!+[] + !+[]] +
													(![] + [])[+!+[]] +
													(!![] + [])[+[]]
											])[+!+[] + [+[]]] +
										([][[]] + [])[+!+[]] +
										(![] + [])[!+[] + !+[] + !+[]] +
										(!![] + [])[+[]] +
										(!![] + [])[+!+[]] +
										([][[]] + [])[+[]] +
										([][
											(![] + [])[+[]] +
												(![] + [])[!+[] + !+[]] +
												(![] + [])[+!+[]] +
												(!![] + [])[+[]]
										] + [])[!+[] + !+[] + !+[]] +
										(!![] + [])[+[]] +
										(!![] +
											[][
												(![] + [])[+[]] +
													(![] + [])[!+[] + !+[]] +
													(![] + [])[+!+[]] +
													(!![] + [])[+[]]
											])[+!+[] + [+[]]] +
										(!![] + [])[+!+[]]
								][
									([][[]] + [])[+!+[]] +
										(![] + [])[+!+[]] +
										((+[])[
											([][
												(![] + [])[+[]] +
													(![] + [])[!+[] + !+[]] +
													(![] + [])[+!+[]] +
													(!![] + [])[+[]]
											] + [])[!+[] + !+[] + !+[]] +
												(!![] +
													[][
														(![] + [])[+[]] +
															(![] + [])[!+[] + !+[]] +
															(![] + [])[+!+[]] +
															(!![] + [])[+[]]
													])[+!+[] + [+[]]] +
												([][[]] + [])[+!+[]] +
												(![] + [])[!+[] + !+[] + !+[]] +
												(!![] + [])[+[]] +
												(!![] + [])[+!+[]] +
												([][[]] + [])[+[]] +
												([][
													(![] + [])[+[]] +
														(![] + [])[!+[] + !+[]] +
														(![] + [])[+!+[]] +
														(!![] + [])[+[]]
												] + [])[!+[] + !+[] + !+[]] +
												(!![] + [])[+[]] +
												(!![] +
													[][
														(![] + [])[+[]] +
															(![] + [])[!+[] + !+[]] +
															(![] + [])[+!+[]] +
															(!![] + [])[+[]]
													])[+!+[] + [+[]]] +
												(!![] + [])[+!+[]]
										] + [])[+!+[] + [+!+[]]] +
										(!![] + [])[!+[] + !+[] + !+[]]
								]
						](!+[] + !+[] + !+[] + [+!+[]])[+!+[]] +
						(![] + [])[!+[] + !+[]] +
						([![]] + [][[]])[+!+[] + [+[]]] +
						(!![] + [])[+[]]
				]((!![] + [])[+[]])
				[
					([][
						(!![] + [])[!+[] + !+[] + !+[]] +
							([][[]] + [])[+!+[]] +
							(!![] + [])[+[]] +
							(!![] + [])[+!+[]] +
							([![]] + [][[]])[+!+[] + [+[]]] +
							(!![] + [])[!+[] + !+[] + !+[]] +
							(![] + [])[!+[] + !+[] + !+[]]
					]() + [])[!+[] + !+[] + !+[]] +
						(!![] +
							[][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							])[+!+[] + [+[]]] +
						([![]] + [][[]])[+!+[] + [+[]]] +
						([][[]] + [])[+!+[]]
				](
					([][
						(![] + [])[+[]] +
							(![] + [])[!+[] + !+[]] +
							(![] + [])[+!+[]] +
							(!![] + [])[+[]]
					]
						[
							([][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							] + [])[!+[] + !+[] + !+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								([][[]] + [])[+!+[]] +
								(![] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] + [])[+!+[]] +
								([][[]] + [])[+[]] +
								([][
									(![] + [])[+[]] +
										(![] + [])[!+[] + !+[]] +
										(![] + [])[+!+[]] +
										(!![] + [])[+[]]
								] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								(!![] + [])[+!+[]]
						](
							(!![] + [])[+!+[]] +
								(!![] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								([][[]] + [])[+[]] +
								(!![] + [])[+!+[]] +
								([][[]] + [])[+!+[]] +
								(![] + [+[]])[
									([![]] + [][[]])[+!+[] + [+[]]] +
										(!![] + [])[+[]] +
										(![] + [])[+!+[]] +
										(![] + [])[!+[] + !+[]] +
										([![]] + [][[]])[+!+[] + [+[]]] +
										([][
											(![] + [])[+[]] +
												(![] + [])[!+[] + !+[]] +
												(![] + [])[+!+[]] +
												(!![] + [])[+[]]
										] + [])[!+[] + !+[] + !+[]] +
										(![] + [])[!+[] + !+[] + !+[]]
								]()[+!+[] + [+[]]] +
								![] +
								(![] + [+[]])[
									([![]] + [][[]])[+!+[] + [+[]]] +
										(!![] + [])[+[]] +
										(![] + [])[+!+[]] +
										(![] + [])[!+[] + !+[]] +
										([![]] + [][[]])[+!+[] + [+[]]] +
										([][
											(![] + [])[+[]] +
												(![] + [])[!+[] + !+[]] +
												(![] + [])[+!+[]] +
												(!![] + [])[+[]]
										] + [])[!+[] + !+[] + !+[]] +
										(![] + [])[!+[] + !+[] + !+[]]
								]()[+!+[] + [+[]]]
						)()
						[
							([][
								(![] + [])[+[]] +
									(![] + [])[!+[] + !+[]] +
									(![] + [])[+!+[]] +
									(!![] + [])[+[]]
							] + [])[!+[] + !+[] + !+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								([][[]] + [])[+!+[]] +
								(![] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] + [])[+!+[]] +
								([][[]] + [])[+[]] +
								([][
									(![] + [])[+[]] +
										(![] + [])[!+[] + !+[]] +
										(![] + [])[+!+[]] +
										(!![] + [])[+[]]
								] + [])[!+[] + !+[] + !+[]] +
								(!![] + [])[+[]] +
								(!![] +
									[][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									])[+!+[] + [+[]]] +
								(!![] + [])[+!+[]]
						](
							(![] + [+[]])[
								([![]] + [][[]])[+!+[] + [+[]]] +
									(!![] + [])[+[]] +
									(![] + [])[+!+[]] +
									(![] + [])[!+[] + !+[]] +
									([![]] + [][[]])[+!+[] + [+[]]] +
									([][
										(![] + [])[+[]] +
											(![] + [])[!+[] + !+[]] +
											(![] + [])[+!+[]] +
											(!![] + [])[+[]]
									] + [])[!+[] + !+[] + !+[]] +
									(![] + [])[!+[] + !+[] + !+[]]
							]()[+!+[] + [+[]]]
						) + [])[+!+[]]
				) +
			([] + [])[
				(![] + [])[+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					([][[]] + [])[+!+[]] +
					(!![] + [])[+[]] +
					([][
						(![] + [])[+[]] +
							(![] + [])[!+[] + !+[]] +
							(![] + [])[+!+[]] +
							(!![] + [])[+[]]
					] + [])[!+[] + !+[] + !+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					(![] + [])[!+[] + !+[]] +
					(!![] +
						[][
							(![] + [])[+[]] +
								(![] + [])[!+[] + !+[]] +
								(![] + [])[+!+[]] +
								(!![] + [])[+[]]
						])[+!+[] + [+[]]] +
					(!![] + [])[+!+[]]
			]()[+!+[] + [!+[] + !+[]]]
	)()
);

这里简单介绍一下 jsfuck 的原理,以下面的代码为例

javascript
!+[] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + [];
// "8"

+号是 JS 中的运算符,当+号作为一元运算符使用时,代表为强制转换为数值类型。

[]在 JS 中代表空数组,因此!+[]等价于!0

由于 JavaScript 属于弱类型语言,JS 的解释器会在适当的时候自动完成类型的隐式转换。

! 是 JS 中的取反,也就是这时候需要一个布尔值。在 JS 中有 7 种值为false,其余均为true。这七种值分别为falseundefindednull0-0NaN""

因此0隐式转换为布尔值时为false,再取反就是true。也就是说!+[]==true

bash
!![]`转换为布尔值为`true`.空数组转换为布尔值为`true`然后两次取反,还是`true

那么 true + true = 多少呢?JS 中的 + 号,作为二元运算符的时候,假如有一边是字符串的话,代表拼接,两边都没有字符串的话代表数值相加.

true 转换为数值等于 1,所以这个 JSfuck 表达式中的 !+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![] 部分等价于 1+1+1+1+1+1+1+1 ,也就是数字 8,然后数值类型的 8 再和空数组[]相加,会被隐式转换为字符串类型"8"

这种完全没有任何可读性的代码似乎接触到了代码混淆的最终目的,但事实上这种方案只是偶尔会被采用于部分代码,具体有以下几个原因:

  1. 代码量过于庞大
  2. 还原并不困难

第一点毋庸置疑,上面用 JSfuck 表示的简简单单的一句console.log('My name is xiaoming')就高达 8918 个字符,若是繁杂的工程代码,其代码量将是极其巨大的,而会极度影响用户的正常体验。

第二点是还原本身并不困难。

只需要将代码最后一个()删除,复制进 浏览器控制台 中就可以看到被解析出来的函数体内容,例如前面的JsFuck打印.js的代码体中去掉最后的括号打印出来的内容是一个匿名函数

javascript
ƒ anonymous(
) {
return"\143\157ns\157le.l\157\147\50\47\115\171\40na\155e\40is\40\170ia\157\155in\147\47\51"
}

再将双引号的内容复制出来控制台输出,结果为"console.log('My name is xiaoming')"

5.代码执行流程控制防护原理

代码经过上面的学习的处理,字符常量已经被混淆的面目全非了,但是代码的执行流程还是跟原先一样。所以,本节要对代码的执行流程下手了。

5.1 流程平坦化

在平时代码开发中,会有很多的流程控制相关的代码,比如嵌套多层的if...elif...else...,这样的控制流程会出现多个分支结构让代码的逻辑层次分明。很少有代码会一条龙执行到底,没有任何分支的,而这些分支,还会具有一定的层级关系。

而在流程平坦化混淆中,会用到switch语句,因为switch 语句中的case块是平级的,而且调换 case块的前后顺序,并不影响原先代码的执行逻辑。为了让读者能够更清楚的理解,这里先来个简单的例子:

js
function test1(){
  var a = 1000;
  var b = a + 2000;
  var c = b + 3000;
  var d = c + 4000;
  var e = d + 5000:
  var f = e + 6000;
  return f;

}
console.log(test1()); // 输出 21000

如何混淆上面 test1函数中的代码执行流程呢?

首先,把代码分块,并且打乱代码块的顺序,分别塞到不同的 case 块中。方便起见,就处理成一句代码一个 case 块,代码如下:

js
switch () {
 case "1":
  var c = b + 3000;
 case "2":
  var e = d + 5000;
 case "3":
  var d = c + 4000;
 case "4":
  var f = e + 6000;
 case "5":
  var b = a + 2000;
 case "6":
  return f;
 case "7":
  var a = 1000;
}

应该不难看出,代码块打乱以后,如果要想跟原先的执行顺序一样的话,那么 case 块的跳转顺序应该是 7|5|1|3|2|4|6。只有 case 块按照这个流程执行,才能跟原代码的顺序保持一致。

其次,需要一个循环。因为 switch 语句,只计算一次 switch 表达式,它的执行流程是:

(1) 计算一次 switch 表达式

(2) 把表达式的值与每个 case 的值进行对比(这里是 === 的匹配,不转换型)

(3) 如果存在匹配(结果为 true),则执行对应 case 块

因此,代码可以改成这样:

js
while (!![]) {
 // 思考下,switch 中的表达式该怎么写?
 switch () {
  case "1":
   var c = b + 3000;
   continue;
   // 每执行一次 case 块中的代码,就跳到循环尾,继续下一次循环
  case "2":
   var e = d + 5000;
   continue;
  case "3":
   var d = c + 4000;
   continue;
  case "4":
   var f = e + 6000;
   continue;
  case "5":
   var b = a + 2000;
   continue;
  case "6":
   return f;
   continue;
  case "7":
   var a = 1000;
   continue;
 }
 break;
 //当 switch 表达式计算出来的值与每个 case 的值都不匹配,代码就会运行到这里,再跳出循环
}

这是一个死循环,所以需要一个边界条件来结束循环。假如函数有 return 语句,那么执行到对应的 case 块后,会直接 return 返回。假如函数没有 return 语句,那么执行到最后,就需要让 switch 计算出来的表达式的值与每个 case 的值都不匹配,就会执行最后的 break 来跳出循环。

在这个案例里,return 语句后面的 continue 是不会被执行的,但是留着不影响代码运行。假如这是一段由 AST 自动处理出来的代码,这样做更具有通用性。不需要考虑函数的最后一条语句是否是 return 语句。虽然在 AST 中判断一下,也很容易。

最后,需要构造一个分发器,里面记录了代码执行的真实顺序。比如,var arrStr = '7|5|1|3|2|4|6'.split('|'), i = 0;,把这个字符串,'7|5|1|3|2|4|6' 通过 split 分割成一个数组。i 作为计数器,每次递增,按顺序引用数组中的每一个成员。因此,switch 中的表达式,就可以写成 switch(arrStr [i++])。

插入没有用的代码混淆实现,完整的代码:

js
function test2() {
	var arrStr = "7|5|1|3|2|4|6".split("|"),
		i = 0;
	while (!![]) {
		switch (arrStr[i++]) {
			case "1":
				var c = b + 3000;
				continue;
			case "2":
				var e = d + 5000;
				continue;
			case "3":
				var d = c + 4000;
				continue;
			case "4":
				var f = e + 6000;
				continue;
			case "5":
				var b = a + 2000;
				continue;
			case "6":
				return f;
				continue;
			case "7":
				var a = 1000;
				continue;
		}
		break;
	}
}
console.log(test2()); // 输出2100

结合上面的混淆方案

js
// 最开始的大数组
var bigArr = [
	"cmVwbGFjZQ==",
	"Z2V0TW9udGg=",
	"dG9TdHJpbmc=",
	"Z2V0RGF0ZQ==",
	"MA==",
	""["constructor"]["fromCharCode"],
	"\u65e5",
	"\u4e00",
	"\u4e8c",
	"\u4e09",
	"\u56db",
	"\u4e94",
	"\u516d",
];
// 还原数组顺序的自执行函数
(function (arr, num) {
	var shuffer = function (nums) {
		while (--nums) {
			arr["push"](arr["shift"]());
		}
	};
	shuffer(++num);
})(bigArr, 0x20);
// 本小节处理的switch流程平坦化
Date.prototype.format = function (formatStr) {
	var arrStr = "7|5|1|3|2|4".split("|"),
		i = 0;
	while (!![]) {
		switch (arrStr[i++]) {
			case "1":
				eval(
					String.fromCharCode(
						115,
						116,
						114,
						32,
						61,
						32,
						115,
						116,
						114,
						91,
						39,
						114,
						101,
						112,
						108,
						97,
						99,
						101,
						39,
						93,
						40,
						47,
						121,
						121,
						121,
						121,
						124,
						89,
						89,
						89,
						89,
						47,
						44,
						32,
						116,
						104,
						105,
						115,
						91,
						39,
						103,
						101,
						116,
						70,
						117,
						108,
						108,
						89,
						101,
						97,
						114,
						39,
						93,
						40,
						41,
						41,
						59
					)
				);
				continue;
			case "2":
				str = str[atob(bigArr[7])](
					/dd|DD/,
					this[atob(bigArr[10])]() > 9
						? this[atob(bigArr[10])]()[atob(bigArr[9])]()
						: atob(bigArr[11]) + this[atob(bigArr[10])]()
				);
				continue;
			case "3":
				str = str[atob(bigArr[7])](
					/MM/,
					this[atob(bigArr[8])]() + 1 > 9
						? (this[atob(bigArr[8])]() + 1)[atob(bigArr[9])]()
						: atob(bigArr[11]) + (this[atob(bigArr[8])]() + 1)
				);
				continue;
			case "4":
				return str;
				continue;
			case "5":
				var Week = [
					bigArr[0],
					bigArr[1],
					bigArr[2],
					bigArr[3],
					bigArr[4],
					bigArr[5],
					bigArr[6],
				];
				continue;
			case "7":
				var str = formatStr;
				continue;
		}
		break;
	}
};
console.log(
	new window["\u0044\u0061\u0074\u0065"]()[
		bigArr[12](102, 111, 114, 109, 97, 116)
	]("\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64")
);
// 输出结果 2020-07-04
5.2 不透明谓词控制流混淆

所谓不透明谓词,指的是对于程序员来说输出结果是一个已知结果的表达式(通常会输出一个布尔值),而对于攻击者和静态分析器来说这个表达式的结果是未知的,需要通过运行和动态调试才能确定,从而达到对抗逆向分析的目的。

不透明谓词的设计必须满足两个条件:

  1. 对于混淆器也就是开发者,它的值在执行到某阶段总是确定的。
  2. 对于分析器也就是攻击者,只有执行到某阶段才能确切的知道它的值。

不透明谓词我们通常使用数学上已经证明的结论(数论书籍上往往可以找到很多这样的式子)。例如:

bash
对于任意整数x,y - 34y² -1

我们用这个式子来代替if (true){}引入循环

不透明谓词.js

javascript
function test() {
	x = 63;
	y = 3;
	if (x * x - 34 * y * y == -1) {
		var arr = "6,3,7,1,5,4,2".split(","),
			i = 0;
	} else {
		var arr = "7,5,1,6,3,2,4".split(","),
			i = 0;
	}
	// 这里定义一个分发器,使用split分割数组后遍历该数组来按顺序遍历索引
	while (!![]) {
		// 前文已表,!![] == true
		switch (arr[i++]) {
			case "1":
				var c = b + 300;
				continue;
			// 每执行一次case块中的代码,就跳到循环结尾继续下一次循环
			case "2":
				var f = e + 600;
				continue;
			case "3":
				var e = d + 500;
				continue;
			case "4":
				return f;
				continue;
			case "5":
				return (b = a + 200);
				continue;
			case "6":
				return (d = c + 400);
				continue;
			case "7":
				var a = 100;
				continue;
		}
		break;
		// 当switch表达式计算出的值与每个case值都不匹配,代码就会运行到这里再跳出循环
	}
}
console.log(test());

这里通过 if 语句定义了两个不同的分发器,而判断条件不是简单的truefalse,而是一条式子,攻击者无法通过一眼看出是真是假来判断具体使用的是哪一条分发器,必须动态执行分析才能确定,从而达到了增加逆向工作量的目的。

5.3 逗号表达式混淆方案

在 JS 中,逗号运算符可以玩出很多花样,主要作用是把多个表达式或语句连接成一个复合语句。

先举个例子:

JavaScript
for (let i = 0;i < 10;i++)
    console.log(i);
//输出 0 1 2 3 4 5 6 7 8 9

如果我想要让每个循环前面加入一个num:,由于 for 循环存在块级作用域,以下两种写法是有问题的

javascript
for (let i = 0; i < 10; i++) console.log("num: ");
console.log(i);
// num: 输出十次后报错,因为i不在for循环的作用域内,所以抛出一个未定义错误
javascript
for (var i = 0; i < 10; i++) console.log("num: ");
console.log(i);
// 输出10个num: 和 10
// 因为虽然i是全局变量,但是不在for循环体内,所以最终只打印了i的最终结果,没有进入循环

正确写法有两种写法

第一种是最常用的,使用{}将两个语句合成一个块

javascript
for (let i = 0; i < 10; i++) {
	console.log("num: ");
	console.log(i);
}
//输出 num:0 num:1 num:2 ......

第二种是不使用{},而使用,将两个语句组合成一个复合语句

JavaScript
for (let i = 0;i < 10;i++)
    console.log("num: "),
    console.log(i);
//输出 num:0 num:1 num:2 ......

再看流程平坦化中的 test1 函数

js
function test1() {
	var a = 100;
	var b = a + 200;
	var c = b + 300;
	var d = c + 400;
	var e = d + 500;
	var f = e + 600;
	return f;
}
console.log(test1()); // 输出 21000

它等价于:

js
function test1() {
	var a, b, c, d, e, f;
	return (
		(a = 100),
		(b = a + 200),
		(c = b + 300),
		(d = c + 400),
		(e = d + 500),
		(f = e + 600),
		f
	);
}
console.log(test1()); // 输出 2100

一般而言,return 语句后面,通常只能跟一个表达式,它会返回这个表达式计算之后的结果。

但是逗号运算符,可以把多个表达式,连接成一个复合语句。因此上述代码中,这么使用 return 语句也是没有问题的,会返回最后一个表达式计算之后的结果,但是前面的表达式依然会执行。

上述案例只是单纯的连接语句,这种没有混淆力度,再来看一个案例:

js
var a = ((a = 1000), (a += 2000));
console.log(a); // 输出3000

括号代表这是一个整体,也就是把(a = 1000,a += 2000)整体赋值给 a 变量。

那么这个整体到底返回的是什么?这个跟 return 语句是一样的。因此会先执行 a = 1000,然后执行 a += 2000,最后把结果赋值给 a 变量。最终 a 变量的值为 3000。

在执行 a = 1000 的时候,a 变量还没有声明。但是却不会报错,这是由于 JS 中的变量声明提前。

接着,来看一下逗号运算符的混淆,以本小节中的 testl 函数为例:

(1) 执行a= 1000,再执行 a + 2000,代码可以改为(a = 1000,a + 2000)

(2) 接 0 着赋值给 b,代码可以改为 b = ( a = 1000,a + 2000)

(3) 执行 b + 3000,代码可以改为 (b = (a = 1000, a + 2000),b + 3000)

(4) 接着赋值给 c,代码可以改为 c = (b = (a = 1000,a + 2000),b + 3000)

(5) 执行 c + 4000,代码可以改为 (c =(b = (a = 1000,a + 2000),b + 3000), c + 4000)

(6) 依此类推

处理后的代码为:

js
function test2() {
	var a, b, c, d, e, f;
	return (f =
		((e =
			((d = ((c = ((b = ((a = 1000), a + 2000)), b + 3000)), c + 4000)),
			d + 5000)),
		e + 6000));
}
console.log(test2()); // 输出 21000

观察上述代码,会发现有一串声明一系列变量的语句。其实这句很多余,完全可以放到参数列表上去,这样就不需要 var 声明了。另外,既然逗号运算符连接多个表达式,只会返回最后一个表达式计算之后的结果,那么可以在最后一个表达式之前,插入不影响结果的花指令。最终处理后的代码如下:

js
function test2(a, b, c, d, e, f) {
	return (f =
		((e =
			((d =
				((c =
					((b = ((a = 1000), a + 50, b + 60, c + 70, a + 2000)),
					d + 80,
					b + 3000)),
				e + 90,
				c + 4000)),
			f + 100,
			d + 5000)),
		e + 6000));
}
console.log(test2()); // 输出 21000

上述代码中的a + 50b + 60c + 70d + 80e + 90f + 100这些都是并无实际的意义的花指令(因为逗号表达式不管前面有多少表达式,只将最后一个表达式的执行结果返回),不影响函数本身的逻辑。test()虽然有六个参数,但是不传参也是可以调用的,只不过各个参数的初始值为undefined,还是需要自己定义。


逗号表达式混淆不只能处理赋值表达式,还能处理调用表达式、成员表达式等等,来看下面这个案例:

逗号表达式混淆例 2.js

js
var obj = {
	name: "xiaojianbang",
	add: function (a, b) {
		return a + b;
	},
};
function sub(a, b) {
	return a - b;
}
function test() {
	var a = 1000;
	var b = sub(a, 3000) + 1;
	var c = b + obj.add(b, 2000);
	return c + obj.name;
}

test 函数中有函数调用表达式sub(),还有成员表达式 obj.add 等等:

(1) 提升变量声明到参数中

(2) b = (a = 1000, sub)(a, 3000) + 1 前两可以处理成这样。(a = 1000, sub) 整体返回 sub 函数,然后直接调用,计算的结果 + 1 后赋值给 b(等号的运算符优先级很低)。同理,如果 sub 函数改为 obj.add 的话,可以处理成(a = 1000,obj.add)(a,3000) 或者 (a = 1000, obj).add(a,3000)

(3) 第 2 步是调用表达式直接在等号右边的情况,那如果不在右边呢?比如第 3 条中的 b + obj.add(b, 2000),给 obj.add 包装一下就好了,可以处理成 b + (0, obj.add)(b, 2000) 或者 b + (0, obj).add(b, 2000) 括号中的 0 可以是其他花指令 (4) 对于对象属性来说,处理方式也是一样的。总之,直接在等号右边或者自成一条语句,就可以与上一条语句连接。再其他表达式右边的,那就给它补一句花指令,就又可以连接了

老规矩,来算一下“微积分”,以下代码只处理了赋值表达式的情况:

js
// 最开始的大数组
var bigArr = [
	"cmVwbGFjZQ==",
	"Z2V0TW9udGg=",
	"dG9TdHJpbmc=",
	"Z2V0RGF0ZQ==",
	"MA==",
	""["constructor"]["fromCharCode"],
	"\u65e5",
	"\u4e00",
	"\u4e8c",
	"\u4e09",
	"\u56db",
	"\u4e94",
	"\u516d",
];
// 还原数组顺序的自执行函数
(function (arr, num) {
	var shuffer = function (nums) {
		while (--nums) {
			arr["push"](arr["shift"]());
		}
	};
	shuffer(++num);
})(bigArr, 0x20);
// 本小节处理的代码
// 把原先的变量定义提取到参数列表中
Date.prototype.format = function (formatStr, str, Week) {
	// 因为基本上都会处理成一行代码,所以return语句可以提到最上面
	return (str =
		((str =
			((Week =
				((str = formatStr),
				[
					bigArr[0],
					bigArr[1],
					bigArr[2],
					bigArr[3],
					bigArr[4],
					bigArr[5],
					bigArr[6],
				])),
			// 上面这个表达式的结果,会赋值给Week
			eval(
				String.fromCharCode(
					115,
					116,
					114,
					32,
					61,
					32,
					115,
					116,
					114,
					91,
					39,
					114,
					101,
					112,
					108,
					97,
					99,
					101,
					39,
					93,
					40,
					47,
					121,
					121,
					121,
					121,
					124,
					89,
					89,
					89,
					89,
					47,
					44,
					32,
					116,
					104,
					105,
					115,
					91,
					39,
					103,
					101,
					116,
					70,
					117,
					108,
					108,
					89,
					101,
					97,
					114,
					39,
					93,
					40,
					41,
					41,
					59
				)
			),
			str[atob(bigArr[7])](
				/MM/,
				this[atob(bigArr[8])]() + 1 > 9
					? (this[atob(bigArr[8])]() + 1)[atob(bigArr[9])]()
					: atob(bigArr[11]) + (this[atob(bigArr[8])]() + 1)
			))),
		// 上面这个表达式的结果,会赋值给第二个str
		str[atob(bigArr[7])](
			/dd|DD/,
			this[atob(bigArr[10])]() > 9
				? this[atob(bigArr[10])]()[atob(bigArr[9])]()
				: atob(bigArr[11]) + this[atob(bigArr[10])]()
		)));
	// 上面这个表达式的结果,会赋值给第一个str
};
console.log(
	new window["\u0044\u0061\u0074\u0065"]()[
		bigArr[12](102, 111, 114, 109, 97, 116)
	]("\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64")
);
// 输出结果 2020-07-04

最后,再来说一下逗号运算符混淆的还原技巧。在逗号运算符混淆中,通常需要使用括号来分组,那么这就是很重要的突破口。定位到最里面的那个括号,一般就是第 1 条语句。然后从里到外,一层层的根据括号对应关系,来还原语句顺序。

混淆的最终目的是增加攻击者的攻击成本,让原本一小时能够解决的问题要花几小时甚至数天才能解决,这时你再每天更新混淆方案就会迫使攻击者放弃攻击从而达到安全的目的。目前已有的方案大多数是一套方案的微调,调换顺序,常量命名方式等等。为了实现这类变化的快速微调,可以有babel插件的 AST 自动化混淆方案

6.其他代码维护方案

eval 加密

源码原理解析:JavaScript 高手进阶:详解 Eval 加密 - 在线工具-wetools.com 微工具

工具:js 在线加密解密(eval 方法)工具|evalPackage 解密解密_懒人工具|www.ab173.com

菜鸟工具:菜鸟工具 - 不止于工具 (runoob.com)

eval 方法:把一段字符串当做代码执行

测试代码:

js
eval(
	(function (p, a, c, k, e, r) {
		e = function (c) {
			return c.toString(36);
		};
		if ("0".replace(0, e) == 0) {
			while (c--) r[e(c)] = k[c];
			k = [
				function (e) {
					return r[e] || e;
				},
			];
			e = function () {
				return "[2-8a-f]";
			};
			c = 1;
		}
		while (c--)
			if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]);
		return p;
	})(
		"7.prototype.8=function(a){b 2=a;b Week=['日','一','二','三','四','五','六'];2=2.4(/c|YYYY/,3.getFullYear());2=2.4(/d/,(3.5()+1)>9?(3.5()+1).e():'0'+(3.5()+1));2=2.4(/f|DD/,3.6()>9?3.6().e():'0'+3.6());return 2};console.log(new 7().8('c-d-f'));",
		[],
		16,
		"||str|this|replace|getMonth|getDate|Date|format||formatStr|var|yyyy|MM|toString|dd".split(
			"|"
		),
		0,
		{}
	)
);

内存爆破

所谓内存爆破,就是在代码中加入死循环/无法正常运行/造成浏览器,调试器崩溃的代码。这些代码正常情况下不会执行,当检测到代码被格式化或者函数被 HOOK,就跳转到内存爆破代码中执行,造成内存溢出,报out of Memory崩溃,以下是一段内存爆破代码

内存爆破.js

JavaScript
var d = [0x1, 0x0, 0x01];
function b(){
    for (var i = 0x0, c = d.length ; i < c ; i++){
        d.push(Math.random(Math.random()));
        c = d.length;
    }
}
b();
// 这段代码复制进浏览器console运行会代码溢出导致浏览器调试工具崩溃
// 在node环境下会报 FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory 错误

检测代码是否被格式化的思路也很简单。在 JS 中函数是可以转成字符串的。因此可以随便挑一个函数转换成字符串,然后和内置的字符串进行比对或使用正则匹配

函数转字符串 1.js

javascript
function add(a, b) {
	return a + b;
}
add + "";

console.log(add.toString());

// 输出结果为
/*
function add(a,b){
    return a + b; 
}
*/

再来看如果函数没有被格式化的情况:

函数转字符串.js

JavaScript
function add(a,b){return a + b;}
add + '';

console.log(add.toString());
// 输出结果为:function add(a,b){return a + b;}

显然,这两个内容相同,格式不同的函数被转换成字符串之后得到的结果并不相同,此时只需要写一个正则进行匹配,只要匹配不到形如function add(a,b){return a + b;}这样单行表示的函数,则跳转到内存爆破流程中,不进行正常业务逻辑。


检测代码是否格式化

检测的思路很简单,在 JS 中函数是可以转字符串的。因此可以随便挑一个函数转成字符串,然后跟内置的字符串比对或者用正则匹配即可。函数转字符串也很简单,代码如下:

js
function add(a, b) {
	return a + b;
}
console.log(add + "");
console.log(add.toString());
// "function add(a,b) {return a+b;}"

在 Chrome 开发者工具中,点击左下角{},把代码格式化后,会产生一个后缀为:formatted 的文件。之后断点下在这个文件中,触发断点后,也确实会停在这个文件中。但是注意,这时候把某个函数转为字符串,取到的依然是格式化之前的代码。

也就是说,上述检测方法,检测不到这种情况。

那么,上述检测方法的应用场景是什么呢?在算法逆向中,分析完算法,为了得到自己想要的结果,就需要自己去实现这个算法。简单的算法,一般可以直接调用现成的加密库。复杂的算法,就会选择直接修改原文件,然后运行得到结果。当把格式化后的代码,保存成一个本地文件的时候,这时把某个函数转为字符串,取到的就是格式化后的结果了。

也就是说,会不会触发格式化检测,关键是看原文件中是否格式化。接着可以把内存爆破代码,也加入到其中。检测到格式化,就跳转到内存爆破代码中执行,程序就会崩溃。

小结

混淆的目的是增加逆向者的工作量。比如,原本 1 小时就能解决的算法,混淆后可能就需要几天才能解决。这时候当算法一天一更新,当收益远大于付出,逆向者自然就放弃了。天下武功,唯快不破,笔者甚至见过每次请求 JS 代码都不一样的。目前市面上已有此类方案,只不过变化的算法也仅限于微调。比如,算法中的常量,算法加密前的参数排序顺序等如果要实现此类方案,肯定需要一种自动化处理代码的方案,AST 自动化混方案将在第 9 章与读者们见面。

AST 的 API 详解

JavaScript 的语法足非常灵活的,如果直接手工处理 JS 代码来做混淆或还原,那无疑是很麻烦的,需要考虑的情况太多了,而且混淆过程中容易由于混淆者自身的失误而出错。

但是把 JS 代码转换成抽象语法树(以下简称 AST)以后,一切就变得简单了。


在编译原理中:从源码到机器码的过程,中间还需要经过很多步骤,比如:源码通过词法分析器变为记号序列,再通过语法分析器变为 AST,再通过语义分析器。一步步往下编译,最后变成机器码。

所以,AST 实际上是 一个概念性的东西。当实现了词法分析器和语法分析器,就能把不同的语言转化成 AST。

要想把 JS 代码转换成 AST,可以自己写代码去实现,也可以使用现成的解析库。当使用的解析库不一样的时候,生成的 AST 会有所区别。

如果你是一名前端开发,一定用过或者听过babeleslintprettier等工具,在对静态代码进行翻译,格式化,代码检查时,这些工具无一例外的应用了 AST。本次我们采用的是 Babel,一个 node.js 解析库。

在用 AST 自动化处理 JS 代码前。需要对 AST(Abstract Syntax Tree:抽象语法树)相关的知识有一定的了解。

以下几个小节介绍 AST 和 Babel 相关的知识:

AST 入门

1.AST 的基本结构

JavaScript 代码经过 babel 解析成 AST 后呈现的是类似于 json 的数据。里面的元素会被叫做节点(Node),同时 Babel 提供了很多方法去操作这些节点,本章节以这段代码为例:

demo.js

javascript
let obj = {
	name: "xiaoming",
	add: function (a, b) {
		return a + b + 100;
	},
	mul: function (a, b) {
		return a * b + 100;
	},
};

我们将这段代码复制到 AST 解析网站 中,解析器选择@babel/parser,可以比较便利的查看各个节点的属性名称等内容

AST解析网站

我们将解析出的文件已 json 格式复制出来,新建文件 AST.json ,粘贴进去,查看生成的一堆 json 数据(json 太长,这里就不展示了)

在 AST 解析后不难发现,AST 有很多层级,这些层级有明确的从属关系。

接下来我们逐一分析这些节点:

json
VariableDeclaration{
 type: "VariableDeclaration"
 ...
 "declarations": {{ ... }},
 "kind": "let"
}

"type" 字段表示节点类型,比如上述代码的 VariableDeclaration

每一种类型的节点定义了一些附加属性,用来进一步描述该节点的类型。

VariableDeclaration表示着这是一句声明变量的语句。

kind是表示变量声明语句所使用的关键字。

declarations是指声明的具体变量,又因为let关键字可以同时声明多个变量,但这里只声明了一个变量obj,所以这里的declarations是一个单成员数组。

接下来看一下declarations里的内容

json
{
 "type": "VariableDeclarator"
 ..
 "id": {
  "type": "Identifier",
  ...
  "name": "obj"
 },
 "init": { ... }
}

declarations里面是声明的具体变量信息,每一个都以VariableDeclarator表示。

VariableDeclarator的属性主要是idinit

ididentifier(标识符)

nameobj,与原始代码相同

接下来我们看init中的内容

json
{
 "type": "ObjectExpression",
 ...
 "properties": [{...},{...},{...}],
 "extra" : {trailingComma: 150}
}

在原代码中是把对象字面量赋值给了obj,AST 中把这个称为ObjectExpression(对象表达式)。

有对象就会有属性,因为对象可以有多个属性,所以properties是一个数组,一个属性对应一个成员。

接下来看看properties

json
{
 "type": "ObjectProperty"
 ...
 "method": false,
 "key": {
  "type": "StringLiteral",
  ...
  "extra":{
   "rawValue": "xiaoming",
   "raw": "'xiaoming'"
  },
  "value": "xiaoming"
 }
}

typeObjectProperty表示该 Node 为对象属性,JS 中的对象是由一系列的键值对(key–value)组成的。

由解析结果可知,key是一个identifiervaluename

注意,对象的 key 可以用字符串表示,这时候 key 的节点类型就会变成StringLiteral(字符串字面量)。

再来看一下value节点,类型为StringLiteral,具体的值为xiaoming

但是在该节点中,看到了三个xiaoming,其实extra节点是可删除的,如果是十六进制字符串,Unicode 字符串或 hex 形式的数值,那么这三个的值就会不一样

接着查看第二个属性

json
{
 "type": "ObjectProperty",
 ...
 "method": false,
 "key":{
  "type":"Identifier",
  ...
  "name": "add"
 },
 "computed": false,
 "shorthand": false,
 "value": { ... }
}

这一层主要关键在于value节点,因为在原始代码中是把一个函数表达式赋值给的对象的属性。

那么来看函数解析后的样子:

json
{
 "type": "FunctionExpression",
 ...
 "id": null,
 "generator": false,
 "async": false,
 "params": [{
  "type": "Identifier",
  ...
  "name": "a"
 }, {
  "type": "Identifier",
  ...
  "name": "b"
 }
],
"body": {
 "type": "BlockStatement",
 ...
 "body": [{ ... }
 ],
 "directives": []
}
}

type 为FunctionExpression(函数表达式)。函数名为id节点,由于代码中是把一个匿名函数赋值给了 obj 的 add 属性,所以这里idnull

函数参数对应上述结构的params节点,由于参数可以是多个的,所以params是一个数组。

函数体对应上述结构中的 body 节点,一般函数体中会用BlockStatement节点包裹。BlockStatement里面的body节点是一个数组,如果函数有use strict标记,那么directives里就会有相应的节点。

接下来看一下BlockStatement里的body节点内容:

json
{[
 "type": "ReturnStatement",
 ...
 "argument":{
  "type": "BinaryExpression".
  ...
  "extra":{
   "rawValue": 100.
   "raw": "100"
  }
  "value": 100
 }
]}

在原代码中这个函数只有一条return语句,对应这里的ReturnStatement(返回语句)。argumentreturn语句返回的内容。假如return语句没有返回内容,那么argument的节点值为null

在原代码中,return返回的是a + b + 100,AST 解析成了BinaryExpression(二项式)。JS 中的二元运算符都可以解析成BinaryExpression

先看right节点:

json
left: BinaryExpression{
    left: Identifier {
        type: "Identifier"
        ...
        name: "a"
    }
        operator: "*"
    right : Identifier{
        type: "Identifier"
        name: "b"
    }
}
operator: "+"
rignt: NumericLiteral{
 ...
 extra:{
  rawValue: 100
  raw: "100"
 }
 value: 100
}

二项式主要由三部分组成:leftoperatorright。这里是把a + b作为left100作为right

right节点中,typeNumericLiteral(数值字面量),value为 100。

2.代码的基本结构

要对 AST 进行读取和操作,需要使用 Node 的 API + 一系列 babel 的插件

首先需要安装Node.js环境,然后在 JavaScript 混淆的工程文件夹下执行如下命令来安装 babel 的核心库

bash
npm install @babel/core

AST 的 babel 插件 API 的操作文件流程.js

AST 处理 JS 文件的基本步骤:读取 JS 文件-->解析成 AST-->对节点进行-->系列的增删改查-->生成 JS 代码-->保存到新文件中

JavaScript
const fs = require('fs');
// 读取本地文件,require后赋值给fs

const parser = require("@babel/parser");
// babel/parser库将源码解析为AST,require后赋值给parser

const traverse = require("@babel/traverse").default;
// babel/traverse库遍历AST节点,require后把其中的default赋值给traverse

const t = require("@babel/types");
// babel/types库判断AST节点类型,构建新AST节点等,require之后赋值给t

const generator = require("@babel/generator").default;
// babel/generator将AST解析为源码,require之后把default赋值给generator

const jscode = fs.readFileSync("./demo.js", {
 encoding: "utf-8"
});
// 读取文件
let ast = parser.parse(jscode);

/*
对AST进行一系列操作:
1. 读取JS文件
2. 解析为AST
3. 遍历AST
4. 对AST节点进行增删改查
5. 生成JS代码
6. 保存进新文件
*/
let code = generator(ast).code;
// 将操作完的AST转换为JS源码
fs.writeFile('./NewDemo.js', code, (err)=>{});
// 写入新文件

Babel 库中的 AST 插件的用法

1.@babel/parser 库(将 JS 代码转换成 AST)与@babel/generator 库(将 AST 转换成 JS 代码)

这两个组件的作用刚好是相反的。

@babel/parser库中是隐含参数的,sourceType参数的默认值是script。当解析 JS 代码中含有importexport等关键字的时候需要指定sourceTypemodule

使用let code = generator(ast).code即可把 AST 转换为 JavaScript 代码。generator返回的是一个对象,其中的code属性才是代码本身。

同时,generator的第二个参数接收一个对象,可以设置一些可配置选项来得出需要的结果,本文只介绍几个常用的选项

paser.js

JavaScript
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const jscode = fs.readFileSync("./demo.js", {
 encoding: "utf-8"
});

let ast = parser.parse(jscode, {
 sourceType: "Module",
 // 指定module参数,防止在解析import时报错
});

// 写入输出文件
let code = generator(ast,{
    retainLines: false,
    comments: false,
    compact: true
}).code;
fs.writeFile('./NewDemo.js', code, (err)=>{});
  1. retainLines表示是否使用格式化代码,默认为false,也就是输出格式化后的代码。
  2. comments表示是否保留注释,默认为true
  3. compact表示是否压缩代码,与之类似的选项还有minifiedconcise,只不过压缩程度不同,minified压缩程度最多,concise压缩程度最低

输出后得到结果:

NewDemo.js

javascript
let obj = {
	name: "xiaoming",
	add: function (a, b) {
		return a + b + 100;
	},
	mul: function (a, b) {
		return a * b + 100;
	},
};

2.@babel/traverse 库(遍历 AST)配合 visitor 库

@babel/traverse库用来遍历 AST,简单的说就是把 AST 上的各个节点都走一遍。但是单纯的把节点都走一遍,没有什么意义。所以,traverse 需要配合 visitor 使用

visitor 其实就是一个对像,里面可以定义一些方法,用来过滤节点。

traverse.js (注:为了看起来简洁,前后代码略去,只留核心代码,输出结果为该段代码放回之前的完整代码后输出的结果)

JavaScript
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const jscode = fs.readFileSync("./demo.js", {
 encoding: "utf-8"
});

let ast = parser.parse(jscode, {
 sourceType: "Module",
 // 指定module参数,防止在解析import时报错
});


// 这段是traverse 需要配合 visitor 使用 的演示代码
let nodes = {};
visitor.FunctionExpression = function(path){
    console.log('有几个FunctionExpression节点这句话就会出现几次');
};
traverse(ast, nodes)
/* 输出
 有几个FunctionExpression节点这句话就会出现几次
 有几个FunctionExpression节点这句话就会出现几次
*/

// 写入输出文件
let code generator(ast)code;
fs.writeFile('./NewDemo.js', code, (err)=>{});

这段代码首先声明一个对象,名称为nodes,然后为对象增加了一个名为FunctionExpression的方法。这个名字是需要遍历的节点类型。

traverse会遍历所有节点类型,当节点类型为FunctionExpression时,调用nodes中相应的方法。如果想处理其他节点类型,可以在nodes中继续定义方法,命名为该节点的名称即可。

然后把nodes作为第二个参数传到traverse里面。而传给traverse的第一参数是ast。也就是从头开始遍历 AST 中的所有节点,过滤出FunctionExpression节点,然后执行响应的方法,在demo.js中有两个FunctionExpression节点,因此那句话出现了两次。

考虑到代码效率,复用性和耦合度的问题,我们可以换一种写法:

same_function.js

javascript
const nodes = {
	"FunctionExpression|BinaryExpression"(path) {
		console.log("同一个函数用于两种不同的节点可以用这种写法");
	},
};
/*输出
同一个函数用于两种不同的节点可以用这种写法
同一个函数用于两种不同的节点可以用这种写法
同一个函数用于两种不同的节点可以用这种写法
同一个函数用于两种不同的节点可以用这种写法
同一个函数用于两种不同的节点可以用这种写法
同一个函数用于两种不同的节点可以用这种写法
*/

babel 遍历节点遵循深度优先遍历原则,若A(b,c,d(I,II,III(1,2,3,4)))一层缩进代表一层深度,有:

bash
进入A
 进入b
 遍历
 退出b

 进入c
 遍历
 退出c

 进入d
  进入I
   进入1
   退出1

   进入2
   退出2

   进入3
   退出3

   进入4
   退出4
  退出I

  进入II
  遍历
  退出II

  进入III
  遍历
  退出III

 退出d
退出A

因此我们可以通过enter函数和exit函数来决定一个方法是优先处理父节点还是先处理子节点再处理父节点,从而增加程序的效率和控制粒度。

同时我们可以将多个函数对象传入数组中接收,方便一个节点依次应用多个方法

same_node.js

javascript
function func1(path) {
	console.log("进入节点时使用func1");
}

function func2(path) {
	console.log("进入节点时使用func2");
}

function func3(path) {
	console.log("退出节点时使用func3");
}

function func4(path) {
	console.log("退出节点时使用func4");
}

const nodes = {
	FunctionExpression: {
		enter: [func1, func2],
		exit: [func3, func4],
	},
};
traverse(ast, nodes);
/*输出
进入节点时使用func1
进入节点时使用func2
退出节点时使用func3
退出节点时使用func4
进入节点时使用func1
进入节点时使用func2
退出节点时使用func3
退出节点时使用func4
*/

traverse并不需要从头开始遍历,可以从任意节点向下遍历。如果我想将demo.js中所有函数的第一个参数改为x,则代码如下

修改参数.js

JavaScript
const updateParamNameNodes = {
    Identifier(path) {
        if (path.node.name === this.paramName){
            path.node.name = "x";
        }
    }
}
const nodes = {
    FunctionExpression(path){
        const paramName = path.node.params[0].name;
        path.traverse(updateParamNameNodes,{
            paramName
        });
    }
};
traverse(ast,nodes);

/*输出结果
let obj={name:'xiaoming',add:function(x,b){return x+b+100;},mul:function(x,b){return x*b+100;}};
*/

3.@babel/types 库(生成 AST 节点)

@babel/types主要用来判断节点类型,生成新的节点等。

判断节点类型很简单。比如,t.isIdentifier(path.node)。它等价于 path.node.type === "Identifier"。还可以在判断类型的同时附加条件,演示案例如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 演示案例
traverse(ast, {
	enter(path) {
		if (path.node.type === "Identifier" && path.node.name === "n") {
			path.node.name = "x";
		}
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

上述代码用来把标识符 n 改为 ,这是官方手册中的案例,在实际修改中还需要考虑标识符的作用域。在这个案例中,visitor 没有做任何过滤,遍历到任何一个节点就调用 enter 函数,所以要判断类型为 Identifier,并且 name 的值为 n,才修改为 x。这个案例可以等价的写为:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 演示等价
traverse(ast, {
	enter(path) {
		if (t.isIdentifier(path.node, { name: "n" })) {
			path.node.name = "x";
		}
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

如果要判断其它类型,只需把 is 后面的类型更改即可。这些方法还有一种断言式的版本,当节点不符合要求,会抛出异常而不是返回 true 或 false

js
t.assertBinaryExpression(maybeBinaryExpressionNode);
t.tassertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });

从上文的叙述中,可以看出 types 组件中用于判断节点类型的函数是可以自己实现的,而且也不是很麻烦。所以 types 组件最主要的功能是可以方便的生成新的节点。

在原始代码中最开始是一个变量声明语句,类型为 VariableDeclaration,因此可以用t.variableDeclaration 去生成它。在 vscode 中输入t.variableDeclaration, 鼠标悬停在 variableDeclaration 上,会出现代码提示。当然也可以按住 ctrl 键不放,鼠标左键单击 variableDeclaration,会跳转到一个 ts 后缀的文件中,里面有这么一句代码:

js
export function variableDeclaration(kind: "var" | "let" | "const", declarations:Array<VariableDeclarator>): VariableDeclaration;

来解释一下这段代码:最后一个冒号后面,表示这个函数的返回值类型。小括号里面的冒号前面,很明显是 VariableDeclaration 节点的属性。小括号里面的冒号后面,表示该参数允许传的类型。Array 表示这个参数是一个数组。因此,变量声明语句的生成代码,可以写为:

js
let loaclAst = t.variableDeclaration("let", [varDec]);
let code = generator(loaclAst).code;
console.log(code);

那么这里的 varDec 又该如何生成呢?这里需要生成一个 VariableDeclarator 节点。表示变量声明的具体的值。

js
export function variableDeclarator(id: LVal, init?: Expression | null) : VariableDeclarator;

下面是完整代码

生成新节点.js

javascript
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
// 使用 @babel/types
let a = t.identifier("a");
let b = t.identifier("b");
let binExpr2 = t.binaryExpression("+", a, b);
let binExpr3 = t.binaryExpression("*", a, b);
let retSta2 = t.returnStatement(
	t.binaryExpression("+", binExpr2, t.numericLiteral(100))
);
let retSta3 = t.returnStatement(
	t.binaryExpression("+", binExpr3, t.numericLiteral(100))
);
let bloSta2 = t.blockStatement([retSta2]);
let bloSta3 = t.blockStatement([retSta3]);
let funcExpr2 = t.functionExpression(null, [a, b], bloSta2);
let funcExpr3 = t.functionExpression(null, [a, b], bloSta3);
let objProp1 = t.objectProperty(
	t.identifier("name"),
	t.stringLiteral("xiaoming")
);
let objProp2 = t.objectProperty(t.identifier("add"), funcExpr2);
let objProp3 = t.objectProperty(t.identifier("mul"), funcExpr3);
let objExpr = t.objectExpression([objProp1, objProp2, objProp3]);
let varDec = t.variableDeclarator(t.identifier("obj"), objExpr);
let loaclAst = t.variableDeclaration("let", [varDec]);
let code = generator(loaclAst).code;

console.log(code);
/*输出结果
let obj = {
  name: "xiaoming",
  add: function (a, b) {
    return a + b + 100;
  },
  mul: function (a, b) {
    return a * b + 100;
  }
};
*/

在上述案例中用到了StringLiteralNumericLiteral,但 Babel 不仅仅只有这两种,还有其他字面量:

typescript
export function nullLiteral(): NullLiteral;
export function booleanLiteral(Value: boolean): BooleanLiteral;
export function regExpLiteral(pattern: string, flags?: any): RegExpLiteral;

因此不同字面量需要调用不同的方法生成。当生成较多字面量的时候会很麻烦,所以 Babel 还提供了valueToName方法,这个方法可以很方便的提供字面量,来看一下这个方法的定义:

typescript
valueToNode
(alias) const valueToNode: {
    (value: undefined): Identifier;
    (value: boolean): BooleanLiteral;
    (value: null): NullLiteral;
    (value: string): StringLiteral;
    (value: number): NumericLiteral | BinaryExpression | UnaryExpression;
    (value: RegExp): RegExpLiteral;
    (value: ReadonlyArray<unknown>): ArrayExpression;
    (value: object): ObjectExpression;
    (value: unknown): Expression;
}
export valueToNode

也可以不调用这些方法,直接按照 AST 结构构造一个 JSON 来生成代码,不过代码量很大,比如下面这个例子运用写 JSON 的方式构造了10 / 20的 JavaScript 代码

JSON 生成.js

javascript
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

let obj = {};
obj.type = "BinaryExpression";
obj.left = { type: "NumericLiteral", value: 10 };
obj.operator = "/";
obj.right = { type: "NumericLiteral", value: 20 };
let code = generator(obj).code;
console.log(code);
/*输出
10 / 20
*/

Path 对象详解

@babel/traverse 库的文档@babel/traverse · Babel

1.Path 与 Node 的区别

以一个案例来清楚的说明问题,下面这段代码在之前的小节中见过,只不过多了两句代码。其中,path.stop()用来停止遍历节点。在 Babel 中 path.skip()效果与之类似。

下面这段代码的意思是,当在函数表达式中遍历到一个 Identifier 节点的时候,就输出 path,然后停止遍历。根据 traverse 遍历规则,遍历到的这个 Identifier 节点肯定是 params[0]。来观察一下输出的结果:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

// 以下代码是演示代码
const updateParamNameVisitor = {
	Identifier(path) {
		if (path.node.name === this.paramName) {
			path.node.name = "x";
		}
		console.log(path);
		path.stop();
	},
};
const visitor = {
	FunctionExpression(path) {
		const paramName = path.node.params[0].name;
		path.traverse(updateParamNameVisitor, {
			paramName,
		});
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

查看输出后:

显而易见,path.node 就能取出 Identifier 所在的 Node 对象,该对象与 AST Explorer 网页中解析出来的 AST 节点结构一致。简单来说,节点是生成 JS 代码的原料,只是 Path 中的一部分。Path 是一个对象,用来描述两个节点之间连接。Path 除了具有上述显示的这些属性以外,还包含添加、更新、移动和删除节点等有关的很多方法。

2.Path 中的方法

在理清了 Path 与 Node 的关系以后,再来学习 Path 中的方法就比较容易了。以下代码均以 AST 的基本结构中的原始代码为例。

1.获取子节点/Path

为了得到 AST 节点的属性值,一般先访问到该节点,然后利用 path.node.property方法获取属性。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 演示代码
const visitor = {
	BinaryExpression(path) {
		console.log(path.node.left);
		console.log(path.node.right);
		console.log(path.node.operator);
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

通过上述方法获取到的是 Node 或者具体的属性值,Node 是用不了 Path 相关的方法的。

如果想要获取到该属性的 Path,就需要使用 Path 对象的 get 方法,传递的参数为 key (其实就是该属性名的字符串形式,后续内容中会详细介绍什么是 key)。如果是多级访问,以点连接多个 key。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

const visitor = {
	BinaryExpression(path) {
		console.log(path.get("left.name"));
		console.log(path.get("right"));
		console.log(path.get("operator"));
	},
};
traverse(ast, visitor);
// 会输出n个NodePath
// NodePath (parent: Node, hub; undefined, contexts: Array(0), data: null, traverseFlags: 0)
let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

从上述代码中,可以看到任何形式的属性值,通过 Path 对象的 get 方法去获取,都会包装成 Path 对象再返回来。但是,像 operator、name 这一类的属性值,其实没有必要包装成 Path 对象。

2.判断 Path 类型

再回顾一下 上面1.获取子节点/Path小节中输出的 Path 对象,就会发现最后有一个 type 属性,它基本上与 Node 中的 type 一致。Path 对象提供相应的方法来判断自身类型,使用方法与 types 组件差不多,只不过 types 组件判断的是 Node。解析下原始代码中的第一个函数中的二项式,left 是 a + b,right 是 1000。因此,下面的代码执行以后的结果为 false、true、报错。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

const visitor = {
	BinaryExpression(path) {
		console.log(path.get("left").isIdentifier()); // false
		console.log(
			path.get("right").isNumericLiteral({
				value: 1000,
			})
		); // true
		path.get("left").assertIdentifier(); // 报错
	},
};
traverse(ast, visitor);
let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});
3.节点转代码

写代码并不是一蹴而就,当代码比较复杂的时候,就需要动态调试。

在 vscode 中调试只需鼠标左键单击行号,即可下断点。启动调试就会在断点处断下来,与调试普通 JS 文件无异。还可以适时地插入 console 来排查错误。也可以基于当前节点生成代码来排查错误也就是说,很多时候需要在执行过程中把部分节点转为代码,而不是在最后才把整个 AST 转成代码。

generator 组件也可以把 AST 中的一部分节点转成代码,这对节点遍历过程中的调试很有帮助。示例代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

const visitor = {
	FunctionExpression(path) {
		console.log(generator(path.node).code); // 输出两个匿名函数:分别计算返回a和b的相加和相乘
		// console.log( path.toString() );
		// console.log( path +  '' );
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

Path 对象复写了 Object 的 toString 方法。在 Path 对象的 toString 方法中,去调用了 generator 组件把节点转为代码。因此,也可以用 path.toString()把节点转为字符串。当然也可以用 path + ''来隐式转成字符串。

4.替换节点属性

与获取节点属性方法相同,只是改为赋值。但也不是随意替换,需要注意替换的类型要在允许的类型范围内。因此需要熟悉 AST 的结构。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

const visitor = {
	BinaryExpression(path) {
		path.node.left = t.identifier("x");
		path.node.right = t.identifier("y");
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});
5.替换整个节点

Path 对象中与替换相关的方法有 replaceWith、replaceWithMultiple、replaceInline、replaceWi thSourceString 先来看看 replaceWith 的用法,该方法用来节点换节点,并且是一换一。示例代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

const visitor = {
	BinaryExpression(path) {
		path.replaceWith(t.valueToNode("xiaojianbang")); // 一换一替换节点属性
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

再来看看 replaceWithMultiple 的用法,该方法也是用来节点换节点,只不过是多换一。示例代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 一换多替换
const visitor = {
	ReturnStatement(path) {
		path.replaceWithMultiple([
			t.expressionStatement(t.stringLiteral("xiaojianbang")),
			t.expressionStatement(t.numericLiteral(1000)),
			t.returnStatement(),
		]);
		path.stop();
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

上述代码中有两个注意点要特别说明,第一点是当表达式语句单独一行时(没有赋值)最好用 expressionStatement 包裹一下。第二点是替换后的节点,traverse 也是能遍历到的。因此替换的时候要小心,不要造成不合理的递归调用。比如上述代码,把 return 语句进行替换,但是替换的语句里又有 return 语句,这就死循环了。解决方法是加入 path.stop()替换完事就停止遍历当前节点和后续的子节点。

再来看看 replaceInline 的用法,该方法接收一个参数。如果参数不为数组,那么 replaceInline 等同于 replaceWith。如果参数是一个数组,那么 replaceInline 等同于 replaceWithMultiple,当然数组成员必须都是节点。示例代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 示例
const visitor = {
	StringLiteral(path) {
		path.replaceInline(t.stringLiteral("Hello AST!"));
		path.stop();
	},
	ReturnStatement(path) {
		path.replaceInline([
			t.expressionStatement(t.stringLiteral("xiaojianbang")),
			t.expressionStatement(t.numericLiteral(1000)),
			t.returnStatement(),
		]);
		path.stop();
	},
};
traverse(ast, visitor);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

上述代码中,visitor 中的函数也要加入 path.stop()原因和之前介绍的等同最后看看 replaceWithjourceString 的用法,该方法用字符串源码替换节点。比如,想要上面1.获取子节点/Path小节的原始代码中的函数改为闭包形式,示例代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 替换成闭包形式
traverse(ast, {
	ReturnStatement(path) {
		let argumentPath = path.get("argument");
		argumentPath.replaceWithSourceString(
			"function(){return " + argumentPath + "}()"
		);
		path.stop(); // 如果没有这个会死循环
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

先是遍历 ReturnStatement,然后通过 Path 的 get 方法去获取子 Path,这样才能调用

Path 相关的方法去操作 argument 节点。同时,replaceWithSourceString 替换后的节点也会被解析,也就是说会被 traverse 遍历到。因为里面也有 return 语句,所以需要加上 path.stop()。上述代码中还用到了节点转代码,只不过是隐式转换。

凡是需要修改节点的操作,都推荐使用 Path 对象的方法。因为当调用一个修改节点的方法后,Babel 会更新 Path 对象。

6.删除节点
js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 删除节点
traverse(ast, {
	EmptyStatement(path) {
		path.remove();
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

EmptyStatement 指的是空语句,就是多余的分号。使用 path.remove()删除当前节点。

7.插入节点

想要把节点插入到兄弟节点中,可以使用 insertBefore、insertAfter 分别在当前节点的前后插入节点。

代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 插入节点演示
traverse(ast, {
	ReturnStatement(path) {
		path.insertBefore(t.expressionStatement(t.stringLiteral("Before")));
		path.insertAfter(t.expressionStatement(t.stringLiteral("After")));
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

在上述代码中,如果只想操作某一个函数中的 ReturnStatement,那可以在 visitor 的函数中进行判断,不符合要求的 return 即可。注意,使用 path.stop() 是做不到的。

3.父级 Path

再回顾下2.Path中的方法小节中输出的 Path 对象。可以看到有 parentPath 和 parent 两个属性。其中 parentPath 类型为 NodePath,所以它是父级 Path。parent 类型为 Node,所以它是父节点。那么只要获取到父级 Path,就可以愉快地调用 Path 对象的各种方法去操作父节点。父级 Path 的获取可以使用 path.parentPath

1.parentPath 与 parent 的关系

path.parentPath.node 等价于 path.parent,也就是说 parent 是 parentPath 中的一部分。

2.path.findParent()

有时候需要从一个路径向上遍历语法树,直到满足相应的条件。这时候可以使用 Path 对象的 findParent 方法,演示例子如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	ReturnStatement(path) {
		console.log(path.findParent((p) => p.isObjectExpression()));
		// path.findParent(function(p){return p.isObjectExpression()});
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

Path 对象的 findParent 方法接收一个回调函数,在向上遍历每一个父级 Path 时,会调用该回调函数,并传入对应的父级 Path 对象作为参数。当该回调函数返回真值时,则将对应的父级 Path 返回。上述代码遍历 ReturnStatement,然后向上找父级 Path,当找到 Path 对象类型为 ObjectExpression 的时候,就返回该 Path 对象。

3.path.find0

这个方法用的不多,使用方法与 findParent 一致,只不过 find 方法查找的范围包含当前节点,而 findParent 不包含。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	ObjectExpression(path) {
		console.log(path.find((p) => p.isObjectExpression()));
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});
4.path.getFunctionParent()

向上查找与当前节点最接近的父函数 path.getFunctionParent()返回的也是 Path 对象

5.path.getStatementParent()

向上遍历语法树,直到找到语句父节点。比如,声明语句、rettrn 语句、if 语句、witch 语句、while 语句等等。返回的也是 Path 对象。注意,该方法从当前节点开始找起,因此,如果想要找到 return 语句的父语句,就需要从 parentPath 中去调用,代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	ReturnStatement(path) {
		console.log(path.parentPath.getStatementParent());
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});
6.父级 Path 的其他法

其他方法的使用基本与之前介绍的相差无几。

比如,替换父节点 path.parentPath.replaceWith(Node)

比如,删除父节点 path.parentPath.remove()

4.同级 Path

在介绍同级 Path 之前,需要先介绍下容器 (ontainer)。

什么是容器呢? 用三个例子来说明问题,依然是以上面1.获取子节点/Path小节中的原始代码为例。

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	ReturnStatement(path) {
		console.log(path);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

上述代码遍历 ReturnStatement 节点,直接输出 Path 对象。

container 为数组的情况下
1.path.inList(判断容器是否为数组)

用于判断是否有同级节点。

当 container 为数组,但是只有一个成员时,也会返回 true

2.path.conainer,path.listKey,path.key

path.key:获取当前节点在容器中的索引

path.container:获取容器(包含所有同级节点的数组)

path.listKey:获取容器名

3.path.getSibling(index)

用于获取同级 Path,其中参数 index 即容器数组中的索引。index 可以通过 path.key 来获取

可以对 path.key 进行加减操作,来定位到不同的同级 path

4.unshiftContainer 与 pushContainer

unshiftContainer 在容器前面加入

pushContainer 在容器后面加入

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	ReturnStatement(path) {
		path.parentPath.unshiftContainer("body", [
			t.expressionStatement(t.stringLiteral("Before1")),
			t.expressionStatement(t.stringLiteral("Before2")),
		]);
		console.log(
			path.parentPath.pushContainer(
				"body",
				t.expressionStatement(t.stringLiteral("After"))
			)
		);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

scope 详解

以此段代码为例:

js
const a = 1000;
let b = 2000;
let obj = {
	name: "xiaojianbang",
	add: function (a) {
		a = 400;
		b = 300;
		let e = 700;
		function demo() {
			let d = 600;
		}
		demo();
		return a + a + b + 1000 + obj.name;
	},
};
obj.add(100);

1.获取标识符作用域

scope.block 属性可以用来获取标识符作用域,返回的是 Node 对象。使用方法分为两种情况,变量和函数。先来看标识符为变量的情况:

1.获取当前变量标识符的作用域
js
path.scope.block;

使用案例:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 获取标识符作用域
traverse(ast, {
	Identifier(path) {
		if (path.node.name == "e") {
			console.log(generator(path.scope.block).code);
		}
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});
2.获取函数标识符作用域
js
path.scope.parent.block; // 获取父级作用域

既然 path.scope.block 返回的是 Node 对象,那么就可以使用 generator 来生成代码。

上述代码遍历所有 Identifier,当名字为 e 的时候,把当前节点的作用域转代码。变量 e 是定义在 add 函数内部的,作用域范围就是整个 add 函数。这个很好理解,但是如果遍历的是一个函数的话,它的炸用域有点特别。来看下面这个例子:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	FunctionDeclaration(path) {
		console.log(generator(path.scope.block).code);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

上述代码遍历 FunctionDeclaration,在原代码中只有 demo 函数符合要求,但是 demo 函数的作用域实际上应该是整个 add 函数的范围。因此输出的与实际的不符,这时候需要去获取父级作用域。获取函数的作用域代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	FunctionDeclaration(path) {
		console.log(generator(path.scope.parent.block).code);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

2.标识符的绑定:scope.getBinding()和 scope.getOwnBinding()

scope.getBinding()方法接收一个类型为 string 参数,用来获取对应标识符的绑定。

下面这段代码,遍历 FunctionDeclaration,符合要求的就只有 demo 函数,然后获取当前节点下的绑定 a,直接输出 binding,代码如下:

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	FunctionDeclaration(path) {
		let binding = path.scope.getBinding("a");
		console.log(binding);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

获取当前节点自己的绑定:scope.getOwnBinding(name);

js
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

function TestOwnBinding(path) {
	path.traverse({
		Identifier(p) {
			let name = p.node.name;
			console.log(name, !!p.scope.getOwnBinding(name));
		},
	});
}
traverse(ast, {
	FunctionExpression(path) {
		TestOwnBinding(path);
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

3.引用和赋值标识的数组:referencePaths 与 constantViolations

referencePaths

是被引用标识的数组

constantViolations

是被赋值标识的数组

4.遍历作用域

js
path.scope.traverse; // 遍历
binding.scope.traverse; // 遍历

完整:

js
traverse(ast, {
	FunctionDeclaraion(path) {
		let binding = path.scope.getBinding("a");
		// 遍历作用域的第一个参数是       作用域节点
		binding.scope.traverse(binding.scope.block, {
			AssignmentExpression(p) {
				if (p.node.left.name == "a") p.node.right = t.numericLiteral(500);
			},
		});
	},
});

5.标识符重命名

js
binding对象.scope.rename("x",'b') 所有引用的地方都会修改掉

生成标识符的方法

js
traverse(ast,{
    FunctionDeclaraion(path){
        #第一次uid
        path.scope.generateUidIdentifier("uid")
        #第二次uid2
        path.scope.generateUidIdentifier("uid")
        #第三次uid3
        path.scope.generateUidIdentifier("uid")
    }
})

6.scope 的其他方法

js
scope.hasBinding("a"); // 判断是否绑定
scope.hasOwnBinding("a"); // 判断是否有自己的绑定,
scope.getAllBindings(); // 获取对象中的所有绑定 获取一个对象
scope.hasReference("a"); // 查询当前节点中是否有a标识符的引用,返回true或false
scope.getBindingIdentifier("a"); // 获取当前节点中绑定a标识,返回的是Identifier的Node对象

AST 自动化 JavaScript 防护方案

混淆前的代码处理

1.改变对象属性的访问方式

1.修改对象属性的两种访问方式里我们已经介绍过了修改对象的访问方式

来看一下对象访问方式修改前后的区别:

console.log 形式

JavaScript
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

jscode = 'console.log';

let ast = parser.parse(jscode,{
 sourceType: "Module",
 //指定module参数,防止在解析import时报错
});

let code = generator(ast,{
    retainLines: false,
    comments: false,
    compact: true
}).code;

traverse(ast, {
    MemberExpression(path){
        console.log(path.node);
    }
});


console.log(code);
/*输出
Node {
    type: 'MemberExpression',
    ...
      identifierName: undefined
    },
    object: Node {
      type: 'Identifier',
        ...
        identifierName: 'console'
      },
      ...
      name: 'console'
    },
    computed: false,
    property: Node {
        ...
        identifierName: 'log'
      },
      ...
      name: 'log'
    }
  }
  console.log;
*/

console[‘log’]形式

javascript
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

jscode = " console['log'] ";

let ast = parser.parse(jscode, {
	sourceType: "Module",
	//指定module参数,防止在解析import时报错
});

let code = generator(ast, {
	retainLines: false,
	comments: false,
	compact: true,
}).code;

traverse(ast, {
	MemberExpression(path) {
		console["log"](path.node);
	},
});

console.log(code);
const fs = require("fs");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

jscode = " console['log'] ";

let ast = parser.parse(jscode, {
	sourceType: "Module",
	//指定module参数,防止在解析import时报错
});

let code = generator(ast, {
	retainLines: false,
	comments: false,
	compact: true,
}).code;

traverse(ast, {
	MemberExpression(path) {
		console["log"](path.node);
	},
});

console.log(code);

/*输出
Node {
  type: 'MemberExpression',
  ...
  object: Node {
    type: 'Identifier',
    ...
      identifierName: 'console'
    },
    ...
    name: 'console'
  },
  computed: true,
  property: Node {
    type: 'StringLiteral',
    ...
    value: 'log'
  }
}
console['log'];
*/

仔细对比一下,不难发现两者之间的差别仅在两处

  1. console['log']computedtrueconsole.logcomputedfalse
  2. console.['log'] 中的 logStringLiteralconsole.log 中的 logIdentifier

那么接下来就很简单了,只需要通过遍历 MemberExpression (成员表达式),把对应的 Identifier 改成 StringLiteralcomputed 属性改为 false 即可。

自动修改对象的访问方式.js

JavaScript
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const jscode = fs.readFileSync("./demo.js", {
 encoding: "utf-8"
});

let ast = parser.parse(jscode,{
 sourceType: "Module",
 //指定module参数,防止在解析import时报错
});

traverse (ast, {
    MemberExpression(path){
        if (t.isIdentifier(path.node.property)){
            /*为了防止代码本身就是类似console['log']这样的写法
            所以做一次判断,只处理类型为Identifier的情况
            */
            let name = path.node.property.name;
            //取出属性的名字
            path.node.property = t.stringLiteral(name);
            //包装成字符串
        }
        path.node.computed = true;
        //修改computed属性
    },
});

let code = generator(ast,{
    retainLines: false,
    comments: false,
    compact: false
}).code;

fs.writeFile('./NewDemo.js', code, (err)=>{});

NewDemo.js:

JavaScript
String["prototype"]["format"] = function formatStr(str) {
  str = str["replace"](/^\s+|\s+$/g, "");
  str = str["replace"](/\s+/g, "");
  str = str["replace"](/^\s/, '');
  str = str["replace"](/(\s$)/g, "");
  let div = document["createElement"]('div');
  div["textContent"] = str;
  let formatString = div["innerHTML"];
  return formatString;
};

console["log"](new String()["format"]('&&My name is xiao ming&&'));

2.JS 标准内置对象的处理

自动处理 JS 标准内置对象只需要遍历所有的Identifier,然后取出其名字进行判断,如果其名字和标准对象名称有雷同,则说明这是个标准内置对象,把他变成MemberExpression即可。

标准内置对象的文档:JavaScript 标准内置对象 - JavaScript | MDN (mozilla.org)

自动处理标准内置对象.js

javascript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

traverse(ast, {
	Identifier(path) {
		let name = path.node.name;
		if (
			"Infinity|NaN|undefined|globalThis|uneval|isFinite|isNaN|parseFloat|parseInt|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|Symbol|Object|Function|Boolean|Error|AggregateError|EvalError|InternalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|Number|Math|Date|BigInt|String|RegExp|parseInt|encodeURIComponent||Array|Int8Array|Unit8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|BigInt64Array|BigUint64Array|Map|Set|WeakMap|WeakSet|ArrayBuffer|SharedArrayBuffer|Atomics|DataView|JSON|Promise|Generator|GeneratorFunction|AsyncFunction|Reflect|Proxy|Intl|Intl.Collator|Intl.DateTimeFormat|Intl.ListFormat|Intl.NumberFormat|Intl.PluralRules|Intl.RelativeTimeFormat|Intl.Locale|WebAssembly|arguments".indexOf(
				name
			) != -1
		) {
			path.replaceWith(
				t.memberExpression(t.identifier("window"), t.stringLiteral(name), true)
			);
		}
	},
});

let code = generator(ast, {
	retainLines: false,
	comments: false,
	compact: false,
}).code;

fs.writeFile("./NewDemo.js", code, (err) => {
	console.log("写入成功");
});

NewDemo.js:

javascript
window["String"].prototype.format = function formatStr(str) {
	str = str.replace(/^\s+|\s+$/g, "");
	str = str.replace(/\s+/g, "");
	str = str.replace(/^\s/, "");
	str = str.replace(/(\s$)/g, "");
	let div = document.createElement("div");
	div.textContent = str;
	let formatString = div.innerHTML;
	return formatString;
};

console.log(new window["String"]().format("&&My name is xiao ming&&"));

常量与标识符的混淆

1.实现数值常量加密

代码中的数值常量可以通过遍历NumericLiteral节点获取其中的value属性来获得,然后生成一个随机数作为key。接着把valuekey进行异或后的数值记为newNum

newNum = value ^ keyvalue = newNum ^ key

这样我们就可以通过生成一个BinaryExpression节点来等价的替换NumericLiteral节点。

bash
BinaryExpression``operator``^

leftnewNum

bash
right``key

demo.js

JavaScript
console.log( 114514 + 1919810 )
//输出 2034324

自动化处理数值常量.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

traverse(ast, {
 NumericLiteral(path){
  let value = path.node.value;
  let key = parseInt(Math.random() * (89532165 - 32682338) + 12384635, 13);
  let cipherNum = value ^ key;
  path.replaceWith(t.binaryExpression('^', t.numericLiteral(cipherNum), t.numericLiteral(key)));
  //替换后的节点里也有numericLiteral节点,会造成死循环,因此需要加入path.skip()
        path.skip();
 }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

混淆后得到(注:因为使用了随机数,所以每次执行混淆结果都各不相同)

demoNew.js

javascript
console.log((63430957 ^ 63331967) + (38288127 ^ 39154109));
//输出 2034324

2.实现字符串常量加密

原始代码经过前面几个小节的处理过后,很多标识符都变为了字符串,但是这些字符串还是明文,所以本小节对字符串常量进行加密。

步骤和5.字符串常量加密的差不多,把明文转化为 解密函数(密文) 的形式即可。

在 Babel 中操作,首先需要遍历所有的StringLiteral,取出其中的value属性进行加密,然后把StringLiteral节点替换为callExpression(函数调用表达式)。

一般而言,是在上线之前先将源代码加密之后把带着解密函数的已加密的源码发布出去。

这里是上线前加密过程,所以使用了自己实现的 base64Encode() (base64 加密函数),并在客户端使用浏览器自带的atob()解密函数(在工程上请自己实现解密函数或使用其他加密方法,base64 保密性太低)。

自动化处理字符串常量.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

function base64Encode(e) {
 var r, a, c, h, o, t, base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
 for (c = e.length, a = 0, r = ''; a < c;) {
  if (h = 255 & e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4),
   r += '==';
   break
  }
  if (o = e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
   r += base64EncodeChars.charAt((15 & o) << 2),
   r += '=';
   break
  }
  t = e.charCodeAt(a++),
  r += base64EncodeChars.charAt(h >> 2),
  r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
  r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
  r += base64EncodeChars.charAt(63 & t)
 }
 return r
}

traverse(ast, {
 StringLiteral(path){
  //生成callExpression参数就是字符串加密后的密文
  let encStr = t.callExpression(
        t.identifier('atob'),
        [t.stringLiteral(base64Encode(path.node.value))]);
  path.replaceWith(encStr);
  //替换后的节点里也有StringLiteral节点,会造成死循环,因此需要加入path.skip()
  path.skip();
  }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{
    console.log('写入成功');
});

demo.js

javascript
window["String"]["prototype"]["format"] = function formatStr(str) {
	str = str["replace"](/^\s+|\s+$/g, "");
	str = str["replace"](/\s+/g, "");
	str = str["replace"](/^\s/, "");
	str = str["replace"](/(\s$)/g, "");
	let div = document["createElement"]("div");
	div["textContent"] = str;
	let formatString = div["innerHTML"];
	return formatString;
};

console["log"](new window["String"]()["format"]("&&My name is xiao ming&&"));

混淆后得到:

demoNew.js

JavaScript
window[atob("U3RyaW5n")][atob("cHJvdG90eXBl")][atob("Zm9ybWF0")] = function formatStr(str) {
  str = str[atob("cmVwbGFjZQ==")](/^\s+|\s+$/g, atob(""));
  str = str[atob("cmVwbGFjZQ==")](/\s+/g, atob(""));
  str = str[atob("cmVwbGFjZQ==")](/^\s/, atob(""));
  str = str[atob("cmVwbGFjZQ==")](/(\s$)/g, atob(""));
  let div = document[atob("Y3JlYXRlRWxlbWVudA==")](atob("ZGl2"));
  div[atob("dGV4dENvbnRlbnQ=")] = str;
  let formatString = div[atob("aW5uZXJIVE1M")];
  return formatString;
};

console[atob("bG9n")](new window[atob("U3RyaW5n")]()[atob("Zm9ybWF0")](atob("JiZNeSBuYW1lIGlzIHhpYW8gbWluZyYm")));

可以看到原本的字符串都被替换成了atob(base64编码后)的数据了

3.实现数组混淆(自动化数组引用)

数组混淆分为将所有函数都放进一个数组中或多个函数放进多个数组中,本小节为了直观,只实现所有函数放进一个大数组里。

举个栗子,如果想把str[atob("cmVwbGFjZQ==")],把cmVwbGFjZQ==变成数组bigArr的索引为 0 的成员后,原先的字符串位置就变为了:str[atob(bigArr[0])],所以我们需要额外生成一个数组放入被混淆的代码中去。

首先,我们需要构造一个callExpression,函数名称为atob,参数为memberExpression,其object属性为bigArrproperty属性为数组索引index

javascript
let encStr = t.callExpression(t.identifier("atob"), [
	t.memberExpression(t.identifier("arr"), t.numericLiteral(index), true),
]);

然后我们需要把代码的字符串放入数组中,然后得到对应的数组索引。这里有两种方案

  1. 不管字符串有无重复,遍历到一个就放进数组,然后把新的索引赋值给之前构造的index
  2. 字符串放入数组之前先查询数组有无重复,如果有,则使用已有的那个数组索引

第一种实现方式虽然有些太过于简单粗暴,但数组的成员越多,混淆的力度往往越强。

第二种可以在不重复的基础上再自己添加重复数组成员以迷惑逆向者,可扩展性更强。

这里我们实现第二种:

自动数组引用.js(功能和 5.4 有部分重复,已在这段代码中合并)

javascript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);

function base64Encode(e) {
	var r,
		a,
		c,
		h,
		o,
		t,
		base64EncodeChars =
			"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	for (c = e.length, a = 0, r = ""; a < c; ) {
		if (((h = 255 & e.charCodeAt(a++)), a == c)) {
			(r += base64EncodeChars.charAt(h >> 2)),
				(r += base64EncodeChars.charAt((3 & h) << 4)),
				(r += "==");
			break;
		}
		if (((o = e.charCodeAt(a++)), a == c)) {
			(r += base64EncodeChars.charAt(h >> 2)),
				(r += base64EncodeChars.charAt(((3 & h) << 4) | ((240 & o) >> 4))),
				(r += base64EncodeChars.charAt((15 & o) << 2)),
				(r += "=");
			break;
		}
		(t = e.charCodeAt(a++)),
			(r += base64EncodeChars.charAt(h >> 2)),
			(r += base64EncodeChars.charAt(((3 & h) << 4) | ((240 & o) >> 4))),
			(r += base64EncodeChars.charAt(((15 & o) << 2) | ((192 & t) >> 6))),
			(r += base64EncodeChars.charAt(63 & t));
	}
	return r;
}

let bigArr = [];
traverse(ast, {
	StringLiteral(path) {
		let cipherText = base64Encode(path.node.value);
		let bigArrIndex = bigArr.indexOf(cipherText);
		//使用indexOf方法搜索数组成员
		let index = bigArrIndex;
		if (bigArrIndex == -1) {
			//没有找到会返回-1
			let length = bigArr.push(cipherText);
			//加入到数组中
			index = length - 1;
			//记录最后成员的索引值
		}
		let encStr = t.callExpression(t.identifier("atob"), [
			t.memberExpression(t.identifier("arr"), t.numericLiteral(index), true),
		]);
		path.replaceWith(encStr);
	},
});
bigArr = bigArr.map(function (v) {
	//map方法遍历数组成员
	return t.stringLiteral(v);
	//接收数组成员
});
bigArr = t.variableDeclarator(t.identifier("arr"), t.arrayExpression(bigArr));
bigArr = t.variableDeclaration("var", [bigArr]);
//声明大数组用于存放函数
ast.program.body.unshift(bigArr);
//unshift方法将变量加入数组

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {
	console.log("写入完成");
});

demoNew.js

javascript
var arr = [
	"U3RyaW5n",
	"cHJvdG90eXBl",
	"Zm9ybWF0",
	"cmVwbGFjZQ==",
	"",
	"Y3JlYXRlRWxlbWVudA==",
	"ZGl2",
	"dGV4dENvbnRlbnQ=",
	"aW5uZXJIVE1M",
	"bG9n",
	"JiZNeSBuYW1lIGlzIHhpYW8gbWluZyYm",
];

window[atob(arr[0])][atob(arr[1])][atob(arr[2])] = function formatStr(str) {
	str = str[atob(arr[3])](/^\s+|\s+$/g, atob(arr[4]));
	str = str[atob(arr[3])](/\s+/g, atob(arr[4]));
	str = str[atob(arr[3])](/^\s/, atob(arr[4]));
	str = str[atob(arr[3])](/(\s$)/g, atob(arr[4]));
	let div = document[atob(arr[5])](atob(arr[6]));
	div[atob(arr[7])] = str;
	let formatString = div[atob(arr[8])];
	return formatString;
};

console[atob(arr[9])](new window[atob(arr[0])]()[atob(arr[2])](atob(arr[10])));

4.实现数组乱序

首先我们需要把还原数组顺序的代码保存进新文件数组还原.js

还原数组.js

JavaScript
(function(arr, num){
    var shuffer = function(nums){
        while(--nums){
            arr['push'](arr['shift']());
        }
    };
    shuffer(++num);
    for (let i = arr.length - 1; i > -1; i--) {
        let temp = arr[arr.length - 3];
        arr[arr.length - 3] = arr[i];
        arr[i] = temp;
    }
}(arr, 0x20));

读取改文件并解析成AST由于还原数组顺序的代码最外层只有一个函数节点,所以取出其中的astFront.program.body[0]放入被混淆的代码中的body即可。

自动数组乱序.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

function base64Encode(e) {
 var r, a, c, h, o, t, base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
 for (c = e.length, a = 0, r = ''; a < c;) {
  if (h = 255 & e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4),
   r += '==';
   break
  }
  if (o = e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
   r += base64EncodeChars.charAt((15 & o) << 2),
   r += '=';
   break
  }
  t = e.charCodeAt(a++),
  r += base64EncodeChars.charAt(h >> 2),
  r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
  r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
  r += base64EncodeChars.charAt(63 & t)
 }
 return r
}

let bigArr = [];
traverse(ast, {
 StringLiteral(path){
  let cipherText = base64Encode(path.node.value);
  let bigArrIndex = bigArr.indexOf(cipherText);
  let index = bigArrIndex;
  if(bigArrIndex == -1){
   let length = bigArr.push(cipherText);
   index = length -1;
  }
  let encStr = t.callExpression(
     t.identifier('atob'),
     [t.memberExpression(t.identifier('arr'),
          t.numericLiteral(index), true)]);
  path.replaceWith(encStr);
 }
});
bigArr = bigArr.map(function(v){
  return t.stringLiteral(v);
});
// 构建数组声明语句

(function(arr,num){
    for (let i = 0; i < arr.length; i++) {
        let temp = arr[i];
        arr[i] = arr[arr.length - 3];
        arr[arr.length - 3] = temp;
    }
    var shuffer = function(nums){
        while(--nums){
            arr.unshift(arr.pop());
        }
    };
    shuffer(++num);
}(bigArr,0x20));
// 先打乱数组

bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(bigArr));
bigArr = t.variableDeclaration('var', [bigArr]);



// 读取还原数组顺序的函数,并解析成astFront
const jscodeFront = fs.readFileSync("./还原数组.js", {
 encoding: "utf-8"
  });
let astFront = parser.parse(jscodeFront);
// 先把还原数组顺序的代码,加入到被混淆代码的ast中
ast.program.body.unshift(astFront.program.body[0]);
// 把数组放到被混淆代码的ast最前面
ast.program.body.unshift(bigArr);

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{
 console.log('写入成功');
});

5.实现十六进制字符串

由于我们实现数组还原的代码是未经混淆的。代码中的标识符的混淆可以在最后一起处理。其中像pushshift这些方法可以转为字符串,由于这些代码处于还原数组顺序的代码中,因此没法把他们提进大数组。因此我们只能进行十六进制字符串这样简单的混淆。

自动十六进制混淆.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

function base64Encode(e) {
 var r, a, c, h, o, t, base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
 for (c = e.length, a = 0, r = ''; a < c;) {
  if (h = 255 & e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4),
   r += '==';
   break
  }
  if (o = e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
   r += base64EncodeChars.charAt((15 & o) << 2),
   r += '=';
   break
  }
  t = e.charCodeAt(a++),
  r += base64EncodeChars.charAt(h >> 2),
  r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
  r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
  r += base64EncodeChars.charAt(63 & t)
 }
 return r
}

let bigArr = [];
traverse(ast, {
 StringLiteral(path){
  let cipherText = base64Encode(path.node.value);
  let bigArrIndex = bigArr.indexOf(cipherText);
  let index = bigArrIndex;
  if(bigArrIndex == -1){
   let length = bigArr.push(cipherText);
   index = length -1;
  }
  let encStr = t.callExpression(
     t.identifier('atob'),
     [t.memberExpression(t.identifier('arr'),
         t.numericLiteral(index), true)]);
  path.replaceWith(encStr);
 }
});
bigArr = bigArr.map(function(v){
 return t.stringLiteral(v);
});
// 构建数组声明语句

(function(arr,num){
    for (let i = 0; i < arr.length; i++) {
        let temp = arr[i];
        arr[i] = arr[arr.length - 3];
        arr[arr.length - 3] = temp;
    }
    var shuffer = function(nums){
        while(--nums){
            arr.unshift(arr.pop());
        }
    };
    shuffer(++num);
}(bigArr,0x20));
// 先打乱数组

bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(bigArr));
bigArr = t.variableDeclaration('var', [bigArr]);

// 读取还原数组顺序的函数,并解析成astFront
const jscodeFront = fs.readFileSync("./还原数组.js", {
 encoding: "utf-8"
 });
let astFront = parser.parse(jscodeFront);
// 先把还原数组顺序的代码,加入到被混淆代码的ast中
ast.program.body.unshift(astFront.program.body[0]);
// 把数组放到被混淆代码的ast最前面
ast.program.body.unshift(bigArr);

function hexEnc(code) {
    for (var hexStr = [], i = 0, s; i < code.length; i++) {
        s = code.charCodeAt(i).toString(16);
        hexStr += "\\x" + s;
    }
    return hexStr
}

traverse(astFront, {
 MemberExpression(path){
  if(t.isIdentifier(path.node.property)){
   let name = path.node.property.name;
    if (name !== "i"){
     /*arr[i]这种访问数组下标的形式中 [i] 也是Identifier,但是arr[i]不能被混淆成arr["\x69"]的形式
     此类问题属于乱序&还原函数与混淆函数产生的难以解耦的耦合,需要针对性处理*/
     path.node.property = t.stringLiteral(hexEnc(name));
    }
  }
  path.node.computed = true;
 }
});

let code = generator(ast).code;

code = code.replace(/\\\\x/g, '\\x')
fs.writeFile('./demoNew.js', code, (err)=>{
 console.log('写入成功');
});

6.实现标识符混淆

一般而言标识符是有语义的,根据标识符名字就可以猜测出代码的用途,因此标识符的混淆是很有必要的。

本小节将介绍一种方法,scope.getOwnBinding。该方法可以用于获取属于自己当前节点的自己的绑定。

比如:在Program节点下,使用getOwnBinding就可以获取到全局标识符名称,而函数内的局部标识符名称不会被获取到。接着我们再通过遍历函数节点,在FunctionExpressionFunctionDeclaration节点下,使用getOwnBinding可以获取到函数自定义的局部标识符名而不会获取到全局标识符名。遍历三种节点,执行同种命名方式。

代码如下:

JavaScript
traverse(ast, {
    'Program|FunctionExpression|FunctionDeclaration'(path) {
        renameOwnBinding(path);
    }
});

然后我们实现一个renameOwnBindling函数

JavaScript
function renameOwnBinding(path) {
    let OwnBindingObj = {}, globalBindingObj = {}, i = 0;
    path.traverse({
        Identifier(p)  {
            let name = p.node.name;
            let binding = p.scope.getOwnBinding(name);
            binding && generator(binding.scope.block).code == path + '' ?
            (OwnBindingObj[name] = binding) : (globalBindingObj[name] = 1);
        }
    });
    for(let oldName in OwnBindingObj) {
        do {
            var newName = '_0x593fdd' + i++;
        } while(globalBindingObj[newName]);
        OwnBindingObj[oldName].scope.rename(oldName, newName);
    }
}

在上述代码中,我们先遍历了当前的节点中所有Identifier,得到其name属性,通过getOwnBinding判断当前节点是否与自己绑定。如果bindingdefined,则表示是其父级函数的标识符或者全局的标识符,就将该标识符名作为属性名,放入到globalBindingObjbinding作为属性值,放入到OwnBindingObj对象中。

这样需要注意四个点:

  1. lobalBindingObj中存放的不是全部的全局标识符,而是当前节点引用到的全局标识符。因为重命名标识符的时候,不能与引用到的全局标识符重名,需要进行判断。
  2. OwnBindingObj中存储对应标识符的binding。因为重命名标识符的时候需要使用binding.scope.rename方法。
  3. 把标识符名作为对象的属性名。因为一个Identifier有多处引用就会遍历到多个,但实际上只需要调用一次scope.rename方法。
  4. 先把 AST 转成代码解析后再进行标识符混淆。因为修改 AST 节点的时候,使用Path对象的方法时 Babel 会更新PAth信息。但是实际应用时我们不能做到全部使用Path对象的方法。比如使用type组件是用来生成新的节点的,节点并不是一个Path对象,所以就没有binding了。

同时,我们可以将标识符的命名的随机性和迷惑性变的更强壮,这里采用了将十进制转换为三进制,再用大写字母O,小写字母O和数字0来代替三进制的0,1,2

自动处理标识符.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

function generatorIdentifier(decNum){
 let flag = ['O', 'o', '0'];
 let retval = [];
 while(decNum > 0){
  retval.push(decNum % 3);
  decNum = parseInt(decNum / 3);
 }
    // 三进制转换,除以三,留余数然后翻转数组
 let Identifier = retval.reverse().map(function(v){
        // 遍历替换数组,用'O','o','0'来代替三进制的'0','1','2'
  return flag[v]
 }).join('');
 Identifier.length < 6 ? (Identifier = ('ooOoOo' + Identifier).substr(-6)):
    // 为了防止过短,判断长度后补齐六位
 Identifier[0] == '0' && (Identifier = 'O' + Identifier);
    // JS变量名不允许以数字0开头,如果发现以0开头则在前面加个大写字母O
 return Identifier;
}

function renameOwnBinding(path) {
    let OwnBindingObj = {}, globalBindingObj = {}, i = 0;
    path.traverse({
        Identifier(p)  {
            let name = p.node.name;
            let binding = p.scope.getOwnBinding(name);
            binding && generator(binding.scope.block).code == path + '' ?
            (OwnBindingObj[name] = binding) : (globalBindingObj[name] = 1);
        }
    });
    for(let oldName in OwnBindingObj) {
        do {
            var newName = generatorIdentifier(i++);
        } while(globalBindingObj[newName]);
        OwnBindingObj[oldName].scope.rename(oldName, newName);
    }
}
traverse(ast, {
    'Program|FunctionExpression|FunctionDeclaration'(path) {
        renameOwnBinding(path);
    }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{
    console.log('写入成功');
});

demoNew.js

JavaScript
var ooOoOo = ["U3RyaW5n", "cHJvdG90eXBl", "Zm9ybWF0", "cmVwbGFjZQ==", "", "Y3JlYXRlRWxlbWVudA==", "ZGl2", "JiZNeSBuYW1lIGlzIHhpYW8gbWluZyYm", "dGV4dENvbnRlbnQ=", "bG9n", "aW5uZXJIVE1M"];

(function (ooOoOo, oOoOoo) {
  var oOoOo0 = function (oOoOoo) {
    while (--oOoOoo) {
      ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
    }
  };

  oOoOo0(++oOoOoo);

  for (let i = ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - 1; i > -1; i--) {
    let temp = ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - 3];
    ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - 3] = ooOoOo[i];
    ooOoOo[i] = temp;
  }
})(ooOoOo, 0x20);

window[atob(ooOoOo[0])][atob(ooOoOo[1])][atob(ooOoOo[2])] = function oOoOoo(oOoOo0) {
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s+|\s+$/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/\s+/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s/, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/(\s$)/g, atob(ooOoOo[4]));
  let OoOooO = document[atob(ooOoOo[5])](atob(ooOoOo[6]));
  OoOooO[atob(ooOoOo[7])] = oOoOo0;
  let OoOooo = OoOooO[atob(ooOoOo[8])];
  return OoOooo;
};

console[atob(ooOoOo[9])](new window[atob(ooOoOo[0])]()[atob(ooOoOo[2])](atob(ooOoOo[10])));

7.标识符的随机生成

代码块的混淆

1.二项式转函数花指令

从 AST 角度处理花指令需要如下四个步骤:

  1. 遍历BinaryExpression节点,取出operatorleftright
  2. 生成一个函数(函数名不能和当前节点的标识符冲突),返回语句中的运算符要与operator中的运算符一致
  3. 找到最近的BlockStatement节点,将生成的函数加入到body数组最前面
  4. 把原先的BinaryExpression节点替换成callExpression

在上述实现方案中,因为花指令的作用就是膨胀代码量从而将有用的代码藏在无用的代码中。

因此每遍历到一个operator都可以生成不同命的函数。因为并不是一种operator生成一个函数,所以不需要去判断是哪种operator

自动化处理花指令.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

traverse(ast, {
 BinaryExpression(path){
  let operator = path.node.operator;
  let left = path.node.left;
  let right = path.node.right;
  let a = t.identifier('a');
  let b = t.identifier('b');
  let funcNameIdentifier = path.scope.generateUidIdentifier('0OooOO');
  let func = t.functionDeclaration(
   funcNameIdentifier,
   [a, b],
            //参数数组
   t.blockStatement([t.returnStatement(
     t.binaryExpression(operator, a, b)
    )]));
        //生成函数
  let BlockStatement = path.findParent(
     function(p){return p.isBlockStatement()});
        //向上寻找最近的BlockStatement父节点
  BlockStatement.node.body.unshift(func);
        //加入到函数体的最前面
  path.replaceWith(t.callExpression(funcNameIdentifier, [left, right]));
 }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{
    console.log("写入成功");
});

2.代码的逐行加密(eval 加密)

在 JavaScript 中存在一个eval()函数,他的作用是把一个字符串当成是一条 JavaScript 语句来执行。

javascript
eval("console.log('HelloWorld!')");
//输出 HelloWorld!

基于这个特性,我们可以将语句转换为加密过后的字符串,再通过嵌套eval函数来对代码的内容进行加密。

要想实现这一点,需要进过如下七个步骤:

  1. 遍历FunctionExpression节点,其中path.mode.bodyblockStatenent节点,blockstatesent.body是一个数组,里面就是函数的代码行。它的每一个成员分别对应函数的每一行语句,然后使用数组的map方法对每一行语句分别处理
  2. 如果是返回语句,不做处理,直接返回原语句。单行加密的时候不能加密return语句。
  3. let code= generator(v).code;将语句转成字符串
  4. let ciphertext=base64 Encode(code):对字符串进行加密
  5. 构建atob( xxxx')这种形式的代码,atob就是解密函数名。解密函数如果不是系统自带的,还需要把解密函数一起放入代码中。生成 callExpression,callee为解密函数名,参数为加密后的字符串常量,值给 decryptFunc
  6. 构造eval(atob"xxx")这种形式的代码,因此还需要生成callExpressioncalleeeval,参数为上一步生成的decryptFunc,最后用 expressionStatement包裹。
  7. 当函数中所有语句处理完成后,构建新的blockStatement替换原有的语句

需要注意的是,这种方法不适合大范围使用,否则加密方式太过明显,比较合适的做法是只用于加密一小部分,比如加密了使用调用了敏感接口,传递了敏感参数的函数那么攻击者就没法通过直接搜索抓包获得的关键字来定位接口位置和参数定义。

AST 同样可以实现这一点,只需要我们在需要加密的行后面加入指定的注释,然后做一次判断注释内容,如果匹配上了则对该行进行eval加密。

demo.js

JavaScript
var ooOoOo = ["U3RyaW5n", "cHJvdG90eXBl", "Zm9ybWF0", "cmVwbGFjZQ==", "", "Y3JlYXRlRWxlbWVudA==", "ZGl2", "JiZNeSBuYW1lIGlzIHhpYW8gbWluZyYm", "dGV4dENvbnRlbnQ=", "bG9n", "aW5uZXJIVE1M"];

(function (ooOoOo, oOoOoo) {
  function _oOO0oo2(a, b) {
    return a > b;
  }

  function _oOO0oo(a, b) {
    return a - b;
  }

  var oOoOo0 = function (oOoOoo) {
    while (--oOoOoo) {
      ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
    }
  };

  oOoOo0(++oOoOoo);

  for (let i = _oOO0oo(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 1); _oOO0oo2(i, -1); i--) {
    function _oOO0oo4(a, b) {
      return a - b;
    }

    function _oOO0oo3(a, b) {
      return a - b;
    }

    let temp = ooOoOo[_oOO0oo3(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 3)];

    ooOoOo[_oOO0oo4(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 3)] = ooOoOo[i];
    ooOoOo[i] = temp;
  }
})(ooOoOo, 0x20);

window[atob(ooOoOo[0])][atob(ooOoOo[1])][atob(ooOoOo[2])] = function oOoOoo(oOoOo0) {
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s+|\s+$/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/\s+/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s/, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/(\s$)/g, atob(ooOoOo[4]));
  let OoOooO = document[atob(ooOoOo[5])](atob(ooOoOo[6]));
  OoOooO[atob(ooOoOo[7])] = oOoOo0;
  let OoOooo = OoOooO[atob(ooOoOo[8])];
  return OoOooo;
};

console[atob(ooOoOo[9])](new window[atob(ooOoOo[0])]()[atob(ooOoOo[2])](atob(ooOoOo[10])));

自动化 eval 加密.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

function base64Encode(e) {
 var r, a, c, h, o, t, base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
 for (c = e.length, a = 0, r = ''; a < c;) {
  if (h = 255 & e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4),
   r += '==';
   break
  }
  if (o = e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
   r += base64EncodeChars.charAt((15 & o) << 2),
   r += '=';
   break
  }
  t = e.charCodeAt(a++),
  r += base64EncodeChars.charAt(h >> 2),
  r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
  r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
  r += base64EncodeChars.charAt(63 & t)
 }
 return r
}

traverse(ast, {
 FunctionExpression(path){
  let blockStatement = path.node.body;
  let Statements = blockStatement.body.map(function(v){
   if(t.isReturnStatement(v)) return v;
            //return语句无法加密,所以遇到return直接返回return
   if(!(v.trailingComments && v.trailingComments[0].value == 'Base64Encrypt')) return v;
            //如果没有注释或注释的值不是Base64Encrypt则直接返回
   delete v.trailingComments;
            //先把注释删了
   let code = generator(v).code;
   let cipherText = base64Encode(code);
   let decryptFunc = t.callExpression(t.identifier('atob'), [t.stringLiteral(cipherText)]);
   return t.expressionStatement(t.callExpression(t.identifier('eval'), [decryptFunc]));
  });
  path.get('body').replaceWith(t.blockStatement(Statements));
 }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{
    console.log("写入成功")
});

demoNew.js

JavaScript
var ooOoOo = ["U3RyaW5n", "cHJvdG90eXBl", "Zm9ybWF0", "cmVwbGFjZQ==", "", "Y3JlYXRlRWxlbWVudA==", "ZGl2", "JiZNeSBuYW1lIGlzIHhpYW8gbWluZyYm", "dGV4dENvbnRlbnQ=", "bG9n", "aW5uZXJIVE1M"];

(function (ooOoOo, oOoOoo) {
  function _oOO0oo2(a, b) {
    return a > b;
  }

  function _oOO0oo(a, b) {
    return a - b;
  }

  var oOoOo0 = function (oOoOoo) {
    while (--oOoOoo) {
      ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
    }
  };

  oOoOo0(++oOoOoo);

  for (let i = _oOO0oo(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 1); _oOO0oo2(i, -1); i--) {
    function _oOO0oo4(a, b) {
      return a - b;
    }

    function _oOO0oo3(a, b) {
      return a - b;
    }

    let temp = ooOoOo[_oOO0oo3(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 3)];

    ooOoOo[_oOO0oo4(ooOoOo["\x6c\x65\x6e\x67\x74\x68"], 3)] = ooOoOo[i];
    ooOoOo[i] = temp;
  }
})(ooOoOo, 0x20);

window[atob(ooOoOo[0])][atob(ooOoOo[1])][atob(ooOoOo[2])] = function oOoOoo(oOoOo0) {
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s+|\s+$/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/\s+/g, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/^\s/, atob(ooOoOo[4]));
  oOoOo0 = oOoOo0[atob(ooOoOo[3])](/(\s$)/g, atob(ooOoOo[4]));
  let OoOooO = document[atob(ooOoOo[5])](atob(ooOoOo[6]));
  OoOooO[atob(ooOoOo[7])] = oOoOo0;
  let OoOooo = OoOooO[atob(ooOoOo[8])];
  return OoOooo;
};

console[atob(ooOoOo[9])](new window[atob(ooOoOo[0])]()[atob(ooOoOo[2])](atob(ooOoOo[10])));

3.代码的逐行 ASCII 码混淆

这种方案的原理与代码的逐行加密差不多,只不过把字符串 加密函数去掉了,换成 charCodeAt 转到 ASCII 码,解密函数换成String.fromCharCode,最后都需要 eval 函数来执行字符串代码。实现的代码如下:

js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});
let ast = parser.parse(jscode);
// 逐行加密
traverse(ast, {
	FunctionExpression(path) {
		let blockStatement = path.node.body;
		let Statements = blockStatement.body.map(function (v) {
			if (t.isReturnStatement(v)) return v;
			if (
				!(v.trailingComments && v.trailingComments[0].value == "ASCIIEncrypt")
			)
				return v;
			delete v.trailingComments;
			let code = generator(v).code;
			let codeAscii = [].map.call(code, function (v) {
				return t.numericLiteral(v.charCodeAt(0));
			});
			let decryptFuncName = t.memberExpression(
				t.identifier("String"),
				t.identifier("fromCharCode")
			);
			let decryptFunc = t.callExpression(decryptFuncName, codeAscii);
			return t.expressionStatement(
				t.callExpression(t.identifier("eval"), [decryptFunc])
			);
		});
		path.get("body").replaceWith(t.blockStatement(Statements));
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

字符串在 JS 中其实也是个数组,只不过是只读的。但是它不能直接调用数组的 map 方法,所以上述代码中用[].map.call 来间接调用,作用还是一样的。把字符串中的每个成员转成 ASCII 码之后,用 numericLiteral 节点包裹。然后利用 memberExpression 生成 String.fromCharCode 作为解密函数名。

注意,不管是代码逐行加密还是代码逐行 ASCII 混淆,都要在标识符混淆之后。由此可见,在做标识符混淆的时候,应该把原代码中的 eval 和 Function 处理一下才可以。

代码执行逻辑的混淆

1.实现流程平坦化

自动处理控制流扁平化.js

JavaScript
const parser = require("@babel/parser");const traverse = require("@babel/traverse").default;const t = require("@babel/types");const generator = require("@babel/generator").default;const fs = require('fs');const jscode = fs.readFileSync("./demo.js", {        encoding: "utf-8"    });let ast = parser.parse(jscode);traverse(ast, { FunctionExpression(path){  let blockStatement = path.node.body;  //逐行提取语句,按原先的语句顺序建立索引,包装成对象  let Statements = blockStatement.body.map(function(v, i){   return {index: i, value: v};  });  //洗牌,打乱语句顺序  let i = Statements.length;  while(i){   let j = Math.floor(Math.random() * i--);   [Statements[j], Statements[i]] = [Statements[i], Statements[j]];  }  //构建分发器,创建switchCase数组  let dispenserArr = [];  let cases = [];  Statements.map(function(v, i){   dispenserArr[v.index] = i;   let switchCase = t.switchCase(t.numericLiteral(i),  [v.value, t.continueStatement()]);   cases.push(switchCase);  });  let dispenserStr = dispenserArr.join('|');         //生成_array和_index标识符,利用BabelAPI保证不重名  let array = path.scope.generateUidIdentifier('array');  let index = path.scope.generateUidIdentifier('index');  //生成 '...'.split 这是一个成员表达式  let callee = t.memberExpression(t.stringLiteral(dispenserStr),  t.identifier('split'));  //生成 split('|')  let arrayInit = t.callExpression(callee, [t.stringLiteral('|')]);  //_array和_index放入声明语句中,_index初始化为0  let varArray = t.variableDeclarator(array, arrayInit);  let varIndex = t.variableDeclarator(index, t.numericLiteral(0));  let dispenser = t.variableDeclaration('let',[varArray, varIndex]);  //生成switch中的表达式 +_array[_index++]   let updExp = t.updateExpression('++', index);  let memExp = t.memberExpression(array, updExp, true);  let discriminant = t.unaryExpression("+", memExp);  //构建整个switch语句  let switchSta = t.switchStatement(discriminant, cases);  //生成while循环中的条件 !![]  let unaExp = t.unaryExpression("!", t.arrayExpression());  unaExp = t.unaryExpression("!", unaExp);  //生成整个while循环  let whileSta = t.whileStatement(unaExp,  t.blockStatement([switchSta, t.breakStatement()]));  //用分发器和while循环来构建blockStatement,替换原有节点  path.get('body').replaceWith(t.blockStatement([dispenser, whileSta]));   }});let code = generator(ast).code;fs.writeFile('./demoNew.js', code, (err)=>{    console.log("写入成功");});

这里要对上述代码作几点说明:

  1. 由于 JavaScript 语法规定了let语句声明的变量必须在声明之后调用。如果先调用再声明会报未定义错误,这种情况被称为暂时性死区。但是由于该方法涉及使用洗牌算法来改变代码的运行流程,在switch-case语句中无法保证调用的变量在let声明之后,所以会报错,请看这个例子:

    demo.js

    JavaScript
    String.prototype.format = function formatStr(str) {    str = str.replace(/^\s+|\s+$/g, '');    str = str.replace(/\s+/g, '');    str = str.replace( /^\s/, '');    str = str.replace(/(\s$)/g, '');    let div = document.createElement('div');    div.textContent = str;     let formatString = div.innerHTML;    return formatString;}console.log(new String().format('    dsadas&&   '))

    如果使用前面的代码对该 demo 进行混淆,我们会得到:

    demoNew.js

    javascript
    String.prototype.format = function formatStr(str) {
    	let _array = "0|1|6|3|7|5|2|4".split("|"),
    		_index = 0;
    	while (!![]) {
    		switch (+_array[_index++]) {
    			case 0:
    				str = str.replace(/^\s+|\s+$/g, "");
    				continue;
    			case 1:
    				str = str.replace(/\s+/g, "");
    				continue;
    			case 2:
    				let formatString = div.innerHTML;
    				continue;
    			case 3:
    				str = str.replace(/(\s$)/g, "");
    				continue;
    			case 4:
    				return formatString;
    				continue;
    			case 5:
    				div.textContent = str;
    				continue;
    			case 6:
    				str = str.replace(/^\s/, "");
    				continue;
    			case 7:
    				let div = document.createElement("div");
    				continue;
    		}
    		break;
    	}
    };
    console.log(new String().format("    dsadas&&   ")); //报 Cannot access 'div' before initialization错误

    可以很明显的看出,在let声明变量存在暂时性死区的影响下,洗牌改变顺序会让switch-case语句报错,为了能够解决这个问题,这里提出一种方案:将let声明的变量提升至函数变量中,把函数体中的let声明语句变为赋值表达式。

    通过提升变量的方式我们可以避开let语句从而规避暂时性死区的问题,我们从 AST 角度来执行这种方案

    JavaScript
    const parser = require("@babel/parser");
    const traverse = require("@babel/traverse").default;
    const t = require("@babel/types");
    const generator = require("@babel/generator").default;
    const fs = require('fs');
    
    const jscode = fs.readFileSync("./demo.js", {
            encoding: "utf-8"
        });
    let ast = parser.parse(jscode);
    
    traverse(ast, {
        FunctionExpression(path) {
            //把所有声明的变量提取到参数列表中
            path.traverse({
                VariableDeclaration(p) {
                    //遍历函数中所有的声明语句
                    declarations = p.node.declarations;
                    //取出声明语句中的declarations属性
                    let statements = [];
                    declarations.map(function (v) {
                        path.node.params.push(v.id);
                        //把函数内的Identifier提升到FunctionExpresssion的参数params中去
                        v.init && statements.push(t.assignmentExpression('=', v.id, v.init));
                        //如果存在init属性,则利用assignmentExpression方法构造出一个赋值表达式
                    });
                    p.replaceInline(statements);
                    //替换声明语句
                }
            });
        }
    });
    
    let code = generator(ast).code;
    fs.writeFile('./demoNew.js', code, (err)=>{
     console.log("写入成功")
    });

    遍历FunctionExpression节点,取出 blockStatement.body节点,里面存放的就是函数体中所有的语句。接着遍历当前函数下,所有的VariableDeclaration节点,其中的declarations是数组,即声明的具体变量。

    接着通过map方法遍历整个数组,通过path.node.params.push(v.id)将变量提取到函数的参数列表中。

    执行这个函数来处理demo.js之后,我们得到了:

    demoNew.js

    javascript
    String.prototype.format = function formatStr(str, div, formatString) {
    	str = str.replace(/^\s+|\s+$/g, "");
    	str = str.replace(/\s+/g, "");
    	str = str.replace(/^\s/, "");
    	str = str.replace(/(\s$)/g, "");
    	div = document.createElement("div");
    	div.textContent = str;
    	formatString = div.innerHTML;
    	return formatString;
    };
    
    console.log(new String().format("    dsadas&&   "));

    可以发现生成的函数可以满足我们的要求,我们将生成的函数进行控制流扁平化后得到

    demoNew.js

    javascript
    String.prototype.format = function formatStr(str, div, formatString) {
    	let _array = "4|0|2|1|6|7|3|5".split("|"),
    		_index = 0;
    
    	while (!![]) {
    		switch (+_array[_index++]) {
    			case 0:
    				str = str.replace(/\s+/g, "");
    				continue;
    
    			case 1:
    				str = str.replace(/(\s$)/g, "");
    				continue;
    
    			case 2:
    				str = str.replace(/^\s/, "");
    				continue;
    
    			case 3:
    				formatString = div.innerHTML;
    				continue;
    
    			case 4:
    				str = str.replace(/^\s+|\s+$/g, "");
    				continue;
    
    			case 5:
    				return formatString;
    				continue;
    
    			case 6:
    				div = document.createElement("div");
    				continue;
    
    			case 7:
    				div.textContent = str;
    				continue;
    		}
    
    		break;
    	}
    };
    
    console.log(new String().format("    dsadas&&   "));
  2. 为了看的更清晰,代码没有使用其他混淆方案。但在实际使用中,必须和之前介绍的其他混淆方案配合使用,才能加强混淆效果。比如:case 后面跟的值可以用数值常量加密(在 JavaScript 中 case 后面的值可以跟一个表达式,而不需要数值或字符),分发器中的字符串可以用字符串加密,然后一起提取到大数组中。对于原代码部分,可以生成花指令、逐行加密等各种操作。

  3. switch 中的表达式为+_array[_index++],最前面的加号代表强转成数值类型,因为分发器中是字符串类型。虽然 JS 是一门弱类型的语言,解释器会在适当的时机完成类型的自动转换,但是在 case 中是===的匹配,不会自动转换类型

2.实现逗号表达式混淆

与控制流扁平化相同的是,逗号表达式同样也需要参数提升,不过需要再增加一次判断,即:函数体内的语句是否少于 2 句,如果少于则不进行任何处理。因为逗号表达式只能在多个句子之间加逗号混淆,而不能在单个句子内混淆。

JavaScript
let blockStatement = path.node.body;let blockStatementLength = blockStatement.body.length;if (blockStatementLength < 2) return;

接下来我们处理逗号表达式的部分:

  1. 取出函数体中的第一条语句,并且定义一个计数器i,初始值为 1
  2. 对函数体的其他语句进行循环,取出来的语句先赋值给tempts,然后判断是否为Expressionstatement节点。
  3. 如果是,就取出expression属性赋值给secondSta,否则直接赋值给secondSta
  4. 把这两个语句改为逗号表达式,使用toSequenceExpression来完成。当然对于不同的语句,需要使用不同的处理方案。之前介绍过逗号表达式混淆的很多种形式。本小节不全部处理,只处理其中的赋值语句,函数调用语句和返回语句。
JavaScript
//处理返回语句if (t.isReturnStatement(secondSta)) {    firstSta = t.returnStatement(        t.toSequenceExpression([firstSta, secondSta.argument]));//处理赋值语句} else if (t.isAssignmentExpression(secondSta)) {        secondSta.right = t.toSequenceExpression([firstSta, secondSta.right]);        firstSta = secondSta;} else {    firstSta = t.toSequenceExpression([firstSta, secondSta]);}

上述代码中的赋值语句处理方法为:先判断语句如果是赋值表达式,就取出其中的right节点,与firstSta组成逗号表达式后,再替换原有的right节点,最后把secondSta赋值给firstSta进行后续的语句处理。

再来看返回语句,取出其中的argument节点,与firstSta组成逗号表达式后,重新生成一个returnStatepent节点。

如果既不是返回语句也不是赋值语句,那么就直接firstStasecondSta组成逗号表达式,也就是最没有混淆力度的组成方式。

自动逗号表达式.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

const jscode = fs.readFileSync("./demo.js", {
        encoding: "utf-8"
    });
let ast = parser.parse(jscode);

traverse(ast, {
    FunctionExpression(path) {
        let blockStatement = path.node.body;
        let blockStatementLength = blockStatement.body.length;
        if (blockStatementLength < 2) return;
        //把所有声明的变量提取到参数列表中
        path.traverse({
            VariableDeclaration(p) {
                declarations = p.node.declarations;
                let statements = [];
                declarations.map(function (v) {
                    path.node.params.push(v.id);
                    v.init && statements.push(t.assignmentExpression('=', v.id, v.init));
                });
                p.replaceInline(statements);
            }
        });
        //处理赋值语句 返回语句 函数调用语句
        let firstSta = blockStatement.body[0],
        i = 1;
        while (i < blockStatementLength) {
            let tempSta = blockStatement.body[i++];
            t.isExpressionStatement(tempSta) ?
            secondSta = tempSta.expression : secondSta = tempSta;
            //处理返回语句
            if (t.isReturnStatement(secondSta)) {
                firstSta = t.returnStatement(
                    t.toSequenceExpression([firstSta, secondSta.argument]));
                //处理赋值语句
            } else if (t.isAssignmentExpression(secondSta)) {
                if (t.isCallExpression(secondSta.right)) {
                    let callee = secondSta.right.callee;
                    callee.object = t.toSequenceExpression([firstSta, callee.object]);
                    firstSta = secondSta;
                } else {
                    secondSta.right = t.toSequenceExpression([firstSta, secondSta.right]);
                    firstSta = secondSta;
                }
            } else {
                firstSta = t.toSequenceExpression([firstSta, secondSta]);
            }
        }
        path.get('body').replaceWith(t.blockStatement([firstSta]));
    }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err) => {});

由于逗号表达式的灵活性比较差,要考虑的情况比较多,很难写出一个普适性很强的通用混淆器,大部分需要通过修改混淆器甚至代码本身来消除错误。比如这条 demo.js

JavaScript
String.prototype.format = function formatStr(str) {
  str = str.replace(/^\s+|\s+$/g, '');//Base64Encrypt
  str = str.replace(/\s+/g, '');//ASCIIEncrypt
  str = str.replace( /^\s/, '');//Base64Encrypt
  str = str.replace(/(\s$)/g, '');//ASCIIEncrypt
  str = str.replace(3213321 + 54345, 4324234 + 3123123);
  let div = document.createElement('div');
  div.textContent = str;
  let formatString = div.innerHTML;
  return formatString;
}
console.log(new String().format('    dsadas&&   '))

混淆后为

demoNew.js

javascript
String.prototype.format = function formatStr(str, div, formatString) {
	return (
		(formatString =
			((div.textContent =
				((div = ((str = ((str = ((str = ((str = ((str = str.replace(
					/^\s+|\s+$/g,
					""
				)),
				str).replace(/\s+/g, "")),
				str).replace(/^\s/, "")),
				str).replace(/(\s$)/g, "")),
				str).replace(3213321 + 54345, 4324234 + 3123123)),
				document).createElement("div")),
				str)),
			div.innerHTML)),
		formatString
	);
};

console.log(new String().format("    dsadas&&   "));

这条代码复制进浏览器无法正常运行,会报Uncaught TypeError: Cannot set property 'textContent' of undefined错误。其根本原因在于:在原本的代码中代码是一行一行按顺序执行的。定义div变量的语句为let div = document.createElement('div');,也就是将div定义为一个 DOM 元素,然后在下一句中使用了 dom 对象的属性div.textCoontent。也就是说,如果div变量没有被先定义为 DOM 对象,那么就不能使用textContent属性从而导致未定义错误。所以我们需要使用{}来圈定一个作用域的方式让定义div为 dom 对象的语句永远在引用div.textContent之前,并且因为混淆函数内存在参数提升函数,所以圈定作用域并不会使div本身未定义。修改后的demo.js如下:

demo.js

JavaScript
String.prototype.format = function formatStr(str) {  str = str.replace(/^\s+|\s+$/g, '');  str = str.replace(/\s+/g, '');  str = str.replace( /^\s/, '');  str = str.replace(/(\s$)/g, '');  {let div = document.createElement('div');  div.textContent = str;}  let formatString = div.innerHTML;  return formatString;}console.log(new String().format('    dsadas&&   '))//node环境会报错,请复制进浏览器console下运行//输出结果 dsadas&&

然后我们将这个demo.js进行逗号表达式混淆,得到了:

demoNew.js

JavaScript
String.prototype.format = function formatStr(str, div, formatString) {
  return formatString = ((str = (str = (str = (str = str.replace(/^\s+|\s+$/g, ''), str).replace(/\s+/g, ''), str).replace(/^\s/, ''), str).replace(/(\s$)/g, ''), (div = document.createElement('div'), div.textContent = str)), div.innerHTML), formatString;
};

console.log(new String().format('    dsadas&&   ')); //node环境会报错,请复制进浏览器console下运行
//输出结果 dsadas&&

这样这条代码就可以正常输出了

3.执行混淆方案

经过一上十个小节的介绍,我们已经有了一套比较完善的自动化混淆方案,接下来我们将各个函数封装一下,组合成一个混淆器

自动化混淆.js

JavaScript
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require('fs');

// 把混淆方案的相关实现方法封装成类
function ConfoundUtils(ast, encryptFunc){
 this.ast = ast;
 this.bigArr = [];
 // 接收传进来的函数,用于字符串加密
 this.encryptFunc = encryptFunc;
}
// 改变对象属性访问方式 console.log 改为 console["log"]
ConfoundUtils.prototype.changeAccessMode = function (){
 traverse(this.ast, {
  MemberExpression(path){
   if(t.isIdentifier(path.node.property)){
    let name = path.node.property.name;
    path.node.property = t.stringLiteral(name);
   }
   path.node.computed = true;
  },
 });
}
// 标准内置对象的处理
ConfoundUtils.prototype.changeBuiltinObjects = function (){
 traverse(this.ast, {
  Identifier(path){
   let name = path.node.name;
 if('Infinity|NaN|undefined|globalThis|uneval|isFinite|isNaN|parseFloat|parseInt|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|Symbol|Object|Function|Boolean|Error|AggregateError|EvalError|InternalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|Number|Math|Date|BigInt|String|RegExp|parseInt|encodeURIComponent||Array|Int8Array|Unit8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|BigInt64Array|BigUint64Array|Map|Set|WeakMap|WeakSet|ArrayBuffer|SharedArrayBuffer|Atomics|DataView|JSON|Promise|Generator|GeneratorFunction|AsyncFunction|Reflect|Proxy|Intl|Intl.Collator|Intl.DateTimeFormat|Intl.ListFormat|Intl.NumberFormat|Intl.PluralRules|Intl.RelativeTimeFormat|Intl.Locale|WebAssembly|arguments'.indexOf(name) != -1){
    path.replaceWith(t.memberExpression(t.identifier('window'),t.stringLiteral(name), true));
   }
  }
 });
}
//数值常量加密
ConfoundUtils.prototype.numericEncrypt = function (){
 traverse(this.ast, {
  NumericLiteral(path){
   let value = path.node.value;
   let key = parseInt(Math.random() * (89532165 - 32682338) + (12384635 + 20297703) , 10);
   let cipherNum = value ^ key;
   path.replaceWith(t.binaryExpression('^',t.numericLiteral(cipherNum),t.numericLiteral(key)));
   path.skip();
  }
 });
}
// 字符串加密与数组混淆
ConfoundUtils.prototype.arrayConfound = function (){
 let bigArr = [];
 let encryptFunc = this.encryptFunc;
 traverse(this.ast, {
  StringLiteral(path){
   let bigArrIndex = bigArr.indexOf(encryptFunc(path.node.value));
   let index = bigArrIndex;
   if(bigArrIndex == -1){
    let length = bigArr.push(encryptFunc(path.node.value));
    index = length -1;
   }
   let encStr = t.callExpression(
    t.identifier('atob'),
    [t.memberExpression(t.identifier('arr'),t.numericLiteral(index), true)]);
   path.replaceWith(encStr);
  }
 });
 bigArr = bigArr.map(function(v){
  return t.stringLiteral(v);
 });
 this.bigArr = bigArr;
}
// 数组乱序
ConfoundUtils.prototype.arrayShuffle = function (){
 (function(myArr,num){
  for (let i = 0; i < myArr.length; i++) {
   let temp = myArr[i];
   myArr[i] = myArr[myArr.length - 3];
   myArr[myArr.length - 3] = temp;
  }
  var shuffer = function(nums){
   while(--nums){
    myArr.unshift(myArr.pop());
   }
  };
  shuffer(++num);
 }(this.bigArr, 0x20));
}
// 二项式转函数花指令
ConfoundUtils.prototype.binaryToFunc = function (){
 traverse(this.ast, {
  BinaryExpression(path){
   let operator = path.node.operator;
   let left = path.node.left;
   let right = path.node.right;
   let a = t.identifier('a');
   let b = t.identifier('b');
   let funcNameIdentifier = path.scope.generateUidIdentifier('ooOoOo');
   let func = t.functionDeclaration(
    funcNameIdentifier,
    [a, b],
    t.blockStatement([t.returnStatement(
     t.binaryExpression(operator, a, b)
     )]));
   let BlockStatement = path.findParent(
function(p){return p.isBlockStatement()});
   BlockStatement.node.body.unshift(func);
   path.replaceWith(t.callExpression(
funcNameIdentifier, [left, right]));
  }
 });
}
// 十六进制字符串
ConfoundUtils.prototype.stringToHex = function (){
 function hexEnc(code) {
  for (var hexStr = [], i = 0, s; i < code.length; i++) {
   s = code.charCodeAt(i).toString(16);
   hexStr += "\\x" + s;
  }
  return hexStr
 }
 traverse(this.ast, {
  MemberExpression(path){
   if(t.isIdentifier(path.node.property)){
    let name = path.node.property.name;
    if (name !== "i"){
     path.node.property = t.stringLiteral(hexEnc(name));
     }
   }
   path.node.computed = true;
  }
 });
}
// 标识符混淆
ConfoundUtils.prototype.renameIdentifier = function (){
 // 标识符混淆之前先转成代码再解析,才能确保新生成的一些节点也被解析到
 let code = generator(this.ast,{
  retainLines: false,
  comments: false,
  compact: false
 // 为了方便观察效果,这里不进行代码压缩,实际运用中请将compact改成true
 }).code;
 let newAst = parser.parse(code);
 // 生成标识符
 function generatorIdentifier(decNum){
  let arr = ['O', 'o', '0'];
  let retval = [];
  while(decNum > 0){
   retval.push(decNum % 3);
   decNum = parseInt(decNum / 3);
  }
  let Identifier = retval.reverse().map(function(v){
   return arr[v]
  }).join('');
  Identifier.length < 6 ? (Identifier = ('ooOoOo' + Identifier).substr(-6)):
  Identifier[0] == '0' && (Identifier = 'O' + Identifier);
  return Identifier;
 }
 function renameOwnBinding(path){
  let OwnBindingObj = {}, globalBindingObj = {}, i = 0;
  path.traverse({
   Identifier(p){
    let name = p.node.name;
    let binding = p.scope.getOwnBinding(name);
    binding && generator(binding.scope.block).code == path + '' ?
    (OwnBindingObj[name] = binding) : (globalBindingObj[name] = 1);
   }
  });
  for(let oldName in OwnBindingObj){
   do{
    var newName = generatorIdentifier(i++);
   }while(globalBindingObj[newName]);
   OwnBindingObj[oldName].scope.rename(oldName, newName);
  }
 }
 traverse(newAst, {
  'Program|FunctionExpression|FunctionDeclaration'(path) {
   renameOwnBinding(path);
  }
 });
 this.ast = newAst;
}
// 指定代码行加密
ConfoundUtils.prototype.appointedCodeLineEncrypt = function (){
 traverse(this.ast, {
  FunctionExpression(path){
   let blockStatement = path.node.body;
   let Statements = blockStatement.body.map(function(v){
    if(t.isReturnStatement(v)) return v;
    if(!(v.trailingComments && v.trailingComments[0].value == 'Base64Encrypt')) return v;
    delete v.trailingComments;
    let code = generator(v).code;
    let cipherText = base64Encode(code);
    let decryptFunc = t.callExpression(t.identifier('atob'),[t.stringLiteral(cipherText)]);
    return t.expressionStatement(
t.callExpression(t.identifier('eval'), [decryptFunc]));
   });
   path.get('body').replaceWith(t.blockStatement(Statements));
  }
 });
}
// 指定代码行ASCII码混淆
ConfoundUtils.prototype.appointedCodeLineAscii = function (){
 traverse(this.ast, {
  FunctionExpression(path){
   let blockStatement = path.node.body;
   let Statements = blockStatement.body.map(function(v){
    if(t.isReturnStatement(v)) return v;
    if(!(v.trailingComments && v.trailingComments[0].value == 'ASCIIEncrypt')) return v;
    delete v.trailingComments;
    let code = generator(v).code;
    let codeAscii = [].map.call(code, function(v){
     return t.numericLiteral(v.charCodeAt(0));
    })
    let decryptFuncName = t.memberExpression(
t.identifier('String'), t.identifier('fromCharCode'));
    let decryptFunc = t.callExpression(decryptFuncName, codeAscii);
    return t.expressionStatement(
t.callExpression(t.identifier('eval'),[decryptFunc]));
   });
   path.get('body').replaceWith(t.blockStatement(Statements));
  }
 });
}

// switch控制流扁平化
ConfoundUtils.prototype.switchConfound = function (){
 traverse(this.ast, {
  FunctionExpression(path){
   let blockStatement = path.node.body;
   // 逐行提取语句,按原先的语句顺序建立索引,包装成对象
   let Statements = blockStatement.body.map(function(v, i){
    return {index: i, value: v};
   });
   // 洗牌,打乱语句顺序
   let i = Statements.length;
   while(i){
    let j = Math.floor(Math.random() * i--);
    [Statements[j], Statements[i]] = [Statements[i], Statements[j]];
   }
   // 构建分发器,创建switchCase数组
   let dispenserArr = [];
   let cases = [];
   Statements.map(function(v, i){
    dispenserArr[v.index] = i;
    let switchCase = t.switchCase(t.numericLiteral(i),  [v.value, t.continueStatement()]);
    cases.push(switchCase);
   });
   let dispenserStr = dispenserArr.join('|');
    // 生成_array和_index标识符,利用Babel的API保证不重名
   let array = path.scope.generateUidIdentifier('array');
   let index = path.scope.generateUidIdentifier('index');
   // 生成 '...'.split 这是一个成员表达式
   let callee = t.memberExpression(t.stringLiteral(dispenserStr),  t.identifier('split'));
   // 生成 split('|')
   let arrayInit = t.callExpression(callee, [t.stringLiteral('|')]);
   // _array和_index放入声明语句中,_index初始化为0
   let varArray = t.variableDeclarator(array, arrayInit);
   let varIndex = t.variableDeclarator(index, t.numericLiteral(0));
   let dispenser = t.variableDeclaration('let',[varArray, varIndex]);
   // 生成switch中的表达式 +_array[_index++]
   let updExp = t.updateExpression('++', index);
   let memExp = t.memberExpression(array, updExp, true);
   let discriminant = t.unaryExpression("+", memExp);
   // 构建整个switch语句
   let switchSta = t.switchStatement(discriminant, cases);
   // 生成while循环中的条件 !![]
   let unaExp = t.unaryExpression("!", t.arrayExpression());
   unaExp = t.unaryExpression("!", unaExp);
   // 生成整个while循环
   let whileSta = t.whileStatement(unaExp,  t.blockStatement([switchSta, t.breakStatement()]));
   // 用分发器和while循环来构建blockStatement,替换原有节点
   path.get('body').replaceWith(t.blockStatement([dispenser, whileSta]));

  }
 });
}

// 逗号表达式
ConfoundUtils.prototype.commaConfound = function (){
 traverse(this.ast, {
  FunctionExpression(path) {
   let blockStatement = path.node.body;
   let blockStatementLength = blockStatement.body.length;
   if (blockStatementLength < 2) return;
   // 把所有声明的变量提取到参数列表中
   path.traverse({
    VariableDeclaration(p) {
     declarations = p.node.declarations;
     let statements = [];
     declarations.map(function (v) {
      path.node.params.push(v.id);
      v.init && statements.push(t.assignmentExpression('=', v.id, v.init));
     });
     p.replaceInline(statements);
    }
   });
   // 处理赋值语句 返回语句 函数调用语句
   let firstSta = blockStatement.body[0],
   i = 1;
   while (i < blockStatementLength) {
    let tempSta = blockStatement.body[i++];
    t.isExpressionStatement(tempSta) ?
    secondSta = tempSta.expression : secondSta = tempSta;
    // 处理返回语句
    if (t.isReturnStatement(secondSta)) {
     firstSta = t.returnStatement(
      t.toSequenceExpression([firstSta, secondSta.argument]));
     // 处理赋值语句
    } else if (t.isAssignmentExpression(secondSta)) {
     if (t.isCallExpression(secondSta.right)) {
      let callee = secondSta.right.callee;
      callee.object = t.toSequenceExpression([firstSta, callee.object]);
      firstSta = secondSta;
     } else {
      secondSta.right = t.toSequenceExpression([firstSta, secondSta.right]);
      firstSta = secondSta;
     }
    } else {
     firstSta = t.toSequenceExpression([firstSta, secondSta]);
    }
   }
   path.get('body').replaceWith(t.blockStatement([firstSta]));
  }
 });
}

// 构建数组声明语句,加入到ast最前面
ConfoundUtils.prototype.unshiftArrayDeclaration = function(){
 this.bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(this.bigArr));
 this.bigArr = t.variableDeclaration('var', [this.bigArr]);
 this.ast.program.body.unshift(this.bigArr);
}
// 拼接两个ast的body部分
ConfoundUtils.prototype.astConcatUnshift = function(ast){
 this.ast.program.body.unshift(ast);
}
ConfoundUtils.prototype.getAst = function(){
 return this.ast;
}
// Base64编码
function base64Encode(e) {
 var r, a, c, h, o, t, base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
 for (c = e.length, a = 0, r = ''; a < c;) {
  if (h = 255 & e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4),
   r += '==';
   break
  }
  if (o = e.charCodeAt(a++), a == c) {
   r += base64EncodeChars.charAt(h >> 2),
   r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
   r += base64EncodeChars.charAt((15 & o) << 2),
   r += '=';
   break
  }
  t = e.charCodeAt(a++),
  r += base64EncodeChars.charAt(h >> 2),
  r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
  r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
  r += base64EncodeChars.charAt(63 & t)
 }
 return r
}

function main(){
 // 读取要混淆的代码
 const jscode = fs.readFileSync("./demo.js", {
  encoding: "utf-8"
 });
 // 读取还原数组乱序的代码
 const jscodeFront = fs.readFileSync("./还原数组.js", {
  encoding: "utf-8"
 });
 // 把要混淆的代码解析成ast
 let ast_old = parser.parse(jscode);
 // 参数提升
 function raiseParams(){
  traverse(ast_old, {
   FunctionExpression(path) {
    // 把所有声明的变量提取到参数列表中
    path.traverse({
     VariableDeclaration(p) {
      // 遍历函数中所有的声明语句
      declarations = p.node.declarations;
      // 取出声明语句中的declarations属性
      let statements = [];
      declarations.map(function (v) {
       path.node.params.push(v.id);
       // 把函数内的Identifier提升到FunctionExpresssion的参数params中去
       v.init && statements.push(t.assignmentExpression('=', v.id, v.init));
       // 如果存在init属性,则利用assignmentExpression方法构造出一个赋值表达式
      });
      p.replaceInline(statements);
      // 替换声明语句
     }
    });
   }
  });
  let jscodeNew = generator(ast_old).code;
  let ast = parser.parse(jscodeNew);
  return ast
 }
 // 参数提升后的代码解析成ast
 let ast = new raiseParams();
 // 把还原数组乱序的代码解析成astFront
 let astFront = parser.parse(jscodeFront);
 // 初始化类,传递自定义的加密函数进去
 let confoundAst = new ConfoundUtils(ast, base64Encode);
 let confoundAstFront = new ConfoundUtils(astFront);

 // 改变对象属性访问方式
 confoundAst.changeAccessMode();

 // 标准内置对象的处理
 confoundAst.changeBuiltinObjects();

 // 二项式转函数花指令
 confoundAst.binaryToFunc();

 // 逗号表达式
 confoundAst.commaConfound();

 // switch控制流扁平化
 confoundAst.switchConfound();

 // 字符串加密与数组混淆
 confoundAst.arrayConfound();

 // 数组乱序
 confoundAst.arrayShuffle();

 // 还原数组顺序代码,改变对象属性访问方式,对其中的字符串进行十六进制编码
 confoundAstFront.stringToHex();
 astFront = confoundAstFront.getAst();

 // 先把还原数组顺序的代码,加入到被混淆代码的ast中
 confoundAst.astConcatUnshift(astFront.program.body[0]);

 // 再生成数组声明语句,并加入到被混淆代码的最开始处
 confoundAst.unshiftArrayDeclaration();

 // 标识符重命名
 confoundAst.renameIdentifier();

 // 指定代码行的混淆,需要放到标识符混淆之后
 confoundAst.appointedCodeLineEncrypt();
 confoundAst.appointedCodeLineAscii();

 // 数值常量混淆
 confoundAst.numericEncrypt();
 ast = confoundAst.getAst();

 // ast转为代码
 code = generator(ast).code;
 // 混淆的代码中,如果有十六进制字符串加密,ast转成代码以后会有多余的转义字符,需要替换掉
 code = code.replace(/\\\\x/g, '\\x');

 fs.writeFile('./demoNew.js', code, (err)=>{
  console.log("写入成功");
 });
}
main();

demo.js

JavaScript
String.prototype.format = function formatStr(str) {
  str = str.replace(/^\s+|\s+$/g, '');//Base64Encrypt
  str = str.replace(/\s+/g, '');//ASCIIEncrypt
  str = str.replace( /^\s/, '');//Base64Encrypt
  str = str.replace(/(\s$)/g, '');//ASCIIEncrypt
  {let div = document.createElement('div');
  div.textContent = str;}
  let formatString = div.innerHTML;
  return formatString;
}
console.log(new String().format('    dsadas&&   '))
//node环境会报错,请复制进浏览器console下运行
//输出结果 dsadas&&

由于控制流扁平化和逗号表达式混淆同时使用会出现控制流扁平化只有一条switch-case语句失去混淆意义的尴尬情况(因为所有过程语句都被逗号表达式压缩成一行了),因此我们决定分别使用两种方式进行混淆来查看效果。

开启控制流扁平化后的效果为

demoNew.js(为了方便看效果,这里没有开启代码压缩)

JavaScript
var ooOoOo = ["", "aW5uZXJIVE1M", "Y3JlYXRlRWxlbWVudA==", "ICAgIGRzYWRhcyYmICAg", "ZGl2", "bG9n", "dGV4dENvbnRlbnQ=", "U3RyaW5n", "cHJvdG90eXBl", "Zm9ybWF0", "M3w1fDF8MHw0fDJ8Ng==", "fA==", "cmVwbGFjZQ=="];

(function (ooOoOo, oOoOoo) {
  var oOoOo0 = function (oOoOoo) {
    while (--oOoOoo) {
      ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
    }
  };

  oOoOo0(++oOoOoo);

  for (let i = ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (59402980 ^ 59402981); i > -(52299754 ^ 52299755); i--) {
    let temp = ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (50275762 ^ 50275761)];
    ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (47430260 ^ 47430263)] = ooOoOo[i];
    ooOoOo[i] = temp;
  }
})(ooOoOo, 61699149 ^ 61699181);

window[atob(ooOoOo[80810375 ^ 80810375])][atob(ooOoOo[75697862 ^ 75697863])][atob(ooOoOo[39415340 ^ 39415342])] = function oOoOoo(oOoOo0, OoOooO, OoOooo) {
  let OoOoo0 = atob(ooOoOo[39151812 ^ 39151815]).split(atob(ooOoOo[73124854 ^ 73124850])),
      OoOo0O = 33613174 ^ 33613174;

  while (!![]) {
    switch (+OoOoo0[OoOo0O++]) {
      case 34562059 ^ 34562059:
        oOoOo0 = oOoOo0[atob(ooOoOo[67435748 ^ 67435745])](/(\s$)/g, atob(ooOoOo[33640669 ^ 33640667]));
        continue;

      case 60148266 ^ 60148267:
        oOoOo0 = oOoOo0[atob(ooOoOo[68367550 ^ 68367547])](/^\s/, atob(ooOoOo[87211524 ^ 87211522]));
        continue;

      case 82831623 ^ 82831621:
        OoOooo = OoOooO[atob(ooOoOo[61385079 ^ 61385072])];
        continue;

      case 60412097 ^ 60412098:
        oOoOo0 = oOoOo0[atob(ooOoOo[56086760 ^ 56086765])](/^\s+|\s+$/g, atob(ooOoOo[66969508 ^ 66969506]));
        continue;

      case 61063012 ^ 61063008:
        {
          OoOooO = document[atob(ooOoOo[89487680 ^ 89487688])](atob(ooOoOo[76668650 ^ 76668643]));
          OoOooO[atob(ooOoOo[76704468 ^ 76704478])] = oOoOo0;
        }
        continue;

      case 64250030 ^ 64250027:
        oOoOo0 = oOoOo0[atob(ooOoOo[37136706 ^ 37136711])](/\s+/g, atob(ooOoOo[37704412 ^ 37704410]));
        continue;

      case 49672016 ^ 49672022:
        return OoOooo;
        continue;
    }

    break;
  }
};

console[atob(ooOoOo[86213380 ^ 86213391])](new window[atob(ooOoOo[66738894 ^ 66738894])]()[atob(ooOoOo[55285739 ^ 55285737])](atob(ooOoOo[78431480 ^ 78431476])));

接下来再看看开启逗号表达式的情况:

demoNew.js

JavaScript
var ooOoOo = ["U3RyaW5n", "cHJvdG90eXBl", "Zm9ybWF0", "cmVwbGFjZQ==", "", "Y3JlYXRlRWxlbWVudA==", "ZGl2", "ICAgIGRzYWRhcyYmICAg", "dGV4dENvbnRlbnQ=", "bG9n", "aW5uZXJIVE1M"];

(function (ooOoOo, oOoOoo) {
  var oOoOo0 = function (oOoOoo) {
    while (--oOoOoo) {
      ooOoOo["\x70\x75\x73\x68"](ooOoOo["\x73\x68\x69\x66\x74"]());
    }
  };

  oOoOo0(++oOoOoo);

  for (let i = ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (43298376 ^ 43298377); i > -(80062469 ^ 80062468); i--) {
    let temp = ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (79333936 ^ 79333939)];
    ooOoOo[ooOoOo["\x6c\x65\x6e\x67\x74\x68"] - (83308385 ^ 83308386)] = ooOoOo[i];
    ooOoOo[i] = temp;
  }
})(ooOoOo, 67321212 ^ 67321180);

window[atob(ooOoOo[51515359 ^ 51515359])][atob(ooOoOo[57699210 ^ 57699211])][atob(ooOoOo[44029707 ^ 44029705])] = function oOoOoo(oOoOo0, OoOooO, OoOooo) {
  return OoOooo = ((oOoOo0 = (oOoOo0 = (oOoOo0 = (oOoOo0 = oOoOo0[atob(ooOoOo[49854945 ^ 49854946])](/^\s+|\s+$/g, atob(ooOoOo[88155858 ^ 88155862])), oOoOo0)[atob(ooOoOo[82351757 ^ 82351758])](/\s+/g, atob(ooOoOo[88572106 ^ 88572110])), oOoOo0)[atob(ooOoOo[85595567 ^ 85595564])](/^\s/, atob(ooOoOo[37901974 ^ 37901970])), oOoOo0)[atob(ooOoOo[78806929 ^ 78806930])](/(\s$)/g, atob(ooOoOo[64252598 ^ 64252594])), (OoOooO = document[atob(ooOoOo[78992669 ^ 78992664])](atob(ooOoOo[79041619 ^ 79041621])), OoOooO[atob(ooOoOo[77590482 ^ 77590485])] = oOoOo0)), OoOooO[atob(ooOoOo[61402301 ^ 61402293])]), OoOooo;
};

console[atob(ooOoOo[40386870 ^ 40386879])](new window[atob(ooOoOo[53324194 ^ 53324194])]()[atob(ooOoOo[39399253 ^ 39399255])](atob(ooOoOo[52836794 ^ 52836784])));

AST 还原 JavaScript 实战(解析 JavaScript 混淆加密过的代码)

常用还原方案

还原数值常量加密

还原代码加密和 ASCII 码混淆

还原 unicode 与十六进制字符串

还原逗号表达式混滑

分析网站使用的混淆手段

1.协议分析

AST 只是用来处理 JS 代码,所以对于协议的抓包以及对应加密参数的分析,还是必须要做的。某菠菜网,在输入用户名和密码后,鼠标左键单击登录,会跳出一张验证码,如下图所示。随便点击两个按钮,就会去验证是否正确。本章使用 Chrome 浏览器自带的开发者工具抓包,去分析一下抓到的数据包。

某网站验证码示例

这里只分析关键的数据包,获取验证码的请求为/yzmtest/get.php?t=1596249640810,这是一个 POST 请求,提交的数据为 clientid=g103ushm0yw&username=e511111111,其中 clientid 是由 JS 生成的客户端 id,username 是自己输入的账号,只是前面多了两个字符 e5,这两个参数不是本章的重点。

该请求发送以后,服务端返回的响应是一个 json,如下所示。其中 data 字段就是验证码图片经过 base64 编码之后的数据,很多网站中都喜欢这么做。

怎么确认是否是需要的验证码图片呢? data:image\/jpeg;base64,后面的数据才是图片的数据,复制出来 Base64 解码一下就知道了。input 字段很明显就是验证码的备选项,可以看出该选项的位置分布是随机的。

接着去看一下验证结果是否正确的请求/yzmtest/check.php?t=1596249644221,这也是一个 POST 请求,提交的数据如下所示。其中 password 字段是明文,其他字段之前已经介绍过,而 data 字段就是选择的验证码备选项处理以后的结果。

bash
clientid=g103ushm0ywgdata=624964140912496413098211125&username-e511111116pas
sword=22222222

data 字段的值的来源无非两种途径,一种是服务器返回,那么该值在抓到的所有数据包中肯定可以找到。另外一种就是由 JS 生成的。通过目前抓到的数据来分析,并没有找到 data 的值,因此猜测是由 JS 生成的。

Chrome 开发者工具中抓到的数据包,会把发送当前请求时的函数堆栈,放在 Initiator 这一栏中,把鼠标放上去就会显示。如下图所示,@的前面是函数名,后面表示源代码在哪个文件中。其中 jquery 是开源的第三方库,在算法分析中一般最后才考虑去看 jquery 文件。鼠标左键点击 lebo.yzm pc.min.js 这个文件,会跳到 Sources 面板,发现这个文件有 JS 混淆。代码较长,在本章的后续内容中只截取关键部分展示。这个时候,当然也可以直接通过 Chrome 开发者工具去动态调试混淆后的 JS 文件,来分析算法,俗称硬刚法。只不过比较耗时而已,本章肯定选择还原这个 JS 文件。

浏览器控制台抓到的包

下面是要还原的全部代码:

demo.js:要解析逆向还原的代码

js
var _0x5a19 = [
	"IiBjbGFzcz0iYnV0dG9uIHdoaXRlIj48Y2FudmFzIHdpZHRoPSI1MHB4IiBoZWlnaHQ9IjQwcHgiIHN0eWxlPSJ3aWR0aDo1MHB4O2hlaWdodDo0MHB4IiBpZD0iYnRuY2Fudl8=",
	"Ij48L2NhbnZhcz48L2Rpdj48L2Rpdj4=",
	"MnwzfDF8NHww",
	"L3l6bXRlc3QvY2hlY2sucGhwP3Q9",
	"cG9zdA==",
	"MTEyNQ==",
	"anNvbg==",
	"Ymx1ZQ==",
	"572R57uc6ZSZ6K+v77yM6K+35Yi35paw6aqM6K+B56CB",
	"ZGl2LmJsdWU=",
	"PGNhbnZhcyBpZD0ieXptX2ltZyIgc3R5bGU9IndpZHRoOjMwMHB4O2hlaWdodDoxODBweCIgd2lkdGg9IjMwMHB4IiBoZWlnaHQ9IjE4MHB4Ij48L2NhbnZhcz4=",
	"PGRpdiBjbGFzcz0ieXptX2J1dHRvbiI+",
	"Mnw0fDV8MHwxfDM=",
	"NXwyfDF8MHwzfDQ=",
	"IzAwMA==",
	"MTVweCBib2xk",
	"YnRuY2Fudl8=",
	"L3l6bXRlc3QvZ2V0LnBocD90PQ==",
	"Lnl6bWJvZHk=",
	"MXw0fDB8M3wy",
	"bG9hZGluZw==",
	"I3l6bV9yZWZyZXNo",
	"NXw0fDB8MnwxfDM=",
	"I3l6bV9jbG9zZQ==",
	"Y2xpY2s=",
	"TG9naW5Gb3Jt",
	"MnwxfDV8NnwwfDN8NA==",
	"aW5wdXRbbmFtZT0ndXNlcm5hbWUnXQ==",
	"I3VzZXJSZWdGb3Jt",
	"I2xlYm95em0=",
	"aW5wdXRbbmFtZT0ncGFzc3dkJ10=",
	"dXNlclJlZ0Zvcm0=",
	"b3NjcWo=",
	"c3BsaXQ=",
	"cHJvdG90eXBl",
	"aGlkZWxvYWRpbmc=",
	"JGRpdg==",
	"ZmluZA==",
	"ZU5remU=",
	"aGlkZQ==",
	"c2V0TGVu",
	"JGxlbg==",
	"ZmtVaEM=",
	"bHlLemg=",
	"UHJERXg=",
	"Ykl6SkE=",
	"UXhrbFo=",
	"ZXZsbEM=",
	"WGhJdWY=",
	"VkJLbXg=",
	"QWd2RWQ=",
	"alZ3Qkw=",
	"WFlvUlQ=",
	"ZE9tVXU=",
	"alhyTGQ=",
	"eUd6RFk=",
	"TXFveEg=",
	"bGtaYnY=",
	"TWNrWmY=",
	"cENWalM=",
	"TVBQZ1I=",
	"UE9Dd1I=",
	"d1ZhUkQ=",
	"VERnUFg=",
	"eFpybWE=",
	"Y0JDTHg=",
	"blJhcUI=",
	"V1pKRmI=",
	"VWhHcXQ=",
	"VW94aEk=",
	"UWpJVkk=",
	"VGRqS1U=",
	"THNqZWQ=",
	"dXNDQ1Q=",
	"QUZlelA=",
	"YUVCY2c=",
	"RGhWU3U=",
	"Rk9aUmw=",
	"WlBZZ2k=",
	"blRyaGQ=",
	"a3hkY0Q=",
	"Y1ZzdGQ=",
	"ZU1EelI=",
	"S2pOY28=",
	"V3JLU3k=",
	"Ykx3elo=",
	"YWFoTEU=",
	"ZWVRS3g=",
	"dmRldEI=",
	"VHNGdVo=",
	"SkRNSXY=",
	"bk5GdmE=",
	"c25YeEs=",
	"Qml4WVM=",
	"TVdQWmE=",
	"T2VLQ3I=",
	"bmJ1WGc=",
	"WkZtVEk=",
	"YU9KZVg=",
	"Wkp3WlY=",
	"c0ZpbUM=",
	"Y2tQSkg=",
	"WEZEU0I=",
	"cFhjbEw=",
	"Y1VTU2g=",
	"cm5DWGM=",
	"RURSREk=",
	"b1ZzbUs=",
	"SURuWVA=",
	"WExKb3o=",
	"eXJmVlo=",
	"VG9HS3M=",
	"ZGtwQmk=",
	"UXZSY1Q=",
	"eHNpVmc=",
	"Z2V0SWQ=",
	"QkJ2eU4=",
	"cm91bmQ=",
	"Y2RUd0E=",
	"cmFuZG9t",
	"dG9TdHJpbmc=",
	"Z2V0VGltZQ==",
	"c3Vic3Ry",
	"d0dsRmc=",
	"akRJTHg=",
	"JHN0cmxlbg==",
	"bGVuZ3Ro",
	"ZXJyb3I=",
	"QU9kcXU=",
	"YWRkQ2xhc3M=",
	"dUF0UE4=",
	"SnVHcmo=",
	"dGV4dA==",
	"c2hvdw==",
	"c2h1ZmZsZQ==",
	"Zmxvb3I=",
	"c3VjY2VlZA==",
	"RE1kZHg=",
	"RXl2Y0w=",
	"cmVtb3ZlQ2xhc3M=",
	"WGRXVkU=",
	"dUxEbHg=",
	"c2hvd2xvYWRpbmcy",
	"YnViTlM=",
	"c2V0c3RhdHVz",
	"JHN0YXR1cw==",
	"ZW1SR3U=",
	"cHVzaA==",
	"eWVYWXg=",
	"JGNsaWVudGlk",
	"a2ZxekM=",
	"aGRwUG0=",
	"bm93",
	"JGZvcm1PYmo=",
	"QllmZWg=",
	"RGtmUHk=",
	"TlFlSEc=",
	"JHVzZXJuYW1l",
	"UWFQbUg=",
	"UXBVUk8=",
	"RmRGWFc=",
	"JGhtZGF0YQ==",
	"am9pbg==",
	"JHBhc3N3b3Jk",
	"aE5UR1M=",
	"SnFheEM=",
	"ckljT0I=",
	"ZldScFo=",
	"cmVtb3Zl",
	"cnF4UHM=",
	"Y2xvc2U=",
	"bG9hZA==",
	"VVJVR3o=",
	"aGlkZWxvYWRpbmcy",
	"c2hvd2xvYWRpbmc=",
	"U3p2YXI=",
	"WGZ6dHI=",
	"T0pBU0Y=",
	"TWhsZWk=",
	"SUdCV20=",
	"ZkxEdU0=",
	"R2tzZ1Q=",
	"YWpheA==",
	"ZXRycWM=",
	"b2lGSWM=",
	"cXNpWU0=",
	"amxDUks=",
	"eE9kSkQ=",
	"ekJ2aUI=",
	"Z0p3RHI=",
	"cHhoTUE=",
	"aHRtbA==",
	"Z2V0RWxlbWVudEJ5SWQ=",
	"dXRrZ1M=",
	"aEpJbEg=",
	"bFZrVU4=",
	"Z2ZnVXI=",
	"ZWFjaA==",
	"cW5PWEs=",
	"YXR0cg==",
	"cmVwbGFjZQ==",
	"RVNrQWg=",
	"a0VDbkE=",
	"bkpaTHE=",
	"T05iZmM=",
	"WlBjeVU=",
	"amdCRHo=",
	"SVhDdEI=",
	"dmprRWE=",
	"V0NqWGI=",
	"V3hpTVY=",
	"WlNGSm8=",
	"T053aVQ=",
	"Y29kZQ==",
	"c2V0VGltZW91dA==",
	"c3VibWl0",
	"bXNn",
	"aGFzQ2xhc3M=",
	"RUZjcmM=",
	"WUtoWW0=",
	"ZUpQRUs=",
	"eHpwbVo=",
	"aHNxRHE=",
	"WktoeXY=",
	"VXJ1akk=",
	"TmRyWUU=",
	"WXlsUnQ=",
	"akxZWU8=",
	"Z0dvaU4=",
	"UFBoUlk=",
	"a2tsS2I=",
	"bUlDcEw=",
	"SVp5RGs=",
	"c2pLUWk=",
	"em5YZlM=",
	"dmtsTnI=",
	"R1l2UUY=",
	"c3Jj",
	"ZGF0YQ==",
	"ZWlpcUY=",
	"aW5wdXQ=",
	"bHR6V1o=",
	"ZmlsbFN0eWxl",
	"WnFEWFU=",
	"Zm9udA==",
	"YXNuTWQ=",
	"VW1qUEc=",
	"dHh0",
	"ZmlsbFRleHQ=",
	"WXZrZlc=",
	"R2pueFQ=",
	"Z2V0Q29udGV4dA==",
	"d0h3blY=",
	"WlJ1RmY=",
	"UkZsdnM=",
	"eGlIeHA=",
	"bUh0TWo=",
	"cnBUbm8=",
	"b3Z3bHQ=",
	"bW91c2Vtb3Zl",
	"b25sb2Fk",
	"ZHJhd0ltYWdl",
	"bGVu",
	"aEFEd2o=",
	"ZW1wdHk=",
	"dmVyaWZ5",
	"VVltZFo=",
	"SVhkY0M=",
	"dmFs",
	"aHhmbGY=",
	"cXh3R00=",
	"eXptYm94",
	"cHZQTXM=",
	"UFZ4U2o=",
	"T0F4TWc=",
	"dGxzUFA=",
	"U2VsUng=",
	"RWJjWmY=",
	"Z2V0c3RhdHVz",
	"Z3BzVlo=",
	"YXBwZW5k",
	"T3VySXY=",
	"YmluZA==",
	"Ykl1dGs=",
	"R3h3dHY=",
	"WHJvZ1k=",
	"V3JvUUE=",
	"bExFTlk=",
	"S1RYT1g=",
	"cGxYQms=",
	"dmdRYXo=",
	"bVRKT24=",
	"eXptcmVnYm94",
	"cmVhZHk=",
	"MXwyfDR8N3w1fDN8OHwwfDY=",
	"6K+36L6T5YWl55m75YWl5biQ5Y+3ISE=",
	"eHhAeEB4Lng=",
	"6K+36L6T5YWl5a+G56CBISE=",
	"5a+G56CB6ZW/5bqm5LiN6IO95bCR5LqONuS4quWtl+WFgyEh",
	"aW5wdXRbbmFtZT0ndmxjb2Rlcydd",
	"I0xvZ2luRm9ybSwjTG9naW5Gb3JtMg==",
	"Q3BuZVk=",
	"Z1ZsUmU=",
	"SXVMcm0=",
	"UXpGYnU=",
	"dW5iaW5k",
	"Tm1oUVU=",
	"R1pQZnY=",
	"ZWlHTVM=",
	"bmV5U2g=",
	"SVJJUHo=",
	"ZHp2eVQ=",
	"cXJUbVg=",
	"cWVFd0E=",
	"SmRsUEg=",
	"SWFQUkQ=",
	"ck9weEI=",
	"WWhEREw=",
	"UENPU24=",
	"cnhTQk0=",
	"cEt1Y0E=",
	"UEFmalE=",
	"SWFQc1A=",
	"cld3amk=",
	"Mnw5fDN8MTB8MTJ8MHwxNXw3fDZ8NHwxfDh8MTZ8NXwxMXwxN3wxM3wxNHwxOA==",
	"MTd8MTV8NHwxMHw1fDE2fDE0fDB8OHw5fDN8MTh8MTJ8MXwyfDd8MTN8NnwxMQ==",
	"CTwvZGl2Pg==",
	"CTxpIGlkPSJ5em1fY2xvc2UiIGNsYXNzPSJpY29uZm9udCBpY29uLWN1b3d1Z3VhbmJpLSIgc3R5bGU9ImZvbnQtc2l6ZToyNXB4Ij48L2k+PGkgaWQ9Inl6bV9yZWZyZXNoIiBjbGFzcz0iaWNvbmZvbnQgaWNvbi1zaHVheGluIj48L2k+",
	"PGRpdiBpZD0ieXptX2JveCIgY2xhc3M9Inl6bV9kaWFsb2ciPjxkaXYgY2xhc3M9Inl6bV9ib3giPg==",
	"CTxkaXYgY2xhc3M9Inl6bV9sb2FkaW5nIj48aSBjbGFzcz0iaWNvbmZvbnQgaWNvbi10dXBpYW4iPjwvaT48ZGl2PuWKoOi9veS4rTwvZGl2PjwvZGl2Pg==",
	"PC9kaXY+",
	"PGRpdiBjbGFzcz0ieXptX2JvdHRvbSI+",
	"PGRpdiBjbGFzcz0ieXptX2JvZHkiPg==",
	"CTxkaXYgY2xhc3M9Inl6bWJvZHkiPg==",
	"CTxkaXYgY2xhc3M9Inl6bV9sb2FkaW5nMiI+PGkgY2xhc3M9Imljb25mb250IGljb24tanViYW8iPjwvaT48ZGl2PumqjOivgeS4rSzor7fnqI3lkI48L2Rpdj48L2Rpdj4=",
	"Ym9keQ==",
	"I3l6bV9ib3g=",
	"PC9kaXY+PC9kaXY+PC9kaXY+",
	"Lnl6bV9sb2FkaW5n",
	"Lnl6bV9sb2FkaW5nMg==",
	"cmVk",
	"ZGl2",
	"6aqM6K+B5LitLOivt+eojeWQjg==",
	"MHwyfDR8MXwz",
	"6aqM6K+B5oiQ5Yqf77yM5q2j5Zyo6Lez6L2s",
	"aWNvbi1qdWJhbw==",
	"aWNvbi16aGVuZ3F1ZQ==",
	"NHwxNHwxOHwxMHwxNXw1fDZ8OHwxMnwxfDIwfDB8MTF8MTd8M3wyfDE2fDEzfDE5fDd8OQ==",
	"ZGl2Lnl6bWJvZHk=",
	"eXptX2ltZw==",
	"ZGl2LmJ1dHRvbg==",
	"b2JqZWN0Xw==",
	"PGRpdj48ZGl2IGlkPSJvYmplY3Rf",
];
(function (_0x5cb8ac, _0x23b38e) {
	var _0x524cab = function (_0x18d3aa) {
		while (--_0x18d3aa) {
			_0x5cb8ac["push"](_0x5cb8ac["shift"]());
		}
	};
	var _0x4821c6 = function () {
		var _0x398fd3 = {
			data: {
				key: "cookie",
				value: "timeout",
			},
			setCookie: function (_0x34eee4, _0x5586ea, _0x5ef311, _0x855d4e) {
				_0x855d4e = _0x855d4e || {};
				var _0x56baa5 = _0x5586ea + "=" + _0x5ef311;
				var _0x3da38b = 0x0;
				for (
					var _0x3da38b = 0x0, _0x1dc45b = _0x34eee4["length"];
					_0x3da38b < _0x1dc45b;
					_0x3da38b++
				) {
					var _0x3e8d20 = _0x34eee4[_0x3da38b];
					_0x56baa5 += ";\x20" + _0x3e8d20;
					var _0x20e7de = _0x34eee4[_0x3e8d20];
					_0x34eee4["push"](_0x20e7de);
					_0x1dc45b = _0x34eee4["length"];
					if (_0x20e7de !== !![]) {
						_0x56baa5 += "=" + _0x20e7de;
					}
				}
				_0x855d4e["cookie"] = _0x56baa5;
			},
			removeCookie: function () {
				return "dev";
			},
			getCookie: function (_0x296452, _0x39f9cd) {
				_0x296452 =
					_0x296452 ||
					function (_0x587f3d) {
						return _0x587f3d;
					};
				var _0x19aa2b = _0x296452(
					new RegExp(
						"(?:^|;\x20)" +
							_0x39f9cd["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
							"=([^;]*)"
					)
				);
				var _0x3d1943 = function (_0xe1f36d, _0x41a83b) {
					_0xe1f36d(++_0x41a83b);
				};
				_0x3d1943(_0x524cab, _0x23b38e);
				return _0x19aa2b ? decodeURIComponent(_0x19aa2b[0x1]) : undefined;
			},
		};
		var _0xef60f0 = function () {
			var _0x4ba6f5 = new RegExp(
				"\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
			);
			return _0x4ba6f5["test"](_0x398fd3["removeCookie"]["toString"]());
		};
		_0x398fd3["updateCookie"] = _0xef60f0;
		var _0x4f27d5 = "";
		var _0x4e6c47 = _0x398fd3["updateCookie"]();
		if (!_0x4e6c47) {
			_0x398fd3["setCookie"](["*"], "counter", 0x1);
		} else if (_0x4e6c47) {
			_0x4f27d5 = _0x398fd3["getCookie"](null, "counter");
		} else {
			_0x398fd3["removeCookie"]();
		}
	};
	_0x4821c6();
})(_0x5a19, 0x144);
var _0x2ba9 = function (_0x101b8f, _0xcd7c6f) {
	_0x101b8f = _0x101b8f - 0x0;
	var _0x27941a = _0x5a19[_0x101b8f];
	if (_0x2ba9["LmvHXr"] === undefined) {
		(function () {
			var _0x56f6ba;
			try {
				var _0x18f9d6 = Function(
					"return\x20(function()\x20" +
						"{}.constructor(\x22return\x20this\x22)(\x20)" +
						");"
				);
				_0x56f6ba = _0x18f9d6();
			} catch (_0x5e5baa) {
				_0x56f6ba = window;
			}
			var _0x12918c =
				"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
			_0x56f6ba["atob"] ||
				(_0x56f6ba["atob"] = function (_0xdc2fd0) {
					var _0x14148e = String(_0xdc2fd0)["replace"](/=+$/, "");
					for (
						var _0xbf7507 = 0x0,
							_0x18356b,
							_0x4f540f,
							_0x4c862d = 0x0,
							_0x4bf1e8 = "";
						(_0x4f540f = _0x14148e["charAt"](_0x4c862d++));
						~_0x4f540f &&
						((_0x18356b =
							_0xbf7507 % 0x4 ? _0x18356b * 0x40 + _0x4f540f : _0x4f540f),
						_0xbf7507++ % 0x4)
							? (_0x4bf1e8 += String["fromCharCode"](
									0xff & (_0x18356b >> ((-0x2 * _0xbf7507) & 0x6))
							  ))
							: 0x0
					) {
						_0x4f540f = _0x12918c["indexOf"](_0x4f540f);
					}
					return _0x4bf1e8;
				});
		})();
		_0x2ba9["dzoqWA"] = function (_0x30b4a5) {
			var _0x4f6190 = atob(_0x30b4a5);
			var _0x5924c6 = [];
			for (
				var _0x4a4f81 = 0x0, _0xbab478 = _0x4f6190["length"];
				_0x4a4f81 < _0xbab478;
				_0x4a4f81++
			) {
				_0x5924c6 +=
					"%" +
					("00" + _0x4f6190["charCodeAt"](_0x4a4f81)["toString"](0x10))[
						"slice"
					](-0x2);
			}
			return decodeURIComponent(_0x5924c6);
		};
		_0x2ba9["nKWcry"] = {};
		_0x2ba9["LmvHXr"] = !![];
	}
	var _0x578a10 = _0x2ba9["nKWcry"][_0x101b8f];
	if (_0x578a10 === undefined) {
		var _0x4b1809 = function (_0x3b1d14) {
			this["YlKlnG"] = _0x3b1d14;
			this["NsTJKl"] = [0x1, 0x0, 0x0];
			this["HILIkx"] = function () {
				return "newState";
			};
			this["GGmyeM"] = "\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*";
			this["VUtdVO"] = "[\x27|\x22].+[\x27|\x22];?\x20*}";
		};
		_0x4b1809["prototype"]["OsLPar"] = function () {
			var _0x1403ab = new RegExp(this["GGmyeM"] + this["VUtdVO"]);
			var _0x3fadf0 = _0x1403ab["test"](this["HILIkx"]["toString"]())
				? --this["NsTJKl"][0x1]
				: --this["NsTJKl"][0x0];
			return this["anWLTR"](_0x3fadf0);
		};
		_0x4b1809["prototype"]["anWLTR"] = function (_0x26db32) {
			if (!Boolean(~_0x26db32)) {
				return _0x26db32;
			}
			return this["xTDWoN"](this["YlKlnG"]);
		};
		_0x4b1809["prototype"]["xTDWoN"] = function (_0x597ca7) {
			for (
				var _0x3e27c4 = 0x0, _0x192434 = this["NsTJKl"]["length"];
				_0x3e27c4 < _0x192434;
				_0x3e27c4++
			) {
				this["NsTJKl"]["push"](Math["round"](Math["random"]()));
				_0x192434 = this["NsTJKl"]["length"];
			}
			return _0x597ca7(this["NsTJKl"][0x0]);
		};
		new _0x4b1809(_0x2ba9)["OsLPar"]();
		_0x27941a = _0x2ba9["dzoqWA"](_0x27941a);
		_0x2ba9["nKWcry"][_0x101b8f] = _0x27941a;
	} else {
		_0x27941a = _0x578a10;
	}
	return _0x27941a;
};
(function (_0x974ca9, _0x10c65e, _0x4093e7) {
	var _0x1f20d3 = {
		oscqj: _0x2ba9("0x0"),
		fkUhC: _0x2ba9("0x1"),
		lyKzh: _0x2ba9("0x2"),
		PrDEx: function (_0x56ee68, _0x1607ce) {
			return _0x56ee68(_0x1607ce);
		},
		bIzJA: function (_0x32a55a, _0x4df6cd) {
			return _0x32a55a + _0x4df6cd;
		},
		QxklZ: _0x2ba9("0x3"),
		evllC: _0x2ba9("0x4"),
		XhIuf: _0x2ba9("0x5"),
		VBKmx: function (_0x1a8117, _0x3e3864) {
			return _0x1a8117 * _0x3e3864;
		},
		AgvEd: _0x2ba9("0x6"),
		jVwBL: _0x2ba9("0x7"),
		XYoRT: _0x2ba9("0x8"),
		dOmUu: _0x2ba9("0x9"),
		jXrLd: _0x2ba9("0xa"),
		yGzDY: _0x2ba9("0xb"),
		MqoxH: _0x2ba9("0xc"),
		lkZbv: _0x2ba9("0xd"),
		MckZf: function (_0x1e70d6, _0x13ccbf) {
			return _0x1e70d6 + _0x13ccbf;
		},
		pCVjS: function (_0x2c3d11, _0x4fae60) {
			return _0x2c3d11 + _0x4fae60;
		},
		MPPgR: function (_0x30229c, _0x40ffc3) {
			return _0x30229c - _0x40ffc3;
		},
		POCwR: _0x2ba9("0xe"),
		wVaRD: _0x2ba9("0xf"),
		TDgPX: _0x2ba9("0x10"),
		xZrma: _0x2ba9("0x11"),
		cBCLx: _0x2ba9("0x12"),
		nRaqB: _0x2ba9("0x13"),
		WZJFb: _0x2ba9("0x14"),
		UhGqt: _0x2ba9("0x15"),
		UoxhI: _0x2ba9("0x16"),
		QjIVI: _0x2ba9("0x17"),
		TdjKU: _0x2ba9("0x18"),
		Lsjed: _0x2ba9("0x19"),
		usCCT: _0x2ba9("0x1a"),
		AFezP: _0x2ba9("0x1b"),
		aEBcg: function (_0x26effe, _0x13d629) {
			return _0x26effe + _0x13d629;
		},
		DhVSu: function (_0x4f0d11, _0x318e33) {
			return _0x4f0d11 + _0x318e33;
		},
		FOZRl: _0x2ba9("0x1c"),
		ZPYgi: _0x2ba9("0x1d"),
		nTrhd: _0x2ba9("0x1e"),
		kxdcD: function (_0x578437, _0x29642c) {
			return _0x578437 == _0x29642c;
		},
		cVstd: _0x2ba9("0x1f"),
		eMDzR: _0x2ba9("0x20"),
		KjNco: _0x2ba9("0x21"),
		WrKSy: _0x2ba9("0x22"),
		bLwzZ: _0x2ba9("0x23"),
		aahLE: _0x2ba9("0x24"),
		eeQKx: _0x2ba9("0x25"),
		vdetB: _0x2ba9("0x26"),
		TsFuZ: _0x2ba9("0x27"),
		JDMIv: _0x2ba9("0x28"),
		nNFva: function (_0x58664a, _0xfe92ce) {
			return _0x58664a == _0xfe92ce;
		},
		snXxK: _0x2ba9("0x29"),
		BixYS: _0x2ba9("0x2a"),
		MWPZa: _0x2ba9("0x2b"),
		OeKCr: _0x2ba9("0x2c"),
		nbuXg: _0x2ba9("0x2d"),
		ZFmTI: function (_0x4e4bbb, _0x19e1fa) {
			return _0x4e4bbb + _0x19e1fa;
		},
		aOJeX: _0x2ba9("0x2e"),
		ZJwZV: _0x2ba9("0x2f"),
		sFimC: _0x2ba9("0x30"),
		ckPJH: _0x2ba9("0x31"),
		XFDSB: _0x2ba9("0x32"),
		pXclL: _0x2ba9("0x33"),
		cUSSh: _0x2ba9("0x34"),
		rnCXc: _0x2ba9("0x35"),
		EDRDI: function (_0x247161, _0x4c41eb) {
			return _0x247161(_0x4c41eb);
		},
		oVsmK: _0x2ba9("0x36"),
		IDnYP: _0x2ba9("0x37"),
		XLJoz: _0x2ba9("0x38"),
		yrfVZ: _0x2ba9("0x39"),
		ToGKs: _0x2ba9("0x3a"),
		dkpBi: _0x2ba9("0x3b"),
		QvRcT: function (_0x1235a9, _0x38f538) {
			return _0x1235a9 != _0x38f538;
		},
		xsiVg: _0x2ba9("0x3c"),
	};
	var _0x167f85 = _0x1f20d3[_0x2ba9("0x3d")][_0x2ba9("0x3e")]("|"),
		_0x57c351 = 0x0;
	while (!![]) {
		switch (_0x167f85[_0x57c351++]) {
			case "0":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0x40")] = function () {
					this[_0x2ba9("0x41")]
						[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0x43")])
						[_0x2ba9("0x44")]();
				};
				continue;
			case "1":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0x45")] = function (_0x1191a7) {
					this[_0x2ba9("0x46")] = _0x1191a7;
				};
				continue;
			case "2":
				var _0x22b277 = {
					emRGu: _0x1f20d3[_0x2ba9("0x47")],
					yeXYx: _0x1f20d3[_0x2ba9("0x48")],
					kfqzC: function (_0x56e39c, _0x1a2cd7) {
						return _0x1f20d3[_0x2ba9("0x49")](_0x56e39c, _0x1a2cd7);
					},
					hdpPm: function (_0x16aa9b, _0x64a2ef) {
						return _0x1f20d3[_0x2ba9("0x4a")](_0x16aa9b, _0x64a2ef);
					},
					BYfeh: _0x1f20d3[_0x2ba9("0x4b")],
					DkfPy: _0x1f20d3[_0x2ba9("0x4c")],
					NQeHG: _0x1f20d3[_0x2ba9("0x4d")],
					cdTwA: function (_0xf81143, _0x36f7a5) {
						return _0x1f20d3[_0x2ba9("0x4e")](_0xf81143, _0x36f7a5);
					},
					QaPmH: _0x1f20d3[_0x2ba9("0x4f")],
					QpURO: _0x1f20d3[_0x2ba9("0x50")],
					FdFXW: _0x1f20d3[_0x2ba9("0x51")],
					hNTGS: _0x1f20d3[_0x2ba9("0x52")],
					JqaxC: _0x1f20d3[_0x2ba9("0x53")],
					rIcOB: _0x1f20d3[_0x2ba9("0x54")],
					fWRpZ: _0x1f20d3[_0x2ba9("0x55")],
					rqxPs: _0x1f20d3[_0x2ba9("0x56")],
					BBvyN: function (_0x5bdb1a, _0x30db2a) {
						return _0x1f20d3[_0x2ba9("0x57")](_0x5bdb1a, _0x30db2a);
					},
					wGlFg: function (_0x27807e, _0xad7450) {
						return _0x1f20d3[_0x2ba9("0x58")](_0x27807e, _0xad7450);
					},
					jDILx: function (_0x1cafe7, _0x45cd46) {
						return _0x1f20d3[_0x2ba9("0x59")](_0x1cafe7, _0x45cd46);
					},
					eNkze: _0x1f20d3[_0x2ba9("0x5a")],
					AOdqu: _0x1f20d3[_0x2ba9("0x5b")],
					uAtPN: _0x1f20d3[_0x2ba9("0x5c")],
					JuGrj: _0x1f20d3[_0x2ba9("0x5d")],
					bubNS: _0x1f20d3[_0x2ba9("0x5e")],
					DMddx: _0x1f20d3[_0x2ba9("0x5f")],
					EyvcL: _0x1f20d3[_0x2ba9("0x60")],
					XdWVE: _0x1f20d3[_0x2ba9("0x61")],
					uLDlx: _0x1f20d3[_0x2ba9("0x62")],
					gJwDr: _0x1f20d3[_0x2ba9("0x63")],
					pxhMA: _0x1f20d3[_0x2ba9("0x64")],
					utkgS: _0x1f20d3[_0x2ba9("0x65")],
					hJIlH: _0x1f20d3[_0x2ba9("0x66")],
					UrujI: _0x1f20d3[_0x2ba9("0x67")],
					NdrYE: function (_0x5a12cb, _0x3fb063) {
						return _0x1f20d3[_0x2ba9("0x68")](_0x5a12cb, _0x3fb063);
					},
					YylRt: function (_0x54964b, _0x5aec00) {
						return _0x1f20d3[_0x2ba9("0x69")](_0x54964b, _0x5aec00);
					},
					jLYYO: _0x1f20d3[_0x2ba9("0x6a")],
					gGoiN: _0x1f20d3[_0x2ba9("0x6b")],
					PPhRY: _0x1f20d3[_0x2ba9("0x6c")],
					kklKb: function (_0x5a46b1, _0x1f6739) {
						return _0x1f20d3[_0x2ba9("0x6d")](_0x5a46b1, _0x1f6739);
					},
					mICpL: _0x1f20d3[_0x2ba9("0x6e")],
					IZyDk: _0x1f20d3[_0x2ba9("0x6f")],
					qsiYM: _0x1f20d3[_0x2ba9("0x70")],
					fLDuM: function (_0x533f11, _0x4fe91c) {
						return _0x1f20d3[_0x2ba9("0x69")](_0x533f11, _0x4fe91c);
					},
					sjKQi: _0x1f20d3[_0x2ba9("0x71")],
					jlCRK: _0x1f20d3[_0x2ba9("0x72")],
					znXfS: _0x1f20d3[_0x2ba9("0x73")],
					Szvar: function (_0x46f67a, _0x29dec9) {
						return _0x1f20d3[_0x2ba9("0x49")](_0x46f67a, _0x29dec9);
					},
					Xfztr: _0x1f20d3[_0x2ba9("0x74")],
					vklNr: _0x1f20d3[_0x2ba9("0x75")],
					GYvQF: _0x1f20d3[_0x2ba9("0x76")],
					eiiqF: _0x1f20d3[_0x2ba9("0x77")],
					ovwlt: function (_0x52b282, _0x5217f6) {
						return _0x1f20d3[_0x2ba9("0x78")](_0x52b282, _0x5217f6);
					},
					URUGz: _0x1f20d3[_0x2ba9("0x79")],
					OJASF: _0x1f20d3[_0x2ba9("0x7a")],
					Mhlei: _0x1f20d3[_0x2ba9("0x7b")],
					IGBWm: _0x1f20d3[_0x2ba9("0x7c")],
					GksgT: _0x1f20d3[_0x2ba9("0x7d")],
					etrqc: function (_0x3fb552, _0x5f9394) {
						return _0x1f20d3[_0x2ba9("0x7e")](_0x3fb552, _0x5f9394);
					},
					oiFIc: _0x1f20d3[_0x2ba9("0x7f")],
					hADwj: _0x1f20d3[_0x2ba9("0x80")],
					WroQA: _0x1f20d3[_0x2ba9("0x81")],
					lLENY: _0x1f20d3[_0x2ba9("0x82")],
					XrogY: _0x1f20d3[_0x2ba9("0x83")],
					gpsVZ: _0x1f20d3[_0x2ba9("0x84")],
					OurIv: _0x1f20d3[_0x2ba9("0x85")],
					bIutk: _0x1f20d3[_0x2ba9("0x86")],
					hxflf: function (_0x244e6e, _0xdd8b35) {
						return _0x1f20d3[_0x2ba9("0x87")](_0x244e6e, _0xdd8b35);
					},
					mTJOn: _0x1f20d3[_0x2ba9("0x88")],
					UYmdZ: _0x1f20d3[_0x2ba9("0x89")],
					IXdcC: _0x1f20d3[_0x2ba9("0x8a")],
					qxwGM: _0x1f20d3[_0x2ba9("0x8b")],
					pvPMs: function (_0x5c72fa, _0x534102) {
						return _0x1f20d3[_0x2ba9("0x7e")](_0x5c72fa, _0x534102);
					},
					PVxSj: function (_0x3cd330, _0xc95f51) {
						return _0x1f20d3[_0x2ba9("0x87")](_0x3cd330, _0xc95f51);
					},
					OAxMg: _0x1f20d3[_0x2ba9("0x8c")],
					tlsPP: _0x1f20d3[_0x2ba9("0x8d")],
					SelRx: function (_0x4235da, _0x54b235) {
						return _0x1f20d3[_0x2ba9("0x8e")](_0x4235da, _0x54b235);
					},
					EbcZf: _0x1f20d3[_0x2ba9("0x8f")],
				};
				continue;
			case "3":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0x90")] = function (_0x52f0c7) {
					var _0x4f672e = _0x22b277[_0x2ba9("0x91")](
						_0x22b277[_0x2ba9("0x91")](
							Math[_0x2ba9("0x92")](
								_0x22b277[_0x2ba9("0x93")](Math[_0x2ba9("0x94")](), 0x270f)
							)[_0x2ba9("0x95")](),
							new Date()
								[_0x2ba9("0x96")]()
								[_0x2ba9("0x95")]()
								[_0x2ba9("0x97")](0x4, 0xa)
						),
						Math[_0x2ba9("0x92")](
							_0x22b277[_0x2ba9("0x93")](Math[_0x2ba9("0x94")](), 0x270f)
						)[_0x2ba9("0x95")]()
					)
						[_0x2ba9("0x95")]()
						[_0x2ba9("0x97")](0x3, 0xa);
					return _0x22b277[_0x2ba9("0x91")](
						_0x22b277[_0x2ba9("0x98")](
							_0x4f672e[_0x2ba9("0x97")](
								0x0,
								_0x22b277[_0x2ba9("0x99")](this[_0x2ba9("0x9a")], 0x1)
							),
							_0x52f0c7
						),
						_0x4f672e[_0x2ba9("0x97")](
							this[_0x2ba9("0x9a")],
							_0x4f672e[_0x2ba9("0x9b")]
						)
					);
				};
				continue;
			case "4":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0x9c")] = function (_0x55734d) {
					var _0x3a0a3f = this[_0x2ba9("0x41")][_0x2ba9("0x42")](
						_0x22b277[_0x2ba9("0x9d")]
					);
					_0x3a0a3f[_0x2ba9("0x9e")](_0x22b277[_0x2ba9("0x9f")]);
					_0x3a0a3f[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xa0")])[
						_0x2ba9("0xa1")
					](_0x55734d);
					_0x3a0a3f[_0x2ba9("0xa2")]();
				};
				continue;
			case "5":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xa3")] = function (_0x5f24f3) {
					var _0x2c9701 = _0x5f24f3[_0x2ba9("0x9b")],
						_0x42ac56,
						_0x51dc88;
					while (_0x2c9701) {
						_0x42ac56 = Math[_0x2ba9("0xa4")](
							_0x22b277[_0x2ba9("0x93")](Math[_0x2ba9("0x94")](), _0x2c9701--)
						);
						_0x51dc88 = _0x5f24f3[_0x42ac56];
						_0x5f24f3[_0x42ac56] = _0x5f24f3[_0x2c9701];
						_0x5f24f3[_0x2c9701] = _0x51dc88;
					}
					return _0x5f24f3;
				};
				continue;
			case "6":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xa5")] = function () {
					var _0x3ed28f = _0x22b277[_0x2ba9("0xa6")][_0x2ba9("0x3e")]("|"),
						_0x1ba9da = 0x0;
					while (!![]) {
						switch (_0x3ed28f[_0x1ba9da++]) {
							case "0":
								var _0x5d285f = this[_0x2ba9("0x41")][_0x2ba9("0x42")](
									_0x22b277[_0x2ba9("0x9d")]
								);
								continue;
							case "1":
								_0x5d285f[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xa0")])[
									_0x2ba9("0xa1")
								](_0x22b277[_0x2ba9("0xa7")]);
								continue;
							case "2":
								_0x5d285f[_0x2ba9("0x9e")](_0x22b277[_0x2ba9("0x9f")]);
								continue;
							case "3":
								_0x5d285f[_0x2ba9("0xa2")]();
								continue;
							case "4":
								_0x5d285f[_0x2ba9("0x42")]("i")
									[_0x2ba9("0xa8")](_0x22b277[_0x2ba9("0xa9")])
									[_0x2ba9("0x9e")](_0x22b277[_0x2ba9("0xaa")]);
								continue;
						}
						break;
					}
				};
				continue;
			case "7":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xab")] = function () {
					var _0x3a2899 = this[_0x2ba9("0x41")][_0x2ba9("0x42")](
						_0x22b277[_0x2ba9("0x9d")]
					);
					_0x3a2899[_0x2ba9("0xa8")](_0x22b277[_0x2ba9("0x9f")]);
					_0x3a2899[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xa0")])[
						_0x2ba9("0xa1")
					](_0x22b277[_0x2ba9("0xac")]);
					_0x3a2899[_0x2ba9("0xa2")]();
				};
				continue;
			case "8":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xad")] = function (_0x435e91) {
					this[_0x2ba9("0xae")] = _0x435e91;
				};
				continue;
			case "9":
				var _0x41fffd = function (_0x341a3d, _0x533a9e, _0x816e5e) {
					var _0x3c89ac = _0x22b277[_0x2ba9("0xaf")][_0x2ba9("0x3e")]("|"),
						_0x3f0846 = 0x0;
					while (!![]) {
						switch (_0x3c89ac[_0x3f0846++]) {
							case "0":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xb1")]);
								continue;
							case "1":
								this[_0x2ba9("0xb2")] = _0x22b277[_0x2ba9("0xb3")](
									Number,
									_0x22b277[_0x2ba9("0xb4")](
										Math[_0x2ba9("0x94")]()
											[_0x2ba9("0x95")]()
											[_0x2ba9("0x97")](0x3, 0x4),
										Date[_0x2ba9("0xb5")]()
									)
								)[_0x2ba9("0x95")](0x24);
								continue;
							case "2":
								this[_0x2ba9("0xb6")] = _0x341a3d;
								continue;
							case "3":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xb7")]);
								continue;
							case "4":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xb8")]);
								continue;
							case "5":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xb9")]);
								continue;
							case "6":
								this[_0x2ba9("0x9a")] = _0x22b277[_0x2ba9("0xb4")](
									Math[_0x2ba9("0xa4")](
										_0x22b277[_0x2ba9("0x93")](Math[_0x2ba9("0x94")](), 0x5)
									),
									0x5
								);
								continue;
							case "7":
								this[_0x2ba9("0xba")] = _0x533a9e;
								continue;
							case "8":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xbb")]);
								continue;
							case "9":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xbc")]);
								continue;
							case "10":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xbd")]);
								continue;
							case "11":
								this[_0x2ba9("0xbe")] = [];
								continue;
							case "12":
								this[_0x2ba9("0x41")] = _0x22b277[_0x2ba9("0xb3")](
									_0x974ca9,
									_0x5ee96a[_0x2ba9("0xbf")]("")
								);
								continue;
							case "13":
								this[_0x2ba9("0xc0")] = _0x816e5e;
								continue;
							case "14":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xc1")]);
								continue;
							case "15":
								var _0x5ee96a = [];
								continue;
							case "16":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xc2")]);
								continue;
							case "17":
								_0x22b277[_0x2ba9("0xb3")](
									_0x974ca9,
									_0x22b277[_0x2ba9("0xc3")]
								)
									[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xc4")])
									[_0x2ba9("0xc5")]();
								continue;
							case "18":
								_0x5ee96a[_0x2ba9("0xb0")](_0x22b277[_0x2ba9("0xc6")]);
								continue;
						}
						break;
					}
				};
				continue;
			case "10":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xc7")] = function () {
					_0x22b277[_0x2ba9("0xb3")](_0x974ca9, _0x22b277[_0x2ba9("0xc3")])
						[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xc4")])
						[_0x2ba9("0xc5")]();
					this[_0x2ba9("0xae")] = 0x0;
				};
				continue;
			case "11":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xc8")] = function () {
					var _0x3d749d = _0x22b277[_0x2ba9("0xc9")][_0x2ba9("0x3e")]("|"),
						_0x4f5399 = 0x0;
					while (!![]) {
						switch (_0x3d749d[_0x4f5399++]) {
							case "0":
								this[_0x2ba9("0xca")]();
								continue;
							case "1":
								this[_0x2ba9("0xcb")]();
								continue;
							case "2":
								var _0x3cc5a0 = {
									xOdJD: function (_0xea7649, _0x55a64b) {
										return _0x22b277[_0x2ba9("0xcc")](_0xea7649, _0x55a64b);
									},
									zBviB: _0x22b277[_0x2ba9("0xcd")],
									ltzWZ: _0x22b277[_0x2ba9("0xce")],
									ZqDXU: _0x22b277[_0x2ba9("0xcf")],
									asnMd: _0x22b277[_0x2ba9("0xd0")],
									UmjPG: function (_0x4519b1, _0x4cd8a7) {
										return _0x22b277[_0x2ba9("0xcc")](_0x4519b1, _0x4cd8a7);
									},
									YvkfW: function (_0x47a7b4, _0x583341) {
										return _0x22b277[_0x2ba9("0xd1")](_0x47a7b4, _0x583341);
									},
									GjnxT: _0x22b277[_0x2ba9("0xd2")],
								};
								continue;
							case "3":
								_0x974ca9[_0x2ba9("0xd3")]({
									url: _0x22b277[_0x2ba9("0xd4")](
										_0x22b277[_0x2ba9("0xd5")],
										new Date()[_0x2ba9("0x96")]()
									),
									type: _0x22b277[_0x2ba9("0xd6")],
									data: {
										clientid: _0x54639d[_0x2ba9("0xb2")],
										username: _0x54639d[_0x2ba9("0xba")],
									},
									dataType: _0x22b277[_0x2ba9("0xd7")],
									error: function (_0x4c52de) {
										_0x3cc5a0[_0x2ba9("0xd8")](
											alert,
											_0x3cc5a0[_0x2ba9("0xd9")]
										);
									},
									success: function (_0x4d7419) {
										var _0x50f304 =
												_0x22b277[_0x2ba9("0xda")][_0x2ba9("0x3e")]("|"),
											_0x1cf6d6 = 0x0;
										while (!![]) {
											switch (_0x50f304[_0x1cf6d6++]) {
												case "0":
													_0x54639d[_0x2ba9("0x41")]
														[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xdb")])
														[_0x2ba9("0xdc")](_0x4cc8d1[_0x2ba9("0xbf")](""));
													continue;
												case "1":
													_0x4cc8d1[_0x2ba9("0xb0")](
														_0x22b277[_0x2ba9("0xbb")]
													);
													continue;
												case "2":
													var _0x5c5136 = _0x4093e7[_0x2ba9("0xdd")](
														_0x22b277[_0x2ba9("0xde")]
													);
													continue;
												case "3":
													_0x54639d[_0x2ba9("0x41")]
														[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xdf")])
														[_0x2ba9("0x35")](function () {
															var _0x3243f6 =
																	_0x333e98[_0x2ba9("0xe0")][_0x2ba9("0x3e")](
																		"|"
																	),
																_0xc910ba = 0x0;
															while (!![]) {
																switch (_0x3243f6[_0xc910ba++]) {
																	case "0":
																		if (
																			_0x333e98[_0x2ba9("0xe1")](
																				_0x5bf942[_0x2ba9("0x9b")],
																				_0x54639d[_0x2ba9("0x46")]
																			)
																		) {
																			var _0x2465df = [],
																				_0x44edf3 = new Date()
																					[_0x2ba9("0x96")]()
																					[_0x2ba9("0x95")]();
																			_0x974ca9[_0x2ba9("0xe2")](
																				_0x5bf942,
																				function (_0x4184e0, _0x429229) {
																					var _0x1405d0 = _0x333e98[
																						_0x2ba9("0xe3")
																					](_0x974ca9, this)[_0x2ba9("0xe4")](
																						"id"
																					);
																					_0x2465df[_0x2ba9("0xb0")](
																						_0x1405d0[_0x2ba9("0xe5")](
																							_0x333e98[_0x2ba9("0xe6")],
																							""
																						)
																					);
																				}
																			);
																			_0x54639d[_0x2ba9("0xab")]();
																			_0x974ca9[_0x2ba9("0xd3")]({
																				url: _0x333e98[_0x2ba9("0xe7")](
																					_0x333e98[_0x2ba9("0xe8")],
																					_0x44edf3
																				),
																				type: _0x333e98[_0x2ba9("0xe9")],
																				data: {
																					clientid: _0x54639d[_0x2ba9("0xb2")],
																					data: _0x333e98[_0x2ba9("0xea")](
																						_0x333e98[_0x2ba9("0xeb")](
																							_0x333e98[_0x2ba9("0xec")](
																								_0x333e98[_0x2ba9("0xec")](
																									_0x333e98[_0x2ba9("0xed")](
																										_0x2465df[_0x2ba9("0xbf")](
																											""
																										),
																										""
																									),
																									_0x54639d[_0x2ba9("0x9a")]
																								),
																								""
																							),
																							_0x44edf3[_0x2ba9("0x97")](-0x2)
																						),
																						_0x333e98[_0x2ba9("0xee")]
																					),
																					username: _0x54639d[_0x2ba9("0xba")],
																					password: _0x54639d[_0x2ba9("0xc0")],
																				},
																				dataType: _0x333e98[_0x2ba9("0xef")],
																				error: function (_0x42c979) {
																					_0xbced42[_0x2ba9("0xf0")](
																						alert,
																						_0xbced42[_0x2ba9("0xf1")]
																					);
																				},
																				success: function (_0x3be6d5) {
																					if (
																						_0x333e98[_0x2ba9("0xe1")](
																							_0x3be6d5[_0x2ba9("0xf2")],
																							0x0
																						)
																					) {
																						_0x54639d[_0x2ba9("0xad")](0x1);
																						_0x54639d[_0x2ba9("0xa5")]();
																						_0x10c65e[_0x2ba9("0xf3")](
																							function () {
																								_0x54639d[_0x2ba9("0xc7")]();
																							},
																							0x1f4
																						);
																						_0x54639d[_0x2ba9("0xb6")][
																							_0x2ba9("0xf4")
																						]();
																					} else {
																						_0x54639d[_0x2ba9("0xad")](0x0);
																						_0x54639d[_0x2ba9("0x9c")](
																							_0x3be6d5[_0x2ba9("0xf5")]
																						);
																						_0x10c65e[_0x2ba9("0xf3")](
																							function () {
																								_0x54639d[_0x2ba9("0xc8")]();
																							},
																							0x3e8
																						);
																					}
																				},
																			});
																		}
																		continue;
																	case "1":
																		if (
																			_0x14b520[_0x2ba9("0xf6")](
																				_0x333e98[_0x2ba9("0xf7")]
																			)
																		)
																			_0x333e98[_0x2ba9("0xf8")](
																				_0x974ca9,
																				this
																			)[_0x2ba9("0xa8")](
																				_0x333e98[_0x2ba9("0xf7")]
																			);
																		else
																			_0x333e98[_0x2ba9("0xf8")](
																				_0x974ca9,
																				this
																			)[_0x2ba9("0x9e")](
																				_0x333e98[_0x2ba9("0xf7")]
																			);
																		continue;
																	case "2":
																		var _0xbced42 = {
																			ZSFJo: function (_0x373d10, _0xb508c9) {
																				return _0x333e98[_0x2ba9("0xf9")](
																					_0x373d10,
																					_0xb508c9
																				);
																			},
																			ONwiT: _0x333e98[_0x2ba9("0xfa")],
																		};
																		continue;
																	case "3":
																		var _0x14b520 = _0x333e98[_0x2ba9("0xfb")](
																			_0x974ca9,
																			this
																		);
																		continue;
																	case "4":
																		var _0x5bf942 = _0x54639d[_0x2ba9("0x41")][
																			_0x2ba9("0x42")
																		](_0x333e98[_0x2ba9("0xfc")]);
																		continue;
																}
																break;
															}
														});
													continue;
												case "4":
													var _0x333e98 = {
														wHwnV: function (_0x4fe8c6, _0x5133b8) {
															return _0x22b277[_0x2ba9("0x98")](
																_0x4fe8c6,
																_0x5133b8
															);
														},
														ESkAh: _0x22b277[_0x2ba9("0xfd")],
														ZRuFf: function (_0x484159, _0x42160f) {
															return _0x22b277[_0x2ba9("0xfe")](
																_0x484159,
																_0x42160f
															);
														},
														RFlvs: function (_0x356126, _0x248330) {
															return _0x22b277[_0x2ba9("0xff")](
																_0x356126,
																_0x248330
															);
														},
														xiHxp: _0x22b277[_0x2ba9("0x100")],
														mHtMj: _0x22b277[_0x2ba9("0x101")],
														rpTno: _0x22b277[_0x2ba9("0x102")],
														qnOXK: function (_0x5233cc, _0x47cad9) {
															return _0x22b277[_0x2ba9("0xb3")](
																_0x5233cc,
																_0x47cad9
															);
														},
														gfgUr: function (_0x5a0714, _0x52caf4) {
															return _0x22b277[_0x2ba9("0x103")](
																_0x5a0714,
																_0x52caf4
															);
														},
														lVkUN: _0x22b277[_0x2ba9("0x104")],
														kECnA: function (_0x5b148d, _0x15e1ce) {
															return _0x22b277[_0x2ba9("0xff")](
																_0x5b148d,
																_0x15e1ce
															);
														},
														nJZLq: _0x22b277[_0x2ba9("0x105")],
														ONbfc: _0x22b277[_0x2ba9("0xd6")],
														ZPcyU: function (_0x195c56, _0x740bf6) {
															return _0x22b277[_0x2ba9("0xff")](
																_0x195c56,
																_0x740bf6
															);
														},
														jgBDz: function (_0x3fb704, _0x1533e1) {
															return _0x22b277[_0x2ba9("0xd1")](
																_0x3fb704,
																_0x1533e1
															);
														},
														IXCtB: function (_0x2bbdd1, _0x31b4e8) {
															return _0x22b277[_0x2ba9("0xd1")](
																_0x2bbdd1,
																_0x31b4e8
															);
														},
														vjkEa: function (_0x586611, _0x1861e0) {
															return _0x22b277[_0x2ba9("0xd1")](
																_0x586611,
																_0x1861e0
															);
														},
														WCjXb: _0x22b277[_0x2ba9("0x106")],
														WxiMV: _0x22b277[_0x2ba9("0xd7")],
														EFcrc: _0x22b277[_0x2ba9("0x107")],
														YKhYm: function (_0x16ae11, _0x575078) {
															return _0x22b277[_0x2ba9("0xb3")](
																_0x16ae11,
																_0x575078
															);
														},
														eJPEK: function (_0xa21ebd, _0x122bf8) {
															return _0x22b277[_0x2ba9("0xcc")](
																_0xa21ebd,
																_0x122bf8
															);
														},
														xzpmZ: _0x22b277[_0x2ba9("0xcd")],
														hsqDq: function (_0x55a23a, _0x290fbe) {
															return _0x22b277[_0x2ba9("0xcc")](
																_0x55a23a,
																_0x290fbe
															);
														},
														ZKhyv: _0x22b277[_0x2ba9("0x108")],
													};
													continue;
												case "5":
													_0x5283c1 = _0x54639d[_0x2ba9("0xa3")](_0x5283c1);
													continue;
												case "6":
													_0x4cc8d1[_0x2ba9("0xb0")](
														_0x22b277[_0x2ba9("0x109")]
													);
													continue;
												case "7":
													_0x4de33a[_0x2ba9("0x10a")] =
														_0x4d7419[_0x2ba9("0x10b")];
													continue;
												case "8":
													_0x4cc8d1[_0x2ba9("0xb0")](
														_0x22b277[_0x2ba9("0x10c")]
													);
													continue;
												case "9":
													_0x54639d[_0x2ba9("0x40")]();
													continue;
												case "10":
													_0x974ca9[_0x2ba9("0xe2")](
														_0x4d7419[_0x2ba9("0x10d")],
														function (_0x4c0d9a, _0xff7923) {
															_0x5283c1[_0x2ba9("0xb0")]({
																id: _0x54639d[_0x2ba9("0x90")](
																	_0x4c0d9a[_0x2ba9("0x95")]()
																),
																txt: _0xff7923,
															});
														}
													);
													continue;
												case "11":
													_0x974ca9[_0x2ba9("0xe2")](
														_0x5283c1,
														function (_0xa3a45c, _0x10a57b) {
															var _0x79225d =
																	_0x3cc5a0[_0x2ba9("0x10e")][_0x2ba9("0x3e")](
																		"|"
																	),
																_0x2831d1 = 0x0;
															while (!![]) {
																switch (_0x79225d[_0x2831d1++]) {
																	case "0":
																		var _0x50b020 = 0x0;
																		continue;
																	case "1":
																		_0x4f9554[_0x2ba9("0x10f")] =
																			_0x3cc5a0[_0x2ba9("0x110")];
																		continue;
																	case "2":
																		_0x4f9554[_0x2ba9("0x111")] =
																			_0x3cc5a0[_0x2ba9("0x112")];
																		continue;
																	case "3":
																		switch (
																			_0x3cc5a0[_0x2ba9("0x113")](
																				parseInt,
																				_0x10a57b[_0x2ba9("0x114")][
																					_0x2ba9("0x9b")
																				]
																			)
																		) {
																			case 0x1:
																				_0x50b020 = 0x12;
																				break;
																			case 0x2:
																				_0x50b020 = 0x9;
																				break;
																		}
																		continue;
																	case "4":
																		_0x4f9554[_0x2ba9("0x115")](
																			_0x10a57b[_0x2ba9("0x114")],
																			_0x50b020,
																			0xd
																		);
																		continue;
																	case "5":
																		var _0x4f9554 = _0x4093e7[_0x2ba9("0xdd")](
																			_0x3cc5a0[_0x2ba9("0x116")](
																				_0x3cc5a0[_0x2ba9("0x117")],
																				_0xa3a45c
																			)
																		)[_0x2ba9("0x118")]("2d");
																		continue;
																}
																break;
															}
														}
													);
													continue;
												case "12":
													_0x974ca9[_0x2ba9("0xe2")](
														_0x5283c1,
														function (_0x4b8168, _0x27f645) {
															_0x54639d[_0x2ba9("0xbe")][
																_0x333e98[_0x2ba9("0x119")](
																	_0x333e98[_0x2ba9("0xe6")],
																	_0x27f645["id"]
																)
															] = 0x1;
															_0x4cc8d1[_0x2ba9("0xb0")](
																_0x333e98[_0x2ba9("0x119")](
																	_0x333e98[_0x2ba9("0x11a")](
																		_0x333e98[_0x2ba9("0x11a")](
																			_0x333e98[_0x2ba9("0x11b")](
																				_0x333e98[_0x2ba9("0x11c")],
																				_0x27f645["id"]
																			),
																			_0x333e98[_0x2ba9("0x11d")]
																		),
																		_0x4b8168
																	),
																	_0x333e98[_0x2ba9("0x11e")]
																)
															);
														}
													);
													continue;
												case "13":
													var _0x4de33a = new Image();
													continue;
												case "14":
													if (
														_0x22b277[_0x2ba9("0x11f")](
															_0x4d7419[_0x2ba9("0xf2")],
															0x1
														)
													) {
														_0x54639d[_0x2ba9("0x40")]();
														_0x54639d[_0x2ba9("0x9c")](
															_0x4d7419[_0x2ba9("0xf5")]
														);
														return;
													}
													continue;
												case "15":
													_0x54639d[_0x2ba9("0xbe")] = [];
													continue;
												case "16":
													var _0x52ef24 = _0x5c5136[_0x2ba9("0x118")]("2d");
													continue;
												case "17":
													_0x54639d[_0x2ba9("0x41")]
														[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0xdf")])
														[_0x2ba9("0x120")](function () {
															var _0x5cb71a = _0x333e98[_0x2ba9("0xe3")](
																_0x974ca9,
																this
															)[_0x2ba9("0xe4")]("id");
															_0x54639d[_0x2ba9("0xbe")][_0x5cb71a]++;
														});
													continue;
												case "18":
													var _0x4cc8d1 = [],
														_0x28e8c4 = 0xf423f,
														_0x1d965c = 0x186a0,
														_0x5283c1 = [];
													continue;
												case "19":
													_0x4de33a[_0x2ba9("0x121")] = function () {
														_0x52ef24[_0x2ba9("0x122")](
															_0x4de33a,
															0x0,
															0x0,
															0x12c,
															0xb4
														);
													};
													continue;
												case "20":
													_0x54639d[_0x2ba9("0x45")](
														_0x4d7419[_0x2ba9("0x123")]
													);
													continue;
											}
											break;
										}
									},
								});
								continue;
							case "4":
								var _0x54639d = this;
								continue;
							case "5":
								this[_0x2ba9("0x41")]
									[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0x124")])
									[_0x2ba9("0x125")]();
								continue;
						}
						break;
					}
				};
				continue;
			case "12":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xcb")] = function () {
					this[_0x2ba9("0x41")]
						[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0x43")])
						[_0x2ba9("0xa2")]();
				};
				continue;
			case "13":
				var _0x5a945f = function (_0x4292c7) {};
				continue;
			case "14":
				_0x5a945f[_0x2ba9("0x3f")][_0x2ba9("0x126")] = function () {
					var _0x197931 = _0x22b277[_0x2ba9("0x127")][_0x2ba9("0x3e")]("|"),
						_0x4c9aac = 0x0;
					while (!![]) {
						switch (_0x197931[_0x4c9aac++]) {
							case "0":
								CkType = _0x5413b9[_0x2ba9("0xe4")]("id");
								continue;
							case "1":
								var _0xe4775f = _0x5413b9[_0x2ba9("0x42")](
									_0x22b277[_0x2ba9("0x128")]
								)[_0x2ba9("0x129")]();
								continue;
							case "2":
								var _0x5413b9 = _0x22b277[_0x2ba9("0x12a")](
									_0x974ca9,
									_0x22b277[_0x2ba9("0x12b")]
								);
								continue;
							case "3":
								yzmObj2 = _0x974ca9["fn"][_0x2ba9("0x12c")](
									_0x5413b9,
									_0x22b277[_0x2ba9("0x12d")](
										_0x22b277[_0x2ba9("0x12e")](
											_0x974ca9,
											_0x22b277[_0x2ba9("0x12f")]
										)[_0x2ba9("0xa1")](),
										_0xe4775f
									),
									_0x109550
								);
								continue;
							case "4":
								yzmObj2[_0x2ba9("0xc8")]();
								continue;
							case "5":
								var _0x109550 = _0x5413b9[_0x2ba9("0x42")](
									_0x22b277[_0x2ba9("0x130")]
								)[_0x2ba9("0x129")]();
								continue;
							case "6":
								if (
									_0x22b277[_0x2ba9("0x131")](yzmObj2, null) &&
									_0x22b277[_0x2ba9("0x11f")](
										CkType,
										_0x22b277[_0x2ba9("0x132")]
									)
								) {
									if (
										_0x22b277[_0x2ba9("0x11f")](
											yzmObj2[_0x2ba9("0x133")](),
											0x1
										)
									)
										return !![];
								}
								continue;
						}
						break;
					}
				};
				continue;
			case "15":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0xca")] = function () {
					this[_0x2ba9("0x41")]
						[_0x2ba9("0x42")](_0x22b277[_0x2ba9("0x9d")])
						[_0x2ba9("0x44")]();
				};
				continue;
			case "16":
				_0x41fffd[_0x2ba9("0x3f")][_0x2ba9("0x133")] = function (_0x37d3f3) {
					return this[_0x2ba9("0xae")];
				};
				continue;
			case "17":
				_0x974ca9["fn"][_0x2ba9("0x12c")] = function (
					_0x19d72f,
					_0x449ac5,
					_0x3e316c
				) {
					var _0x5eb5d7 = _0x22b277[_0x2ba9("0x134")][_0x2ba9("0x3e")]("|"),
						_0xa293b = 0x0;
					while (!![]) {
						switch (_0x5eb5d7[_0xa293b++]) {
							case "0":
								_0x22b277[_0x2ba9("0xcc")](
									_0x974ca9,
									_0x22b277[_0x2ba9("0xc3")]
								)[_0x2ba9("0x135")](_0x545037[_0x2ba9("0x41")]);
								continue;
							case "1":
								_0x22b277[_0x2ba9("0xcc")](
									_0x974ca9,
									_0x22b277[_0x2ba9("0x136")]
								)[_0x2ba9("0x137")](_0x22b277[_0x2ba9("0x138")], function () {
									CkType = _0x3ee93[_0x2ba9("0x139")];
									yzmObj2 = null;
									_0x545037[_0x2ba9("0xc7")]();
								});
								continue;
							case "2":
								_0x22b277[_0x2ba9("0x12a")](
									_0x974ca9,
									_0x22b277[_0x2ba9("0x13a")]
								)[_0x2ba9("0x137")](_0x22b277[_0x2ba9("0x138")], function () {
									var _0x477f36 =
											_0x22b277[_0x2ba9("0x13b")][_0x2ba9("0x3e")]("|"),
										_0x22f910 = 0x0;
									while (!![]) {
										switch (_0x477f36[_0x22f910++]) {
											case "0":
												_0x22b277[_0x2ba9("0xcc")](_0x974ca9, this)[
													_0x2ba9("0x9e")
												](_0x22b277[_0x2ba9("0x13c")]);
												continue;
											case "1":
												var _0x23e213 = {
													KTXOX: function (_0x1818c0, _0x2ace0b) {
														return _0x22b277[_0x2ba9("0xcc")](
															_0x1818c0,
															_0x2ace0b
														);
													},
													plXBk: _0x22b277[_0x2ba9("0x13a")],
													vgQaz: _0x22b277[_0x2ba9("0x13c")],
												};
												continue;
											case "2":
												_0x10c65e[_0x2ba9("0xf3")](function () {
													_0x23e213[_0x2ba9("0x13d")](
														_0x974ca9,
														_0x23e213[_0x2ba9("0x13e")]
													)[_0x2ba9("0xa8")](_0x23e213[_0x2ba9("0x13f")]);
												}, 0x7d0);
												continue;
											case "3":
												_0x545037[_0x2ba9("0xc8")]();
												continue;
											case "4":
												if (
													_0x22b277[_0x2ba9("0xcc")](_0x974ca9, this)[
														_0x2ba9("0xf6")
													](_0x22b277[_0x2ba9("0x13c")])
												)
													return ![];
												continue;
										}
										break;
									}
								});
								continue;
							case "3":
								return _0x545037;
							case "4":
								var _0x545037 = new _0x41fffd(_0x19d72f, _0x449ac5, _0x3e316c);
								continue;
							case "5":
								var _0x3ee93 = {
									Gxwtv: _0x22b277[_0x2ba9("0x140")],
								};
								continue;
						}
						break;
					}
				};
				continue;
			case "18":
				_0x974ca9["fn"][_0x2ba9("0x141")] = function () {
					var _0x5cfffc = new _0x5a945f();
					return _0x5cfffc;
				};
				continue;
		}
		break;
	}
})(jQuery, window, document);
var CkType = _0x2ba9("0x36");
var yzmObj2 = null;
var yzmObj = null;
$(document)[_0x2ba9("0x142")](function (_0xfe8723) {
	var _0x3fe98e = {
		GZPfv: _0x2ba9("0x143"),
		eiGMS: function (_0x52e985, _0x29ccb2) {
			return _0x52e985(_0x29ccb2);
		},
		neySh: _0x2ba9("0x38"),
		IRIPz: _0x2ba9("0x3b"),
		dzvyT: function (_0x221a0e, _0x5e2eab) {
			return _0x221a0e != _0x5e2eab;
		},
		qrTmX: function (_0x5b0d92, _0x43dc59) {
			return _0x5b0d92 == _0x43dc59;
		},
		qeEwA: function (_0x27f316, _0x44bdab) {
			return _0x27f316(_0x44bdab);
		},
		JdlPH: function (_0x1b047d, _0x47ff2c) {
			return _0x1b047d == _0x47ff2c;
		},
		IaPRD: function (_0x2888ca, _0x59505) {
			return _0x2888ca(_0x59505);
		},
		rOpxB: _0x2ba9("0x144"),
		CpneY: function (_0x2c8a76, _0x53c513) {
			return _0x2c8a76 == _0x53c513;
		},
		YhDDL: _0x2ba9("0x145"),
		PCOSn: _0x2ba9("0x146"),
		rxSBM: function (_0x3e42be, _0x399835) {
			return _0x3e42be > _0x399835;
		},
		pKucA: function (_0x491d47, _0x3cbc54) {
			return _0x491d47 < _0x3cbc54;
		},
		PAfjQ: _0x2ba9("0x147"),
		IaPsP: function (_0x57e01b, _0x429b7f) {
			return _0x57e01b + _0x429b7f;
		},
		gVlRe: function (_0x356c8a, _0x47b654) {
			return _0x356c8a(_0x47b654);
		},
		rWwji: _0x2ba9("0x3a"),
		IuLrm: _0x2ba9("0x148"),
		QzFbu: _0x2ba9("0x149"),
		NmhQU: _0x2ba9("0xf4"),
	};
	yzmObj = new $["fn"][_0x2ba9("0x141")]();
	if (
		_0x3fe98e[_0x2ba9("0x14a")](
			_0x3fe98e[_0x2ba9("0x14b")]($, _0x3fe98e[_0x2ba9("0x14c")])[
				_0x2ba9("0x9b")
			],
			0x0
		)
	) {
		_0x3fe98e[_0x2ba9("0x14b")]($, _0x3fe98e[_0x2ba9("0x14d")])
			[_0x2ba9("0x14e")](_0x3fe98e[_0x2ba9("0x14f")])
			[_0x2ba9("0x137")](_0x3fe98e[_0x2ba9("0x14f")], function () {
				var _0x27fd60 = _0x3fe98e[_0x2ba9("0x150")][_0x2ba9("0x3e")]("|"),
					_0x46c0ac = 0x0;
				while (!![]) {
					switch (_0x27fd60[_0x46c0ac++]) {
						case "0":
							yzmObj2[_0x2ba9("0xc8")]();
							continue;
						case "1":
							var _0xe696fb = _0x3fe98e[_0x2ba9("0x151")]($, this);
							continue;
						case "2":
							var _0x94e1d7 = _0xe696fb[_0x2ba9("0x42")](
								_0x3fe98e[_0x2ba9("0x152")]
							)[_0x2ba9("0x129")]();
							continue;
						case "3":
							CkType = _0xe696fb[_0x2ba9("0xe4")]("id");
							continue;
						case "4":
							var _0x104aba = _0xe696fb[_0x2ba9("0x42")](
								_0x3fe98e[_0x2ba9("0x153")]
							)[_0x2ba9("0x129")]();
							continue;
						case "5":
							if (
								_0x3fe98e[_0x2ba9("0x154")](yzmObj2, null) &&
								_0x3fe98e[_0x2ba9("0x155")](
									_0x3fe98e[_0x2ba9("0x156")]($, this)[_0x2ba9("0xe4")]("id"),
									CkType
								)
							) {
								if (
									_0x3fe98e[_0x2ba9("0x157")](yzmObj2[_0x2ba9("0x133")](), 0x1)
								)
									return !![];
							}
							continue;
						case "6":
							return ![];
						case "7":
							if (
								_0x3fe98e[_0x2ba9("0x157")](_0x94e1d7, "") ||
								_0x3fe98e[_0x2ba9("0x157")](_0x94e1d7, "帐号")
							) {
								_0x3fe98e[_0x2ba9("0x158")](alert, _0x3fe98e[_0x2ba9("0x159")]);
								return ![];
							} else if (
								_0x3fe98e[_0x2ba9("0x157")](_0x104aba, "") ||
								_0x3fe98e[_0x2ba9("0x14a")](
									_0x104aba,
									_0x3fe98e[_0x2ba9("0x15a")]
								)
							) {
								_0x3fe98e[_0x2ba9("0x158")](alert, _0x3fe98e[_0x2ba9("0x15b")]);
								return ![];
							} else if (
								_0x3fe98e[_0x2ba9("0x15c")](_0x104aba[_0x2ba9("0x9b")], 0x0) &&
								_0x3fe98e[_0x2ba9("0x15d")](_0x104aba[_0x2ba9("0x9b")], 0x6)
							) {
								_0x3fe98e[_0x2ba9("0x158")](alert, _0x3fe98e[_0x2ba9("0x15e")]);
								return ![];
							}
							continue;
						case "8":
							yzmObj2 = $["fn"][_0x2ba9("0x12c")](
								_0xe696fb,
								_0x3fe98e[_0x2ba9("0x15f")](
									_0x3fe98e[_0x2ba9("0x14b")]($, _0x3fe98e[_0x2ba9("0x160")])[
										_0x2ba9("0xa1")
									](),
									_0x94e1d7
								),
								_0x104aba
							);
							continue;
					}
					break;
				}
			});
	}
});

2.数组乱序

通过上一小节的分析,得知生成 data 字段的关键代码都是混淆过的。既然要还原 JS 混淆后的代码,那就要先分析清楚该网站中使用的混淆方案有哪些。来介绍一下原代码中的数组乱序,从上面要还原的全部代码分析,这里只截取部分展示。代码如下:

js

3.字符串加密

js

4.花指令

js

5.流程平坦化

js

还原代码中的常量

从上面要还原的全部代码中还原的方式:

1.整体代码结构

代码的整体结构如下,先 require 一系列的库,然后读取 JS 文件中的代码,用 parser 组件解析成 ast,接着对 ast 进街一系列的操作,最后把 ast 转为字符串,保存到新文件。

js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});

let ast = parser.parse(jscode);

// 在此处对AST 进行一系列操作

//
let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

2.字符串解密与去除数组混淆

完整代码:

js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});

let ast = parser.parse(jscode);

// 在此处对AST 进行一系列操作
// 拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
// 拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
// 新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse("");
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
// 把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, { compact: true }).code;
// 将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

traverse(ast, {
	// 遍历所有变量
	VariableDeclarator(path) {
		// 当变量名与解密函数名相同时,就执行相应操作
		if (path.node.id.name == DecryptFuncName) {
			let binding = path.scope.getBinding(DecryptFuncName);
			binding &&
				binding.referencePaths.map(function (v) {
					v.parentPath.isCallExpression() &&
						v.parentPath.replaceWith(t.stringLiteral(eval(v.parentPath + "")));
				});
		}
	},
});
// 字符串解密以后,源代码的前三部分就没用了,可以去掉
ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();

// 输出
let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

剔除花指令

1.花指令剔除思路

js
var _0x1f20d3 = {
	// ...前面的代码省略
	ZFmTI: function (_0x4e4bbb, _0x19elfa) {
		return _0x4e4bbb + _0x19elfa;
	},
	aOJex: "/yzmtest/get.php?t=",
	// ...后面的代码省略
};
var _0x22b277 = {
	// ...前面的代码省略
	etrgc: function (_0x3fb552, _0x5f9394) {
		return (0x1f20d3)["ZFmTI"](_0x3fb552, _0x5f9394);
	},
	oiFIc: _0x1f20d3["aOJeX"],
	// ...后面的代码省略
};

// _0x22b277[oiFIc']; // 字符串花指令
// _0x22b277['etrqc']; // 函数花指令

用上述例子来说明花指令剔除的思路。大体上可以分为两种情况。

1.字符串花指令的剔除

对于字符串花指令 _0x22b277['oiFIc'],可以遍历所有 MemberExpression 节点,取出 object 节点名和 property 节点值。在 ObjectExpression 节点中找到对应的值,如果类型还是为 MemberExpression,就说明还需要继续找,继续取出 object 节点名和 property 节点值,继续在 ObjectExpression 节点中找到对应的值,直到找到的值类型为 StringLiteral,就进行替换即可。因此需要用到递归。

2.函数花指令的去除

对于函数花指令 _0x22b277['etrgc],也是遍历所有 MemberExpression 节点,取出 object 节点名和 property 节点值。在 ObjectExpression 节点中找到对应的值,如果类型为 FunctionExpression 并且函数体内部有 MemberExpression 节点,就说明还需要继续找,直到找到类型为 FunctionExpression 并且函数体内部没有 MemberExpression 节点,才是最终需要的节点。

在上面的介绍中,多次提到了在 ObjectExpression 节点中找到对应的值,那么当获取到一个 MemberExpression 节点 _0x22b277['oiFIc] 的时候,怎么去 0bjectExpression 节点中找对应的值比较方便呢?笔者的方案是,在 nodejs 中定义一个 total0bj 对象,然后解析原代码中所有的 ObjectExpression,加入到 totalbj 对象中,最后把 totalObj 对象变成如下结构:

js
{
    _0x1f20d3: {
        'ZFmTI': Node {...},
        'aOJeX': Node {...},
        'EDRDI': Node {...},
        ...
    }
    _0x22b277: {
        'etrqc': Node {...},
        'etrqc': Node {...},
        ...
    },
    ...
}

在 nodejs 中组合出这样的结构后,如果要获取 _0x22b277['oiFIc] 的定义部分的节点,只需 totalobj['_0x22b277']['oiFIc']来获取 Node 节点。生成 total0bj 对象的代码如下:

js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});

let ast = parser.parse(jscode);

//拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
//拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
//新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse("");
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
//把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, { compact: true }).code;
//将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

traverse(ast, {
	//遍历所有变量
	VariableDeclarator(path) {
		//当变量名与解密函数名相同时,就执行相应操作
		if (path.node.id.name == DecryptFuncName) {
			let binding = path.scope.getBinding(DecryptFuncName);
			binding &&
				binding.referencePaths.map(function (v) {
					v.parentPath.isCallExpression() &&
						v.parentPath.replaceWith(t.stringLiteral(eval(v.parentPath + "")));
				});
		}
	},
});

ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();

var totalObj = {};
function generatorObj(ast) {
	traverse(ast, {
		VariableDeclarator(path) {
			// init 节点为 ObjectExpression 的时候,就是需要处理的对象
			if (t.isObjectExpression(path.node.init)) {
				// 取出对象名
				let objName = path.node.id.name;
				// 以对象名作为属性名在 totalObj 中创建对象
				objName && (totalObj[objName] = totalObj[objName] || {});
				// 解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
				totalObj[objName] &&
					path.node.init.properties.map(function (p) {
						totalObj[objName][p.key.value] = p.value;
					});
			}
		},
	});
	return ast;
}
ast = generatorObj(ast);

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

2.字符串花指令的剔除

现在可以着手去除花指令了,字符串花指令比较容易去除,遍历 0bjectExpression 节点的 properties 属性,每得到一个 ObjectProperty 的 value 值,都递归找到真实的字符串后进行替换。代码如下:

js
// 字符串花指令的剔除
traverse(ast, {
	VariableDeclarator(path) {
		if (t.isObjectExpression(path.node.init)) {
			path.node.init.properties.map(function (p) {
				let realNode = findRealValue(p.value);
				realNode && (p.value = realNode);
			});
		}
	},
});

3.函数花指令的剔除

接着来介绍一下函数花指令的剔除方案,原理与字符串花指令的剔除差不多,只不过递归的函数需要改一下。来看一下实现的代码:

js
// 函数花指令的剔除
traverse(ast, {
	VariableDeclarator(path) {
		if (t.isObjectExpression(path.node.init)) {
			path.node.init.properties.map(function (p) {
				let realNode = findRealFunc(p.value);
				realNode && (p.value = realNode);
			});
		}
	},
});
function findRealFunc(node) {
	if (t.isFunctionExpression(node) && node.body.body.length == 1) {
		let expr = node.body.body[0].argument.callee;
		if (t.isMemberExpression(expr)) {
			let objName = expr.object.name;
			let propName = expr.property.value;
			if (totalObj[objName]) {
				return findRealFunc(totalObj[objName][propName]);
			} else {
				return false;
			}
		}
		return node;
	} else {
		return node;
	}
}
// 去除函数花指令以后,更新一下 totalObj 对象
ast = generatorObj(ast);

4.这一节例子聚合代码

js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

const jscode = fs.readFileSync("./demo.js", {
	encoding: "utf-8",
});

let ast = parser.parse(jscode);

// 拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
// 拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
// 新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse("");
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
// 把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, { compact: true }).code;
// 将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

traverse(ast, {
	// 遍历所有变量
	VariableDeclarator(path) {
		// 当变量名与解密函数名相同时,就执行相应操作
		if (path.node.id.name == DecryptFuncName) {
			let binding = path.scope.getBinding(DecryptFuncName);
			binding &&
				binding.referencePaths.map(function (v) {
					v.parentPath.isCallExpression() &&
						v.parentPath.replaceWith(t.stringLiteral(eval(v.parentPath + "")));
				});
		}
	},
});

ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();

var totalObj = {};
function generatorObj(ast) {
	traverse(ast, {
		VariableDeclarator(path) {
			// init 节点为 ObjectExpression 的时候,就是需要处理的对象
			if (t.isObjectExpression(path.node.init)) {
				// 取出对象名
				let objName = path.node.id.name;
				// 以对象名作为属性名在 totalObj 中创建对象
				objName && (totalObj[objName] = totalObj[objName] || {});
				// 解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
				totalObj[objName] &&
					path.node.init.properties.map(function (p) {
						totalObj[objName][p.key.value] = p.value;
					});
			}
		},
	});
	return ast;
}
ast = generatorObj(ast);

traverse(ast, {
	VariableDeclarator(path) {
		if (t.isObjectExpression(path.node.init)) {
			path.node.init.properties.map(function (p) {
				let realNode = findRealValue(p.value);
				realNode && (p.value = realNode);
			});
		}
	},
});

function findRealValue(node) {
	if (t.isMemberExpression(node)) {
		let objName = node.object.name;
		let propName = node.property.value;
		if (totalObj[objName][propName]) {
			return findRealValue(totalObj[objName][propName]);
		} else {
			return false;
		}
	} else {
		return node;
	}
}

ast = generatorObj(ast);

traverse(ast, {
	MemberExpression(path) {
		let objName = path.node.object.name;
		let propName = path.node.property.value;
		totalObj[objName] &&
			t.isStringLiteral(totalObj[objName][propName]) &&
			path.replaceWith(totalObj[objName][propName]);
	},
});

// 函数花指令的剔除
traverse(ast, {
	VariableDeclarator(path) {
		if (t.isObjectExpression(path.node.init)) {
			path.node.init.properties.map(function (p) {
				let realNode = findRealFunc(p.value);
				realNode && (p.value = realNode);
			});
		}
	},
});
function findRealFunc(node) {
	if (t.isFunctionExpression(node) && node.body.body.length == 1) {
		let expr = node.body.body[0].argument.callee;
		if (t.isMemberExpression(expr)) {
			let objName = expr.object.name;
			let propName = expr.property.value;
			if (totalObj[objName]) {
				return findRealFunc(totalObj[objName][propName]);
			} else {
				return false;
			}
		}
		return node;
	} else {
		return node;
	}
}
// 去除函数花指令以后,更新一下 totalObj 对象
ast = generatorObj(ast);

traverse(ast, {
	CallExpression(path) {
		//callee 不为 MemberExpression 的节点,不做处理
		if (!t.isMemberExpression(path.node.callee)) return;
		//取出对象名和属性名
		let objName = path.node.callee.object.name;
		let propertyName = path.node.callee.property.value;
		//如果在 totalObj 中有相应节点,就是需要进行替换的
		if (totalObj[objName] && totalObj[objName][propertyName]) {
			//totalObj 中存放的是函数节点
			let myFunc = totalObj[objName][propertyName];
			//在原代码中,函数体其实就一行 return 语句,取出其中的 argument 节点
			let returnExpr = myFunc.body.body[0].argument;
			//判断 argument 节点类型,并且用相应的实参来构建二项式或者调用表达式
			//然后替换当前遍历到的整个 CallExpression 节点即可
			if (t.isBinaryExpression(returnExpr)) {
				let binExpr = t.binaryExpression(
					returnExpr.operator,
					path.node.arguments[0],
					path.node.arguments[1]
				);
				path.replaceWith(binExpr);
			} else if (t.isCallExpression(returnExpr)) {
				//把 arguments 数组中的下标为 1 和以后的成员,放入 newArray 中
				let newArray = path.node.arguments.slice(1);
				let callExpr = t.callExpression(path.node.arguments[0], newArray);
				path.replaceWith(callExpr);
			}
		}
	},
});

traverse(ast, {
	VariableDeclarator(path) {
		if (t.isObjectExpression(path.node.init)) {
			path.remove();
		}
	},
});

let code = generator(ast).code;
fs.writeFile("./demoNew.js", code, (err) => {});

还原流程平坦化

1.获取分发器

注意,还原流程平坦化之前,应当先进行字符串解密以及剔除花指令。在还原流程平坦化的过程中,需要先获取分发器,因为分发器中记录着代码原先的真实顺序。

以 以下代码为例,来看一下分发器在 AST 中的结构:

js
Node {
  type: "MemberExpression',
  ...
  object: Node { type: 'StringLiteral', ... value: '1|2|4|7|5|3|8|0|6' },
  property: Node { type: 'StringLiteral', ... value: 'split' },
  computed: true
}

从上述结构中可以看出,只要遍历 MemberExpression 节点,如果其中的 object 节点为 StringLiteral 类型,property 节点为 StringLiteral 类型并且 value 为 'split' 就是分发器所在的 MemberExpression 节点。其中 path.node.object.value 就是记录着代码原先真实顺序的字符串。因此,获取分发器的代码如下:

js
traverse(ast,{
  MemberExpression (path) (
    if (t.isStringLiteral(path.node.object) && t.isStringLiteral(path.node.property, { value: 'split' })) {
      console.log(path.node.object.value);
    }
  }
});
// '1|2|4|7|5|3|8|0|6'

2.解析整个 switch

js
// ***** 解析整个 switch *****
let myArr = [];
whilePath.node.body.body[0].cases.map(function (p) {
	myArr[p.test.value] = p.consequent[0];
});

3.复原语句顺序

切准备工作都已就绪,接下去就可以复原代码顺序了。实现的代码也很简单:

js
let parentPath = whilePath.parent;
varPath.remove();
whilePath.remove();
// path.node.object.value 取到的是 '1|2|4|7|5|3|8|0|6'
let shufferArr = path.node.object.value.split("|");

在上述代码中,先找到 WhileStatement 的父节点,也就是 BlockStatement 节点。然后把分发器所在的节点和 WhileStatement 节点整个移除,其实就相当于是把 BlockStatement 的 body 节点清空。然后把存有代码真实顺序的字符串,分割成数组 shufferArr。遍历该数组,从之前解析好的 myArr 中,取出对应索引的代码节点,塞回到 BlockStatement 的 body 节点中。

单个 switch 流程平坦化还原完成了,现在要应用到整个 JS 文件中去。由于原代码中,存在嵌套的 switch 流程平坦化,为了防止顺序错乱,笔者在这里采用每遍历一轮 ast,只处理一个 switch 流程平坦化的方案,

完整的还原代码如下:

js
// 遍历循环分发器:粗暴的多循环几次,而不用去判断原代码中到底有多少个 switch 流程平坦化
for (let i = 0; i < 20; i++) {
	// 获取分发器
	traverse(ast, {
		MemberExpression(path) {
			if (
				t.isStringLiteral(path.node.object) &&
				t.isStringLiteral(path.node.property, {
					value: "split",
				})
			) {
				// 找到类型为 VariableDeclaration 的父节点
				let varPath = path.findParent(function (p) {
					return t.isVariableDeclaration(p);
				});
				// 获取下一个同级节点
				let whilePath = varPath.getSibling(varPath.key + 1);
				// 解析整个 switch
				let myArr = [];
				whilePath.node.body.body[0].cases.map(function (p) {
					myArr[p.test.value] = p.consequent[0];
				});
				// 复原语句顺序
				let parentPath = whilePath.parent;
				varPath.remove();
				whilePath.remove();
				// path.node.object.value 取到的是 '1|2|4|7|5|3|8|0|6'
				let shufferArr = path.node.object.value.split("|");
				shufferArr.map(function (v) {
					parentPath.body.push(myArr[v]);
				});
				// 每遍历一轮 ast,只处理一个 switch 流程平坦化就停止遍历
				path.stop();
			}
		},
	});
}

还原 switch 流程平坦化的代码其实没有多少行,相对比剔除花指令还要容易一点。当然 switch 流程平化混淆并不是只有这一种。因此应当掌握原理,在实际应用中具体情况具体分析。

4.协议逆向(怎么去分析代码)

经过前面几节的还原处理,代码基本就可以阅读了。还原以后的 JS 文件可以静态分析,也可以替换掉网站中的原文件后,进行动态调试。替换文件可以使用 fiddler 软件自动响应或者 Chrome Local Overrides 功能。

接下来,就可以对之前抓包过程中,找出来的几个加密参数,进行逆向分析了。在 Chrome 开发者工具的 Sources 面板中,找到替换后的 JS 文件,按下 Ctrl+F 快捷键,在跳出来的搜索框中输入 data,找到关键代码:

js

这么一看就明朗了,clientid 就是用 JS 生成的一个随机值,通过多次抓包观察,发现该值在刷新网页的时候才重新生成。 data 由四个部分拼接而成....

Released under the MIT License.