尝试对 jsjiami 加密结果手工解密
source link: https://segmentfault.com/a/1190000040735240
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
看了下 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 // 注意,第一次循环已经把头尾两个元素移出了数组 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]
把我们需要的字符串恢复出来。算法肯定是加密工具自己设计的,懒得去分析了。计算都不难,就是烧脑,需要仔细,一点不能出差错。
是的,结束了,戛然而止。
写这篇文章的目的并不是要把代码完全解出来,只是证明其可能性,同时介绍分析方法和工具应用。第 2 部分写完就该结束的,因为后面也没有用到什么新的方法。
总的来说,jsjiami 向原始代码中添加了非常多无用而烧脑的程序来提高解码的难度,这么简单的一句话都解了这么久,生产代码就更不用说了。代价也是有的 —— 真烧 CPU。
好吧,我又干了一件无聊的事情!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK