一、布局混淆
1.1 删除无效代码
1.2 标识符重命名
二、数据混淆
2.1 数字混淆
2.1.1 进制转换
2.1.2 数学技巧
2.1.3 数字拆解
2.2 布尔混淆
2.2.1 类型转换
2.2.2 构造随机数
2.3 字符串混淆
2.4 undefined和null混淆
三、 控制混淆
3.1 不透明谓词
3.2 插入冗余代码
3.3 控制流平坦化
四、 预防混淆
代码混淆是增加黑产静态分析难度而牺牲运行效率的一种技术方案。
JS代码混淆是指通过逻辑变换算法等技术手段将受保护的代码转化为难以分析的等价代码的一种技术方案。
JS混淆通常分为四类:布局混淆、数据混淆、控制混淆、预防混淆。
一、布局混淆
布局混淆是指删除或混淆与执行无关的辅助文本信息,增加攻击者阅读和理解代码的难度,比如注释文本、调试信息。具体如下。
1.1 删除无效代码
删除注释文本、调试信息(如console.log('test')、debugger)、无用函数和数据(优化代码或需求变更后遗留内容)、缩进和换行符。
1.2 标识符重命名
标识符一般指常量名、变量名、函数名。这一步就是把标识符变得让人难以理解,反代码规范而行,比如var password='123'改成var abc='123'。常见的变形方式包括:单字母(示例:a)、十六进制字符(示例:_0x1a8c)、蛋形结构(OQ0的组合)。
标识符重名的原则是:在同一个作用域链内要避免命名碰撞,在不同作用域链中标识符命名尽可能重复。
二、数据混淆
JS常见数据类型为:数字(number)、字符串(string)、布尔值(boolean)、undefined、null、对象(object,包括Array、Function、RegExp、Date)、符号(symbols)。
混淆数据类型同样能够提升攻击者的分析难度。
2.1 数字混淆
数字混淆包括进制转换、数学技巧、数字拆解等。
2.1.1 进制转换
比如将十进制数字123转换为如下进制:
// 二进制
var num = 0b1111011
// 八进制
var num = 0173
// 十六进制
var num = 0x7b
2.1.2 数学技巧
数学技巧通常是通过变换数据类型能表示的值的范围达到混淆的目的。比如原始代码为:
var i = 1;
var A = [];
while (i < 1000) {
A[i] = i * 16;
i++;
}
可以转换为:
var i = 11;
while (i < 8003) {
A[(i-3)/8] = 2*i -6;
i += 8;
}
2.1.3 数字拆解
数字拆解是指将一个数字拆分为表达式的形式,比如将0拆分为100-36-64。
2.2 布尔混淆
布尔混淆主要利用了JavaScript隐式类型强转机制。典型的方式包括类型转换和构造随机数。
2.2.1 类型转换
“类型转换”是指将一个值从一个类型隐式地转换到另一个类型的操作。JavaScript 在强制转换boolean 值时遵循规则:JavaScript 在强制转换boolean 值时遵循两个规则:如果被强制转换为 boolean,那么将成为false 的值;其他的一切值将变为true。
强制转换boolean时变为false的值包括:undefined、null、false、+0、-0、NaN、"",混淆使用方式比如为!undefined。
2.2.2 构造随机数
利用乘法构造特定的随机数混淆布尔值。例如可以设定能被3整除的数表示true,能被7整除的数表示false。布尔值大多数代码应用场景在代码控制流中,这变相地将代码的控制流走向变得模糊,无疑增加了代码分析的难度。
2.3 字符串混淆
字符串通常包含重要的语义信息,例如提示“密码输入错误”,攻击者可以根据这个信息找到登录模块的校验、加密逻辑。常见的字符串混淆技巧,有以下几种:
将关键字符串(如加密密钥)分解成许多片段,并把它们分散在程序的各个角落;
用一个常量对字符串进行异或操作,这里异或操作相当于加密,解密时再使用该常量异或密文即可,关键点是不要暴露这个常量;
对字符串进行常规的加解密操作(base64、md5、des等);
使用Mealy状态机转换字符串的编码;
使用字符编码,即直接使用Unicode字符的码点来代替字符。写法是“反斜杠+u+码点”,比如将字母a转写成\u0061。
2.4 undefined和null混淆
利用JS的语言特性进行混淆,比如将undefined转换为void 0或者一个声明却未赋值的变量。
三、 控制混淆
控制混淆是对程序的控制流进行变换,更改程序中原有的控制流达到让代码非常难以阅读和理解的目的。控制混淆是混淆方法中效果相对较好的代码防护手段,它不同于数据混淆只是在形式上有对源代码有所更改,还会对源代码的结构产生一定影响,所以其混淆的风险也较高。
3.1 不透明谓词
【图3】 不透明谓词变换后代码流程
对于复杂化的不透明谓词也会给程序的性能带来额外的开销,降低程序运行的性能。因此尽量选择在程序的核心算法
或容易受到攻击的位置
注入不透明谓词以提高程序的安全性和阅读的可理解性,平衡由于不透明谓词带来的性能开销。
3.2 插入冗余代码
冗余代码是指与程序中的其他代码没有任何调用关系的代码,插入冗余代码的方式可以采用上文的不透明谓词。
3.3 控制流平坦化
控制流平坦化是指将程序的条件分支和循环语句组成的控制分支结构转化为单一的分发器结构,可以使用这种方法对代码中原有的控制流进行混淆,增加控制流的复杂度。控制流平坦化使攻击者在阅读代码时无法线性地阅读整个代码的运行逻辑和流程,必须按照分发器的逻辑模拟代码运行的轨迹。一个简单的分发器大多是由switch语句组成。比如有这样的原始代码:
var a = 1;
var b = 0;
while (a <= 100) {
b += a;
a ++;
}
经过混淆后的代码为:
var bVar = 1;
while (bVar != 0) {
switch(bVar) {
case 1: {
var a = 1;
var b = 0;
bVar = 2;
break
}
case 2: {
if (a <= 10)
bVar = 3;
else
bVar = 0;
break
}
case 3: {
b += a;
a ++
bVar = 2;
break
}
}
}
四、 预防混淆
预防混淆的目的是提高现有的反混淆技术破解代码的难度或检测现有的反混淆器中存在的问题,并针对现有的反混淆器
中的漏洞设计混淆算法,增加其破解代码的难度。