- 1. 第一步,可读化
- 2. 然后,一点点来分析
- 3. 剩下的代码简单分析下
- 4. 结束
2.2.4. 分析参数作用
接下就很有意思了,看 localFunc1(++p2, p3)
调用,只传入了两个参数,所以除了刚才去掉的 lp5
之外,形参 lp3
、lp4
并没有起到参数的作用,而是当作局部变量来用的。这里可以把它们从参数列表中删除,使用 let
定义为局部变量 —— 当然,这一步做不做无所谓。
而 p2
和 p3
的值是外部 IIFE 传入的:
(function (p2, p3) { ... }(0x1c7, 0x1c700));
乍一看像变量,仔细一看都是 0x
前缀,明明就是整数。而且 p3
就是比 p2
后面多缀两个 0
。
再看 localFunc1
内部第一句话就是 lp2 = lp2 >> 0x8
(记住 lp2
是传入的 p3
),这不就是把 0x1c700
后面两个 0
给去掉变成 0x1c7
吗 —— 现在 lp2
和 p2
的值一样了。而 lp1
是传入的 ++p2
,所以在现在 lp1 === lp2 + 1
。
这样就满足了 if
条件 (lp2 < lp1)
,这个 if
语句没用了,可以直接解掉。
2.2.5. 神奇的循环
接下来是一个神奇的循环,while (--lp1) { }
,中间没有 break
,也就是说,需要循环 0x1c7 + 1
次,也就是 456
次。基本上可以猜测这个循环干的就是没用的事情,浪费 CPU 而已。
来分析一下是不是:
既然刚才已经说了 lp3
和 lp4
就是局部变量,不妨再改个名,分别改为 local1
和 local2
,好识别。现在的 while
循环是这样:
let local1, local2; while (--lp1) { local2 = constArray.shift(); if (lp2 === lp1) { lp2 = local2; local1 = constArray.pop(); } else if (lp2 && local1.replace(/[QHMuLSPVlrtZMLzQ=]/g, "") === lp2) { constArray.push(local2); } }
刚才还分析了 lp1 === lp2 + 1
,所以 while (--lp1)
第一次执行的时候,lp1
和 lp2
就相等了,进入 if (lp2 === lp1)
分支;此后,都不会再进入这个分支,因为 lp1
一直在减小。
那么第一次循环执行的内容可以写成:
local2 = constArray.shift(); // toolVersion,即 "jsjiami.com.v6" lp2 = local2; // "jsjiami.com.v6" local1 = constArray.pop(); // "jsQHMujiLamiSP.Vcom.lrtZMLzvQ6=="
此后,这个循环中再没有对 lp2
和 local1
赋过值。而此时 constArray
的值是:
["wrvCucKGS1U=", "CGdK"] // shift() 和 pop() 操作把头尾的元素干掉了
后面的 local1.replace(...)
这句话可以直接拿到控制台去跑一下,结果让人哭笑不得,就是 "jsjiami.com.v6"
。从这个结果来看,else if (...)
条件除第一次不执行,之后都是 true
,也就是说,总是执行,那不就和 else
一样了嘛。
好嘛,除去第一次循环,这个循环变成了:
lp1 = 455; // 0x1c7 // 注意,第一次循环已经把头尾两个元素移出了数组 constArray = ["wrvCucKGS1U=", "CGdK"]; while (--lp1) { local2 = constArray.shift(); constArray.push(local2); }
没别的,就是转圈,一共转了 455 - 1 = 454
次!次数如果算不清楚,写一个循环跑一下就知道了:
let a = 455; let c = 0 while (--a) { c++ }; console.log(c);
local2
之后再没使用,所以 while
中的两句话可以合并成一句:
constArray.push(constArray.shift())
这和 while
循环之后那一句完全一样。所以这句话执行的次数一共是 454 + 1
,也就是 455
次。由于 constArray
现在有两个元素,而 455
是奇数,所以跑完之后 constArray
是这样:
constArray = ["CGdK", "wrvCucKGS1U="];
2.2.6. 都是没用的代码
至此,第一小段代码分析完成,除了改变 constArray
没干任何有意义的事情。
至于这段代码里的两句 return
,没半点用,因为外层 IIFE 的返回值直接被丢弃了。所以返回语句里的位运算,都懒得去算了。
整个这一段代码最终变成一句话:
constArray = ["CGdK", "wrvCucKGS1U="];
而且猜测 constArray
其实没啥用
3. 剩下的代码简单分析下
分析了半天,基本上没啥有用的代码。而且基本上可以断定,后面的几十行代码也只是在浪费 CPU。
因为我们知道原代码是 console.log("James")
。所以为了加快分析速度,就不再一行行往下读了,直接从后往前看。一眼就看到了
console[_0x2a10("0", "]o48")](_0x2a10("1", "WCmN"));
反推,_0x2a10("0", "]o48")
的结果就是 "log"
,而 _0x2a10("1", "WCmN")
的结果就是 "James"
。
猜测,_0x2a10
就是个拼字符串的函数,而第 1 个参数,就是个标记,作分支用。
3.1. 来看 _0x2a10
既然都已经知道 _0x2a10
是拼字符串的了,那改名叫 getString
吧。第一个参数是标记,改名为 flag
,第二个参数多半是计算用的初始值,就叫 initValue
好了。
其中第一句:flag = ~~"0x".concat(flag);
。这句就是把 flag
按 16 进制转换成数值类型的值而已。根据实际的调用参数,去控制台跑一下 ~~"0x1"
和 ~~"0x2"
就知道了,还可以试验一下 ~~"0xa"
。
接下来的 var _0x1fb2c5 = constArray[flag];
也就好理解了,而且到这里总算明白了,原来 constArray
是用来提供拼接字符串的部分因素的。既然如此,给它更名为 factor
。
3.2. 接下来是个长长的 if 语句
如果不管这个长长的 if
语句内部那些复杂的逻辑,精简下来就是:
var getString = function (flag, initValue) { if (getString.iOaiiU === undefined) { ... getString.LaMLHS = _0xbe9954; getString.WTsNMX = {}; getString.iOaiiU = !![]; } ... }
也就是在第一次运行 getString
的时候对它进行初始化。
其中 .iOaiiU
只有两处引用,一处判断,一处赋值 —— 明显是个初始化标记,可以改名为 initialized
。只不过这时候 rename 重构工具似乎不能用,手工更名吧。
3.3. 确保 globalThis 上有 atob()
if
分支内第一段代码又是个 IIFE,单独拷贝出来放到一个独立的 js
文件中,VSCode 并没有提示找不到变量之类的事情。所以这段代码是可以独立运行的。
(function () { var _0xea3c63 = typeof window !== "undefined" ? window : typeof process === "object" && typeof require === "function" && typeof global === "object" ? global : this; var _0x5b626 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; _0xea3c63.atob || (_0xea3c63.atob = function (_0x1e0fac) { var _0x57beec = String(_0x1e0fac).replace(/=+$/, ""); for (var _0x1f3b8d = 0x0, _0x154b1d, _0xad5277, _0x306ad8 = 0x0, _0xcb4400 = ""; _0xad5277 = _0x57beec.charAt(_0x306ad8++); ~_0xad5277 && (_0x154b1d = _0x1f3b8d % 0x4 ? _0x154b1d * 0x40 + _0xad5277 : _0xad5277, _0x1f3b8d++ % 0x4) ? _0xcb4400 += String.fromCharCode(0xff & _0x154b1d >> (-0x2 * _0x1f3b8d & 0x6)) : 0x0) { _0xad5277 = _0x5b626.indexOf(_0xad5277); } return _0xcb4400; }); }());
第一句很明显是在找 global
对象,相当于 var _0xea3c63 = globalThis
。
第二句先忽略,第三句明显是看 globalThis
上有没有 atob()
,如果没有就给它一个。既然 atob()
在多数环境下都存在,那就不用纠结其内容了。
那么,这段 IIFE 就是保证 atob()
可用,可以直接删掉不看。
3.4. 一个看起来比较有用的函数
接下来又定义了一个函数,去掉内容,长这样:
var _0xbe9954 = function (_0x333549, _0x3c0fbb) { ... }; getString.LaMLHS = _0xbe9954;
通过后面的调用来用,应该是个比较有用的函数。为了方便识别,把两个参数分别更名为 first
和 second
。
我们也把它摘出来拷贝到一个独立的 .js
文件中,发现也没有缺失变量,说明可以单独拿出来分析,就是个工具函数。
这个函数一来定义了 5 个变量,先不管,用到的时候再去找。
3.4.1. 用到了 atob
下面的代码是:
let _0x2591ef = ""; // 5 个变量中的一个 first = atob(first); for (var i = 0x0, len = first.length; i < len; i++) { _0x2591ef += "%" + ("00" + first.charCodeAt(i).toString(0x10)).slice(-0x2); } first = decodeURIComponent(_0x2591ef);
这段代码不用仔细看,大概知道是把一个 Base64 转成 %xx
的形式,而这个形式的字符串用 decodeURICompoment()
可以再转成字符串(绕好大一圈)。
回想一下 constArray
的元素,确实长得像 Base64,所以这里应该是处理那些元素了。
3.4.2. 然后是烧脑时刻
接下来的代码就是通过一大堆的数学计算,从 initValue
和 constArray[i]
把我们需要的字符串恢复出来。算法肯定是加密工具自己设计的,懒得去分析了。计算都不难,就是烧脑,需要仔细,一点不能出差错。
4. 结束
是的,结束了,戛然而止。
写这篇文章的目的并不是要把代码完全解出来,只是证明其可能性,同时介绍分析方法和工具应用。第 2 部分写完就该结束的,因为后面也没有用到什么新的方法。
总的来说,jsjiami 向原始代码中添加了非常多无用而烧脑的程序来提高解码的难度,这么简单的一句话都解了这么久,生产代码就更不用说了。代价也是有的 —— 真烧 CPU。
作者:边城,