JSFuck原理解析(一) —— 基础原理
本系列博客是对jsfuck代码的一些分析,暂定为三篇。第一篇为jsfuck基础原理,第二篇为官方案例分析,第三篇为源码解析。
不出意外的话就是这三篇,我实在是比较懒,第一篇过了一年半才去写第二篇,但愿第三篇不会再拖了……
如果你也有对知识的渴望,可以直接访问原作者的GitHub:https://github.com/aemkei/jsfuck 原作者其实已经解释很清楚了。不过学习嘛,自己分析才有乐趣。
那下面开始正文:
什么是jsfuck?
想象一下自己第一次见到代码的样子,满屏的奇怪字符?WTF?而当我看到jsfuck的时候,再次体会到了这种感觉。请看下面的代码:
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()
WTF???
没错!这确实是一串js代码,你可以直接放到控制台去执行,最后执行的结果是alert(1)。JS就是这么一门神奇的语言。本着对知识的渴求与对未知事物的探索精神(雾),下面我们就一起来研究下jsfuck的原理。
在讲jsfuck之前,先要说起一个js的经典面试题:[]==![]
这道题是考验js基础的,利用的是js的一些基本原理——类型转换。类型转换大家都了解,比如简单的 ‘a’+1结果就是’a1’,在这个计算过程中1被隐式转换成string类型,这道面试题也是这个原理。Js语法中’==’两边如果不是同一类型则会进行类型转换。
但是相信很多接触过前端的同事见到这个题目时候的第一反应,是这个表达式的结果是false,然而拿到控制台执行之后却发现是true,这让很多人十分不解。特别是当我们在控制台打印代码的时候,会出现一些令人费解的事情。如:
Boolean(![]) //false
其实上述代码,我们进行了强制转换,在js中在把对象做布尔值转换的时候,会把所有对象都转换为true(包括创建的new Boolean(false)对象转换之后也是true,具体可参考犀牛书3.8.3)。而相等运算符却不是这么计算的,在相等运算符中,如果运算符两边的值有一个是布尔类型,则会把true转换成1,false则转换为0。而在这道题目中![]的结果肯定是布尔值false,则会转换为0。
[]==0
这时候解释起来就简单了很多,相等运算符如果有一边是数字,而另一边是对象,则会把对象转换成数字。而空的数组对象转换成数字是0(犀牛书3.8)那么结果便是:
0==0 //true
讲了这么多,终于可以进入正题了。之所以开篇讲这么一个题目,是为了更好理解jsfuck的原理。在官网上(直接百度jsfuck就行),我们能看到官方给出的jsfuck的Basics,下面我们来逐条分析。(水平有限,如果有发现错误可以指出,大家一起研究)
false => ![]
//非运算符会把[]的布尔值取反
true => !![]
//再取反则是true
undefined => [][[]]
//这一个就比较复杂,在js中数组的索引可以为负数和非整数,而在这种情况下会将索引转换成字符串,作为对象的属性访问,我们知道[]转换为字符串为空字符串,则上面等价于:[][‘’]。然而数组对象并没有名为’’的属性,在js中访问一个对象并不存在的属性时,会给出undefined
NaN => +[![]]
//这里利用了+运算符的特性,这里+不是加号运算符(姑且这么叫)而是一元运算符的一种,一元加法,用于把操作数转换为数字或者NaN并返回这个数字。上式等价于+[false],而根据类型转换的规则我们知道,这样得到的结果为NaN
0 => +[]
//根据类型转换规则,[]转换为数字为0
1 => +!+[]
//本式为上面的变式,根据运算符的优先级,先执行+[]为0,再转换成布尔取反为true,true转换为数字为1
2 => !+[]+!+[]
//中间一个+为加法运算符优先级比较低,等价于true+true,值为2
10 => [+!+[]]+[+[]]
//可拆分成[+!+[]]加[+[]],左边为[1],右边为[0],+作为连接字符串运算符,而根据数组转换成字符串我们可以知道上面等价于‘1’+‘0’,得到的是字符串‘10’
Array => []
//[]就是数组对象的事例,用于获取字符串’Array’,用["constructor"]找到对应函数,再将其变成字符串,在最后一篇源码解析中会讲到。
Number => +[]
//与上式相同,后面会详细讲解
String => []+[]
//与上式相同,后面会详细讲解
Boolean => ![]
//与上式相同,后面会详细讲解
Function => []["filter"]
//与上式相同,后面会详细讲解
eval => []["filter"]["constructor"]( CODE )()
//这里比较复杂,我们先来看前半部分[]["filter"]["constructor"],这一部分可以看到拿的是一个数组对象的‘filter’属性下面的‘constructor’属性,‘filter’属性是数组的一个方法,但在这里并没有用到该方法,你把它替换成别的方法也可以照常运行(例如‘map’),关键是下面这一步,filter的constructor属性,我们知道在js中任何方法都可以看做是Function对象的一个实例,而Function就作为所有函数的构造函数。这里拿到的就是函数的Function()构造函数。而Function()构造函数的最后一个参数会作为函数体执行。因此这段语句会执行‘CODE里的内容’。
window => []["filter"]["constructor"]("return this")()
//与上式相同,拿到的是函数的构造函数,第一个括号内便是函数体。并且Fuction()构造函数所创建的函数并不使用词法作用域(划重点!),他是直接执行在顶层函数中的,也就是全局作用域,因此this返回window。
说了这么多,却还是无法解释开篇那外星语言一般的代码,但是有了这篇博客的基础,接下来就方便了很多下一篇会详细分析jf里面的一些字母与语句的由来,敬请期待
出处:https://blog.csdn.net/qq_36539075/article/details/79946099
=========================================================================
JSfuck原理解析(二) —— 官方例子分析
上一篇我们已经解释了jsfuck的基本原理,简单来说,如果我们想要用jsfuck加密一段可执行代码,那么代码最后应该是这样的类型:
Function(code)()
在上一篇中我们提到,Function()()这一段可以转换成如下代码:
[]["filter"]["constructor"]()()
那么,我们就得到了一段完全由“ ()+[]! ”与字符串组合的代码,接下来我们只需要把字符串也加密成“ ()+[]! ”就可以了!
所需要加密的字符串,包含了所有可显示字符在此,我们依然拿“ alert(1) ”来举例:
字符串“ alert(1) ”可拆解为a、l、e、r、t、(、1、),八个字符,这些字符我们很容易在上一篇文章中找到:
1 => +!+[]
false => ![]
true => !![]
由上可得:
'a' == 'false'[1] == (false + '')[1] == (![]+[])[+!+[]]
'l' == 'false'[2] == (false + '')[2] == (![]+[])[!+[]+!+[]]
'e' == 'true'[0] == (true + '')[3] == (!![]+[])[!+[]+!+[]+!+[]]
'r' == 'true'[0] == (true + '')[1] == (!![]+[])[+!+[]]
't' == 'true'[0] == (true + '')[0] == (!![]+[])[+[]]
则:'alert' == (![]+[])[+!+[]] + (![]+[])[!+[]+!+[]] + (!![]+[])[!+[]+!+[]+!+[]] + (!![]+[])[+!+[]] + (!![]+[])[+[]]
那么我们如何得到括号字符呢?首先我们要找到包含括号的字符串--方法,方法的基本样式就是function name (){ code }这样我们只需要把它转换成字符串,按照上面的方法实现一遍就行了。jsfuck官方给出的方法是:
([][[]]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[!+[]+!+[]]]
拆分可得:
(undefined + []['fill'])['22'] == "undefinedfunction fill() { [native code] }"['22']
这里只是用了数组的fill方法,和方法本身没有关系,换成find没有影响,一样可以得到括号。即:
(undefined + []['find'])['22'] == "undefinedfunction find() { [native code] }"['22']
既然得到了左括号,那么右括号只需要把数字22改成23就可以得到,即:
([+[]]+![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[+[]]]
把以上加密后的代码组合(部分代码记得加上括号),我们便得到了:
(![]+[])[+!+[]] + (![]+[])[!+[]+!+[]] + (!![]+[])[!+[]+!+[]+!+[]] + (!![]+[])[+!+[]] + (!![]+[])[+[]]+([][[]]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[!+[]+!+[]]]+(+!+[])+([+[]]+![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[+[]]] == 'alert(1)'
到目前为止,字符串我们已经拿到了,接下来只需要按照上面的方法,拿到Function就行了,最终组合成:
[]["filter"]["constructor"]('alert(1)')() == Function('alert(1)')()
这样一个完美的jsfuck加密代码已经完成了!
出处:https://blog.csdn.net/qq_36539075/article/details/102463521
===================================================================================================
JSFuck原理解析(三) —— 源码解析
通过前两章我们已经了解了jsfuck的基本原理与实现,现在不妨先设想一下,假设我们要自己实现这么一个加密代码,应该如何去做。
首先,我们拿到了一段明文代码“alert(1)”,为了把他变成jsf的模式,我们要按照第二章的描述对各个字符挨个加密,最后拼接成想要的代码。那么如果我们想把这个过程工程化,我们就需要一个map,里面有每个字符对应的jsf代码,这样我们加密一串代码只需要拼接就行了,事实上jsf的原作者也是这么做的。
打开github:https://github.com/aemkei/jsfuck,根目录下有一个叫做jsfuck.js的文件,里面部分代码如下:
const SIMPLE = {
'false': '![]',
'true': '!![]',
'undefined': '[][[]]',
'NaN': '+[![]]',
'Infinity': '+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])' // +"1e1000"
};
const CONSTRUCTORS = {
'Array': '[]',
'Number': '(+[])',
'String': '([]+[])',
'Boolean': '(![])',
'Function': '[]["fill"]',
'RegExp': 'Function("return/"+false+"/")()',
'Object': '[]["entries"]()'
};
const MAPPING = {
'a': '(false+"")[1]',
'b': '([]["entries"]()+"")[2]',
'c': '([]["fill"]+"")[3]',
'd': '(undefined+"")[2]',
'e': '(true+"")[3]',
'f': '(false+"")[0]',
'g': '(false+[0]+String)[20]',
'h': '(+(101))["to"+String["name"]](21)[1]',
……
在这里我们可以看到三个常量,其中的MAPPING确实如我们所想的一样,是一个字符与代码的对应,不过对应的值看起来并不是jsf的代码,并且有些甚至是缺失的,比如:'P': USE_CHAR_CODE。这些字符我们不能如a、b、c那样轻易的在现有的字符串中找到,需要特殊的处理。
那么jsf的源码到底是怎么工作的呢,接下来我们就来整体分析一下jsfuck.js文件的代码。
我们可以把代码折叠一下,得到下图:
由上图可看出,代码分为三个部分,常量、方法、执行。
MAPPING补全:
我们直接来看第三部分,方法的执行,第一个方法fillMissingDigits,字面意思可以看出是填补缺失的数字,代码如下:
function fillMissingDigits(){ var output, number, i; for (number = 0; number < 10; number++){ output = "+[]"; if (number > 0){ output = "+!" + output; } for (i = 1; i < number; i++){ output = "+!+[]" + output; } if (number > 1){ output = output.substr(1); } MAPPING[number] = "[" + output + "]"; } }
这段代码很简单,从中可以看出,这个方法的功能是填补MAPPING中数字(0 - 9)的键值对,如:0: '[+[]]'、1: '[+!+[]]'。(在外面加了一个括号可以方便的转换为字符串。)
那么很自然就得出下面的fillMissingChars方法的功能是填补MAPPING中缺失的其他字符的键值对,下面的一串代码也比较少,我也把它贴出来:
function fillMissingChars(){ var base16code, escape; for (var key in MAPPING){ if (MAPPING[key] === USE_CHAR_CODE){ //Function('return"\uXXXX"')() base16code = key.charCodeAt(0).toString(16); escape = ('0000'+base16code).substring(base16code.length).split('').join('+'); MAPPING[key] = 'Function("return"+' + MAPPING['"'] + '+"\u"+' + escape + '+' + MAPPING['"'] + ')()'; } } }
看代码可以知道,此方法处理了MAPPING中的所有值为USE_CHAR_CODE的字符,原理比较简单暴力,直接用到了charCodeAt,由于key是一个字符,就相当于返回了key这个字符的Unicode编码,并转换成16进制。我们拿'a'举例,会得到'61'
接下来的代码做了三件事:
1. 将得到的16进制数转换成4位,缺少的位数在左方补0。(例如’61‘ => '0061')
2. 分割字符串
3. 用'+'合并字符串。(例如'0061' => '0+0+6+1')
为什么要这么做呢?下面的代码我们就能得到答案,下面的代码是拼接字符串。
我们可以在MAPPING中找到MAPPING['"']的值为:'("")["fontcolor"]()[12]',我们将字符串拼接之后就会得到如下字符串:
'Function("return"+("")["fontcolor"]()[12]+"\u"+0+0+6+1+("")["fontcolor"]()[12])()'
我们已经知道Function( code )()的作用,它可以将字符串作为可执行代码执行,那么我们只需要把code部分组合出来就能得到:
"return"+("")["fontcolor"]()[12]+"\u"+0+0+6+1+("")["fontcolor"]()[12]
这段字符串执行了之后,其实就是:return "u0061",得到的Unicode字符就是'a'.
到此为止我们已经填充了所有的缺失字符,但是他并不是我们需要的jsf代码,而是半成品。
替换字符:
从下面两个方法的名字就能看出来,接下来要做的是把所有字符全部替换成[]()!+这些符号。
我们先来看第一个替换方法:replaceMap。源代码太长,就不贴了,大家可以看github的jsfuck.js文件的146行。我们可以看到这个方法依然是先定义了三个方法,然后执行了一个循环MIN(32) => MAX(126)细心的同学肯定发现了,这个定义好的范围正好是ascii表的所有可显示字符,相当于遍历了一遍MAPPING的所有键值对。
在循环中首先执行了String.fromCharCode方法,将数字转换成了Unicode字符,相当于MAPPING中的键,然后在通过索引拿到对应的键中的值。
首先执行了如下代码:
-
for (key in CONSTRUCTORS){
-
replace("\b" + key, CONSTRUCTORS[key] + '["constructor"]');
-
}
我们拿 'A': '(+[]+Array)[10]'举例,经过调用,最终的执行代码是这样的:
newRegExp("Array", "gi"),
'[]["constructor"]'
);
得到的结果是字符串:'(+[]+[]["constructor"])[10]'。
这个替换我们可以理解为,将含有最开始定义的CONSTRUCTORS下的key全部替换掉,替换原因是这些key包含的都是一个个对象,并不能经过简单的字符串拼接就能得到。这样我们距离最终的加密代码又紧了一步。
第二步与第一步意义相同,是将含有SIMPLE的key全部替换掉,这些是可以直接使用的jsf代码成品。
再接下来的是连着六个replace,分别为:
1. 将两位以上的数字全部替换。(两位以上的数字作为Number替换,因为如果我们直接用MAPPING,只会打得到相加的字符串,
例:MAPPING[1] + MAPPING[1] = [+!+[]]+[+!+[]] = '11')
2. 将‘(Number)’替换成MAPPING[key]。
3. 将‘[Number]’替换成MAPPING[key]。
4. 将'GLOBAL'替换成Function("return this")()。
5. 将'+""'替换成+[]。(+""是为了转换成字符串)
6. 将""替换成[]+[]。(括号中的空字符串)
经过这一轮替换,我们得到的MAPPING离目标更近了一步,得到如下图的MAPPING。
下一步便是最后一个方法:replaceStrings。这个方法开头便定义了一个正则表达式:
var regEx = /[^[]()!+]{1}/g
它匹配了所有的非目标字符( 非[]()!+ ),那么我们就能猜测到,经过这一轮替换,将得到我们的最终map。那么它是如何工作的呢?我们逐行来看。首先定义了一些变量和方法,然后执行了下面一段代码:
for (all in MAPPING){ MAPPING[all] = MAPPING[all].replace(/"([^"]+)"/gi, mappingReplacer); }
这段代码替换了MAPPING中的所有带双引号的字符串( “fill” => f+i+l+l ),完成之后是一个while循环,这个循环涉及到了两个方法,我们来一一分析:
function findMissing(){ var all, value, done = false; missing = {}; for (all in MAPPING){ value = MAPPING[all]; if (value.match(regEx)){ missing[all] = value; done = true; } } return done; }
代码很简单,循环了MAPPING,如果有值含有非jsf字符,则返回true,一遍循环之后,生成了一个对象missing,里面是MAPPING的所有含有非jsf字符的key。
2. valueReplacer:更简单,用来替换非jsf字符,如果MAPPING[key]全是jsf字符,则直接替换,不是则不变。
经过一遍又一遍的循环,最终将所有的非jsf字符替换成了jsf字符。最终的MAPPING就诞生了。
不过这里有一个问题,我们可以留意一下作者写下的一段error提示:
if (count-- === 0){ console.error("Could not compile the following chars:", missing); }
上述循环其实是有可能造成死循环的,如果几个非jsf代码的value相互引用,且最终无法转换成jsf字符,就会造成死循环。其实我们最初看作者定义的常量的时候就能看出,这些被定义的常量都是经过筛选的,不会造成循环引用。
最后一个方法就是暴露出encode方法,很简单就不做分析了。
(填坑完毕)
拓展:
AAencode:js颜文字加密
Brainfuck:Brainfuck是一种极小化的计算机语言,它是由Urban Müller在1993年创建的。jsfuck就是按照这种思维模式开发出来的。
其他字符编码还有很多,例如:JJencode,可以从网上搜索看看
图灵完备:图灵完备是指机器执行任何其他可编程计算机能够执行计算的能力。图灵完备也意味着你的语言可以做到能够用图灵机能做到的所有事情,可以解决所有的可计算问题。
出处:https://blog.csdn.net/qq_36539075/article/details/102502775