原文:Reverse Engineering One Line of JavaScript
作者:Alex Kras
翻译:雁惊寒
译者注:本文作者结合数学知识,图文并茂、非常细致地对一行神奇的混淆过的js代码进行了剖析。以下是译文。
几个月前,我看到一封电子邮件,询问是否有人可以破解这一行JavaScript代码。
<script>n=setInterval("for(n+=7,i=k,P='p.\\n';i-=1/k;P+=P[i%2?(i%2*j-j+n/k^j)&1:2])j=k/i;p.innerHTML=P",k=64)script>
这行代码可以渲染出下面这个动态图,你可以在这个网页上看到。它的作者是Mathieu ‘p01’ Henri,www.p01.org的站长,你可以在他这个网站上找到很多很酷的东西。
好,我来接受挑战!
首先,将HTML标签保存在HTML文件中,将JavaScript代码保存为code.js
文件。同时,用双引号把id="p"
中的p引起来。
index.html
<script src="code.js">script>
<pre id="p">pre>
我注意到有一个变量k
,它只是一个常量,所以我把它从那一行代码中移出来,并重命名为delay
。
code.js
var delay = 64;
var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var n = setInterval(draw, delay);
接着,var draw
只是一个字符串,它在setInterval中被执行,跟eval
的效果一样。因为setInterval可以接受要评估的函数或字符串。我把它移到一个实际的函数中。 旧代码仍然保留在那以供参考。
我注意到的另一件事是,元素p
实际上是指在HTML中id为p的DOM元素,就是上面我用引号引起来的变量。 只要id仅由字母或数字组成,则可以通过JavaScript的id来引用元素。 我添加了一行document.getElementById("p")
,这样看起来更直观。
var delay = 64;
var p = document.getElementById("p"); // < --------------
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
for (n += 7, i = delay, P = 'p.\n'; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
j = delay / i; p.innerHTML = P;
}
};
var n = setInterval(draw, delay);
接着,我声明了变量i
、p
、j
,并把他们放到函数的最前面。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay; // < ---------------
var P ='p.\n';
var j;
for (n += 7; i > 0 ;P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
j = delay / i; p.innerHTML = P;
i -= 1 / delay;
}
};
var n = setInterval(draw, delay);
我将for
循环分解成一个while
循环。 仅保留for
中的CHECK_EVERY_LOOP部分(for循环总共包括3个部分,分别为:RUNS_ONCE_ON_INIT; CHECK_EVERY_LOOP; DO_EVERY_LOOP),并将其他部分移到循环体的内部或外部。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) { // <----------------------
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];
}
};
var n = setInterval(draw, delay);
将三元操作符( condition ? do if true : do if false
) P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];
展开。
i%2
用于检查i
是偶数还是奇数。 如果i
是偶数,就返回2。如果i
是奇数,则返回(i % 2 * j - j + n / delay ^ j) & 1;
这个魔术值。
最后,index
用于在字符串P
内进行偏移,因此变为P += P[index];
。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0); // <---------------
if (iIsOdd) { // <---------------
index = (i % 2 * j - j + n / delay ^ j) & 1;
} else {
index = 2;
}
P += P[index];
}
};
var n = setInterval(draw, delay);
我把index = (i % 2 * j - j + n / delay ^ j) & 1
中的& 1
放到另一个if语句中。
这个方法用于检查括号中的结果是奇数还是偶数,如果是偶数,则返回0,奇数则返回1。 &
是按位与运算符。按位与的逻辑如下:
因此,something & 1
会把something
转换成二进制的表示形式,还会根据需要在前面填充0,以匹配某个东西的长度,并且只返回最后一位的AND结果。 例如,二进制中的5是101
,如果与1
进行“与”,则将得到以下结果:
101
AND 001
001
换句话说,5是奇数,5&1的结果是1。在JavaScript控制台中很容易就能确认这个逻辑关系。
0 & 1 // 0 - even return 0
1 & 1 // 1 - odd return 1
2 & 1 // 0 - even return 0
3 & 1 // 1 - odd return 1
4 & 1 // 0 - even return 0
5 & 1 // 1 - odd return 1
请注意,我还将其余的index
重命名为magic
,所以转换了&1
的代码如下所示。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = (i % 2 * j - j + n / delay ^ j);
let magicIsOdd = (magic % 2 != 0); // &1 < --------------------------
if (magicIsOdd) { // &1 <--------------------------
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
P += P[index];
}
};
var n = setInterval(draw, delay);
接下来,我将P += P[index];
转换成switch语句。现在很清楚了,index只能是0、1、2这三个值中的一个。因此,P
总是用这个值进行初始化:var P ='p.\n';
。 其中0指向p
,1指向.
,2指向\n
(换行符)。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = (i % 2 * j - j + n / delay ^ j);
let magicIsOdd = (magic % 2 != 0); // &1
if (magicIsOdd) { // &1
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
switch (index) { // P += P[index]; <-----------------------
case 0:
P += "p"; // aka P[0]
break;
case 1:
P += "."; // aka P[1]
break;
case 2:
P += "\n"; // aka P[2]
}
}
};
var n = setInterval(draw, delay);
我清理了var n = setInterval(draw, delay);
中的魔术值。 setInterval返回一个从1开始的整数,每次调用“setInterval”时,它就会递增1。 该整数可作为clearInterval
(取消定时)的参数。 在我们这个例子中,setInterval
只调用一次,而n
只是简单地设置为1。
我还将delay
命名为DELAY
,以提醒开发者它只是一个常数。
最后,我将括号放在i % 2 * j - j + n / DELAY ^ j
中,是要指出^
按位异或的优先级低于%
、*
、-
、+
和/
运算符。 换句话说,上述所有计算将在^
之前执行。 新的表达式为:(i % 2 * j - j + n / DELAY) ^ j)
。
const DELAY = 64; // approximately 15 frames per second 15 frames per second * 64 seconds = 960 frames
var n = 1;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
/**
* Draws a picture
* 128 chars by 32 chars = total 4096 chars
*/
var draw = function() {
var i = DELAY; // 64
var P ='p.\n'; // First line, reference for chars to use
var j;
n += 7;
while (i > 0) {
j = DELAY / i;
i -= 1 / DELAY;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = ((i % 2 * j - j + n / DELAY) ^ j); // < ------------------
let magicIsOdd = (magic % 2 != 0); // &1
if (magicIsOdd) { // &1
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
switch (index) { // P += P[index];
case 0:
P += "p"; // aka P[0]
break;
case 1:
P += "."; // aka P[1]
break;
case 2:
P += "\n"; // aka P[2]
}
}
//Update HTML
p.innerHTML = P;
};
setInterval(draw, 64);
你可以在此处看到最终结果。
var i = DELAY;
将i
的初始值设为64,并且在每个循环中,i -= 1 / DELAY;
将i
递减1/64(0.015625)。一直到i
不再大于0循环才结束 while (i > 0) {
。每循环一次,i
减少1/64,所以,64次循环后,i
将减少1(64/64 = 1)。所以,需要减少64×64 = 4096次,才会小于0。
该图像由32行字符组成,每行128个字符。很简单,64 x 64 = 32 x 128 = 4096。当i
是偶数的时候,iIsOdd才会等于0(let iIsOdd = (i % 2 != 0);
)。这种情况会出现32次,分别是i
等于64、62、60……的时候。这32次,index
会被设置为2 index = 2;
,并且新增一行字符P += "\n"; // aka P[2]
。该行剩下的127个字符将被设置为p
或.
。
但是我们应该在什么时候设置为p
,什么时候设置为.
呢?
当魔术值let magic = ((i % 2 * j - j + n / DELAY) ^ j);
是奇数的时候,会设置为.
,当魔术值是偶数的时候,设置为p
。
var P ='p.\n';
...
if (magicIsOdd) { // &1
index = 1; // second char in P - .
} else {
index = 0; // first char in P - p
}
但是这个魔术值什么时候是奇数,什么时候是偶数呢?在谈论这个之前,让我们先来看另外一个东西。
如果我们从let magic = ((i % 2 * j - j + n / DELAY) ^ j);
中删除+ n/DELAY
,我们最终会得到如下的静态布局。
现在,让我们看一下已经删掉+ n/DELAY
的magic
。我们最终如何得到上面那个漂亮的图片呢?
(i % 2 * j - j) ^ j
注意,对于每一个循环,我们都有:
j = DELAY / i;
i -= 1 / DELAY;
我们可以把给i
的表达式带入到给j
的表达式中,这样就变成j = DELAY /(i + 1 / DELAY)
,由于1/DELAY
是一个非常小的数字,所以我们可以丢弃+ 1/DELAY
并简化为j = DELAY/i = 64/i
。
同时,我们将(i % 2 * j - j) ^ j
变为(i % 2 * 64/i - 64/i) ^ 64/i
。
我们来使用在线图形计算器绘制其中的一些函数。
首先,我们来绘制i%2
。
如果我们绘制64/i
,将会看到这样的图形。
如果我们绘制左边的表达式,我们得到的图形看起来就像是以上两个图形的组合。
最后,我们把两个函数并列,可以得到以下图形。
我们知道,如果魔术值(i % 2 * j - j) ^ j
是偶数,则添加p
,如果是奇数,则添加.
。
我们放大一下图表的前16行,即i
的值从64到32。
JavaScript中的按位异或会将小数点右边的值丢弃,这个有点像对一个数字做Math.floor
运算。
j
的值从1开始,并慢慢接近2,但始终小于2,所以我们可以将把他看成是1(Math.floor(1.9999) === 1
),我们需要左边表达式的值为1,这样就能得到结果0(意思是偶数),并最终得到一个p
。
换句话说,每条绿色的斜线代表了图表中的一行。对于前16行,j
始终是高于1但低于2,我们可以得到奇数值的唯一方法是(i % 2 * j - j) ^ j
,也就是i % 2 * 64/i - 64/i
,也就是绿色斜线要大于1或者小于-1。
以下是JavaScript控制台的一些输出,0或-2意味着结果是偶数,1表示结果是奇数。
1 ^ 1 // 0 - even p
1.1 ^ 1.1 // 0 - even p
0.9 ^ 1 // 1 - odd .
0 ^ 1 // 1 - odd .
-1 ^ 1 // -2 - even p
-1.1 ^ 1.1 // -2 - even p
看一下画出来的图形,我们能看到右边的斜线几乎刚巧超过1或小于-1(几乎没有奇数,也就是几乎没有p
),下一行则更接近一点。第16行刚巧小于2或大于-2。第16行后,静态图换了一种模式。
第16行j
跨越2行,预期结果翻转。现在,当绿色斜线大于2,小于-2,或者2和-2之间,但不等于1和-1时,我们会得到一个偶数。这就是为什么我们从第17行开始看到两组或两组以上的p
。
如果仔细观察运动图像的底部几行,你会注意到,它们不再遵循相同的模式。
现在我们回到+ n/DELAY
上来。在代码中,我们可以看到,n是从8开始,然后每次setInteval触发后就会增加7。
当n变为64时,图形则变成如下这样。
生成HTML如下所示。与我们的预期相符。
在这一点上来讲,p
的数量已经增长到一个恒定值。例如,对于第一行,有一半是偶数。而从现在开始,p
和.
只是改变他们之间的位置。
为了说明这一点,当n在下一个setInterval增加7时,图形将略有变化。
请注意,第一行的斜线已经移动了大约1个小方块。假设4个更大的正方形代表128个字符,则1个大的正方形代表32个字符,1个小的正方形将代表32/5 = 6.4个字符(近似值)。如果看一下渲染的HTML,我们能看到第一行实际上是向右移动了7个字符。
当setInterval被调用7次以上,n等于64+9×7时,会发生什么。
对于第一行,j
仍然等于1。现在,64左右的红色斜线的上半部分为〜2,底部为〜1。从1^2 = 3 // 奇数 - .
和1 ^ 1 = 0 //偶数 - p
开始,图片翻转了。所以我们预计是一堆点,然后是个p
。
渲染出来的HTML是这样的。
这个图形会以类似的方式无限循环下去。