JS代码安全防护常见的方式

文章介绍了JavaScript代码混淆的各种方法,包括常量混淆、增加逆向分析难度、代码执行流程的防护以及其他代码防护方案,如eval加密和内存爆破,以增强代码的安全性和反爬虫能力。混淆的主要目标是增加逆向工程的难度,例如通过使用Base64编码、数组乱序、花指令和jsfuck等技术。文章还提及了AST在理解和处理混淆代码中的作用。
摘要由CSDN通过智能技术生成


正式学AST之前,还是有必要了解一下常见的JS代码安全防护方式,
最近看了一本名叫《反爬虫AST原理与还原混淆实战》的书,对于常见的JS代码安全防护方式,做一下学习笔记记录总结。

1. 常量的混淆

1.1 十六进制字符串

'yyyy-MM-dd'

====>

'\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64'

1.2 unicode字符串

var Week = ['日', '一', '二', '三', '四', '五', '六']

======>
    
var Week = ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d']

1.3 字符串的ASCII码混淆

console.log('x'.charCodeAt())
console.log('b'.charCodeAt())
console.log(String.fromCharCode(120,98))

结果:
120
98
xb
// 字符串转字节数组
function stringToByte(str) {
    var byteArr = [];
    for (var i = 0; i < str.length; i++) {
        byteArr.push(str.charCodeAt(i));
    }
    return byteArr;
}

console.log(stringToByte('AstIsGood'));

// 结果
[
   65, 115, 116,  73,
  115,  71, 111, 111,
  100
]

1.4 字符串常量加密

​ 字符串常量加密的核心思想是,先把字符串加密得到密文,然后在使用前,调用对应的解密函数去解密,得到明文。代码中仅出现解密函数和密文。当然,也可以使用不同的加密方法去加密字符串,再调用不同的解密函数去解密。

比如,字符串加密方式采用最简单的 Base64编码:

replace  Base64编码后为 cmVwbGFjZQ==
getMonth Base64编码后为 Z2V0TW9udGg=
getDate  Base64编码后为 Z2V0RGF0ZQ==
0        Base64编码后为 MA==
toString Base64编码后为 dG9TdHJpbmc=

浏览器中有自带的Base64编码和解码的函数,其中btoa用来编码, atob用来解码。但在实际的混淆应用中,最好还是自己去实现它们,然后加以混淆。注意,字符串加密后,需要把对应的解密函数也放入代码中,才能正常运行。

下面有一段js代码:

