JS代码混淆基础

Every flight begins with a fall

Basic Confusions About JavaScript

JS混淆:通过复杂冗余的代码替换,把原来可读性高的代码转化为可读性低的代码,且前后执行效果等同

Date.prototype.format = function(formatStr) {
    var str = formatStr;
    var WEEK = ['', '', '', '', '', '', ''];
    str = str.replace(/yyyy|YYYY/, this.getFullYear())
    .replace(/mm|MM/, (this.getMonth() + 1) > 9 ? (this.getMonth() + 1).toString() : '0' + (this.getMonth() + 1).toString())
    .replace(/dd|DD/, this.getDate() > 9 ? this.getDate().toString() : '0' + this.getDate().toString());
    return str;
}
console.log(new Date().format('yyyy-MM-dd'));

上面是一段简单的代码,用于实现日期格式化

但若这是某网站前端的一段关键代码,如前后端的通信过程,那么在代码发布后就很可能被某些有心人利用,而JS混淆做的事情就是增加破解的成本,用以防护JS代码。

0x01 String obfuscation

Object Attributes Access

function Dog(name) {
    this.name = name;
}
Dog.prototype.bark = function(){
    console.log("Hello");
}
var d = new Dog('taco');
console.log(d.name);  // taco
d.bark();  // Hello
console.log(d['name']);  // taco
d['bark'](); // Hello 

有两种方式访问对象的属性(方法可以看作特殊的属性):

  • d.name:name为一个标识符,无法加密或拼接

  • d['name']:name为一个字符串,可以进行加密或拼接

Date是JS的内置对象,在JS中,很多内置对象都是window对象的属性。实际上JS代码中定义的全局变量/方法都是全局对象window的属性/方法,全局对象的属性和方法在调用时可省略对象名

替换脚本:

所以上面的代码可以改写为如下:

将对象属性/方法的访问方式改成中括号后,就能进行下一步字符串混淆了

Hex

JS中的字符串支持十六进制表示

十六进制的字符串放到控制台回车就能还原

unicode

JS中不止字符串可以用unicode,标识符也支持unicode形式

new \u0044\u0061\u0074\u0065() = new Date()

非常之amazing啊

但实际JS混淆中,标识符一般不会替换成unicode形式,因为要还原十分简单,直接放控制台输出就行。通常的混淆方式是替换成没有语义的,但看上去相似的名字,如_0x21dd83_0x21dd84,或者是由大写字母O、小写字母o、数字0组成的名字,如O0o00OO0o00o(标识符不以数字开头)

ASCII Array

既可以用来混淆字符串,也可以用来混淆代码

使用fromCharCode来还原,但这个函数接收可变长度参数,不接收数组,可以使用apply(apply方法从Function.prototype继承过来)

通过eval来执行字符串代码

String Constant

将字符串编码得到密文,使用前再调用相应的解码函数去解密,得到明文

建议减少使用JS自带的函数,而是自己去实现相应的函数,因为不管如何混淆,最终执行过程中,JS自带的函数名是固定的,可以通过Hook技术定位到关键代码。

Number Constant

在使用一些加密算法或哈希算法中,会用到一些数值常量,如MD5中的0x674523010xefcdab890x98badcfe,通过这些常量可以识别出使用的算法,因此有必要对这些常量进行编码。如使用异或的特性:a ^ b = c 则 c ^ b = a,要替换掉a,c相当于密文,b相当于密钥

0x02 Reference obfuscation

Array Index

JS的数组可以存在各种类型,将代码中的字符串、布尔值、数组、函数、对象等放到一个大数组中,使用的时候再进行引用,可以有效降低代码可读性

Array Shuffle

上面数组混淆成员与索引是一一对应的,可以将数组打乱以增加逆向工作量,但引用前需要进行还原。

  • pop:右边弹出

  • shift:左边弹出

  • unshift:左边插入

  • push:右边插入

fromCharCode由原本的6换到了4

Junk Code

一些没有意义但可以混淆视听的代码

上面将加法二项式拆成一个函数

函数调用表达式也可以处理成类似的花指令

JsFuck

将js代码转化为只用6个字符就能表示的代码

  • JS中的七种假值,其余均为真

    • false

    • undefined

    • null

    • 0

    • -0

    • NaN

    • ""

  • +作一元运算符可以强转为数值类型

    • +[] => 0

    • ![] => false

    • !+[] => true

    • !![] => true

