天天看点

尝试对 jsjiami 加密结果手工解密

看了下 jsjiami,简单的一个 <code>console.log("james")</code>,加密出来的结果居然有 3k,说明这个加密转了不知道多少弯在里面。如果要把真正一段业务代码拿来手工解密,应该会挺累的,但是本文不研究工作量的问题,只是尝试一下手工解密,向各位读者介绍一下分析方法和工具应用。

同一句话在 jsjiami 里可能会加密出不同的结果,我相信这个工具上加入了随机因素。但是为了节约篇幅,这里就不贴我用于试验的加密结果了。分析过程中会贴一些代码段。

毋庸置疑,要想人工识别,首先需要断句。幸好目前美化(格式化)js 的工具还是不少,随便找两个试下,看哪个效果好。我这里是用的浏览器插件 fehelper。

然后注意到,所有变量都改了名字,数字加字母的,怎么读都难受。所以需要使用“重命名”重构工具来改名。这事让 vscode 干毫无压力。

这一句声明了两个变量,一个显然是 jsjiami 的版本版本;另一个是一个数组,除版本信息外,内容猜测是 base64,上网用 base64 解码试了一下,解出来乱码,所以先放着,后面再来看是啥。

为了便于识别,可以 rename 重构一下,顺便按规范拆分声明:

这个 iife 的三个形参,顺便改个名字:<code>p1</code>、<code>p2</code>、<code>p3</code>。iife 里定义了一个局部函数,给它更名为 <code>localfunc1</code>。这个函数定义完之后直接调用,查了一下,没有递归,所以相当于又是一个 iife。同样,它的 5 个参数给改个没啥意义,但是好识别的名字,结果:

注意到,外层 iife 的 <code>p1</code> 就是上面改名为 <code>constarray</code> 的那个数组,反正都是作用域内,干脆一不做二不休,给它换掉:

将 <code>p1</code> 更名为 <code>constarray</code>,跟外面的数组同名

同时删除外层 iife 的第一个形参和实参

既然已经知道 <code>constarray</code> 是个数组,作用在上面的所有属性都应该跟数组相关。就这几行 代码,观察一下不难发现:

<code>lp5</code> 只参与了一个表达式,结果是 <code>"pop"</code>

<code>var _0x1e174c = "shift", _0x5428fe = "push"</code> 两个变量只是当常量使用的,把 <code>var</code> 改成 <code>const</code> 可以让编辑器帮忙检查是否有写操作 —— 当然结果是没有。

不过很遗憾,vscode 没提供内联 (inline) 重构工具,所以只能手工操作,把这两个变量直接替换成常量。以 <code>_0x1e174c = "shift"</code> 为例,先把 <code>"shift"</code>(含引号)复制到剪贴板中,然后在 <code>_0x1e174c</code> 使用若干次 &lt;kbd&gt;ctrl+d&lt;/kbd&gt; 把所有 <code>_0x1e174c</code> 都选中,再 &lt;kbd&gt;ctrl+v&lt;/kbd&gt; 即可。如法炮制处理掉 <code>_0x5428fe = "push"</code>。然后删除两个声明。

尝试对 jsjiami 加密结果手工解密

不过 <code>constarray["shift"]()</code> 这种写法看起来很不习惯,最好能改成 <code>constarray.shift()</code> —— 这就需要借助一下 eslint 了。将当前目录初始化为 npm module 项目,安装并初始化 eslint,然后在配置里添加一条规则:

这时候 vscode 会提示

将鼠标移过去,使用快捷修复自动把所有 <code>[]</code> 调用改为 <code>.</code> 调用。

尝试对 jsjiami 加密结果手工解密

接下就很有意思了,看 <code>localfunc1(++p2, p3)</code> 调用,只传入了两个参数,所以除了刚才去掉的 <code>lp5</code> 之外,形参 <code>lp3</code>、<code>lp4</code> 并没有起到参数的作用,而是当作局部变量来用的。这里可以把它们从参数列表中删除,使用 <code>let</code> 定义为局部变量 —— 当然,这一步做不做无所谓。