Date.prototype.format = function (formatStr) {
    var str = formatStr;
    var Week = ['日', '一', '二', '三', '四', '五', '六'];
    str = str.replace(/yyyy|YYYY/, this.getFullYear());
    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'));

通过以上几种方式混淆之后:

Date.prototype.\u0066\u006f\u0072\u006d\u0061\u0074 = function(formatStr) {
    var \u0073\u0074\u0072 = \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072;
    var Week = ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'];
    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 = str[atob('cmVwbGFjZQ==')](/MM/, (this[atob('Z2V0TW9udGg=')]() + 1) > 9 ? (this[atob('Z2V0TW9udGg=')]() + 1)[atob('dG9TdHJpbmc=')]() : atob('MA==') + (this[atob('Z2V0TW9udGg=')]() + 1));
	str = str[atob('cmVwbGFjZQ==')](/dd|DD/, this[atob('Z2V0RGF0ZQ==')]() > 9 ? this[atob('Z2V0RGF0ZQ==')]()[atob('dG9TdHJpbmc=')]() : atob('MA==') + this[atob('Z2V0RGF0ZQ==')]());
    return str;
}
console.log( new \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()[String.fromCharCode(102, 111, 114, 109, 97, 116)]('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

1.5 数值常量加密

​ 算法加密过程中,会使用一些固定的数值常量,如 MD5中的常量0x67452301、0xefcdab89、0x98badcfe 和 0x10325476,

参考:JS 实现MD5加密

以及sha1中的常量0x67452301、0xefcdab89、0x98badcfe、0x10325476和0xc3d2elf0。

参考:JS实现 Sha1 加密

因此,在标准算法逆向中,会通过搜索这些数值常量,来定位代码关键位置,或者确定使用的是哪个算法。当然,在代码中不一定会写十六进制形式,如0x67452301,在代码中可能会写成十进制的1732584193。

sha1 : 0xefcdab89 4023233417

这里只是提供一种思路,实际上现在很多比较新的网站用的是自己写的MD5或者魔改之后的MD5,通过搜索以上变量不一定能搜到算法位置。

2. 增加逆向分析难度

2.1 数组混淆

该类混淆没有统―称呼,将所有的字符串都提取到一个数组中,然后在需要引用字符串的地方,全部都以数组下标的方式访问数组成员。

例如:

var bigArr = ['Date', 'getTime', 'log'];
console[bigArr[2]](new window[bigArr[0]]()[bigArr[1]]());

这里展示的代码,阅读难度已经大大增加。当代码为上千行,数组提取的字符串也有上千个。在代码中要引用字符串时,全都以bigArr[1001]和 bigArr[1002]访问,就会大大增加理解难度,不容易建立对应关系。

在JavaS语言中,语法灵活,同一个数组中,可以同时存放各种类型,如布尔值、字符串﹑数值、数组、对象和函数等。例如:

var bigArr = [
    true,
    'astisGood',
    1000,
    [100, 200, 300], {
        name: 'astisGood',
        money: 0
    },
    function () {
        console.log('Hello')
    }
];
console.log(bigArr[0]);			//true
console.log(bigArr[1]);			//astisGood
console.log(bigArr[2]);			//1000
console.log(bigArr[3][0]);		//100
console.log(bigArr[4].money);	//0
console.log(bigArr[5]());		//Hello

因此,可以把代码中的一部分函数提取到大数组中。并且为了安全,通常会对提取到数组中的字符串进行加密处理,把代码处理成字符串就可以进行加密了。对于1.4那段js代码,可以改写为以下形式:

var bigArr = [
	'\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', 
	'\u516d', 'cmVwbGFjZQ==', 'Z2V0TW9udGg=', 'dG9TdHJpbmc=', 
	'Z2V0RGF0ZQ==', 'MA==', ""['constructor']['fromCharCode']
];
Date.prototype.\u0066\u006f\u0072\u006d\u0061\u0074 = function(formatStr) {
    var \u0073\u0074\u0072 = \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072;
    var Week = [bigArr[0], bigArr[1], bigArr[2], bigArr[3], bigArr[4], bigArr[5], bigArr[6]];
    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 = 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])]());
    return str;
}
console.log( new \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()[bigArr[12](102, 111, 114, 109, 97, 116)]('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

2.2 数组乱序

​ 观察以上处理后的代码,数组成员与被引用的地方是一一对应的。如引用bigArr[12]的地方,需要的是String. fromCharCode函数,而该数组中下标为12的成员,也是这个函数。

​ 将数组顺序打乱可以解决这个问题,不过在数组顺序混乱后,本身的代码也引用不到正确的数组成员。此处的解决方案是,在代码中内置一段还原顺序的代码。

​ 可以使用以下代码打乱数组顺序:

var bigArr = [
	'\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', 
	'\u516d', 'cmVwbGFjZQ==', 'Z2V0TW9udGg=', 'dG9TdHJpbmc=', 
	'Z2V0RGF0ZQ==', 'MA==', ""['constructor']['fromCharCode']
];
(function(arr, num){
	var shuffer = function(nums){
		while(--nums){
			arr.unshift(arr.pop());
		}
	};
	shuffer(++num);
}(bigArr, 0x20));
console.log( bigArr );

//["cmVwbGFjZQ==", "Z2V0TW9udGg=", "dG9TdHJpbmc=", "Z2V0RGF0ZQ==", "MA==", f, "日", "一", "二", "三", "四", "五", "六"]

在这段代码中,有一个自执行的匿名函数。实参部分传入的是数组和一个任意数值。在这个函数内部,通过对数组进行弹出和压入操作来打乱顺序。除此之外,只要控制台输出,Unicode处理后的字符串就变成原来的中文。这就是之前说的十六进制字符串和Unicode都很容易被还原。
String.fromCharCode函数被移动到了下标为5的地方,但代码处引用的仍是bigArr[12],所以需要把还原数组顺序的函数放入代码中,还原数组顺序的代码逆向编写即可,如下所示:

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.\u0066\u006f\u0072\u006d\u0061\u0074 = function(formatStr) {
    var \u0073\u0074\u0072 = \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072;
    var Week = [bigArr[0], bigArr[1], bigArr[2], bigArr[3], bigArr[4], bigArr[5], bigArr[6]];
    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 = 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])]());
    return str;
}
console.log( new \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()[bigArr[12](102, 111, 114, 109, 97, 116)]('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

2.3 花指令

添加一些没有意义却可以混淆视听的代码,是花指令的核心。

这里介绍一种比较简单的花指令实现方式,以 6.1节的代码为例:

str = str.replace(/MM/, (this.getMonth() + 1) > 9 ? (this.getMonth() + 1).toString() : '0' + (this.getMonth() + 1));

把this.getMonth()+1这个二项式改为如下形式:

function _0x20ab1fxe1(a, b){
	return a + b;
}
//_0x20ab1fxe1(this.getMonth(), 1);
_0x20ab1fxe1(new Date().getMonth(), 1);	
//为了能够在控制台正常运行,把this改成new Date()

本质是把二项式拆开成三部分:二项式的左边、二项式的右边和运算符。二项式的左边和右边作为另外一个函数的两个参数,二项式的运算符作为该函数的运行逻辑。这个函数本身是没有意义的,但它能瞬间增加代码量,从而增加JS逆向者的工作量。

这个案例较为简单,但是在实际混淆中,代码可能有几千行,函数定义部分与调用部分往往相差甚远。

另外,具有相同运算符的二项式,并不是一定要调用相同的函数。如下所示代码:

function _0x20ab1fxe2(a, b){
	return a + b;
}
function _0x20ab1fxe1(a, b){
	return _0x20ab1fxe2(a, b);
}
_0x20ab1fxe1(new Date().getMonth(), 1);

上面介绍的是二项式转变为函数的花指令,其实函数调用表达式也可以处理成类似的花指令。代码如下:

function _0x20ab1fxe2(a, b){
	return a + b;
}
function _0x20ab1fxe1(a, b){
	return _0x20ab1fxe2(a, b);
}
function _0x20ab1fxe3(a, b){
	return a + b;
}
function _0x20ab1fxe4(a, b){
	return _0x20ab1fxe3(a, b);
}
_0x20ab1fxe4('0', _0x20ab1fxe1(new Date().getMonth(), 1));

2.4 jsfuck

jsfuck也可以算是一种编码。它能把JS代码转化成只用6个字符就可以表示的代码,并可以正常执行。

这6个字符分别是“(”、“+”、“!”、“[”、”]”和“)”。

转换后的JS代码难以阅读,可作为简单的保密措施,如数值常量8转成jsfuck后为:

!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]

接下来介绍jsfuck 的基本原理:

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

[]在JS中表示空数组,因此+[]等于0,!+[]等同于! 0。

JS是一种弱类型的语言,弱类型并不是代表没有类型,是指JS引擎会在适当的时候,自动完成类型的隐式转换。

!是JS中的取反,这时需要一个布尔值。在JS中,七种值为假值,其余均为真值。这七种值分别是 false、undefined , null、 0 、-0、NaN 和""。因此,0转换为布尔值为false,再取反就是true,也就是!+[]== true。又如!![],数组转换成布尔值为true,然后两次取反,依旧等于true。

JS中的+作为二元运算符时,假如有一边是字符串,就代表着拼接;两边都没有字符串,就代表着数值相加。true转换为数值等于1。剩余的部分原理相同,不再赘述。

在实际开发中,jsfuck 的应用有限,只会应用于JS文件中的一部分代码。主要原因是它的代码量非常庞大且还原它较为容易。例如,把上述代码直接输入控制台运行,就会输出8。

遇到jsfuck混淆,可以拿到控制台输出分析,也可以使用在线解析网站解析如:jsfunck在解析线网站

一些网站之所以用它进行加密,是因为个别情况下,把整段jsfuck代码输人控制台运行会报错,尤其是当它跟别的代码混杂时。

可以看一下网洛者第四题:jsfuck混淆

3. 代码执行流程的防护

3.1 流程平坦化

在一般的代码开发中,会有很多的流程控制相关代码,即代码中有很多分支,这些分支会具有一定的层级关系。

在流程平坦化混淆中,会用到switch语句,因为switch语句中的case块是平级的,而且调换case块的前后顺序并不影响代码原先的执行逻辑。

为了方便理解,这里举一个简单的例子,代码如下:

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块的跳转顺序应该是7、5、1、3、2、4、6。

只有case块按照这个流程执行,才能跟原始代码的执行顺序保持一致。

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

(1)计算一次 switch表达式。

(2)把表达式的值与每个case的值进行对比。

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

可处理代码如下:

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() );
//输出 21000

