JS代码混淆 | js 逆向系列

JavaScript/前端
85
0
0
2024-03-08

0x01 简介

文章较长,为保证有效传递知识,已经为大家准备了 PDF 版本,放在了文末

JavaScript代码混淆是一种通过对代码进行转换和修改,使其难以理解和逆向工程的技术。它的主要目的是增加代码的复杂性和混淆性,从而提高代码的安全性和保护知识产权的能力。

下面是混淆JavaScript代码的一些主要意义:

  • 防止代码被逆向工程:混淆使得代码的逻辑变得晦涩难懂,使攻击者难以理解代码的运行原理。这可以防止恶意用户或竞争对手直接分析、修改或复制您的代码。
  • 保护知识产权:混淆代码可以防止他人盗用和复制您的代码。通过混淆,您可以更好地保护您的知识产权,确保您的代码不会被滥用或未经授权使用。
  • 减少代码大小:混淆技术可以压缩和优化代码,从而减小代码的大小,提高加载速度和性能。
  • 提高安全性:通过混淆代码,可以隐藏敏感信息、算法和逻辑,从而增加代码的安全性。这对于处理敏感数据或执行关键任务的应用程序特别重要。
  • 避免自动化攻击:混淆代码可以使自动化攻击工具难以识别和分析代码。这可以有效地阻止一些常见的攻击,如代码注入、XSS(跨站点脚本)和CSRF(跨站点请求伪造)等。

0x02 混淆与压缩

代码压缩技术主要追求的就是文件变小,这样可以提交网页或程序加载速度,一般来说,代码压缩过程中会将代码中的空格、换行符、注释和不必要的字符等删除

// 原代码
let v = "Hello World";

function foo(param) {
    console.log(param);
}

foo(v);
// 压缩后
let v="Hello World";function foo(param){console.log(param)}foo(v);

压缩后的代码虽然没有改变变量、函数等的名字,但是变成了“一坨”,对于我们阅读仍然具有干扰作用,好在压缩后的代码基本上都可以在一些在线网站上直接格式化

https://c.runoob.com/front-end/51/ https://tool.ip138.com/javascript/ https://www.sojson.com/yasuojs.html

代码混淆技术则不同,根据采用的技术不同,大多数都会将原有代码变长,但这不是重点,关键是让代码不可读,根本看不明白那种

// 原代码
let v = "Hello World";

function foo(param) {
 console.log(param)
}
foo(v);
// 简单混淆后
eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c--)r[c]=k[c]||c;k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('3 0="4 5";6 1(2){7.8(2)}1(0);',9,9,'v|foo|a|let|Hello|World|function|console|log'.split('|'),0,{}))

混淆后的代码看起来多少有奇怪,不像是人类干出来的事情,给大家分析带来了一定的干扰,但是这类是可以直接在一些在线网站解混淆的

https://matthewfl.com/unPacker.html

解混淆后发现完整的代码都被恢复了,你说它是混淆吧,很鸡肋;你说它是压缩吧,好像也没有使代码量变小

我们尝试看一下混淆前后的代码执行时间

// 混淆前
const t0 = performance.now();
let v = "Hello World";

function foo(param) {
    console.log(param);
}

foo(v)
const t1 = performance.now();
console.log(t1 - t0, 'milliseconds');

// 混淆后
const t0 = performance.now();

eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c--)r[c]=k[c]||c;k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('3 0="4 5";6 1(2){7.8(2)}1(0);',9,9,'v|foo|a|let|Hello|World|function|console|log'.split('|'),0,{}))

const t1 = performance.now();
console.log(t1 - t0, 'milliseconds');

通过这个不严谨的测试来看,似乎也没能加快代码执行的速度

这种 packer 技术,将压缩和混淆放在了一起,虽然没能有效保护源代码,但是至少在一定程度上阻止了程序自动化分析

接下来我们看一个稍微复杂的混淆

// 原代码
let v = "Hello World";

function foo(param) {
    console.log(param);
}

foo(v);
// 混淆后
const _0x44c967=_0x635b;(function(_0x32c587,_0xad783a){const _0x5f038b=_0x635b,_0xdd5abd=_0x32c587();while(!![]){try{const _0x36db7d=parseInt(_0x5f038b(0x1a4))/0x1*(-parseInt(_0x5f038b(0x1b0))/0x2)+parseInt(_0x5f038b(0x1a8))/0x3+-parseInt(_0x5f038b(0x1a5))/0x4+parseInt(_0x5f038b(0x1aa))/0x5*(parseInt(_0x5f038b(0x1a7))/0x6)+-parseInt(_0x5f038b(0x1af))/0x7*(parseInt(_0x5f038b(0x1ad))/0x8)+-parseInt(_0x5f038b(0x1ae))/0x9+-parseInt(_0x5f038b(0x1a6))/0xa*(-parseInt(_0x5f038b(0x1a9))/0xb);if(_0x36db7d===_0xad783a)break;else _0xdd5abd['push'](_0xdd5abd['shift']());}catch(_0x32d69b){_0xdd5abd['push'](_0xdd5abd['shift']());}}}(_0x2573,0x793ce));let v=_0x44c967(0x1ab);function foo(_0xddf157){const _0x57965d=_0x44c967;console[_0x57965d(0x1ac)](_0xddf157);}function _0x2573(){const _0x160bca=['7054758QHxQzs','7RhaGlF','2gnZgYk','139201SpYUUf','3355792nqHQax','9583830DxWDsR','3389808yzigdv','413817ZWQdzF','22UIVwDV','5AHuxCv','Hello\x20World','log','2888576mOrpTw'];_0x2573=function(){return _0x160bca;};return _0x2573();}function _0x635b(_0x119657,_0x5aecf8){const _0x257388=_0x2573();return _0x635b=function(_0x635be2,_0x5b4e35){_0x635be2=_0x635be2-0x1a4;let _0xe244e9=_0x257388[_0x635be2];return _0xe244e9;},_0x635b(_0x119657,_0x5aecf8);}foo(v);

要不是有几个关键字,我都不敢确定这是 javascript 代码

对于这类混淆进行代码还原可能就比较困难了,但是我们可以看到,foo 函数名并没有被更改,估计还是有戏的,但是值不值得是个问题

从上面的几个案例可以看到,代码压缩与代码混淆并不是说完全是两种技术,只是目的不同而已,两者经常会结合起来使用,所以下面部分的文章不再详细区分代码压缩和代码混淆,都用代码混淆来统称

0x03 代码混淆实战

在这个章节,我们会介绍一些现有的混淆方法,我们的侧重点并不在于如何解混淆,在我看来,除非有现成的工具,不然复杂的混淆解起来时间成本太高,不值得这么做,下面的内容更倾向于通过剖析各种方法来了解其所利用的语言特性以及特殊方法

1. UglifyJS

UglifyJS 是一个 JavaScript 解析器、压缩器、压缩器和美化器工具包。 https://lisperator.net/uglifyjs/ https://github.com/mishoo/UglifyJS/ https://github.com/LiPinghai/UglifyJSDocCN/blob/master/README.md

使用方法

npm install uglify-js -g
uglifyjs example.js -c -m --mangle-props
  • -c 代码压缩
  • -m 代码混淆
  • --mangle-props 混淆属性名
  • -b 美化显示
// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name)
console.log(greeting);
console.log(greeting2);
// -c 压缩后
const person={age:18,name:"Tom"};function greet(name){return"Hello, "+name+"!"}var userName="John",greeting=greet(userName),greeting2=greet(person.name);console.log(greeting),console.log(greeting2);

我们通过 -b 参数美化显示

// -c 压缩后 -b 美化输出
const person = {
    age: 18,
    name: "Tom"
};

function greet(name) {
    return "Hello, " + name + "!";
}

var userName = "John", greeting = greet(userName), greeting2 = greet(person.name);

console.log(greeting), console.log(greeting2);

-c 压缩后

  • 函数内部的变量赋值语句被删除了
  • 三行的 var 赋值语句合并成了一行
  • 两个 console.log 合并成了一行
// -m 混淆后 -b 美化输出
const person = {
    age: 18,
    name: "Tom"
};

function greet(e) {
    var r = "Hello, " + e + "!";
    return r;
}

var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name);
console.log(greeting);
console.log(greeting2);

-m 混淆后

  • 将函数参数名改成了单字母 name -> e
  • 函数内部的变量名称改成了单字母 message -> r
// -m --mangle-props 后 -b 美化输出
const person = {
    g: 18,
    name: "Tom"
};

function greet(e) {
    var r = "Hello, " + e + "!";
    return r;
}

var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name);
console.log(greeting);
console.log(greeting2);

-m --mangle-props

  • 将函数参数名改成了单字母 name -> e
  • 函数内部的变量名称改成了单字母 message -> r
  • 对象内部的属性名被修改了 age -> g
// -c -m --mangle-props 后 -b 美化输出
const person = {
    g: 18,
    name: "Tom"
};

function greet(e) {
    return "Hello, " + e + "!";
}

var userName = "John", greeting = greet(userName), greeting2 = greet(person.name);

console.log(greeting), console.log(greeting2);

基本就是上面的效果加起来,但是可以看到,最外层的变量依旧没有被混淆,想要最高作用于中的变量都被混淆的话,需要用到 --toplevel 参数

// --toplevel -c -m --mangle-props 后 -b 美化输出
function o(o) {
    return "Hello, " + o + "!";
}

o("John"), o("Tom");
console.log("Hello, John!"), console.log("Hello, Tom!");

这个说实话就比较狠了,直接把我们调用函数的执行结果都给我们算出来了,而且我们的对象直接被干掉了,可能是因为后续没有用到这个对象吧,现在我们尝试修改原代码,在输出后再给对象中的属性赋值

// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name)
console.log(greeting);
console.log(greeting2);
person.name = "Kevin"
// --toplevel -c -m --mangle-props 后 -b 美化输出
var o = {
    o: 18,
    name: "Tom"
};

function n(o) {
    return "Hello, " + o + "!";
}

n("John");

var e = n(o.name);

console.log("Hello, John!"), console.log(e), o.name = "Kevin";

这回由于我们对对象中的属性进行了赋值操作,所以代码帮助我们保存了对象,使用 --toplevel 后,除了执行结果一样,大部分内容都变了

但是,这里可以看到,对象中的 name 属性以及 console.log 并没有发生改变,从官网的描述来看, UglifyJSjavascript 原生的函数、属性的名字不进行混淆

通过对 UglifyJS 的混淆结果分析,发现可以从以下几个方面进行混淆
  • 全局、局部变量名
  • 对象内部属性名称
  • 函数内部多余的变量赋值操作
  • 甚至是函数调用后取得返回值的步骤也是可以修改的

2. 站长之家混淆

https://tool.chinaz.com/tools/jscodeconfusion.aspx

// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name)
console.log(greeting);
console.log(greeting2);
person.name = "Kevin"
// 混淆后的代码
const person = {
 age: 18,
 name: '\x54\x6f\x6d'
}

function greet(ifBR1) {
 var rQnCc2 = "\x48\x65\x6c\x6c\x6f\x2c " + ifBR1 + "\x21";
 return rQnCc2;
}
var zj3 = "\x4a\x6f\x68\x6e";
var $t$4 = greet(zj3);
var HhUkMNt5 = greet(person["\x6e\x61\x6d\x65"]) 
console["\x6c\x6f\x67"]($t$4);
console["\x6c\x6f\x67"](HhUkMNt5);
person["\x6e\x61\x6d\x65"] = "\x4b\x65\x76\x69\x6e"
  • person 对象中的 name 属性的值由 Tom -> \x54\x6f\x6d
  • 函数 greet 中的形式参数被重命名了 name -> ifBR1
  • 函数 greet 中的赋值语句中的字符串被替换 Hello, -> \x48\x65\x6c\x6c\x6f\x2c , ! -> \x21
  • 变量被重命名 userName -> zj3, greeting ->
  • 对象属性访问模式发生了改变,由点号表示法变成了方括号表示法 person.name -> person['name']
  • 对象属性访问属性名称被重命名 name -> \x6e\x61\x6d\x65
  • javascript 内置方法 log 被重命名 log -> \x6e\x61\x6d\x65

上面这些是基于前后代码变化总结的,对于变量名和函数形式参数的名称改变大家肯定是认为很常见了,因为只要前后一致,就不会改变代码含义。这里需要注意的是,似乎在 javascript 中字符串本身与其 ascii hex 的表示形式效果是一样的(在很多语言里都是这样的)

这种混淆方法可以用在以下位置

  • 字符串值中
  • 对象的属性名称中

这种混淆如果写自动化还原应该也不是很难,可能部分在线平台也是可以完成的

https://www.sojson.com/jsjiemi.html

3. eval packer

https://tool.chinaz.com/js.aspx
// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name)
console.log(greeting);
console.log(greeting2);
person.name = "Kevin"
// 混淆后
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"',19,19,'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|'),0,{}))

先格式化一下

eval(function(p, a, c, k, e, d) {
    e = function(c) {
        return (c < a ? "": e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
    };
    if (!''.replace(/^/, String)) {
        while (c--) d[e(c)] = k[c] || e(c);
        k = [function(e) {
            return d[e]
        }];
        e = function() {
            return '\\w+'
        };
        c = 1;
    };
    while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
    return p;
} ('c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"', 19, 19, 'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|'), 0, {}))

整体是 eval(code) 这种形式,其实就是执行 code 的内容,这里 code 是一个字符串类型的变量,code 内容如下

function(p, a, c, k, e, d) {
    e = function(c) {
        return (c < a ? "": e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
    };
    if (!''.replace(/^/, String)) {
        while (c--) d[e(c)] = k[c] || e(c);
        k = [function(e) {
            return d[e]
        }];
        e = function() {
            return '\\w+'
        };
        c = 1;
    };
    while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
    return p;
} ('c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"', 19, 19, 'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|'), 0, {})

抽象来说是 function(param){}(params)这种形式,这种形式是立即调用函数表达式,简单来说就是直接调用该匿名函数,参数就是 params ,举个例子

(function(param){
   console.log(param);
})("Hello, World!");

所以也就是说,eval 函数的参数是这个立即调用函数表达式的返回结果,应该是个字符串值

了解了形式后,我们先看一下立即调用函数表达式的参数是什么样的

'c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"', 19, 19, 'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|'), 0, {}

与匿名函数的形式参数对应关系如下

  • p -> 'c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"'
  • a -> 19
  • c -> 19
  • k ->'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|')

  • e -> 0
  • d -> {}

解析来分析匿名函数

function(p, a, c, k, e, d) {
    e = function(c) {
        return (c < a ? "": e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
    };
    if (!''.replace(/^/, String)) {
        while (c--) d[e(c)] = k[c] || e(c);
        k = [function(e) {
            return d[e]
        }];
        e = function() {
            return '\\w+'
        };
        c = 1;
    };
    while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
    return p;
} 

匿名函数分为三个部分

  • e 赋值为一个函数
  • if 判断语句
  • while 循环

先看 if 语句

if (!''.replace(/^/, String)) {
    while (c--) d[e(c)] = k[c] || e(c);
    k = [function(e) {
        return d[e]
    }];
    e = function() {
        return '\\w+'
    };
    c = 1;
};

条件语句为 true,接着看其中的内容

while (c--) d[e(c)] = k[c] || e(c);

c 的值为 19, k[c]

显然,在这个 while 循环中,k[c] 都是有值的,我们看一下 e(c) 的结果

while 循环后,其实是将数组变成了一个对象 d

k = [function(e) {
        return d[e]
    }];

将一个数组赋值给 k 其中第一个成员是一个函数,根据属性名称从 d 对象中获取属性的值

e = function() {
        return '\\w+'
    };

c = 1;

将一个函数赋值给 e ,这个函数很简单,就是返回 \\w+ 字符串; 给变量 c 赋值为 1

此时各个变量内容如下

  • p -> 'c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"'
  • a -> 19
  • c -> 1
  • k -> 一个数组
  • e -> 一个函数
  • d -> 一个对象
while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);

这里 while (c--) 是一个障眼法了,因为上面已经把 c 设置为 1 ,因此等价于下面这段代码

c = 0
if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);

判断条件中 k[c] 等价于一个函数,一个函数定义语句当然是 true,所以代码简化为

c = 0 
func = function(e) {
        return d[e]
    }
p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), func);

其中 e 函数其实根本就没有形式参数,所以 e(c) 中的 c 根本没有意义,e(c)等价于 '\\w+',所以上面的代码相当于

p = 'c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"'
func = function(e) {
        return d[e]
    }
p = p.replace(new RegExp('\\b\\w+\\b', 'g'), func);

这段代码是一个替换语句,先是通过正则表达式匹配所有的单词

  • \\b:表示单词的边界,用于匹配单词的开头或结尾。使用双斜杠 \\ 是因为在正则表达式中,反斜杠 \ 是一个特殊字符,需要进行转义。
  • \\w+:表示一个或多个字母、数字或下划线字符。\w 是一个预定义的字符类,匹配字母、数字和下划线。
  • \\b:再次表示单词的边界,用于确保匹配的单词完整。

如果匹配到了如何处理呢? 将匹配到的内容作为 func 的参数,执行 func 函数,并将返回值替换匹配到的内容,返回替换后的字符串

return p

最终返回替换后的字符串变量 p

所以说,既然最终被执行的代码经过一顿替换后,返回字符串变量peval 执行,如果我们将 p 直接输出会怎么样

// 修改后
console.log(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('c 3={d:b,0:\'a\'}e 2(0){1 5="h, "+0+"!";i 5}1 7="f";1 4=2(7);1 6=2(3.0)8.9(4);8.9(6);3.0="g"',19,19,'name|var|greet|person|greeting|message|greeting2|userName|console|log|Tom|18|const|age|function|John|Kevin|Hello|return'.split('|'),0,{}))

我们的源代码就恢复了

此时大家可以想一下,这个替换过程中具体是怎么执行的这事儿重要吗?

根本就不重要,因为它完全可以做成一个黑盒子,随意自定义的对原代码的字符串进行修改,之后提供最终的字符串以及逆向还原的过程,最终通过 eval 来执行,所以回到我们的初衷,我们分析这种混淆压缩所采用的知识点

  • eval 函数将字符串当作代码来执行
  • 立即调用函数表达式 function(param){}(params)

没了,其他都是可以变化的

4. JShaman

https://www.jshaman.com/ JShaman 是国内公司开发的js代码加密商业产品

免费版可以直接使用

// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name);
console.log(greeting);
console.log(greeting2);
person.name = "Kevin";
// 混淆后
const person={"\u0061\u0067\u0065":0x12,"\u006e\u0061\u006d\u0065":"\u0054\u006f\u006d"};function greet(_0x753eb2){var _0x4445de=" ,olleH".split("").reverse().join("")+_0x753eb2+"\u0021";return _0x4445de;}var userName="nhoJ".split("").reverse().join("");var greeting=greet(userName);var greeting2=greet(person["\u006e\u0061\u006d\u0065"]);console["\u006c\u006f\u0067"](greeting);console["\u006c\u006f\u0067"](greeting2);person["\u006e\u0061\u006d\u0065"]="niveK".split("").reverse().join("");

我们格式化一下

