0


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

文章目录

正式学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))

结果:
12098
xb
// 字符串转字节数组functionstringToByte(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(newDate().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]](newwindow[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){varshuffer=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){varshuffer=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(newDate().getMonth(),1);//为了能够在控制台正常运行,把this改成new Date()

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

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

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

function_0x20ab1fxe2(a, b){return a + b;}function_0x20ab1fxe1(a, b){return_0x20ab1fxe2(a, b);}_0x20ab1fxe1(newDate().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(newDate().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块的前后顺序并不影响代码原先的执行逻辑。

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

functiontest1(){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块。

可处理代码如下:

functiontest2(){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代码:

functiontest1(){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

等价于:

functiontest1(){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语句的使用也是没有问题的,它会返回最后一个表达式计算后的结果,但是前面的表达式依然会执行。

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

functiontest2(){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声明了。

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

最终处理后的代码如下:

functiontest2(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;}}functionsub(a, b){return a - b;}functiontest(){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;}}functionsub(a, b){return a - b;}functiontest(){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){varshuffer=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){varshuffer=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(newDate().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(newRegExp('\\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];functionb(){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入门与实战


本文转载自: https://blog.csdn.net/weixin_44327634/article/details/129048168
版权归原作者 冰履踏青云 所有, 如有侵权,请联系我们删除。

“JS代码安全防护常见的方式”的评论:

还没有评论