3.2 逗号表达式

逗号运算符的主要作用是把多个表达式或语句连接成一个复合语句。

如下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

等价于:

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

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

上述案例只是单纯的连接语句,没有混淆力度。可以再做进一步处理,代码如下:

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声明了。

另外,既然逗号运算符连接多个表达式,只会返回最后一个表达式计算后的结果,那么可以在最后一个表达式之前插入不影响结果的花指令。

最终处理后的代码如下:

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+50,b+60,c+70、d+80,e+90,f+100这些花指令并无实际意义,不影响原先的代码逻辑。

test2虽有6个参数,但是不传参也可以调用,只不过各参数的初始值为undefined。

逗号表达式混淆不仅能处理赋值表达式,还能处理调用表达式、成员表达式等。

如:

var obj = {
	name: 'astisgood',
	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
}

========>

var obj = {
	name: 'astisgood',
	add: function(a, b){
		return a + b;
	}
}
function sub(a, b){
	return a - b;
}
function test() {
    return c = (b = (a = 1000, sub)(a, 3000) + 1, b + (0, obj).add(b, 2000)),
    c + (0, 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)。

第⒉种方法是调用表达式在等号右边的情况。例如 test函数中的第3条语句里面的b+obj. add(b,2000),可以对obj. add进行包装,处理成b+(0,obj.add)(b,2000)或者b+(0,obj).add(b,2000),括号中的0可以是其他花指令。

了解以上知识之后,我们可以接着对之前的代码进行混淆,

混淆前:

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.\u0066\u006f\u0072\u006d\u0061\u0074 = function(formatStr) {
    var \u0073\u0074\u0072 = \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072;
    var Week = [bigArr[0], bigArr[1], bigArr[2], bigArr[3], bigArr[4], bigArr[5], bigArr[6]];
    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 = 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])]());
    return str;
}
console.log( new \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()[bigArr[12](102, 111, 114, 109, 97, 116)]('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

混淆后:

//最开始的大数组
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.\u0066\u006f\u0072\u006d\u0061\u0074 = function(formatStr, str, Week) {
//因为基本上都会处理成一行代码,所以return语句可以提到最上面
return str = 	
		(str = (
			Week = (
				\u0073\u0074\u0072 = \u0066\u006f\u0072\u006d\u0061\u0074\u0053\u0074\u0072, 
				[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 \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()[bigArr[12](102, 111, 114, 109, 97, 116)]('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

再看看它最初始的样子:

Date.prototype.format = function (formatStr) {
    var str = formatStr;
    var Week = ['日', '一', '二', '三', '四', '五', '六'];
    str = str.replace(/yyyy|YYYY/, this.getFullYear());
    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'));

大眼一瞅,对比一下,已经面目全非了。

4. 其他代码防护方案

4.1 eval加密

看以下代码:

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, {}))

这段代码的一个eval()函数,它用来把一段字符串当作JS代码来执行。也就是说,传给eval()的参数是一段字符串。但在上述代码中,传给eval()函数的参数是一个自执行的匿名函数。这说明,这个匿名函数执行后会返回一段字符串,并且用eval()执行这段字符串,执行效果与eval加密前的代码效果等同。那就可以把这个匿名函数理解成是一个解密函数了。由此可见, eval加密其实和eval()关系不大, eval()只是用来执行解密出来的代码。

再来观察传给这个匿名函数的实参部分。观察第1个实参p和第4个实参k。可以看出处理方式很简单,提取原始代码中的一部分标识符,然后用它自己的符号占位,最后再对应替换回去就解密了。

最后介绍eval解密。这个比较容易,既然这个自执行的匿名函数就是解密函数,把上述代码中的eval删去,剩余代码在控制台中执行,就得到原始代码。

4.2 内存爆破

内存爆破是在代码中加入死代码,正常情况下这段代码不执行,当检测到函数被格式化或者函数被Hook,就跳转到这段代码并执行,直到内存溢出,浏览器会提示Out of Memory程序崩溃。内存爆破的代码如下所示:

var d =[0x1,0x0,0x0];
function b(){
	for(var i=0x0,c=d.length; i<c; i++){
		d.push(Math.round(Math.random()));
		c=d.length;
	}
}

for循环的结束条件是i<c,其中c的初始化值是数组的大小。看着像是一个遍历数组的操作,但是在循环中,又往数组中 push了成员,接着又重新给c赋值为数组的大小。这时这段代码就永远也不会结束了,直到内存溢出。

这段代码中的for循环是一个死循环,它的形式不像while(true)这样明显。尤其是考虑将代码混淆以后,更具迷惑性,增加了逆向分析难度。

4.3 检测代码是否格式化

​ 检测的思路很简单,在JS中,函数是可以转为字符串的。因此可以选择一个函数转为字符串,然后跟内置的字符串对比或者用正则匹配。

函数转为字符串很简单,直接.toString()就🆗了。

在Chrome开发者工具中,把代码格式化后,会产生一个后缀为:formatted的文件。之后这个文件中设置断点,触发断点后,会停在这个文件中。但是,这时把某个函数转为字符串,取到的依然是格式化之前的代码。

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

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

5. 小结

混淆的目的是增加逆向开发者的工作量。例如,原本一小时就可以解决的算法,混淆后可能需要几天才能解决。当算法每天更新,逆向开发者自然就放弃了。目前市面上已有此类方案,只不过变化的算法仅限于微调,如算法中的常量、算法加密前的参数顺序等。如果要实现此类方案,需要一种自动化处理代码的方案,AST为此而生。

文章到此结束,感谢您的阅读,下篇文章见!

AST学习课程推荐,市面上关于AST的课程不多,这里就推荐我学过的两门吧
xjb课程:反爬虫AST混淆JavaScript与还原实战
蔡老板和风佬课程:AST入门实战+零基础JavaScript补环境课程
也可以看蔡老板的知识星球学习:AST入门与实战

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰履踏青云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值