標題:JavaScript 反混淆

taibeihacker

Moderator

JavaScript 反混淆​

1 常量的混淆原理​

1.1 对象属性的两种访问方式​

JavaScript 中,對象訪問屬性有兩種方式。
用點訪問
用括號訪問
1
2
3
4
5
6
7
8
9
10
11
12
13
function People(name) {
this.name=name;
}
People.prototype.sayHello=function () {
console.log('Hello');
}
var p=new People('pName');
console.log(p.name); //pName
p.sayHello(); //Hello
console.log(p['name']);//pName
p['sayHello'](); //Hello
用括號訪問的方式可以針對其中的字符串做混淆

1.2 常见的混淆方式​

1.2.1 十六进制字符串​

以下面這段代碼為例:
1
2
3
4
5
6
7
8
9
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'));
針對代碼中出現的字符串,可以將其轉換為16 進制。
1
2
3
4
5
6
7
8
9
10
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('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64'));

1.2.2 unicode 字符串​

在JS 中,字符串除了可以表示成十六進制的形式外,還支持unicode 編碼。
以var Week=['日', '一', '二', '三', '四', '五', '六']; 為例,可以轉換成:
1
var Week=['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'];
此外,JS 解析器同樣支持unicode 編碼出現在標識符中:
1
2
3
4
5
6
7
8
9
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'];
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 \u0077\u0069\u006e\u0064\u006f\u0077['\u0044\u0061\u0074\u0065']()['format']('\x79\x79\x79\x79\x2d\x4d\x4d\x2d\x64\x64') );

1.2.3 字符串的 ASCII 码​

為了完成字符串的ASCII 碼混淆,在這裡需要使用兩個函數,一個是String 對像下的charCodeAt 方法,另外一個是String 類下的fronCharCode 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('G'.charCodeAt(0)); //71
console.log('e'.charCodeAt(0)); //101
console.log(String.fromCharCode(71, 101));
function stringToByte(str) {
var byteArr=[];
for (var i=0; i str.length; i++) {
byteArr.push(str.charCodeAt(i));
}
return byteArr;
}
console.log(stringToByte('Geekby'));
將代碼轉換成字符串後,利用eval 函數去執行:
1
2
3
4
5
6
7
8
9
10
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['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 \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') );
//輸出結果2020-07-04
202111181532498.png-water_print

1.2.4 字符串常量编码或加密​

字符串常量編碼或加密的核心思想是,先把字符串編碼或加密得到密文,然後在使用之前,調用對應的解碼或解密函數去解密,得到明文。
JS 中自帶Base64 編碼解碼的函數,btoa 用來編碼,atob 用來解碼。
但在實際的混淆應用中,最好還是採用自定義函數的方式,然後加以混淆。在這裡就用atob 來代替Base64 解碼,處理後的代碼為:
1
2
3
4
5
6
7
8
9
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.2.5 数值常量转换​

算法加密過程中,會使用到一些固定的數值常量。比如,MD5 中的常量0x67452301,0xefcdab89,0x98badcfe,0x10325476。
SHA1 中的常量0x67452301,0xefcdab89,0x98badcfe,0x10325476,0xc3d2e1f0。
因此,在標準算法逆向中,經常會通過搜索這些數值常量,來定位代碼關鍵位置,或者確定使用的是哪個算法。當然,在代碼中不一定會寫十六進制形式。比如,0x67452301,在代碼成可能會寫成十進制的1732584193。安全起見,可以把這些數值常量也簡單加密下。
可以使用位異或的特性來加密。比如,a^b=c,那麼c^b=a。
以SHA1 算法中的0xc3d2e1f0 常量為例,0xc3d2e1f0 ^0x12345678=0xd1e6b788, 那麼在代碼中可以用0xd1e6b788 ^0x12345678 來代替0xc3d2e1f0,其中0x12345678 可以理解成密鑰,可以隨機生成。
總之,混淆方案並不一定是單一使用,各種方案之間也可以結合使用。

2 增加逆向工作量​

2.1 数组混淆​

把代碼中所有的字符串,都提取到一個數組中,然後需要引用字符串的地方,全都以數組下標的方式去訪問數組成員。
1
2
3
4
var bigArr=['Date', 'getTime', 'log'];
console[bigArr[2]](new window[bigArr[0]]()[bigArr[1]]());
console['log'](new window.Date().getTime())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 数组乱序​

將上述提取出來的數組順序打亂,在取元素時,進行還原。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 );

2.3 花指令​

給代碼添加一些沒有意義的代碼,是花指令的核心。例如:把this.getMonth() + 1 這個二項式稍作變動:
1
2
3
4
5
function _0x20ab1fxe1(a, b){
return a + b;
}
//_0x20ab1fxe1(this.getMonth(), 1);
_0x20ab1fxe1(new Date().getMonth(), 1);//輸出11
還可以進一步嵌套:
1
2
3
4
5
6
7
function _0x20ab1fxe2(a, b){
return a + b;
}
function _0x20ab1fxe1(a, b){
return _0x20ab1fxe2(a, b);
}
_0x20ab1fxe1(new Date().getMonth(), 1);//輸出11
此外,為了進一步增加代碼複雜度,可以將相同功能的代碼放到不同的函數當中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
//輸出'11'

2.4 jsfuck​

jsfuck 可以算是一種編碼。它能把JS 代碼轉化成只用6 個字符就可以表示的代碼,且完全可以正常執行。
這6 個字符分別是[]()!+。轉換之後的JS 代碼難以閱讀,可以作為些簡單的混淆措施。
在線編碼的網站:http://www.jsfuck.com

3 代码执行流程的防护原理​

3.1 流程平坦化​

代碼本來是依照邏輯順序執行的,控制流平坦化是把原來的代碼的基本塊拆分。
把本來順序執行的代碼塊用switch case 打亂分發,用分發器和一個變量,把原本代碼的邏輯連接起來。
如下面這段代碼:
1
2
3
4
5
6
7
8
9
10
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() );
經過流程平坦化之後,變為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
 
返回
上方