// 格式化后
const person = {
    "\u0061\u0067\u0065": 0x12,
    "\u006e\u0061\u006d\u0065": "\u0054\u006f\u006d"
};
function greet(_0x753eb2) {
    var _0x4445de = " ,olleH".split("").reverse().join("") + _0x753eb2 + "\u0021";
    return _0x4445de;
}
var userName = "nhoJ".split("").reverse().join("");
var greeting = greet(userName);
var greeting2 = greet(person["\u006e\u0061\u006d\u0065"]);
console["\u006c\u006f\u0067"](greeting);
console["\u006c\u006f\u0067"](greeting2);
person["\u006e\u0061\u006d\u0065"] = "niveK".split("").reverse().join("");
Unicode 是编码表的名字,Unicode 编码实际上有不同的变现形式,其中 \u0000 这种形式为Unicode转义序列,是其中一种表现形式,常被大家称为 Unicode 编码,下文也将使用 Unicode 编码来称呼,希望没有给大家造成困扰
  • 对象的属性名称被转化为了 Unicode 编码
  • 对象的属性对应的值有变化
  • 字符串被转化为了 Unicode 编码
  • 数值型被转化为了 16 进制的形式
  • 函数的形式参数被替换成了 _0x 开头的形式的字符
  • 函数内部的变量名称被替换为 _0x 开头的形式的字符
  • 对于字符串拼接的语法进行了不止一种变化
  • "Hello, " -> " ,olleH".split("").reverse().join("") 说到底就是对原本字符进行了过程变化,结果不变
  • ! -> \u0021 部分字符变成 Unicode 编码
  • 全局字符串变量的值也被修改了 "John" -> "nhoJ".split("").reverse().join("")
  • 对象属性调用也是使用 Unicode 编码形式

免费版本采用的技术也比较简单,可以总结的主要是

  • 对象的属性名称和字符串不仅可以使用16进制,也可以通过 Unicode 形式来表示

5. JavaScript obfuscator

https://obfuscator.io/ https://github.com/javascript-obfuscator/javascript-obfuscator 一个免费且高效的 JavaScript 混淆器(包括对 ES2022 的支持)。使您的代码更难以复制并防止人们窃取您的工作成果。这个工具是一个优秀的Web UI(并且开源)

img

官网放这个图标可能是想说这个项目加密后的代码让人看起来想流泪吧

1) 默认配置

这个工具可配置项非常多,我们先用官网默认的形式看一下效果

// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}
  
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name);
console.log(greeting);
console.log(greeting2);
person.name = "Kevin";
// 混淆后
var _0x5665ce=_0x1ec8;(function(_0x24a0fc,_0x32277b){var _0x470fc7=_0x1ec8,_0x314734=_0x24a0fc();while(!![]){try{var _0x19fb94=-parseInt(_0x470fc7(0xca))/0x1+parseInt(_0x470fc7(0xc8))/0x2+parseInt(_0x470fc7(0xc9))/0x3+-parseInt(_0x470fc7(0xc5))/0x4+-parseInt(_0x470fc7(0xc4))/0x5+-parseInt(_0x470fc7(0xcb))/0x6+parseInt(_0x470fc7(0xcd))/0x7;if(_0x19fb94===_0x32277b)break;else _0x314734['push'](_0x314734['shift']());}catch(_0x11f2da){_0x314734['push'](_0x314734['shift']());}}}(_0xd8be,0xcb4d8));const person={'age':0x12,'name':'Tom'};function greet(_0x232d03){var _0x158862='Hello,\x20'+_0x232d03+'!';return _0x158862;}function _0x1ec8(_0x200d59,_0x33a7d8){var _0xd8be2c=_0xd8be();return _0x1ec8=function(_0x1ec879,_0x3bf5b6){_0x1ec879=_0x1ec879-0xc4;var _0x4acfea=_0xd8be2c[_0x1ec879];return _0x4acfea;},_0x1ec8(_0x200d59,_0x33a7d8);}var userName=_0x5665ce(0xc6),greeting=greet(userName),greeting2=greet(person[_0x5665ce(0xcc)]);function _0xd8be(){var _0x303cd1=['4525950NEPdZY','5502416viTywx','John','log','2506568dDijWh','1830627XxDgMI','46606dGrOiI','2093556sEUamv','name','11518927TWTtrQ'];_0xd8be=function(){return _0x303cd1;};return _0xd8be();}console['log'](greeting),console[_0x5665ce(0xc7)](greeting2),person[_0x5665ce(0xcc)]='Kevin';

格式化一下试试

// 格式化后
var _0x5665ce = _0x1ec8; (function(_0x24a0fc, _0x32277b) {
    var _0x470fc7 = _0x1ec8,
    _0x314734 = _0x24a0fc();
    while ( !! []) {
        try {
            var _0x19fb94 = -parseInt(_0x470fc7(0xca)) / 0x1 + parseInt(_0x470fc7(0xc8)) / 0x2 + parseInt(_0x470fc7(0xc9)) / 0x3 + -parseInt(_0x470fc7(0xc5)) / 0x4 + -parseInt(_0x470fc7(0xc4)) / 0x5 + -parseInt(_0x470fc7(0xcb)) / 0x6 + parseInt(_0x470fc7(0xcd)) / 0x7;
            if (_0x19fb94 === _0x32277b) break;
            else _0x314734['push'](_0x314734['shift']());
        } catch(_0x11f2da) {
            _0x314734['push'](_0x314734['shift']());
        }
    }
} (_0xd8be, 0xcb4d8));
const person = {
    'age': 0x12,
    'name': 'Tom'
};
function greet(_0x232d03) {
    var _0x158862 = 'Hello,\x20' + _0x232d03 + '!';
    return _0x158862;
}
function _0x1ec8(_0x200d59, _0x33a7d8) {
    var _0xd8be2c = _0xd8be();
    return _0x1ec8 = function(_0x1ec879, _0x3bf5b6) {
        _0x1ec879 = _0x1ec879 - 0xc4;
        var _0x4acfea = _0xd8be2c[_0x1ec879];
        return _0x4acfea;
    },
    _0x1ec8(_0x200d59, _0x33a7d8);
}
var userName = _0x5665ce(0xc6),
greeting = greet(userName),
greeting2 = greet(person[_0x5665ce(0xcc)]);
function _0xd8be() {
    var _0x303cd1 = ['4525950NEPdZY', '5502416viTywx', 'John', 'log', '2506568dDijWh', '1830627XxDgMI', '46606dGrOiI', '2093556sEUamv', 'name', '11518927TWTtrQ'];
    _0xd8be = function() {
        return _0x303cd1;
    };
    return _0xd8be();
}
console['log'](greeting),
console[_0x5665ce(0xc7)](greeting2),
person[_0x5665ce(0xcc)] = 'Kevin';

如果我们不知道原代码是什么,看起来可以说是比较绝望,但是我们在知道原代码的情况下,还是可以分析的

// 对象定义部分
// 原代码
const person = {
    age: 18,
    name: 'Tom'
}

// 混淆后
const person = {
    'age': 0x12,
    'name': 'Tom'
};

对象定义部分主要是将属性值中数值型使用了 16 进制的表现形式

// 函数定义部分
// 原代码
function greet(name) {
    var message = "Hello, " + name + "!";
    return message;
}

// 混淆后
function greet(_0x232d03) {
    var _0x158862 = 'Hello,\x20' + _0x232d03 + '!';
    return _0x158862;
}

函数定义部分变化主要如下

  • 形式参数被修改为 _0x 的形式
  • 函数内部变量名称被修改为 _0x 的形式
  • 字符串部分字符使用了 16 进制的形式表示 ' ' -> \x20
// 全局变量定义部分
// 原代码
var userName = "John";
var greeting = greet(userName);
var greeting2 = greet(person.name);

// 混淆后
var userName = _0x5665ce(0xc6),
greeting = greet(userName),
greeting2 = greet(person[_0x5665ce(0xcc)]);

原本普通的一个字符串赋值给一个变量,现在变成了一个函数 _0x5665ce(0xc6) ,而且还有参数,参数还不是原本字符串以及其各种编码,我们把函数代码粘过来

var _0x5665ce = _0x1ec8; 

_0x5665ce的值等于 _0x1ec8 ,我们看一下 _0x1ec8

function _0x1ec8(_0x200d59, _0x33a7d8) {
    var _0xd8be2c = _0xd8be();
    return _0x1ec8 = function(_0x1ec879, _0x3bf5b6) {
        _0x1ec879 = _0x1ec879 - 0xc4;
        var _0x4acfea = _0xd8be2c[_0x1ec879];
        return _0x4acfea;
    },
    _0x1ec8(_0x200d59, _0x33a7d8);
}

这个函数形式有点意思,上来调用一个函数,并将函数执行结果赋值一个变量,之后就 return 了,看似return 两个返回值,第一个返回值是新的 _0x1ec8 函数;第二个返回值是对于新 _0x1ec8 函数的调用返回值,传入的参数就是原 _0x1ec8 函数传入的参数,没有变动

var userName = _0x5665ce(0xc6)

我们知道 userName 的值为 John,但是目前看起来 userName 的值应该是一个函数定义呀,我们输出一下

这里我们就可以对比 eval packer 了,它只是简单的字符串替换,即使将原代码中的部分提取出来,通过数组、字典等各种形式存储、拼接、替换等,最终进行还原,这里面没有利用到复杂的语法以及js 语言本身的特性,所以我们一点点解开也学不到什么; 这个代码就不一样了,我们一步一步解开它,尝试去学习其中的思路

现在我已经上面的赋值、函数执行等部分结果产生了疑惑了,与我之前的理解不一样,所以我们将混淆后的代码抽象一下

// 部分代码抽象后
function func1(param1, param2) {
    var var_1 = 'test';
    return func1 = function(new_param1, new_param2) {
        new_param1 = new_param1 - 0xc4;
        var var_2 = new_param1;
        return var_2;
    },
    func1(param1, param2);
}