如下在控制台输入会输出hello

实际开发中,jsfuck应用有限,一般只应用于一部分代码(jsfuck代码量太大)

0x03 Protection in Execution Flow

Control Flow Flattening

代码开发中会有很多流程控制相关的代码,即代码中有很多分支语句(if语句、switch语句),其中switch语句中case块是平级的,这就能应用到控制流平坦化。以下面代码为例

首先打乱语句顺序,将其放入switch语句中,再通过循环遍历分发器来决定执行顺序

  • 假如函数有return语句,执行到最后一个case就会退出(这里为case '5'

  • 假设函数没有return语句,JS中数组越界会返回undefined,匹配不到case会执行到break

Comma Expression

逗号运算符把多个表达式或语句连成一个复合语句

上面的test函数可以修改为如下:

(这里把变量声明都放到了函数的形参,JS允许参数实参和形参数量不一致,没有传入实参为undefined

但这样只是把多条语句写在一行,没有太大的混淆力度,可以试试嵌套的逗号表达式

逗号表达式会顺序执行每条语句,并返回最后一条语句的执行结果,因此可以在中间添加没有意义的花指令(如这里在e + 600前加了a + 50c + 80

逗号表达式混淆不仅可以处理赋值表达式,也可以处理函数调用和对象成员表达式。试着使用逗号表达式混淆下面的test函数

0x04 Other Protection Strategies

Eval Encryption

上面已经讲过了eval会把字符串当js代码执行,但直接传字符串未免太显眼,除了上面的fromCharCode来还原字符串再传入eval,还可以把一个自执行函数作为eval的参数,自执行函数(可以看成一个解密函数)将返回一个字符串。

Memory Explosion

往代码中加入死代码,正常情况下这段代码不会执行,当检测到函数被格式化或者函数被Hook,就会跳转到这段代码并执行,直到内存溢出,浏览器会提示Out of memory程序崩溃

上面这段代码不像while(true)那么明显,它不断往数组push,循环结束条件是i<c,但每次都更新c为数据长度,这段代码就永远不会结束了。

Formatted Code Detection

先将代码压缩,并在代码里面检测代码是否格式化,在Chrome开发者工具中把代码格式化后会产生一个后缀为formatted的文件。

js中函数是可以转字符串的,func.toString()func + ''

这里利用的是格式化后多出来的换行符

执行上面这段代码会发现走不到log("end"),接着程序就卡死了

但若去掉t函数的回车换行就可以了

serach 与 indexof 类似,不同的是 search 使用正则表达式匹配,而 indexOf 只是按字符串匹配的。serach将(((.+)+)+)+$视为正则表达式进行匹配。多了换行符就会无限递归下去。

下面是混淆后的版本:

Forever Debugger Loop

写个定时器死循环来无限debug

0x05 Sum up

本文介绍了常见的JS混淆手段

JS中的对象访问可以通过点访问也能通过中括号访问,改成中括号访问可以进一步对字符串进行混淆。对于字符串,可以通过各种编码或自定义的编码函数进行混淆,如JS支持字符串以十六进制表示,JS标识符支持unicode形式,还可以用ASCCI数组、base64等。对于数值常量,可以将其改写成两个数值异或。对字符串进行混淆后,在代码中可通过数组索引的方式来引用,再把字符串数组乱序。表达式、函数调用可以替换成花指令,即没有意义的嵌套。jsfuck将代码转化为6字符的表示,但由于其代码量太大,实际中只能少部分代码利用。接着是执行流程的混淆,控制流平坦化是利用switch case语句,在一个死循环中遍历一个分发器,分发器决定代码的执行顺序(case),最终匹配不到,跳到default执行break退出循环。还有就是Eval混淆,将一串编码后的字符串输入到解码函数后,得到待执行的代码字符串,再传入eval执行。这种混淆的特征太明显了,就是一大段字符串传入一个函数。或许可以改进一下,把字符串拆分为多段,再放入一个数组,可以再乱序一下,最后通过数组索引拼接后传入eval

还有一些反调试手段。如对代码进行压缩,并检测代码是否被格式化,若格式化则进行内存爆破(死循环增大数组、正则无限回溯);无限debugger。

Last updated