桂林手机网站建设抖音seo怎么做的
文章目录
- 1. 常量的混淆
- 1.1 十六进制字符串
- 1.2 unicode字符串
- 1.3 字符串的ASCII码混淆
- 1.4 字符串常量加密
- 1.5 数值常量加密
- 2. 增加逆向分析难度
- 2.1 数组混淆
- 2.2 数组乱序
- 2.3 花指令
- 2.4 jsfuck
- 3. 代码执行流程的防护
- 3.1 流程平坦化
- 3.2 逗号表达式
- 4. 其他代码防护方案
- 4.1 eval加密
- 4.2 内存爆破
- 4.3 检测代码是否格式化
- 5. 小结
正式学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入门与实战