而 <code>p2</code> 和 <code>p3</code> 的值是外部 iife 传入的:

乍一看像变量,仔细一看都是 <code>0x</code> 前缀,明明就是整数。而且 <code>p3</code> 就是比 <code>p2</code> 后面多缀两个 <code>0</code>。

再看 <code>localfunc1</code> 内部第一句话就是 <code>lp2 = lp2 &amp;gt;&amp;gt; 0x8</code>(记住 <code>lp2</code> 是传入的 <code>p3</code>),这不就是把 <code>0x1c700</code> 后面两个 <code>0</code> 给去掉变成 <code>0x1c7</code> 吗 —— 现在 <code>lp2</code> 和 <code>p2</code> 的值一样了。而 <code>lp1</code> 是传入的 <code>++p2</code>,所以在现在 <code>lp1 === lp2 + 1</code>。

这样就满足了 <code>if</code> 条件 <code>(lp2 &amp;lt; lp1)</code>,这个 <code>if</code> 语句没用了,可以直接解掉。

接下来是一个神奇的循环,<code>while (--lp1) { }</code>,中间没有 <code>break</code>,也就是说,需要循环 <code>0x1c7 + 1</code> 次,也就是 <code>456</code> 次。基本上可以猜测这个循环干的就是没用的事情,浪费 cpu 而已。

来分析一下是不是:

既然刚才已经说了 <code>lp3</code> 和 <code>lp4</code> 就是局部变量,不妨再改个名,分别改为 <code>local1</code> 和 <code>local2</code>,好识别。现在的 <code>while</code> 循环是这样:

刚才还分析了 <code>lp1 === lp2 + 1</code>,所以 <code>while (--lp1)</code> 第一次执行的时候,<code>lp1</code> 和 <code>lp2</code> 就相等了,进入 <code>if (lp2 === lp1)</code> 分支;此后,都不会再进入这个分支,因为 <code>lp1</code> 一直在减小。

那么第一次循环执行的内容可以写成:

此后,这个循环中再没有对 <code>lp2</code> 和 <code>local1</code> 赋过值。而此时 <code>constarray</code> 的值是

后面的 <code>local1.replace(...)</code> 这句话可以直接拿到控制台去跑一下,结果让人哭笑不得,就是 <code>"jsjiami.com.v6"</code>。从这个结果来看,<code>else if (...)</code> 条件除第一次不执行,之后都是 <code>true</code>,也就是说,总是执行,那不就和 <code>else</code> 一样了嘛。

好嘛,除去第一次循环,这个循环变成了:

没别的,就是转圈,一共转了 <code>455 - 1 = 454</code> 次!次数如果算不清楚,写一个循环跑一下就知道了:

<code>local2</code> 之后再没使用,所以 <code>while</code> 中的两句话可以合并成一句:

这和 <code>while</code> 循环之后那一句完全一样。所以这句话执行的次数一共是 <code>454 + 1</code>,也就是 <code>455</code> 次。由于 <code>constarray</code> 现在有两个元素,而 <code>455</code> 是奇数,所以跑完之后 <code>constarray</code> 是这样:

至此,第一小段代码分析完成,除了改变 <code>constarray</code> 没干任何有意义的事情。

至于这段代码里的两句 <code>return</code>,没半点用,因为外层 iife 的返回值直接被丢弃了。所以返回语句里的位运算,都懒得去算了。

整个这一段代码最终变成一句话:

而且猜测 <code>constarray</code> 其实没啥用

分析了半天,基本上没啥有用的代码。而且基本上可以断定,后面的几十行代码也只是在浪费 cpu。

因为我们知道原代码是 <code>console.log("james")</code>。所以为了加快分析速度,就不再一行行往下读了,直接从后往前看。一眼就看到了

