如无特殊说明,以下为处理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 window['Date']()['format']('yyyy-MM-dd'));
//输出结果 2020-07-04
1.混淆前的代码处理
1.改变对象属性的访问方式
js中对象属性的访问方式有两种:
1.console.log() 用.属性名访问
2.console"log" 用[key]的方式访问
调用属性的语句在ast中type为MemberExpression。在ast中可以看到这两种访问属性的区别。
console.log在ast中其MemberExpression下的property为Identifier标识符
console[“log”]在ast中其MemberExpression下的property为StringLiteral字面量
在js混淆中常会见到object[“属性名”],这种方式。解混淆也非常简单,观察这两种在ast节点中的区别然后把object[“属性名”]这种ast节点中的值替换为object.属性名 这种在ast节点中的值。实例代码如下:
traverse(ast, {
MemberExpression(path) {
// 属性为StringLiteral的是["属性名"]这种调用方式
if (t.isStringLiteral(path.node.property)) {
console.log(path + '')
// 获取属性名
let name = path.node.property.value;
// 将属性名改成Identifier并替换掉原先的属性
path.node.property = t.identifier(name);
}
// 属性调用方式,false为标识符调用,true为stringLiteral字符串调用,["属性名"]这种调用方式时该值为true,这里改为false
path.node.computed = false;
},
});
// console.log(generator(ast).code)
/*
// 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 window.Date().format('yyyy-MM-dd'));
*/
2.标准内置对象的处理
js中有全局函数和标准内置对象,其都算是window对象的属性。这样,这些全局函数和标准内置对象也有两种访问方式。1.window.name 2.window[‘name’]。常用的windo下的属性如下:
eval, parseInt, encodeURIComponent, Object, Function, Boolean, Number, Math, Date, String, RegExp, Array。
处理代码如下:
traverse(ast, {
Identifier(path){
let name = path.node.name;
if('eval|parseInt|encodeURIComponent|Object|Function|Boolean|Number|Math|Date|String|RegExp|Array'.indexOf(name) != -1){
path.replaceWith(t.memberExpression(t.identifier('window'), t.stringLiteral(name), true));
}
}
});
// console.log(generator(ast).code)
/*
// js源码在本文开头
// 处理后
window["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 window['Date']()['format']('yyyy-MM-dd'));
*/
2.常量与标识符的混淆
1.实现数值常量加密
利用异或^的特性来进行数值常量的加密
随机生成一个值记为key,则对 常量value有 ,value^key = cipherNum ,value = cipherNum ^ key,所以我们可以把常量a节点替换成一个二项式 (binaryExpression)节点来进行加密,其operator = ^,left为cipherNum,right为key。实例代码如下:
traverse(ast, {
NumericLiteral(path){
let value = path.node.value;
let key = parseInt(Math.random() * (999999 - 100000) + 100000, 10);
let cipherNum = value ^ key;
path.replaceWith(t.binaryExpression('^', t.numericLiteral(cipherNum), t.numericLiteral(key)));
//替换后的节点里也有numericLiteral节点,会造成死循环,因此需要加入path.skip()
path.skip();
}
});
// console.log(generator(ast).code)
/*
// js源码在本文开头
// 处理后
Date.prototype.format = function (formatStr) {
var str = formatStr;
var Week = ['日', '一', '二', '三', '四', '五', '六'];
str = str['replace'](/yyyy|YYYY/, this['getFullYear']());
str = str['replace'](/MM/, this['getMonth']() + (709031 ^ 709030) > (212571 ^ 212562) ? (this['getMonth']() + (366139 ^ 366138))['toString']() : '0' + (this['getMonth']() + (801393 ^ 801392)));
str = str['replace'](/dd|DD/, this['getDate']() > (914272 ^ 914281) ? this['getDate']()['toString']() : '0' + this['getDate']());
return str;
};
console.log(new window['Date']()['format']('yyyy-MM-dd'));
};
2.实现字符串加密
实例如下:
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();
}
});
// console.log(generator(ast).code)
/*
// js源码在本文开头
// 处理后
Date.prototype.format = function (formatStr) {
var str = formatStr;
var Week = [atob("5Q=="), atob("AA=="), atob("jA=="), atob("CQ=="), atob("2w=="), atob("lA=="), atob("bQ==")];
str = str[atob("cmVwbGFjZQ==")](/yyyy|YYYY/, this[atob("Z2V0RnVsbFllYXI=")]());
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 window[atob("RGF0ZQ==")]()[atob("Zm9ybWF0")](atob("eXl5eS1NTS1kZA==")));
};
*/
3.数组混淆
实例代码如下:
let bigArr = [];
traverse(ast, {
StringLiteral(path){
let cipherText = base64Encode(path.node.value);
// 获取加密值在数组中的索引
let bigArrIndex = bigArr.indexOf(cipherText);
// 索引赋值给index
let index = bigArrIndex;
// 在数组中没有找到加密值的索引
if(bigArrIndex == -1){
// 将加密值push到数组中,并接收数组的长度
let length = bigArr.push(cipherText);
// 加密值在数组中的索引
index = length -1;
}
// 生成atob(arr[index])节点
let encStr = t.callExpression(
t.identifier('atob'),
[t.memberExpression(t.identifier('arr'),
t.numericLiteral(index), true)]);
// 替换节点
path.replaceWith(encStr);
}
});
// 混淆后需要将生成的数组也放到混淆后的js代码中,这样混淆后的代码才能执行
// 将数组中的成员转换成节点
bigArr = bigArr.map(function(v){
return t.stringLiteral(v);
});
// 生成var arr = [...]
bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(bigArr));
bigArr = t.variableDeclaration('var', [bigArr]);
// 将新生成的数组添加到js代码的开头。unshift添加到开头,push添加到末尾
ast.program.body.unshift(bigArr);
/*
var arr = ["5Q==", "AA==", "jA==", "CQ==", "2w==", "lA==", "bQ==", "cmVwbGFjZQ==", "Z2V0RnVsbFllYXI=", "Z2V0TW9udGg=", "dG9TdHJpbmc=", "MA==", "Z2V0RGF0ZQ==", "RGF0ZQ==", "Zm9ybWF0", "eXl5eS1NTS1kZA=="];
Date.prototype.format = function (formatStr) {
var str = formatStr;
var Week = [atob(arr[0]), atob(arr[1]), atob(arr[2]), atob(arr[3]), atob(arr[4]), atob(arr[5]), atob(arr[6])];
str = str[atob(arr[7])](/yyyy|YYYY/, this[atob(arr[8])]());
str = str[atob(arr[7])](/MM/, this[atob(arr[9])]() + 1 > 9 ? (this[atob(arr[9])]() + 1)[atob(arr[10])]() : atob(arr[11]) + (this[atob(arr[9])]() + 1));
str = str[atob(arr[7])](/dd|DD/, this[atob(arr[12])]() > 9 ? this[atob(arr[12])]()[atob(arr[10])]() : atob(arr[11]) + this[atob(arr[12])]());
return str;
};
console.log(new window[atob(arr[13])]()[atob(arr[14])](atob(arr[15])));
*/
4.数组乱序
实例代码如下:
let bigArr = [];
traverse(ast, {
StringLiteral(path){
let cipherText = base64Encode(path.node.value);
// 获取加密值在数组中的索引
let bigArrIndex = bigArr.indexOf(cipherText);
// 索引赋值给index
let index = bigArrIndex;
// 在数组中没有找到加密值的索引
if(bigArrIndex == -1){
// 将加密值push到数组中,并接收数组的长度
let length = bigArr.push(cipherText);
// 加密值在数组中的索引
index = length -1;
}
// 生成atob(arr[index])节点
let encStr = t.callExpression(
t.identifier('atob'),
[t.memberExpression(t.identifier('arr'),
t.numericLiteral(index), true)]);
// 替换节点
path.replaceWith(encStr);
}
});
// 混淆后需要将生成的数组也放到混淆后的js代码中,这样混淆后的代码才能执行
// 将数组中的成员转换成节点
bigArr = bigArr.map(function(v){
return t.stringLiteral(v);
});
// 数组乱序写的较为简单,指定一个循环次数将数组尾部的成员pop出放到数组头部,还原顺序时只需逆向操作即可
(function(arr, num) {
function gtf(arr, num) {
while(--num){
arr.unshift(arr.pop())
}
};
gtf(arr, num);
})(bigArr, 0x10)
//构建数组声明语句
bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(bigArr));
bigArr = t.variableDeclaration('var', [bigArr]);
//读取还原数组顺序的函数,并解析成astFront
const jscodeFront = fs.readFileSync("./demoFront.js", {
encoding: "utf-8"
});
let astFront = parser.parse(jscodeFront);
//先把还原数组顺序的代码,加入到被混淆代码的ast中
ast.program.body.unshift(astFront.program.body[0]);
//把数组放到被混淆代码的ast最前面
ast.program.body.unshift(bigArr);
demoFront.js中的js代码为还原顺序代码。如下:
(function(arr, num) {
function gtf(arr, num) {
while(--num){
arr.push(arr.shift())
}
};
gtf(arr, num);
})(arr, 0x10)
5.十六进制编码
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;
path.node.property = t.stringLiteral(hexEnc(name));
}
path.node.computed = true;
}
});
6.标识符混淆
这里用scope.getOwnBinding()方法来实现标识符混淆,这个方法可以获得当前节点的自己的绑定。比如,在program节点下使用该方法可以获得全局标识符名,而函数内部局部标识符名不会被获取到。那么要获取函数局部标识符,可以遍历函数节点。在FunctionExpression和FunctionDeclaration节点下使用getOwnBinding会获取的局部标识符,而不会获取到全局标识符名。遍历三种节点,执行同一个重命名方法,代码如下:
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);
}
});
}
traverse(ast, {
'Program|FunctionExpression|FunctionDeclaration'(path) {
renameOwnBinding(path);
}
});
上述代码先遍历当前节点中所有的Identifier,得到Identifier 的name属性,通过getOwnBinding判断是否是当前节点自己的绑定。如果binding 为undefined,则表示是其父级函数的标识符或者是全局的标识符,就将该标识符名作为属性名,放入到globalBindingObj对象中。如果 binding存在,则表示是当前节点自己的绑定,就将该标识符作为属性名,binding 作为属性值,放入到OwnBinding0bj对象中。
这里有四点需要注意一下:
(1) globalBinding0bj中存放的不是所有的全局标识符,而是当前节点引用到的全局
标识符。因为重命名标识符的时候,不能与引用到的全局标识符重名,需要进行判断。至于没有引用到的全局标识符名,就是要重名才更具迷惑性,反正最后使用的还是当前节点定义的局部标识符。
(2)OwnBinding0bj中存储对应标识符的binding。因为重命名标识符的时候,需要使
用binding. scope.rename方法。
(3)把标识符名作为对象的属性名。因为一个Identifier有多处引用就会遍历到多个,
但实际上只需要调用一次scope.rename即可完成所有引用处的重命名。而对象属性名具有唯一性,就可以只保留最后一个同名标识符。
(4))最好把ast先转成代码,再次进行解析后再进行标识符混淆。修
改AST节点的时候,使用Path对象的方法的话,Babel会更新Path信息。但是实际应用中并不能做到全部使用Path对象的方法。比如,用types组件生成新节点,由于types组件生成的是节点,并不是Path对象,那么binding 从何而来?
接下来肯定是要遍历OwnBinding0bj对象中的属性,来进行重命名了,代码如下:
for(let oldName in OwnBindingObj) {
do {
var newName = '_0x2ba6ea' + i++;
} while(globalBindingObj[newName]);
OwnBindingObj[oldName].scope.rename(oldName, newName);
}
上述代码中使用do… .while循环来随机取一个标识符名,直到与当前节点引用到的全局标识符名不一样的时候,进行重命名。上述代码看似完美,但实际上 getOwnBinding会取到当前函数中的子函数的参数名。虽然不影响最后结果,因为在当前函数中对子函数的参数进行重命名了,当遍历到子函数的时候,还会再次重命名。
7.标识符的随机生成
在前一小节中,重命名标识符的时候使用的是固定的_0x2ba6ea加上一个自增的数字来作为新的标识符名。这一小节将使用大写字母0、小写字母o、数字0这三个字符来组成标识符名。把前一小节中var newName = ‘_0x2ba6ea’ + i++;这句代码改成var newName =generatorIdentifier (i++) ;来看一下generatorIdentifier 的实现代码:
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){
return flag[v]
}).join('');
Identifier.length < 6 ? (Identifier = ('OOOOOO' + Identifier).substr(-6)):
Identifier[0] == '0' && (Identifier = 'O' + Identifier);
return Identifier;
}
3.代码块的混淆
1.二项式转函数花指令
花指令用来尽可能的隐藏原代码的真实意图。花指令有很多种实现方案。比如,要把c + d,转换为以下这种形式:
function xxx (a,b){
return a +b;
xxx(c,d);
其实不止二项式,代码中的函数调用表达式,也可以处理成类似的花指令,比如: c(d)可以转换成以下这种形式:
function xxx (a,b){
return a(b);
xxx(c,d);
这里只实现二项式的情况。从AST的角度去考虑如何实现这个方案:
(1)遍历BinaryExpression节点,取出operator、left、right
(2)生成一个函数,函数名要与当前节点中的标识符不冲突,参数固定为a和 b,返回
语句中的运算符与operator一致。
(3)找到最近的BlockStatement节点,将生成的函数加入到body数组中的最前面
(4)把原先的BinaryExpression节点替换为callExpression,callee就是函数名,
_arguments就是二项式的left和right
上述实现方案中,标识符名随意起,因为最后标识符混淆的时候,会一起处理成相似的名字。花指令的目的就是膨胀代码量,隐藏代码的真实意图。因此每遍历到一个operator,都可以生成不同名字的函数,不需要去判断是哪种operator,因为并不是一种 operator生成一个函数。实现的代码如下:
traverse(ast, {
// 遍历二项表达式
BinaryExpression(path){
// 获取计算符号
let operator = path.node.operator;
// 获取二项式左边值
let left = path.node.left;
// 获取二项式右边值
let right = path.node.right;
// 生成a节点
let a = t.identifier('a');
// 生成b节点
let b = t.identifier('b');
// 生成函数名标识符
let funcNameIdentifier = path.scope.generateUidIdentifier('xxx');
// 生成函数
let func = t.functionDeclaration(
funcNameIdentifier,
[a, b],
t.blockStatement([t.returnStatement(
t.binaryExpression(operator, a, b)
)]));
// 向上找父节点,知道找到当前函数体的BlockStatement节点
let BlockStatement = path.findParent(
function(p){return p.isBlockStatement()});
// 将生成的函数放到函数体的最前面
BlockStatement.node.body.unshift(func);
// 将当前节点替换为函数调用
path.replaceWith(t.callExpression(funcNameIdentifier, [left, right]));
}
});
2.代码的逐行加密
traverse(ast, {
FunctionExpression(path){
let blockStatement = path.node.body;
let Statements = blockStatement.body.map(function(v){
if(t.isReturnStatement(v)) return v;
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));
}
});
上述代码处理步骤如下:
(1)遍历FunctionExpression节点,其中path. node.body 即 blockStatement节点,
blockStatement.body是一个数组,里面就是函数的代码行。它的每一个成员,分别对应函数的每一行语句。然后使用数组的map方法对每一行语句分别处理。
(2)如果是返回语句,不做处理,直接返回原语句。单行加密的时候不能加密返回语句,
整个函数加密的时候,函数中可以有返回语句。
(3) let code = generator(v).code;将语句转成字符串。
(4) let cipherText = base64Encode(code);对字符串进行加密。
(5)构建atob(‘xxxx’)这种形式的代码,atob就是解密函数名。解密函数如果不是系
统自带的,还需要把解密函数一起放入代码中。生成callExpression,callee为解密函数名,参数为加密后的字符串常量,赋值给decryptFunc
(6)构建eval (atob ( ‘xxxx’))这种形式的代码,因此还需要生成callExpression,
callee为eval,参数为上一步生成的decryptFunc,最后用expressionStatement包裹。
(7)当函数中所有语句处理完后,构建新的 blockStatement替换原有的即可。
代码逐行加密方案,不建议大量应用,标志太过明显。可以只用来隐藏其中几句代码,隐藏掉某些变量的关键赋值位置。当然自动化处理过程中没法知晓是否是关键代码,除非用户在代码中加入标记,表示要隐藏哪几句代码。这个标记可以是注释。
3.ascii码混淆
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));
}
});