var a, b = func1(0xc6)
console.log(a)   // 执行结果是 undefined
console.log(b)   // 执行结果是 2 ,也就是一个数值,而不是函数定义

如果我们用两个变量来接收 func1 的执行结果会是什么呢

这回变成了变量 b 的值为 2 ,aundefined

如果我们用三个变量来接收 func1 的执行结果会怎么样

// 部分代码抽象后
function func1(param1, param2) {
    var var_1 = 'test';
    return func1 = function(new_param1, new_param2) {
        new_param1 = new_param1 - 0xc4;
        var var_2 = new_param1;
        return var_2;
    },
    func1(param1, param2);
}

var a, b, c = func1(0xc6)
console.log(a)   
console.log(b)  
console.log(b)

这回 c 的值为 2 了

如果我们只有变量 a 来接收 func1 的返回值,但是修改 func1 的返回值,只留下新的函数定义

function func1(param1, param2) {
    var var_1 = 'test';
    return func1 = function(new_param1, new_param2) {
        new_param1 = new_param1 - 0xc4;
        var var_2 = new_param1;
        return var_2;
    }
}

var a = func1(0xc6)
console.log(a)

这回返回结果是一个函数定义,此时如果我们使用两个变量来接收 func1 的执行结果

function func1(param1, param2) {
    var var_1 = 'test';
    return func1 = function(new_param1, new_param2) {
        new_param1 = new_param1 - 0xc4;
        var var_2 = new_param1;
        return var_2;
    }
}

var a, b = func1(0xc6)
console.log(a)
console.log(b)

此时变量 b 的值为函数定义了

所以这里有两个维度的复杂场景,我们再次抽象,新的函数定义不再与原函数同名了

function func1(o1) {
    return func2 = function(n1) {
        return n1;
    }, 2
}

var a, b, c = func1(0xc6)
console.log(a)
console.log(b)
console.log(c)

结果似乎没有什么变化,难道说,函数定义和其他类型同时返回的时候,它的值就自动变成 undefined 了吗?

function func1(o1) {
    return () => {}, 2, "3"
}

var a, b, c = func1(0xc6)
console.log(a)
console.log(b)
console.log(c)

这次我们设计三个返回值,分别是函数定义、数值、字符串

看到这,我都蒙了,经过查询资料,我找到了两个维度的复杂的原因

JavaScript 中函数只能有一个返回值,你就说这玩意如果没学过 js谁能想到吧!而且 return 1, 2 也不会报错,而是会按照一定的规范执行,最终返回最后一个值

此时我们就不需要抽象了,可以继续看 _0x1ec8 涉及的另一个函数 _0xd8be

function _0xd8be() {
    var _0x303cd1 = ['4525950NEPdZY', '5502416viTywx', 'John', 'log', '2506568dDijWh', '1830627XxDgMI', '46606dGrOiI', '2093556sEUamv', 'name', '11518927TWTtrQ'];
    _0xd8be = function() {
        return _0x303cd1;
    };
    return _0xd8be();
}

这个函数就是返回一个数组,接下来的执行流程就与 eval packer 没啥区别了,如何处理字符串的事儿了

console['log'](greeting),
console[_0x5665ce(0xc7)](greeting2),
person[_0x5665ce(0xcc)] = 'Kevin';

这部分也没啥可说的了,无非就是个偏移量的问题

除了我们讨论过的代码,还有一段一直都没有讨论过,但是它执行了

(function(_0x24a0fc, _0x32277b) {
    var _0x470fc7 = _0x1ec8,
    _0x314734 = _0x24a0fc();
    while ( !! []) {
        try {
            var _0x19fb94 = -parseInt(_0x470fc7(0xca)) / 0x1 + parseInt(_0x470fc7(0xc8)) / 0x2 + parseInt(_0x470fc7(0xc9)) / 0x3 + -parseInt(_0x470fc7(0xc5)) / 0x4 + -parseInt(_0x470fc7(0xc4)) / 0x5 + -parseInt(_0x470fc7(0xcb)) / 0x6 + parseInt(_0x470fc7(0xcd)) / 0x7;
            if (_0x19fb94 === _0x32277b) break;
            else _0x314734['push'](_0x314734['shift']());
        } catch(_0x11f2da) {
            _0x314734['push'](_0x314734['shift']());
        }
    }
} (_0xd8be, 0xcb4d8));

这段代码看起来是一个立即调用函数表达式,里面涉及了大量的内容,难以理解,但是当删除这段代码后,对整个执行结果没有任何影响,所以这段代码就是给我们添堵的死代码

接下来我们就可以总结 JavaScript obfuscator 默认配置混淆采用的一些技术了

  • 添加无意义的立即调用函数表达式死代码,混淆视听
  • 修改对象属性值为数值型为16进制形式
  • 修改函数形式参数以及内部变量名
  • 修改字符串部分内容为 16 进制表示形式
  • 将全局变量的字符串值、对象属性调用的调用名等字符串通过 函数(参数) 的返回值来获取 函数具体如何执行,并不重要,它可以是变化的,但是函数中涉及了一下干扰点
  • 分多个函数,且函数名等为毫无意义的 _0x ,同时函数内部涉及到计算,计算过程中涉及的数字使用16进制(0x) 来表示,这样 _0x0x 就很容易混淆,你说多坏
  • 在函数内部会通过类似闭包的设计,覆盖掉原本的函数定义
  • 函数参数包含无意义的参数
  • 函数返回值利用了javascript 函数只有一个返回值的特性,迷惑人

我们接下来看一下 JavaScript obfuscator 这些配置项分别会对代码造成哪些影响

https://obfuscator.io/#code

官网提供了图形化配置项以及对应的配置项解析,大家也可以从官网查看

2) Options Preset

在预设置中,有图片中四种预设,单独的设置项应该有几十种,四种预设分别采用不同的配置,有点像中杯、大杯、超大杯的意思

配置项比较多,截图不够完整,大家可以去官网查看

3) Target

指定代码的执行环境

  • 浏览器
  • 浏览器无 eval
  • nodejs

这个就看具体的执行环境了

4) seed

类型: string|number 默认: 0

随机数种子,虽然官方在描述中类型说是 string|number ,但是从官网设置来看,应该是只支持 number , 配置随机数种子可以使随机这件事更有保障一些,好像前段时间有师傅就分析了一些随机数生成算法导致的漏洞

5) disableConsoleOutput

Boolean 类型的配置选项

此选项全局禁用所有脚本的控制台调用

通过将 console.log、console.info、console.error、console.warn、console.debug、console.exception 和 console.trace 替换为空函数来禁用它们。这使得调试器的使用更加困难

我们勾选测试一下这个选项,此时尽量取消其他选项

// 原代码
console.log("Hello World!");
// 混淆后
var b = (function () {
    var c = !![];
    return function (d, e) {
        var f = c ? function () {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        } : function () {
        };
        c = ![];
        return f;
    };
}());
var a = b(this, function () {
    var c;
    try {
        var d = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
        c = d();
    } catch (l) {
        c = window;
    }
    var f = c['console'] = c['console'] || {};
    var g = [
        'log',
        'warn',
        'info',
        'error',
        'exception',
        'table',
        'trace'
    ];
    for (var h = 0x0; h < g['length']; h++) {
        var i = b['constructor']['prototype']['bind'](b);
        var j = g[h];
        var k = f[j] || i;
        i['__proto__'] = b['bind'](b);
        i['toString'] = k['toString']['bind'](k);
        f[j] = i;
    }
});
a();
console['log']('Hello\x20World!');

我们执行一下混淆后的代码

可以看到,确实是将控制台输出禁止了

我们简单分析一下是如何做到的

代码整体来看有四部分,最后一部分就不说了,前面主要是创建了两个变量,之后调用了其中一个属性为函数的变量,使其执行,我们可以在 ab 变量之间输出一下,看看此时是否已经被禁止

a 执行前尝试做同样的事

可以看出,前面都是铺垫,执行 a 函数后控制台输出就被禁止了

有了大概的了解后,可以开始逐步分析,先是 b 的部分

// b 的部分
var b = (function () {
    var c = !![];
    return function (d, e) {
        var f = c ? function () {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        } : function () {
        };
        c = ![];
        return f;
    };
}());

大家看这个应该比较熟悉了,又是一个立即调用函数表达式,那我们可以直接看一下这个表达式执行后的结果是什么

可以看到,这个表达式的结果是一个匿名函数,将匿名函数赋值给变量 b

这段代码的具体执行取决于 c 的值,接下来我们看 a 的部分

// a 的部分
var a = b(this, function () {
    var c;  
    try {
        var d = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
        c = d();
    } catch (l) {
        c = window;
    }
    var f = c['console'] = c['console'] || {};
    var g = [
        'log',
        'warn',
        'info',
        'error',
        'exception',
        'table',
        'trace'
    ];
    for (var h = 0x0; h < g['length']; h++) {
        var i = b['constructor']['prototype']['bind'](b);
        var j = g[h];
        var k = f[j] || i;
        i['__proto__'] = b['bind'](b);
        i['toString'] = k['toString']['bind'](k);
        f[j] = i;
    }
});

整体来看 ab 函数调用的返回值,参数为 (this, function)

function (d, e) {
        var f = c ? function () {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        } : function () {
        };
        c = ![];
        return f;
};

这里就有意思了,上来就是个三元运算符 f = c ? func1(){} : func2(){} ,这里根据 c 的值来进行判断 f 变量最终值

让人容易疑惑的是, c 到底在哪里,好像没有 c 的定义,参数也没有,反而是 b 的立即调用表达式里 return 前有这个值

(function(){
   var c = !![];
  return function(){}
})

但是这里的 c 虽然相对 return 的函数来说属于外层,但是整体还是在函数体内部的,在当前这种情况下还能有效吗?

