共计 6584 个字符,预计需要花费 17 分钟才能阅读完成。
看了下 jsjiami,简略的一个 console.log("James")
,加密进去的后果竟然有 3K,阐明这个加密转了不晓得多少弯在外面。如果要把真正一段业务代码拿来手工解密,应该会挺累的,然而本文不钻研工作量的问题,只是尝试一下手工解密,向各位读者介绍一下分析方法和工具利用。
同一句话在 jsjiami 里可能会加密出不同的后果,我置信这个工具上退出了随机因素。然而为了节约篇幅,这里就不贴我用于试验的加密后果了。剖析过程中会贴一些代码段。
1. 第一步,可读化
毋庸置疑,要想人工辨认,首先须要断句。幸好目前丑化(格式化)JS 的工具还是不少,轻易找两个试下,看哪个成果好。我这里是用的浏览器插件 FeHelper。
而后留神到,所有变量都改了名字,数字加字母的,怎么读都好受。所以须要应用“重命名”重构工具来改名。这事让 VSCode 干毫无压力。
2. 而后,一点点来剖析
2.1. 先看前两行
var _0xodm = "jsjiami.com.v6", | |
_0x47c5 = [_0xodm, "wrvCucKGS1U=", "CGdK", "jsQHMujiLamiSP.Vcom.lrtZMLzvQ6=="]; |
这一句申明了两个变量,一个显然是 jsjiami 的版本版本;另一个是一个数组,除版本信息外,内容猜想是 Base64,上网用 Base64 解码试了一下,解进去乱码,所以先放着,前面再来看是啥。
为了便于辨认,能够 rename 重构一下,顺便按标准拆分申明:
var toolVersion = "jsjiami.com.v6"; | |
var constArray = [toolVersion, "wrvCucKGS1U=", "CGdK", "jsQHMujiLamiSP.Vcom.lrtZMLzvQ6=="]; |
2.2. 接下来是一个 IIFE
这个 IIFE 的三个形参,顺便改个名字:p1
、p2
、p3
。IIFE 里定义了一个部分函数,给它更名为 localFunc1
。这个函数定义完之后间接调用,查了一下,没有递归,所以相当于又是一个 IIFE。同样,它的 5 个参数给改个没啥意义,然而好辨认的名字,后果:
(function (p1, p2, p3) {var localFunc1 = function (lp1, lp2, lp3, p14, lp5) { | |
lp2 = lp2 >> 0x8, lp5 = "po"; | |
var _0x1e174c = "shift", | |
_0x5428fe = "push"; | |
if (lp2 < lp1) {while (--lp1) {p14 = p1[_0x1e174c](); | |
if (lp2 === lp1) { | |
lp2 = p14; | |
lp3 = p1[lp5 + "p"]();} else if (lp2 && lp3["replace"](/[QHMuLSPVlrtZMLzQ=]/g, "") === lp2) {p1[_0x5428fe](p14); | |
} | |
} | |
p1[_0x5428fe](p1[_0x1e174c]()); | |
} | |
return 0xaa95b; | |
}; | |
return localFunc1(++p2, p3) >> p2 ^ p3; | |
}(constArray, 0x1c7, 0x1c700)); |
2.2.1. 参数干掉一个是一个
留神到,外层 IIFE 的 p1
就是下面改名为 constArray
的那个数组,反正都是作用域内,罗唆一不做二不休,给它换掉:
- 将
p1
更名为constArray
,跟里面的数组同名 - 同时删除外层 IIFE 的第一个形参和实参
2.2.2. 把绕远的数据操作改回来
既然曾经晓得 constArray
是个数组,作用在下面的所有属性都应该跟数组相干。就这几行 代码,察看一下不难发现:
lp5
只参加了一个表达式,后果是"pop"
var _0x1e174c = "shift", _0x5428fe = "push"
两个变量只是当常量应用的,把var
改成const
能够让编辑器帮忙查看是否有写操作 —— 当然后果是没有。
不过很遗憾,VSCode 没提供内联 (inline) 重构工具,所以只能手工操作,把这两个变量间接替换成常量。以 _0x1e174c = "shift"
为例,先把 "shift"
(含引号)复制到剪贴板中,而后在 _0x1e174c
应用若干次 Ctrl+D 把所有 _0x1e174c
都选中,再 Ctrl+V 即可。如法炮制解决掉 _0x5428fe = "push"
。而后删除两个申明。
2.2.3. 简化一下代码,越简略越好懂
不过 constArray["shift"]()
这种写法看起来很不习惯,最好能改成 constArray.shift()
—— 这就须要借助一下 ESLint 了。将当前目录初始化为 npm module 我的项目,装置并初始化 eslint,而后在配置里增加一条规定:
"dot-notation": "error"
这时候 VSCode 会提醒
["shift"] is better written in dot notation.
将鼠标移过去,应用快捷修复主动把所有 []
调用改为 .
调用。
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 | |
// 留神,第一次循环曾经把 toolVersion 移出了数组 | |
constArray = ["wrvCucKGS1U=", "CGdK", "jsQHMujiLamiSP.Vcom.lrtZMLzvQ6=="]; | |
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。
好吧,我又干了一件无聊的事件!