ast自动化javascript防护方案

如无特殊说明,以下为处理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));
	}
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值