这里的语法在 javascript 中被称为闭包,也算是 javascript 语言的特性

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
function outer() {
  var outerVar = 'Hello';

  function inner() {
    var innerVar = ' World';
    console.log(outerVar + innerVar);
  }

  return inner;
}

var closure = outer();
closure();

这里看似 closure 变量等于的是 inner 函数,由于闭包语法的存在,实则其等于 inner 以及其上层的环境(包括变量)的组合,可以从执行结果中验证闭包的效果

所以调用 b 函数的时候,c 的值为 true,因此简化为

function (d, e) {
        var f = function () {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        };
        c = ![];
        return f;
};

可以看到,其实这里还是闭包,在匿名函数内部建立了一个匿名函数,传递给变量 f 跟我们上面的案例没有区别,这里所谓的环境就是de, d 其实就是全局对象, e 是一个函数,当然还有手动重置为 falsec

接下来来到最后一部分 a() ,其实也就是 f(),简化如下

var d = window;
var e = function () {
    var c;
    try {
        var d = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
        c = d();
    } catch (l) {
        c = window;
    }
    var f = c['console'] = c['console'] || {};
    var g = [
        'log',
        'warn',
        'info',
        'error',
        'exception',
        'table',
        'trace'
    ];
    for (var h = 0x0; h < g['length']; h++) {
        var i = b['constructor']['prototype']['bind'](b);
        var j = g[h];
        var k = f[j] || i;
        i['__proto__'] = b['bind'](b);
        i['toString'] = k['toString']['bind'](k);
        f[j] = i;
    }
};

var a = function () {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        };

a();

显然 e 的值不是 false,因此进入条件语句中,其实也就是一句话

var g = e['apply'](d, arguments);

我们的 e 是一个函数,一个函数的 apply 属性是什么意思呢?

这里就又涉及到javascript 函数的特性了 —— apply

Function 实例的 apply() 方法会以给定的 this 值和作为数组(或类数组对象)提供的 arguments 调用该函数。 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
apply(thisArg)
apply(thisArg, argsArray)
  • thisArg

调用 func 时提供的 this 值。如果函数不处于严格模式(Strict_mode),则 nullundefined 会被替换为全局对象,原始值会被转换为对象。

  • argsArray 可选

一个类数组对象,用于指定调用 func 时的参数,或者如果不需要向函数提供参数,则为 nullundefined

举个例子

function greet(msg) {
    console.log(`Hello, ${this.name}!`);
    console.log(msg)
  }
  
  var person1 = {
    name: 'John'
  };
  
  var persion2 = {
    name: 'Alice'
  };
  
greet.apply(person1, ["John is best!"]);
console.log("----------")
greet.apply(persion2, ["Alice, come on."]);

这里 greet 函数具体是向谁打招呼,取决于 this 代表谁,而 apply 的作用就是指定函数调用时候的this 的值。

匿名函数是没有 apply 方法的

arguments 是一个特殊的对象,它包含了函数调用时传递的所有参数,无论是否在函数定义中定义了这些参数。它类似于一个数组,但它不是一个真正的数组。它具有类似数组的属性和方法,如 length 属性和索引访问,但它没有数组的其他方法。

所以上面的代码的含义是指定了e 函数执行时的 this 是全局对象,之后按照原本的参数执行

接下来我们就看一下 e 函数

var e = function () {
    var c;
    try {
        var d = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
        c = d();
    } catch (l) {
        c = window;
    }
    var f = c['console'] = c['console'] || {};
    var g = [
        'log',
        'warn',
        'info',
        'error',
        'exception',
        'table',
        'trace'
    ];
    for (var h = 0x0; h < g['length']; h++) {
        var i = b['constructor']['prototype']['bind'](b);
        var j = g[h];
        var k = f[j] || i;
        i['__proto__'] = b['bind'](b);
        i['toString'] = k['toString']['bind'](k);
        f[j] = i;
    }
};

先是通过一个 try...catchc 进行了赋值

先看一下try 内部代码

var d = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
c = d();

似乎是创建了一个函数 d ,之后又调用 d 函数,并将返回值赋值给 c

这段代码使用了 Javascript 内置的构造函数 Function ,它可以接受一个字符串参数,并将其解析为一个函数对象,这么干的还真不多见,因为这样写可读性会非常差,没想到正好用在混淆这里了,也是 JavaScript 的特性了

bf976b12gy1ghfo72u6qlg206m05ydg2

将 16 进制表示的字符还原后如下

var d = Function('return (function() ' + '{}.constructor("return this")( )' + ');');
c = d();

这段代码其实是返回了一个立即调用函数表达式,用于获取全局对象,所以 c 的值为全局对象

其实从 catch 中的代码也能看出端倪

var f = c['console'] = c['console'] || {};
var g = [
    'log',
    'warn',
    'info',
    'error',
    'exception',
    'table',
    'trace'
];

定义两个变量fg, f 是个对象;g 是一个数组,其实也就是 f 的一些成员属性名称,f[g] 就是要被禁止的,因此下面的代码应该会是将两者组合起来,之后通过一些方式禁止掉

for (var h = 0x0; h < g['length']; h++) {
        var i = b['constructor']['prototype']['bind'](b);
        var j = g[h];
        var k = f[j] || i;
        i['__proto__'] = b['bind'](b);
        i['toString'] = k['toString']['bind'](k);
        f[j] = i;
    }

一段 for 循环代码,定义了 i j k 三个变量

var i = b['constructor']['prototype']['bind'](b);

b 是第一部分立即调用表达式返回的函数,所以这里也就是 func.constructor.prototype.bind(func)

这里又是 javascript 的特性了

这里涉及几个概念constructorprototypeproto

img

constructor 是构造函数,prototype 构造函数有一个 prototype属性,指向实例对象的原型对象。实例对象有一个proto属性,指向该实例对象对应的原型对象

这看起来很复杂,尤其是他们的名字是如此具有迷惑性,让人没有记住的欲望,就像我对于辈分的记忆,只能用到的时候套一下

图片来自以下文章 https://www.cnblogs.com/xiaohuochai/p/5721552.html https://blog.csdn.net/cc18868876837/article/details/81211729

我们现在开始分析上面这段代码

b['constructor'] 也就是在寻找实力对象 b 的构造函数,它的构造函数就是 Function

b['constructor']['prototype'] 就相当于 Functionprototype 属性,它指向实例对象b的原型对象

b['constructor']['prototype']['bind'](b) 调用了 bind 方法,这个方法 Function.prototype中存在,那在实例 b 中也是存在的

bind 方法就是改变调用者的环境(this) ,具体就是将this 指向参数,举例来说

const obj = {
    x: 1
}

x = 0
var b = function() {
    console.log("1111")
    console.log(this.x)
}

console.log('-- 1 --')
var i = b
i()
console.log('-- 2 --')
i = b.bind(obj)
i()

可以看到,通过 bind 方法,将新生成的函数 ithiswindow/global 变成了 obj

接下来我把这个案例扩展一下

const obj = {
    x: 1
}

x = 0
var b = function() {
    console.log("1111")
    console.log(this.x)
}

console.log('-- 1 --')
var i = b
i()
console.log('-- 2 --')
i = b.bind(obj)
i()
console.log('-- 3 --')
i = b.constructor.prototype.bind(obj)
i()

这里就有趣了,为什么到 b.constructor.prototype.bind(obj) 这里就没有输出了呢?

这个模型是我想通后抽象出来的,我对着原来的代码查了各种资料,反复思考了很久都没明白,也理解不了

其实是因为调用 bind 方法的不是 b 而是 Function.prototype ,所以得到的 i 根本就不是以 b 函数为基础更改了环境的,所以才导致上面的情况

这下我们就可以回来分析这段代码了

var i = b['constructor']['prototype']['bind'](b);

虽然最开始是 b,传递的参数也是 b ,但是其实生成新函数的模板并不是 b,所以 i 自然也就和 b 功能不一样了

var j = g[h];
var k = f[j] || i;

j 就是个辅助变量-属性名,帮助 k获取属性的值,例如 console.log 的值,因此 k 就是要被禁用的方法了

i['__proto__'] = b['bind'](b);
i['toString'] = k['toString']['bind'](k);
f[j] = i;

这里是帮 i 函数补一些属性,比如链关系,代码字符等,但是其实并没有特别多的意义,直接删掉前两行几乎没有影响,主要就是最后一行,将 i 函数赋值给要屏蔽的方法,也就是说调用 console.log 等就相当于调用 i ,那当然不会有什么输出,做到了屏蔽的效果

这部分分析有些长了,接下来的部分主要以理解配置项为主
6) selfDefending

类型:Boolean 默认值:false⚠️ 使用此选项混淆后,不要以任何方式更改混淆的代码,因为任何诸如丑化代码之类的更改都会触发自我防御,代码将不再起作用! ⚠️此选项强制将compact设置为true 此选项使输出代码能够抵抗格式化和变量重命名。如果尝试在混淆的代码上使用 JavaScript 美化器,代码将不再工作,从而使其更难以理解和修改。

// 原代码
console.log(123)
// 混淆后
var b=(function(){var c=!![];return function(d,e){var f=c?function(){if(e){var g=e['apply'](d,arguments);e=null;return g;}}:function(){};c=![];return f;};}());var a=b(this,function(){return a['toString']()['search']('(((.+)+)+)+$')['toString']()['constructor'](a)['search']('(((.+)+)+)+$');});a();console['log'](0x7b);

代码可正常运行

尝试美化/格式化代码

// 美化后
var b = (function() {
    var c = !![];
    return function(d, e) {
        var f = c ?
        function() {
            if (e) {
                var g = e['apply'](d, arguments);
                e = null;
                return g;
            }
        }: function() {};
        c = ![];
        return f;
    };
} ());
var a = b(this,
function() {
    return a['toString']()['search']('(((.+)+)+)+$')['toString']()['constructor'](a)['search']('(((.+)+)+)+$');
});
a();
console['log'](0x7b);