反推,<code>_0x2a10("0", "]o48")</code> 的结果就是 <code>"log"</code>,而 <code>_0x2a10("1", "wcmn")</code> 的结果就是 <code>"james"</code>。

猜测,<code>_0x2a10</code> 就是个拼字符串的函数,而第 1 个参数,就是个标记,作分支用。

既然都已经知道 <code>_0x2a10</code> 是拼字符串的了,那改名叫 <code>getstring</code> 吧。第一个参数是标记,改名为 <code>flag</code>,第二个参数多半是计算用的初始值,就叫 <code>initvalue</code> 好了。

其中第一句:<code>flag = ~~"0x".concat(flag);</code>。这句就是把 <code>flag</code> 按 16 进制转换成数值类型的值而已。根据实际的调用参数,去控制台跑一下 <code>~~"0x1"</code> 和 <code>~~"0x2"</code> 就知道了,还可以试验一下 <code>~~"0xa"</code>。

接下来的 <code>var _0x1fb2c5 = constarray[flag];</code> 也就好理解了,而且到这里总算明白了,原来 <code>constarray</code> 是用来提供拼接字符串的部分因素的。既然如此,给它更名为 <code>factor</code>。

如果不管这个长长的 <code>if</code> 语句内部那些复杂的逻辑,精简下来就是:

也就是在第一次运行 <code>getstring</code> 的时候对它进行初始化。

其中 <code>.ioaiiu</code> 只有两处引用,一处判断,一处赋值 —— 明显是个初始化标记,可以改名为 <code>initialized</code>。只不过这时候 rename 重构工具似乎不能用,手工更名吧。

<code>if</code> 分支内第一段代码又是个 iife,单独拷贝出来放到一个独立的 <code>js</code> 文件中,vscode 并没有提示找不到变量之类的事情。所以这段代码是可以独立运行的。

第一句很明显是在找 <code>global</code> 对象,相当于 <code>var _0xea3c63 = globalthis</code>。

第二句先忽略,第三句明显是看 <code>globalthis</code> 上有没有 <code>atob()</code>,如果没有就给它一个。既然 <code>atob()</code>在多数环境下都存在,那就不用纠结其内容了。

那么,这段 iife 就是保证 <code>atob()</code> 可用,可以直接删掉不看。

接下来又定义了一个函数,去掉内容,长这样:

通过后面的调用来用,应该是个比较有用的函数。为了方便识别,把两个参数分别更名为 <code>first</code> 和 <code>second</code>。

我们也把它摘出来拷贝到一个独立的 <code>.js</code> 文件中,发现也没有缺失变量,说明可以单独拿出来分析,就是个工具函数。

这个函数一来定义了 5 个变量,先不管,用到的时候再去找。

下面的代码是:

这段代码不用仔细看,大概知道是把一个 base64 转成 <code>%xx</code> 的形式,而这个形式的字符串用 <code>decodeuricompoment()</code> 可以再转成字符串(绕好大一圈)。

回想一下 <code>constarray</code> 的元素,确实长得像 base64,所以这里应该是处理那些元素了。

接下来的代码就是通过一大堆的数学计算,从 <code>initvalue</code> 和 <code>constarray[i]</code> 把我们需要的字符串恢复出来。算法肯定是加密工具自己设计的,懒得去分析了。计算都不难,就是烧脑,需要仔细,一点不能出差错。

是的,结束了,戛然而止。

写这篇文章的目的并不是要把代码完全解出来,只是证明其可能性,同时介绍分析方法和工具应用。第 2 部分写完就该结束的,因为后面也没有用到什么新的方法。

总的来说,jsjiami 向原始代码中添加了非常多无用而烧脑的程序来提高解码的难度,这么简单的一句话都解了这么久,生产代码就更不用说了。代价也是有的 —— 真烧 cpu。

好吧,我又干了一件无聊的事情!

继续阅读