此时执行会卡住,一直不返回结果

通过 top 我们发现,电脑的 CPU占用率 node 几乎达到了 100%

既然我们知道原代码,也就是最后一行,所以卡住肯定是上面的原因,通过不断输出,看看问题出在哪里

最终发现是这段代码让我们的电脑CPU 占用率飙升

return a['toString']()['search']('(((.+)+)+)+$')['toString']()['constructor'](a)['search']('(((.+)+)+)+$');

这是一种利用正则匹配来进行防御的方法,格式化后的字符串由于换行、空格等字符出现,会使正则匹配进入无限循环,这类问题其实偶尔也会出现在正常的功能中,大家把它称为正则炸弹,详情可以看《白帽子讲web安全 第二版》370

7) debugProtection

类型:Boolean 默认值:false⚠️ 如果您打开开发者工具,可能会冻结您的浏览器。 此选项几乎无法使用开发人员工具的调试器功能(无论是在基于 WebKit 的还是 Mozilla Firefox 上)。

// 原代码
console.log(123)
// 混淆后
var H = (function () {
    var m = !![];
    return function (I, T) {
        var s = m ? function () {
            if (T) {
                var y = T['apply'](I, arguments);
                T = null;
                return y;
            }
        } : function () {
        };
        m = ![];
        return s;
    };
}());
(function () {
    H(this, function () {
        var m = new RegExp('function\x20*\x5c(\x20*\x5c)');
        var I = new RegExp('\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)', 'i');
        var T = a('init');
        if (!m['test'](T + 'chain') || !I['test'](T + 'input')) {
            T('0');
        } else {
            a();
        }
    })();
}());
console['log'](0x7b);
function a(m) {
    function I(T) {
        if (typeof T === 'string') {
            return function (s) {
            }['constructor']('while\x20(true)\x20{}')['apply']('counter');
        } else {
            if (('' + T / T)['length'] !== 0x1 || T % 0x14 === 0x0) {
                (function () {
                    return !![];
                }['constructor']('debu' + 'gger')['call']('action'));
            } else {
                (function () {
                    return ![];
                }['constructor']('debu' + 'gger')['apply']('stateObject'));
            }
        }
        I(++T);
    }
    try {
        if (m) {
            return I;
        } else {
            I(0x0);
        }
    } catch (T) {
    }
}

通过堆栈分析可以看出来,debug 是由图中代码导致的

如果你看到代码自己去分析,而不是直接看接下来的内容的话,估计你也会产生一个疑问:从代码逻辑来看,正常就应该会走到这里,好像没有看到哪里存在判断条件来判断是否为调试环境?

没错,是这样的,这个事也困扰了我很久,经过一顿分析,我发现了ob 使用的手法

它就是要走到这里,而且这里 I 函数是一个递归函数,所以这些 debug 代码就是会执行的,而且还会不断执行,在前端页面感觉没啥,但是一旦进入开发者工具,就会卡住

但这里还有一个问题,如何停下来呢,如果一直执行的话,浪费用户资源部说,还可能导致代码无法继续执行

这里就不得不说一个所有开发语言都有的东西了 —— 异常

递归调用的 debug 会导致页面异常,如果不捕捉一场,就会导致代码中断,我们看下面这个代码片段

try {
    if (m) {
        return I;
    } else {
        I(0x0);
    }
} catch (T) {
}

如果你看了上面的代码应该知道,代码先会进入 return I ; 之后第二次进入 I(0x0),此时开始进入无休止的 debug ,但是系统不会傻傻地让操作系统崩溃掉,所以会导致异常,从代码可以看出,异常发生后,会被 try...catch 捕获到,之后啥也不干,继续执行剩余逻辑代码

如何证明这个观点呢,分两步

首先得证明在不开控制台的情况下,后台在不断的执行debug。我们可以在 debug 的代码中加入 alert(1) 代码,分析代码后发现 alert(T) 是最合适的

这里需要注意,由于后面代码是一个立即调用表达式,所以这里必须得加 ; ,不然会报错

浏览器不开浏览器调试工具,刷新,看看效果

点击确定后会不断蹦出来

这样就验证了第一步,确实后台在一直进行 debug

其次我们需要验证,无限 debug 会导致异常

我们直接输出异常信息,如果进入过异常的话,就会以弹窗的形式反馈到页面上

刷新页面

异常信息 RangeError: Maximum call stack size exceeded,点击确定后页面继续执行

这就验证了我们的第二步,所以 ob 并没有使用什么高深技术,就是一个循环,之后对循环进行了混淆

debugProtectionInterval

类型:数字 默认值:0 ⚠️ 可以冻结您的浏览器!使用风险自负。 如果设置,则会使用以毫秒为单位的时间间隔来强制“控制台”选项卡上的调试模式,从而使使用开发人员工具的其他功能变得更加困难。如果启用了 debugProtection,则有效。建议值在 2000 到 4000 毫秒之间。

关于 debugger 的攻防,已经有很成熟的文章教大家如何绕过,可以参考

https://cloud.tencent.com/developer/article/2176916
8) ignoreRequireImports

类型:Boolean 默认值:false防止混淆 require 导入。在某些情况下,当由于某种原因运行时环境需要仅使用静态字符串进行导入时,这可能会有所帮助。

9) domainLock

类型:字符串数组 默认值:[]⚠️ 此选项不适用于目标:'node'允许仅在特定域和/或子域上运行混淆的源代码。这使得某人很难复制并粘贴您的源代码并在其他地方运行它。 如果源代码未在此选项指定的域上运行,则浏览器将重定向到传递到 domainLockRedirectUrl 选项的 URL。 多个域和子域 可以将您的代码锁定到多个域或子域。例如,要锁定它,以便代码仅在 www.example.com 上运行,请添加 www.example.com。要使其在包括任何子域(example.com、sub.example.com)的根域上运行,请使用 .example.com

// 原代码
console.log(123)

设置运行的域名为 vvvvvvv.com

// 混淆后

var b = (function () {
        var c = !![];
        return function (d, e) {
            var f = c ? function () {
                if (e) {
                    var g = e['apply'](d, arguments);
                    return e = null, g;
                }
            } : function () {
            };
            return c = ![], f;
        };
    }()), a = b(this, function () {
        var c;
        try {
            var f = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
            c = f();
        } catch (G) {
            c = window;
        }
        var g = new RegExp('[BXLZnQeBEYJRfanRwqdJEWKiMKnktgsV]', 'g'), h = 'vBvXvvvvvLZn.QecBEYoJRfanmRwqdJEWKiMKnktgsV'['replace'](g, '')['split'](';'), j, k, l, m, n = function (H, I, J) {
                if (H['length'] != I)
                    return ![];
                for (var K = 0x0; K < I; K++) {
                    for (var L = 0x0; L < J['length']; L += 0x2) {
                        if (K == J[L] && H['charCodeAt'](K) != J[L + 0x1])
                            return ![];
                    }
                }
                return !![];
            }, o = function (H, I, J) {
                return n(I, J, H);
            }, p = function (H, I, J) {
                return o(I, H, J);
            }, q = function (H, I, J) {
                return p(I, J, H);
            };
        for (var r in c) {
            if (n(r, 0x8, [
                    0x7,
                    0x74,
                    0x5,
                    0x65,
                    0x3,
                    0x75,
                    0x0,
                    0x64
                ])) {
                j = r;
                break;
            }
        }
        for (var s in c[j]) {
            if (q(0x6, s, [
                    0x5,
                    0x6e,
                    0x0,
                    0x64
                ])) {
                k = s;
                break;
            }
        }
        for (var t in c[j]) {
            if (p(t, [
                    0x7,
                    0x6e,
                    0x0,
                    0x6c
                ], 0x8)) {
                l = t;
                break;
            }
        }
        if (!('~' > k))
            for (var u in c[j][l]) {
                if (o([
                        0x7,
                        0x65,
                        0x0,
                        0x68
                    ], u, 0x8)) {
                    m = u;
                    break;
                }
            }
        if (!j || !c[j])
            return;
        var v = c[j][k], w = !!c[j][l] && c[j][l][m], x = v || w;
        if (!x)
            return;
        var y = ![];
        for (var z = 0x0; z < h['length']; z++) {
            var k = h[z], A = k[0x0] === String['fromCharCode'](0x2e) ? k['slice'](0x1) : k, B = x['length'] - A['length'], C = x['indexOf'](A, B), D = C !== -0x1 && C === B;
            D && ((x['length'] == k['length'] || k['indexOf']('.') === 0x0) && (y = !![]));
        }
        if (!y) {
            var E = new RegExp('[yRfrmZJUPqRCIfxNmCiQpmppBKF]', 'g'), F = 'aybRfourtm:ZJUbPlanqkRCIfxNmCiQpmppBKF'['replace'](E, '');
            c[j][l] = F;
        }
    });
a(), console['log'](0x7b);

这个难度应该也不大,这里将我们的域名混入了字符串中,然后在通过索引筛选出来,之后和环境变量去对比,一样就执行,不一样就跳转

10) Domain Lock Redirect Url

上一条中跳转的网址

11) sourceMap

类型:Boolean 默认值:false启用混淆代码的源映射生成。 源映射可帮助您调试混淆的 JavaScript 源代码。如果您想要或需要在生产中进行调试,您可以将单独的源映射文件上传到一个秘密位置,然后将浏览器指向那里。

这个与混淆技术没有直接关系,主要看使用方的调试需求

接下来的几个都是和字符变化有关的,以字符串数组为主的各种变化

12) stringArray

类型:Boolean 默认值:true删除字符串文字并将它们放入特殊数组中。例如,var m = "Hello World"; 中的字符串“Hello World”将被替换为 var m = _0x12c456[0x1];

在之前我们已经见过这种方式了,将字符变成数组索引来取值

13) stringArrayRotate

类型:Boolean 默认值:true⚠️ stringArray 必须启用 将 stringArray 数组移动固定和随机(在代码混淆时生成)位置。这使得将删除的字符串的顺序与其原始位置匹配变得更加困难。

简单来说应该就是取值的时候更加随机

14) stringArrayShuffle

类型:Boolean 默认值:true⚠️ stringArray 必须启用 随机打乱 stringArray 数组项。

15) stringArrayThreshold

类型:数字 默认值:0.8 最小值:0 最大值:1 ⚠️ stringArray 选项必须启用 您可以使用此设置来调整将字符串文字插入 stringArray 的概率(从 0 到 1)。 此设置对于大型代码特别有用,因为它会重复调用字符串数组并会减慢代码速度。stringArrayThreshold: 0 等于 stringArray: false

官网上配置项默认是 0.75 ,并不是 0.8

16) stringArrayIndexShift

类型:Boolean 默认值:true⚠️ stringArray 选项必须启用 为所有字符串数组调用启用额外的索引移位

17) stringArrayIndexesType

类型:字符串数组 默认值:['hexadecimal-number']⚠️ stringArray 选项必须启用 允许控制字符串数组调用索引的类型。 每个 stringArray 调用索引都将根据传递列表中随机选取的类型进行转换。这使得使用多种类型成为可能。 可用值:

  • 'hexadecimal-number'(默认):将字符串数组调用索引转换为十六进制数字
  • 'hexadecimal-numeric-string':将字符串数组调用索引转换为十六进制数字字符串
在 2.9.0 版本之前,javascript-obfuscator 将所有字符串数组调用索引转换为十六进制数字字符串类型。这使得一些手动反混淆变得稍微困难,但它允许自动反混淆器轻松检测这些调用。 新的十六进制数字类型使代码中字符串数组调用模式的自动检测变得更加困难。 未来将添加更多类型。
18) stringArrayCallsTransform

类型:Boolean 默认值:false⚠️ stringArray 选项必须启用 启用对 stringArray 的调用的转换。根据 stringArrayCallsTransformThreshold 值,这些调用的所有参数都可以提取到不同的对象。因此,自动查找对字符串数组的调用变得更加困难。

stringArrayCallsTransformThreshold

类型:数字 默认值:0.5 ⚠️ stringArraystringArrayCallsTransformThreshold 选项必须启用 您可以使用此设置来调整对字符串数组的调用被转换的概率(从 0 到 1)。

// 原代码
function hi() {
  console.log("Hello World!");
}
hi();
// 勾选该项混淆后
function hi() {
    var c = {
        d: 0x0,
        e: 0x1
    };
    console[b(c.d)](b(c.e));
}
function b(c, d) {
    var e = a();
    b = function (f, g) {
        f = f - 0x0;
        var h = e[f];
        return h;
    };
    return b(c, d);
}
hi();
function a() {
    var d = [
        'log',
        'Hello\x20World!'
    ];
    a = function () {
        return d;
    };
    return a();
}
// 未勾选该项混淆后
function hi() {
    console[b(0x0)](b(0x1));
}
function b(c, d) {
    var e = a();
    b = function (f, g) {
        f = f - 0x0;
        var h = e[f];
        return h;
    };
    return b(c, d);
}
function a() {
    var c = [
        'log',
        'Hello\x20World!'
    ];
    a = function () {
        return c;
    };
    return a();
}
hi();

可以看到勾选后出现了一些类似 c.d、c.e 这种调用方式

19) stringArrayWrappersCount

类型:数字 默认值:1 ⚠️ stringArray 选项必须启用 设置每个根或函数作用域内字符串数组的包装器计数。每个范围内包装器的实际数量受到该范围内文字节点数量的限制。

这个选项我也是查了一些资料才理解,ob项目把字符串存储在数组里,之后再取值这个事之前已经聊过了,这里的包装器,指的就是一个函数吧,这个函数负责从数组里根据索引取值,具体如何包装就看 ob 的了,这个配置项就是来设置有多少个这种函数,并不是有多少个用来存储的数组哈

20) stringArrayWrappersType

类型:字符串 默认值:variable⚠️ stringArraystringArrayWrappersCount 选项必须启用 允许选择由 stringArrayWrappersCount 选项附加的包装器类型。 可用值:

  • 'variable':在每个作用域的顶部附加变量包装器。性能快。
  • 'function':在每个作用域内的随机位置附加函数包装器。性能比变量慢,但提供更严格的混淆。

当性能损失对混淆的应用程序影响不大时,强烈建议使用函数包装器进行更高程度的混淆。

指定包装器类型,上面我说包装器就是一个函数吧,从官方配置上来看,只是其中一种类型,还可以是变量包装器,但是官方并没有给出变量包装器的案例,官网还是拿函数包装器举例的

那咱们就自己实践一下

// 原代码
const foo = 'foo';

function test () {
    const bar = 'bar';
    console.log(foo, bar);
}

test();
// 混淆后  变量包装器
const d = b;
function a() {
    const f = [
        'foo',
        'bar',
        'log'
    ];
    a = function () {
        return f;
    };
    return a();
}
const foo = d(0x0);
function b(c, d) {
    const e = a();
    b = function (f, g) {
        f = f - 0x0;
        let h = e[f];
        return h;
    };
    return b(c, d);
}
function test() {
    const e = b;
    const c = e(0x1);
    console[e(0x2)](foo, c);
}
test();
// 混淆后 函数包装器
function b(c, d) {
    const e = a();
    b = function (f, g) {
        f = f - 0x0;
        let h = e[f];
        return h;
    };
    return b(c, d);
}
const foo = 'foo';
function test() {
    const c = d(0xe4, 0xe5);
    function d(c, e) {
        return b(e - 0xe5, c);
    }
    console[d(0xe5, 0xe6)](foo, c);
}
function a() {
    const f = [
        'bar',
        'log'
    ];
    a = function () {
        return f;
    };
    return a();
}
test();

通过对比,主要不同的代码如下

// 变量包装器
function test() {
    const e = b;
    const c = e(0x1);
    console[e(0x2)](foo, c);
}

// 函数包装器
function test() {
    const c = d(0xe4, 0xe5);
    function d(c, e) {
        return b(e - 0xe5, c);
    }
    console[d(0xe5, 0xe6)](foo, c);
}

我倒是没有感觉有啥意义

21) stringArrayWrappersParametersMaxCount

类型:数字 默认值:2 ⚠️ stringArray 选项必须启用 ⚠️ 目前此选项仅影响由 stringArrayWrappersType 函数选项值添加的包装器 允许控制字符串数组包装器参数的最大数量。默认最小值为 2。建议值介于 2 和 5 之间。

这个是函数包装器才有的

22) stringArrayWrappersChainedCalls

类型:Boolean 默认值:true⚠️ stringArraystringArrayWrappersCount 选项必须启用 启用字符串数组包装器之间的链式调用。

23) stringArrayEncoding

类型:字符串数组 默认值:[]⚠️ stringArray 选项必须启用 此选项可能会减慢您的脚本速度。 使用 base64 或 rc4 对 stringArray 的所有字符串文字进行编码,并插入用于在运行时对其进行解码的特殊代码。 每个 stringArray 值都将通过从传递的列表中随机选择的编码进行编码。这使得使用多种编码成为可能。 可用值:

  • 'none'(布尔值):不编码 stringArray 值
  • 'base64'(字符串):使用 base64 编码 stringArray 值
  • 'rc4'(字符串):使用 rc4 对 stringArray 值进行编码。比 base64 慢约 30-50%,但获取初始值更困难。建议在使用 rc4 编码时禁用 unicodeEscapeSequence 选项,以防止混淆代码过大。

这应该是对存储的字符串进行编码存储

24) splitStrings

类型:Boolean 默认值:false将文字字符串拆分为长度为 splitStringsChunkLength 选项值的块。

25) splitStringsChunkLength

类型:数字 默认值:10 设置 splitStrings 选项的块长度。

26) unicodeEscapeSequence

类型:Boolean 默认值:false允许启用/禁用字符串转换为 unicode 转义序列。 Unicode 转义序列大大增加了代码大小,并且字符串可以轻松恢复到其原始视图。建议仅针对小型源代码启用此选项。

这个之前也见过了,没啥新奇的

27) forceTransformStrings

类型:字符串数组 默认值:[]启用字符串文字的强制转换,该字符串文字与传递的 RegExp 模式相匹配。 ⚠️ 此选项仅影响不应由 stringArrayThreshold (或将来可能的其他阈值)转换的字符串 该选项优先于reservedStrings选项,但不优先于条件注释。

如果有一些一定要混淆的字符串,可以添加到数组中,会强制转换

28) reservedStrings

类型:字符串数组 默认值:[]禁用字符串文字的转换,该字符串文字与传递的 RegExp 模式相匹配。

一定要保留的字符串,可以放在数组中

接下来就进入标识符名称部分了,也就是变量名啦之类的

29) identifierNamesGenerator

类型:字符串 默认值:hexadecimal设置标识符名称生成器。 可用值:

  • dictionary 来自identifiersDictionary列表的标识符名称
  • hexadecimal 像 _0xabc123这类标识符名称,
  • manged:短标识符名称,如 a、b、c
  • mangled-shuffled:与 mangled 相同,但字母顺序打乱

这个很重要,这些变量名采用哪种形式生成,之前的演示为了简单,都选择的 manged

30) identifiersDictionary

类型:字符串数组 默认值:[]identifierNamesGenerator设置标识符字典:dictionarydictionary中的每个标识符都将用于几个变体中,每个字符的大小写不同。因此,dictionary中标识符的数量应该取决于原始源代码中的标识符数量。

如果你有一个字典(字符数组),里面有一堆字符,这样你可以指定使用这个字典来替换那些要被替换的标识符

31) identifiersPrefix

类型:字符串 默认值:''设置所有全局标识符的前缀。 当您想要混淆多个文件时,请使用此选项。此选项有助于避免这些文件的全局标识符之间的冲突。每个文件的前缀应该不同。

32) renameGlobals

类型:Boolean 默认值:false⚠️这个选项可能会破坏你的代码。仅当您知道它的作用时才启用它! 启用全局变量和函数名称与声明的混淆。

33) renameProperties

类型:Boolean 默认值:false⚠️此选项可能会破坏您的代码。仅当您知道它的作用时才启用它! 启用属性名称重命名。所有内置 DOM 属性和核心 JavaScript 类中的属性都将被忽略。

  • 要在此选项的安全模式和不安全模式之间切换,请使用 renamePropertiesMode 选项。
  • 要设置重命名的属性名称的格式,请使用identifierNamesGenerator选项。
  • 要控制哪些属性将被重命名,请使用reservedNames选项。
34) renamePropertiesMode

类型:字符串 默认值: safe⚠️ 即使在safe模式下,renameProperties 选项也可能会破坏您的代码。 指定 renameProperties 选项模式:

  • safe 2.11.0 发布后的默认行为。尝试以更安全的方式重命名属性以防止运行时错误。使用此模式,某些属性将被排除在重命名之外。
  • unsafe 2.11.0 版本之前的默认行为。以不安全的方式重命名属性,没有任何限制。

如果一个文件正在使用其他文件的属性,请使用identifierNamesCache 选项在这些文件之间保留相同的属性名称。

35) reservedNames

类型:字符串数组 默认值:[]禁用标识符的混淆和生成,这些标识符与传递的 RegExp 模式相匹配。

36) compact

类型:Boolean 默认值:true紧凑的代码输出在一行上。

就是把代码压缩成一行

37) simplify

类型:Boolean 默认值:true通过简化启用额外的代码混淆。 ⚠️ 在未来的版本中,布尔文字的混淆 (true => !![]) 将移至此选项下。

这个选项时代码简化,把一些易读的代码逻辑修改为三元运算符等,还有一些细节,可以研究研究。

38) transformObjectKeys

类型:Boolean 默认值:false启用对象键的转换。

// 原代码
var object = {
  foo: 'test1',
  bar: {
    baz: 'test2'
  }
};
// 混淆后
var a = {};
a['baz'] = 'test2';
var b = {};
b['foo'] = 'test1';
b['bar'] = a;
var object = b;

看起来对对象的键转换的不是很明显,没有达到官方的效果

可能是得配合开启其他选项吧,不只设置 ``hexadecimal 模式

39) numbersToExpressions

类型:Boolean 默认值:false

允许数字转换为表达式

// 原代码
var demo1 = 23;
var demo2 = 24;

var result = demo1 + demo2;
// 混淆后
var demo1 = 0x29d * -0x2 + -0x22f7 + -0x2 * -0x1424;
var demo2 = -0x17b8 * 0x1 + -0x1933 * 0x1 + 0x3103;
var result = demo1 + demo2;

一开始我勾选错了选项,还觉得有点简单,现在可以了

40) controlFlowFlattening

类型:Boolean 默认值:false⚠️ 此选项极大地影响性能,运行速度最多减慢 1.5 倍。使用 controlFlowFlatteningThreshold 设置受控制流扁平化影响的节点百分比。 启用代码控制流扁平化。控制流扁平化是一种阻碍程序理解的源代码结构转换。

关于代码控制流扁平化可以参考一些文章

https://www.cnblogs.com/ichunqiu/p/7383045.html

看到这个扁平化,我总能想起《中国合伙人》里的一句台词: “一位母亲用二十年的时间让他长大成人,而另一个女人只用了二十分钟就让他变回了傻瓜”

41) controlFlowFlatteningThreshold

类型:数字 默认值:0.75 最小值:0 最大值:1controlFlowFlattening 变换将应用于任何给定节点的概率。 此设置对于大型代码特别有用,因为大量控制流转换会减慢代码速度并增加代码大小。controlFlowFlatteningThreshold: 0 等于 controlFlowFlattening: false

42) deadCodeInjection

类型:Boolean 默认值:false⚠️ 显着增加混淆代码的大小(高达 200%),仅当混淆代码的大小不重要时才使用。使用 deadCodeInjectionThreshold 设置受死代码注入影响的节点百分比。 ⚠️ 该选项强制启用 stringArray 选项。 使用此选项,随机的死代码块将被添加到混淆的代码中。

死代码也很烦人

// 原代码
console.log(1)
// 混淆后
var _0x54c615 = _0x4d77;
(function (_0x2f01cd, _0x282040) {
    var _0xfd38d6 = _0x4d77;
    var _0x418071 = _0x2f01cd();
    while (!![]) {
        try {
            var _0x5384d1 = parseInt(_0xfd38d6(0x1ed)) / 0x1 + -parseInt(_0xfd38d6(0x1f2)) / 0x2 + -parseInt(_0xfd38d6(0x1eb)) / 0x3 + -parseInt(_0xfd38d6(0x1e9)) / 0x4 * (-parseInt(_0xfd38d6(0x1f1)) / 0x5) + -parseInt(_0xfd38d6(0x1ec)) / 0x6 * (parseInt(_0xfd38d6(0x1f0)) / 0x7) + parseInt(_0xfd38d6(0x1ef)) / 0x8 + parseInt(_0xfd38d6(0x1ea)) / 0x9;
            if (_0x5384d1 === _0x282040) {
                break;
            } else {
                _0x418071['push'](_0x418071['shift']());
            }
        } catch (_0x99f199) {
            _0x418071['push'](_0x418071['shift']());
        }
    }
}(_0x2499, 0x8b99e));
function _0x4d77(_0x53739e, _0x427c10) {
    var _0x249929 = _0x2499();
    _0x4d77 = function (_0x4d7790, _0x3cb070) {
        _0x4d7790 = _0x4d7790 - 0x1e9;
        var _0x4fc869 = _0x249929[_0x4d7790];
        return _0x4fc869;
    };
    return _0x4d77(_0x53739e, _0x427c10);
}
console[_0x54c615(0x1ee)](0x1);
function _0x2499() {
    var _0x3c0042 = [
        '4uTkwuQ',
        '8809173qEYNab',
        '1628760aaOeDK',
        '84qlULXN',
        '99299HMwqSP',
        'log',
        '4523368ohaBbo',
        '437311NMYccT',
        '1998515lIAtLG',
        '107744EAbXbU'
    ];
    _0x2499 = function () {
        return _0x3c0042;
    };
    return _0x2499();
}
43) deadCodeInjectionThreshold

类型:数字 默认值:0.4 最小值:0 最大值:1 允许设置受 deadCodeInjection 影响的节点百分比。

如果你跟着我完整看了一遍 ob 的混淆方式,恭喜你,基本上已经对混淆的手段有了了解,ob 是个优秀的项目,如果项目有赞赏方式,这篇文章所有的赞赏都会加倍传递给这个项目

接下来的内容就会轻松很多了,有趣不压抑,相信刚刚走出 ob 的你尤其需要它,如果不需要,也可以跳过

6. jjencode

https://utf-8.jp/public/jjencode.html

相信从它的名字,你已经知道,这是一种通过编码来实现混淆的方式,但想不到的可能是它编码后的奇葩样子

// 原代码
console.log("Hello, JavaScript")
// 编码后
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$$__+$._$+"\\"+$.__$+$.$_$+$.$$_+"\\"+$.__$+$.$$_+$._$$+$._$+(![]+"")[$._$_]+$.$$$_+"."+(![]+"")[$._$_]+$._$+"\\"+$.__$+$.$__+$.$$$+"(\\\"\\"+$.__$+$.__$+$.___+$.$$$_+(![]+"")[$._$_]+(![]+"")[$._$_]+$._$+",\\"+$.$__+$.___+"\\"+$.__$+$.__$+$._$_+$.$_$_+"\\"+$.__$+$.$$_+$.$$_+$.$_$_+"\\"+$.__$+$._$_+$._$$+$.$$__+"\\"+$.__$+$.$$_+$._$_+"\\"+$.__$+$.$_$+$.__$+"\\"+$.__$+$.$$_+$.___+$.__+"\\\")"+"\"")())();

原理其实不复杂,但是设计它的人应该还挺有趣的,如果你想详细了解是如何设计的,可以参考以下文章

https://yuanbug.github.io/2019/08/14/2019/my-js-obfuscation-encode/

这种方式解码也非常简单,有很多现在平台可以实现解密

http://www.hiencode.com/jjencode.html

7. AAencode

https://utf-8.jp/public/aaencode.html
// 原代码
console.log("Hello, JavaScript")
// 混淆后
゚ω゚ノ= /`m´)ノ ~┻━┻   //*´∇`*/ ['_']; o=(゚ー゚)  =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');

解码网站

http://www.metools.info/code/aaencode214.html

8. jsfuck

https://jsfuck.com/
// 原代码
console.log(1)
// 混淆后
[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(+[![]]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+!+[]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[!+[]+!+[]])+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]])()(([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[+[]]+([]+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[+!+[]+[!+[]+!+[]+!+[]]]+[+!+[]]+([+[]]+![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[!+[]+!+[]+[+[]]])

0x04 一些感想

代码混淆是一个代码保护以及阻止逆向分析的优解,但是我感觉不像是最终解,我相信代码混淆会在最终解中继续扮演重要角色

感谢创造代码混淆工具、方法以及与其对抗寻求解密的人😀