好东西要分享

sojson.v5最牛加密脱壳解密破解去混淆

这边文章是网上看到的,以前有看到过sojson.v4的解密,但是现在想去找找不到了,这里就顺手记录下sojson.v5的过程,以后在慢慢研究
加密很典型,开头有一个;var encode_version = 'sojson.v5'据网上大牛们讨论JS是不可能有真正的加密,最多是混淆。
这个加密把js的变量名之类的已经打乱,所以是不太可能100%恢复了,只能自己阅读代码去还原了。
sojson.v5最牛加密脱壳解密破解去混淆

脱壳解密脚本

;(function (js_body) {      // 脱壳 && 解密      let js_arr = js_body.split("n").pop().split(';'),          fun_name = /vars+(_0x[a-z0-9]+)=/.exec(js_arr[6])[1],          reg_str = fun_name + '\(' + "'([^']+)',s*'([^']+)'" + '\)',          js_str = js_arr.slice(54, js_arr.length - 4).join(' ;'),          code_shell = js_arr.slice(0, 54).join(';'),          shell_obj = eval("(function(){" + code_shell + "return " + fun_name + "})()");      js_str = js_str.replace(new RegExp(reg_str, 'g'), function (str, id, key) {          return '"' + shell_obj(id, key) + '"';      }).replace(/([a-z0-9-_A-Z)]]+)s?[["']([^"']+)["']]/g, '$1.$2').replace(/\(? 0) {              for (const key in obj) {                  if (!obj.hasOwnProperty(key)) continue;                  if (typeof obj[key] == 'function') {                      let fun_info = /function s*_0x[0-9a-z]+(([^)]*)){return s*([^;]+);}/.exec(obj[key].toString());                      js_str = js_str.replace(new RegExp(name + '\. ' + key + '\(([^())]* )\)', 'g'), function (string, args_str) {                          let args = args_str.split(','),                              fun_args = fun_info[1].split(','),                              fun_body = fun_info[2];                          fun_args.forEach(function (item, index) {                              fun_body = fun_body.replace(item, args[index]);                          });                          return fun_body;                      });                  } else if (typeof obj[key] == 'string ') {                      js_str = js_str.replace(name + '.' + key, '"' + obj[key] + '"');                  } else {                      js_str = js_str.replace(name + '.' + key, obj[key].toString());                  }              }          }      }      return js_str;  })($('#resultSource').val() || $('#jsdata').val());

手动解密过程

先讲一下我所知道的JS加密/混淆有哪些
其实我个人并不认为下面讲的这些是加密,顶多是混淆罢了,所以本文后续尽量不用加密来描述。
eval加密
最常见的如packer等
典型特征
代码内使用eval方法,大致原理是对代码进行文本级别的抽取可复用参数进行压缩混淆。
而使用时将混淆代码还原为字符串,然后使用eval运行代码。
怎么破解
也许在eval之前的代码可能确实是用算法加密了,但毫无意义,因为总是要解密。
我们跳过他加解密的过程,直接在他最终执行代码的地方获取原文即可。
比如直接将eval改为console.log基本可直接输出代码原文,
如果不行或者找不到,则手动覆盖eval方法,然后执行代码,然后真正的代码就会从参数传进来。
变量混淆
例如接下来要讲的Obfuscator(本文针对的sojson的混淆即由此衍生,或者是抄袭?)
还有所谓的二次元加密?
典型特征
没有特征,就是一堆看不懂名字的代码,
原理直白的说,就是把你用的变量名/方法名替换成无规律的名称实现干扰。
例如,你可以认为很多代码压缩自带混淆功能,因为压缩以后人肉理解比较困难。
正式开始
首先,我们需要几个工具:
一个代码编辑器,如:VS Code
Chrome 浏览器,用来调试代码
需要解密的JS文件,我们直接以这个sojson.v5为准了
JS代码格式化工具,例如:https://tool.css-js.com/、http://www.bejson.com/jshtml_format/
一个聪明的脑瓜子,例如:你
格式化并整理代码
格式完的代码,我们会看到Unicode编码的数据已经自动转换回来了,
如果没有转换,可以自己考虑写代码转换,例如:

code_str.replace(/\x([a-f0-9]{2})/g, function (match_str, group_1) {      return String.fromCharCode(parseInt(group_1, 16).toString(10));  });

现在我们先逐段分析每一段代码,有些代码可能太长,会有部分截断。
我们在翻译代码的时候,会将一些变量给命名,建议使用前缀尽量不要和后面的代码有可能相同的变量名,
不然后续就很蛋疼了,并且重命名以后不要丢弃原来的变量名,指不定什么时候发现这个变量没替换到,
而你又不记得是什么地方来的。
这里,我们把变量名前缀定义为qs_
然后我们文章内所有用到行数的,均以格式化后的完整代码为准(jiami_format.js)。
第一段(定义了三个变量)

var encode_version = "sojson.v5",      brfui = "",      _0x2c67 = ["VHtkTcOA", "DcOPVEB/", "UMK...o9WPA=="];

这里定义了三个变量,第一个是作者的版权,第二个暂时不知道,第三个是一个 base64 编码的数组;
一般这种数据后面很有可能用得到,变量名改成qs_base_data然后全局批量替换这个变量
结果:

var encode_version = "sojson.v5",      brfui = "",      qs_base_data /* _0x2c67 */ = ["VHtkTcOA", "DcOPVEB/", "UMK...o9WPA=="];

然后浏览器打开一个空白页面,手动复制这段代码到控制台进去,执行,为我们接下来的调试做准备。
或者创建一个空白页面,把这行代码放进去准备执行。

第二段 (一个匿名函数)

(function (_0x2ac5c, _0x3068dd) {      var _0xe9c838 = function (_0x23ef01) {          while (--_0x23ef01) {              _0x2ac5c["push"](_0x2ac5c["shift"]());          }      };      var _0x3421c5 = function () {          var _0x5d77e7 = {              data: {                  key: "cookie",                  value: "timeout"              },              setCookie: function (_0x3c4b91, _0x3bd290, _0x5a5c5a, _0x529726) {                  _0x529726 = _0x529726 || {};                  var _0xe0bed = _0x3bd290 + "=" + _0x5a5c5a;                  var _0x45867b = 0;                  for (var _0x45867b = 0, _0x568496 = _0x3c4b91["length"]; _0x45867b < _0x568496; _0x45867b++) {                      var _0x24edef = _0x3c4b91[_0x45867b];                      _0xe0bed += "; " + _0x24edef;                      var _0x35ae72 = _0x3c4b91[_0x24edef];                      _0x3c4b91["push"](_0x35ae72);                      _0x568496 = _0x3c4b91["length"];                      if (_0x35ae72 !== !![]) {                          _0xe0bed += "=" + _0x35ae72;                      }                  }                  _0x529726["cookie"] = _0xe0bed;              },              removeCookie: function () {                  return "dev";              },              getCookie: function (_0x475fb9, _0x248e39) {                  _0x475fb9 = _0x475fb9 || function (_0x34f7a4) {                      return _0x34f7a4;                  };                  var _0x202cdc = _0x475fb9(new RegExp("(?:^|; )" + _0x248e39["replace"](/([.$?*|{}()[]/+^])/g, "$1") + "=([^;]*)"));                  var _0x48a974 = function (_0x267c1a, _0x45329b) {                      _0x267c1a(++_0x45329b);                  };                  _0x48a974(_0xe9c838, _0x3068dd);                  return _0x202cdc ? decodeURIComponent(_0x202cdc[1]) : undefined;              }          };          var _0x1dbeed = function () {              var _0x4885e3 = new RegExp("\w+ *\(\) *{\w+ *['|"].+['|"];? *}");              return _0x4885e3["test"](_0x5d77e7["removeCookie"]["toString"]());          };          _0x5d77e7["updateCookie"] = _0x1dbeed;          var _0x1cefc6 = "";          var _0x170183 = _0x5d77e7["updateCookie"]();          if (!_0x170183) {              _0x5d77e7["setCookie"](["*"], "counter", 1);          } else if (_0x170183) {              _0x1cefc6 = _0x5d77e7["getCookie"](null, "counter");          } else {              _0x5d77e7["removeCookie"]();          }      };      _0x3421c5();  })(qs_base_data, 202);

现看这个匿名函数的入口点,传入了两个参数,qs_base_data, 202
所以参数一的名字也可以替换了,参数二格式化前其实是一个16进制0xca,这里自动转换为10进制了。
我们暂且把参数二称为qs_arg_num,再次全局替换。
然后看方法里面第一段,定义了一个方法:

    var _0xe9c838 = function (_0x23ef01) {          while (--_0x23ef01) {              qs_base_data["push"](qs_base_data["shift"]());          }      };

然后我们会看到qs_base_data["push"](qs_base_data["shift"]());这一段,
我们记得qs_base_data是一个数组,["push"]其实就是调用这个数组的方法。
我们先全局处理一遍,把qs_base_data["push"]替换为qs_base_data.push这样更方便我们查看。
编辑器内用正则表达式替换([a-zA-z0-9_]+)[['"]([^"']+)['"]]替换为$1.$2,记得勾选.*表示正则替换。
替换完以后如果还能找到就继续替换,一直到找不到匹配项为止。
虽然我们不知道这段代码是什么用途(当然,通过阅读代码,大致可以理解是把数组后面的一定数量移动到数组前面),
我们可以找哪里调用了这个方法_0xe9c838,好知道参数一是从哪里来的。

我们会看到,全局只有一个地方用到了这个方法(名),_0x48a974(_0xe9c838, qs_arg_num);
如果有多个地方用的,那么就要挨个去测试哪里真正调用的。
继续找_0x48a974发现前一行定义了这个方法,内容是将参数二自增后传递给参数一。

var _0x48a974 = function (_0x267c1a, _0x45329b) {      _0x267c1a(++_0x45329b);  };

我们就知道参数一是最开始入口点的qs_arg_num,给对应的方法起名。
最终相关代码应该是这样的。

var qs_arr_move /* _0xe9c838 */ = function (qs_arg_num /* _0x23ef01 */) {      while (--qs_arg_num) {          qs_base_data.push(qs_base_data.shift());      }  };  var call_fun /* _0x48a974 */ = function (callback_fun /* _0x267c1a */, qs_arg_num /* _0x45329b */) {      callback_fun(++qs_arg_num);  };  call_fun(qs_arr_move, qs_arg_num);

到了这一步,我现在接下来的动作有两个不同的方向:
继续寻找这个方法是从哪里调用的
回到最开始,按解释器角度继续阅读代码
我这里选择按解释器来执行代码,我们回到最开始的qs_arr_move方法后面。
后面又定义了一个_0x3421c5的方法,并且紧接着执行了这个方法。
所以,我们可以看下这个方法内部做了什么,由于没有参数,直接进入方法第一行往下看。
在方法内,又定义了一个_0x5d77e7的对象,并且定义了datasetCookieremoveCookiegetCookie几个方法,
我们先给这个对象(_0x5d77e7)起名,就叫他qs_obj_cookie吧。
由于并没有执行,我们先不管,继续往下…

var _0x1dbeed = function () {      var _0x4885e3 = new RegExp("\w+ *\(\) *{\w+ *['|"].+['|"];? *}");      return _0x4885e3.test(qs_obj_cookie.removeCookie.toString());  };  qs_obj_cookie.updateCookie = _0x1dbeed;  var _0x170183 = qs_obj_cookie.updateCookie();

后面定义了_0x1dbeed这个方法,并赋值给了qs_obj_cookie.updateCookie,所以我们叫他qs_updateCookie吧。
然后只定义了一个空变量后,就执行了这个方法并保存返回值到_0x170183
我们回过头阅读下这个updateCookie里面到底做了啥。
定义一个正则表达式,并把removeCookie方法转换为字符串进行匹配。
代码拿到浏览器控制台执行,

var removeCookie = function () {      return "dev";  }  var _0x1dbeed = function () {      var _0x4885e3 = new RegExp("\w+ *\(\) *{\w+ *['|"].+['|"];? *}");      return _0x4885e3.test(removeCookie.toString());  }  _0x1dbeed();

我们看到最终执行的是,并返回了false

/w+ *() *{w+ *['|"].+['|"];? *}/.test('function () {      return "dev";  }');

但是,这里有一个坑,如果熟悉正则表达式的朋友应该会发现,这个正则表达式不匹配换行,
而我们toString得到的方法字符串是带有换行的,因为我们格式化过了,
如果不熟的话,也应该有一个警觉,removeCookie方法本身没有多大实际意义,
然后这里用正则表达式去匹配一个方法是什么目的?
其实,这个代码就是用来防格式化的,因为格式化以后就会返回false,我们拿未格式化的代码进去就会返回true
先不管,我们知道这里应该为true就好,继续往下执行,
下面是一个if判断,用来判断刚才返回的是true还是false,我们实际是true,直接进入对应条件。
_0x1cefc6 = qs_obj_cookie.getCookie(null, "counter");
回到getCookie的定义,我们看下都干了啥。

getCookie: function (_0x475fb9, _0x248e39) {      _0x475fb9 = _0x475fb9 || function (_0x34f7a4) {          return _0x34f7a4;      };      var _0x202cdc = _0x475fb9(new RegExp("(?:^|; )" + _0x248e39.replace(/([.$?*|{}()[]/+^])/g, "$1") + "=([^;]*)"));      var call_fun /* _0x48a974 */ = function (callback_fun /* _0x267c1a */, qs_arg_num /* _0x45329b */) {          callback_fun(++qs_arg_num);      };      call_fun(qs_arr_move, qs_arg_num);      return _0x202cdc ? decodeURIComponent(_0x202cdc[1]) : undefined;  }

先给参数一赋值一个方法,方法直接把参数返回…
所以,参数一的名字有了,qs_fun_back真的很随意的起了名字…
参数二是一个字符串,就先起名叫qs_name_str吧…
紧接着往下面看,定义了一个_0x202cdc变量并赋值了一个正则表达式然后在最末尾返回了,
而最外面调用getCookie的地方虽然保存到了_0x1cefc6,但并没有任何地方使用,暂时先忽略这里的代码,继续往下看。
这里就会看到我们最开始调用qs_arr_move的地方了,然后整个大的方法就执行完了。
总结一下这个方法做了什么:
尝试判断代码是否被格式化
如果没有格式化,就去移动数组的元素。
我们直接在浏览器控制台执行最终有意义的代码:

var qs_arr_move /* _0xe9c838 */ = function (qs_arg_num /* _0x23ef01 */) {      while (--qs_arg_num) {          qs_base_data.push(qs_base_data.shift());      }  };  qs_arr_move(qs_base_data, 203 /* ++qs_arg_num */);

实际理解代码,就是把数组从某个位置截断,前面的附加在后面。

arr = [].concat(arr.splice(202 % arr.length, arr.length - (202 % arr.length)) arr);

第三段 (又一个匿名函数)
由于上面已经讲了一些调试的方法,所以这里可能会跳过一些和上面相似但可能重要的流程。
继续看,先定义了一个方法,叫_0xdf0e,然后全局搜索这个方法名,发现有很多处调用,这里先找到第一个调用作为理解。
是在一个定时器里面循环调用的,先不管定时器做了啥,我们先拿着参数回到方法看方法要干啥。

setInterval(function () {      var _0x346bb8 = {          gHwtC: function _0x4f689e(_0x2f31c2) {              return _0x2f31c2();          }      };      _0x346bb8[_0xdf0e("0x0", "*naR")](_0x10c488);  }, 4e3);

在方法体内寻找这两个参数在什么地方使用,相关关键代码如下:

_0x532a72 = _0x532a72 - 0;  var _0x44d30c = qs_base_data[_0x532a72];    _0x44d30c = _0xdf0e.rc4(_0x44d30c, _0x422b57);

方法最开始对参数一_0x532a72(0x0) -0操作,实际就是将16进制的字符串转为十进制数值。
然后又从qs_base_data取出一个成员,最后放进rc4里面,根据理解,应该是取出密文用密钥解密。
那么,参数分别就起名qs_arg_idqs_arg_str,相关变量为qs_data_item
为了保险起见,我们可以继续看下后面的代码都做了啥(其实上面的代码就是所有需要的了,其他的都是多余的“花指令”)
首先在对象未初始化时,获取/创建windowatob(base64 decode)
然后创建_0x481c88方法赋值给_0xdf0e.rc4 = _0x481c88;
就是所谓的 rc4 解密了。
初始化后,再次获取了var qs_data_item2 /* _0x5597dd */ = _0xdf0e.data[qs_arg_id];
然后,关键代码有这样一段(已经整理后的代码)

var qs_data_item2 /* _0x5597dd */ = _0xdf0e.data[qs_arg_id];  if (qs_data_item2 === undefined) {      if (_0xdf0e.once === undefined) {          var qs_rc4_state /* _0xccf703 */ = function (rc4Bytes /* _0x499f89 */) {              this.rc4Bytes = rc4Bytes;              this.states = [1, 0, 0];              this.newState = function () {                  return "newState";              };              this.firstState = "\w+ *\(\) *{\w+ *";              this.secondState = "['|"].+['|"];? *}";          };          //...          new qs_rc4_state(_0xdf0e).checkState();          _0xdf0e.once = !![];      }      qs_data_item = _0xdf0e.rc4(qs_data_item, qs_arg_str);      _0xdf0e.data[qs_arg_id] = qs_data_item;  } else {      qs_data_item = qs_data_item2;  }

他把至今没有定义名字的_0xdf0e传递给qs_rc4_state并保存在了rc4Bytes
所以我们也可以把这个变量_0xdf0e定义为rc4Bytes了。
继续跟代码,和上面 cookies 那一段几乎一样目的的代码,还是判断代码是否格式化。
如果弄不清楚代码执行结果,就把代码放到浏览器执行一遍就可以了。

var reg_ex = new RegExp("\w+ *\(\) *{\w+ *" + "['|"].+['|"];? *}");  var newState = function(){return'newState';}; // 这里不要格式化,因为就是测试她是否格式化的  var states = [1, 0, 0];  reg_ex.test(newState.toString()) ? --states[1] : --states[0];

这个代码最后会输出-1,然后进入runState方法

qs_rc4_state.prototype.runState = function (_0x24db9c) {      if (!Boolean(~_0x24db9c)) {          return _0x24db9c;      }      return this.getState(this.rc4Bytes);  };

!Boolean(~-1)返回true所以这里就返回了,没有后续。
而如果代码被格式化了,后续的代码则是进入死循环…
跳出这个方法,我们看后续又做了什么,

var qs_data_item2 /* _0x5597dd */ = qs_rc4Bytes.data[qs_arg_id];  if (qs_data_item2 === undefined) {      if (qs_rc4Bytes.once === undefined) {          // 防止代码格式化的代码      }      qs_data_item = qs_rc4Bytes.rc4(qs_data_item, qs_arg_str);      qs_rc4Bytes.data[qs_arg_id] = qs_data_item;  } else {      qs_data_item = qs_data_item2;  }  return qs_data_item;

最终执行的意思即是:
qs_base_data取出密文并根据调用时的密钥使用rc4解密,
然后保存到自身的解密结果数组中
下次这直接使用解密后的结果
返回解密的结果
PS:其实这也是他的代码中最核心的一部分代码了,其他的都是围绕这个功能打掩护而已。
我们将最终整理出来的核心代码放入浏览器执行,用以后续代码的理解。

var qs_base_data = ["VHtkTcOA", "DcOPVEB/", "UMKdX8KJ", "SsOnD3DDtQ==", "wolyw6h7QA==", "DlVDwp0=", "w6LCvBIsw6U=", "a8OcBkHDng==", "V8OeWyjDvQ==", "woAMIcKxMg==", "MBVaGsO5", "wr98woZiw6s=", "bho7", "NMOJdivCu1nDmCHCg8KBw4HDqy0=", "YsOKwrvCpk3DqMOgwq1N", "wpVhDsKkw6ws", "JErDp8KOJcKLw7Fh", "DMO/w6dSE0DDmk3CihTCmMKtw7g=", "6L+15pum5LuZ5LuI5Lqg57CV5Ym4B8Oc5pKR5L2h44Cs", "56qx6ZS45o6U6aqB57iwFOKBpmDDo+WLkOWsouKCicOX5ZCYw6nigKIpYeikhOWskOKDvMK7776t5L2T5Y6m5L+t55u3XsOlw67jgbE=", "5aaB5pyP5oKq55uuw6c36Yel5bev5aeL5LugdMK3WO+8pnY1DOahqeesne+9p+ethuetvuWGqeS6pOmciGQ1wrTDoBDCgMKiXMO/I+eYveS7r+egtO+/vOiup+aPl+WNsuWGpeafpeWHleWIreWvleODhOi8n+S7vOW3heWGu+S5tOiCheWIvuWunnDCsQ3jgrhzKMOX56+45quw54uK5Yah5ayU", "U8KCCSTCtw==", "SEB6ccOl", "dsOwwqrCi0c=", "ZcOXwq/ClhA=", "bBtxw4DDjw==", "KcOhwr5Qwps=", "wolww5BvcA==", "w7/Ds0TDhcOd", "Z8Kmw60Rw6Q=", "SsOvwrnCqiQ=", "E8OAdnZ6", "IVDDp8KePsKOw7s=", "wp3DnEzCoS3CrBk=", "JsOYw6vCvxzDvcK4wrQbw6Ekw6JrwpbDhMKdMQ==", "w6AmGMKMwqU=", "JMOGcEZt", "wrfCgHlh", "woHDv8OkQcKFJzcpw70=", "V19x", "S8OnwpFZw4M=", "woUbKcKqJQ==", "acKTw5oH", "w7N/eHbCrQ==", "wqJgXQvCtw==", "wpfDt8OrTcKB", "w6Eqwp8AVU5H", "WMK7wrjCgcOF", "dMOLwrHCsETDrcOr", "w65jf3o=", "O8OQw5LChUvCtcOo", "wonDklDCvA==", "fcKdw4Yaw5rDtcK2", "w4TCmcOp", "TcO6wo1Fw57CmsOS", "YMONJzLDu1HCgjLDmQ==", "w7EEw4fDmMOh", "OMOuwrhhKitT", "M8OTflp+", "wplxw5lCUg==", "w5c9w5TDhcOp", "dsOTHiDDuA==", "wqloEcKKw6k=", "RcONM8Oow5U=", "S8KBw5gfw7E=", "wqYyH8KyMQ==", "w6vCoycLw78=", "McOgTX5f", "Ul5wdw==", "wqlNw7ZsVg==", "TcKtwrDCjMOGwoE=", "w7DCgcKpRRU=", "wo1iFsKvw6Y2Cg==", "w6HDi0LDqcOY", "wppyw6HDjznDqsKswooL", "w7nCvD0Uw6nCnQEVLw==", "dsOIwrrCsV8=", "5Lma6IGL5Yi86ZmLasKzcGATw7wbwpcp", "K8O6wq1Uwo8t", "wqFjw5RRZ8ON", "a8Otwptiw6A=", "EFdWwo/CtA==", "AAVow4vDiQ==", "T8O2wqZcw4U=", "w51lQVbCvg==", "worDn8O2bMKx", "E8OYwpB+Ew==", "esO7wq16dA==", "c8OEOXzDjw==", "WcKrw5stw5Q=", "ecOVKMOgwoY=", "w7MmAcKCwrQ=", "wrLDo04Nw4M1w7XDt8KG", "CUBcwoDCkA==", "wqBpw5kYZsOBQMK0w5nCoHnCjsOHZw==", "w6luN3LCpsKcwqBBw7E4w4Y=", "e8OpwqPCkxvCiQ==", "cMOjbgPDmQ==", "w7LDuggUwoU6wqM=", "acO8awQ=", "Bi5hw5bDnEw=", "GiZfPcO4WQ==", "wpbCkMOOw63CtcOSKMKzw4TDssOSw7kmDxop", "V3ViwqbCqMKEXzXCrsOXwqbDuMOvQ3VDUn/DtjXDnmN2H8Kawo0Ew5liEMOww63CqcOfL8K6CMObwqzCkcO5w53CiMKoAhzCh8KldD5QYVbDt3pVVQLCuGE0ew==", "R8O7wopC", "YMO7YwPDgw==", "w6PCvScSw7I=", "woAOO8KzOw==", "w5sNOMKVwp8=", "cyY4wqI4", "NMOlwo9WPA=="];  var qs_arr_move = function (qs_arg_num) {      while (--qs_arg_num) {          qs_base_data.push(qs_base_data.shift());      }  };  qs_arr_move(203);  var qs_rc4Bytes = function (qs_arg_id, qs_arg_str) {      qs_arg_id = qs_arg_id - 0;      var qs_data_item  = qs_base_data[qs_arg_id];      if (qs_rc4Bytes.initialized === undefined) {          qs_rc4Bytes.rc4 = function (_0x8f6596, _0x40168c) {              var _0x155004 = [], _0x132490 = 0, _0x55539c, _0x432c06 = "", _0x4c5e8c = "";              _0x8f6596 = atob(_0x8f6596);              for (var _0x330500 = 0, _0x16d6ad = _0x8f6596.length; _0x330500 < _0x16d6ad; _0x330500++) {                  _0x4c5e8c += "%" + ("00" + _0x8f6596.charCodeAt(_0x330500)["toString"](16))["slice"](-2);              }              _0x8f6596 = decodeURIComponent(_0x4c5e8c);              for (var _0x163351 = 0; _0x163351 < 256; _0x163351++) {                  _0x155004[_0x163351] = _0x163351;              }              for (_0x163351 = 0; _0x163351 < 256; _0x163351++) {                  _0x132490 = (_0x132490 + _0x155004[_0x163351] + _0x40168c.charCodeAt(_0x163351 % _0x40168c.length)) % 256;                  _0x55539c = _0x155004[_0x163351];                  _0x155004[_0x163351] = _0x155004[_0x132490];                  _0x155004[_0x132490] = _0x55539c;              }              _0x163351 = 0;              _0x132490 = 0;              for (var _0x4ebeb6 = 0; _0x4ebeb6 < _0x8f6596.length; _0x4ebeb6++) {                  _0x163351 = (_0x163351 + 1) % 256;                  _0x132490 = (_0x132490 + _0x155004[_0x163351]) % 256;                  _0x55539c = _0x155004[_0x163351];                  _0x155004[_0x163351] = _0x155004[_0x132490];                  _0x155004[_0x132490] = _0x55539c;                  _0x432c06 += String.fromCharCode(_0x8f6596.charCodeAt(_0x4ebeb6) ^ _0x155004[(_0x155004[_0x163351] + _0x155004[_0x132490]) % 256]);              }              return _0x432c06;          };          qs_rc4Bytes.data = {};          qs_rc4Bytes.initialized = true;      }      var qs_data_item2  = qs_rc4Bytes.data[qs_arg_id];      if (qs_data_item2 === undefined) {          qs_data_item = qs_rc4Bytes.rc4(qs_data_item, qs_arg_str);          qs_rc4Bytes.data[qs_arg_id] = qs_data_item;      } else {          qs_data_item = qs_data_item2;      }      return qs_data_item;  };

然后调用第一个qs_rc4Bytes("0x0", "*naR")进行测试,得到gHwtC
定时器的目的大概是定时调用一个方法,对应方法的定义在下面…
由于后面的代码很多都是被加密的,所以写个JS脚本批量替换后面加密的数据为原文

# 这里的 js_str 就是 160 行 之后的所有代码(字符串)  var js_str = $('#compressor-textarea').val(), // 此文代码格式页面的文本框      reg_ex = /qs_rc4Bytes(['"](0x[0-9a-f]+)['"],s*['"]([^"']+)['"])/g;  js_str = js_str.replace(reg_ex, function(match_str, str, key){      return '"' + qs_rc4Bytes(str, key) + '"';  })  // 再次去除数组调用的对象方法  .replace(/([a-zA-z0-9_]+)[['"]([^"']+)['"]]/g, function(match_str, str, key) {      return str + '.' + key;  });  $('#compressor-textarea').val(js_str);

替换后的代码存于jiemi.js
第四段 去混淆(解密后的代码,又一段新的历程)
接下来的代码行数以解密后的jiemi.js文件为基准
第一段是一个定时器,定时器以4000ms的间隔调用一个_0x10c488方法,
里面定义了一个 Object,这个方式在后面会多次出现。
即定义一个对象,里面定义几个方法,将参数返回出来。
比如这个,gHwtC方法里面就是调用参数一,简化后为:

setInterval(function () {      _0x10c488();  }, 4e3);

往后找一下,定义方法的地方,在第 278 行:
依然定义了一个对象_0x5d1305,然后定义了一个_0x3e0578方法,先不管,继续往后找。
一个try结构,判断_0x43c6a1也就是参数一是否有值,这里调用没有传参,直接走else流程。
调用了_0x5d1305.Nktyo方法,传递_0x3e0578进入。
找到Nktyo的定义,只是把参数二放入参数一执行,然后回头看下这个方法_0x3e0578怎么去混淆
第一个if是判断_0x5d1305.ExxTQ(typeof _0x53a0e4, _0x5d1305.jgffP),找到刚才的定义,分别理解。
只是一个判断参数一是否为字符串而已,现在找到调用这个 对象属性的地方,挨个替换回来,其他的也是一样方法替换。
整个大方法处理完以后,可以把最上面的对象删除了,结果如下:

function _0x10c488(_0x43c6a1) {      function _0x3e0578(_0x53a0e4) {          if (typeof _0x53a0e4 === "string") {              var _0x3d9b38 = function () {                  while (!![]) { }              };              return _0x3d9b38();          } else {              if ((("" + (_0x53a0e4 / _0x53a0e4))["length"] !== 1) || ((_0x53a0e4 % 20) === 0)) {                  debugger;              } else {                  debugger;              }          }          _0x3e0578(++_0x53a0e4);      }      try {          if (_0x43c6a1) {              return _0x3e0578;          } else {              _0x3e0578(0);          }      } catch (_0x29e1b1) { }  }

可以看到,这个方法是一个反调试,直接删除即可,开头的定时器也可以一并删除了。
回到开头,看第二段代,一个大的try,里面定义了一个字符串拆散为数组,并死循环switch
最终目的其实就是将switch里面的代码按照_0xd94d8c定义的顺序执行一遍而已,我们直接提取结果出来看下:

// case "2":  var _0x1d8312 = ["domain", "split", "reverse", "join", "search", "hr" + "ef", "random", !0];  // case "0":  var _0x1dd019 = document[_0x1d8312[0]];  // case "4":  var _0x3f8779 = function (_0x2e5797) {      return _0x2e5797[_0x1d8312[1]]("")[_0x1d8312[2]]()[_0x1d8312[3]]("");  };  // case "3":  var _0x5a0580 = function (_0xc98a64, _0x503b08) {      var _0x1dda52 = {          Scjbh: "function *( *)",          odYDy: "++ *(?:_0x(?:[a-f0-9]){4,6}|(?:b|d)[a-z0-9]{1,4}(?:b|d))",          oKrUQ: function _0x2206d5(_0x42cced, _0x535344) {              return _0x42cced(_0x535344);          },          ZyHof: "init",          hoEKc: function _0x1b7042(_0x219f40, _0x2085aa) {              return _0x219f40 + _0x2085aa;          },          DtRMS: "chain",          ITLzH: "input",          TMYBP: function _0x4c778b(_0x5c060e, _0x4090f3) {              return _0x5c060e(_0x4090f3);          },          arjnp: function _0xa52a54(_0x57076d) {              return _0x57076d();          },          OcNch: function _0x36a26e(_0x1cc753, _0x475023, _0x5f1b96) {              return _0x1cc753(_0x475023, _0x5f1b96);          },          XRkCn: function _0x2ccfd7(_0x13e206, _0x2235db) {              return _0x13e206 === _0x2235db;          },          yWahq: function _0x425b13(_0x12e926, _0x4f260f) {              return _0x12e926(_0x4f260f);          }      };      var _0xceb034 = function () {          var _0x4236ae = !![];          return function (_0x4e3ff6, _0xc225f7) {              var _0x3d1152 = _0x4236ae ? function () {                  if (_0xc225f7) {                      var _0x23d38f = _0xc225f7.apply(_0x4e3ff6, arguments);                      _0xc225f7 = null;                      return _0x23d38f;                  }              } : function () { };              _0x4236ae = ![];              return _0x3d1152;          };      }();      (function () {          _0x1dda52.OcNch(_0xceb034, this, function () {              var _0x66effa = new RegExp(_0x1dda52.Scjbh);              var _0x5b9f27 = new RegExp(_0x1dda52.odYDy, "i");              var _0x2755c6 = _0x1dda52.oKrUQ(_0x10c488, _0x1dda52.ZyHof);              if (!_0x66effa.test(_0x1dda52.hoEKc(_0x2755c6, _0x1dda52.DtRMS)) || !_0x5b9f27.test(_0x1dda52.hoEKc(_0x2755c6, _0x1dda52.ITLzH))) {                  _0x1dda52.TMYBP(_0x2755c6, "0");              } else {                  _0x1dda52.arjnp(_0x10c488);              }          })();      })();      return _0x1dda52.XRkCn(_0x1dda52.yWahq(_0x3f8779, _0xc98a64)[_0x1d8312[4]](_0x503b08), 0);  };  // case "1":  if (!(_0x5a0580(_0x1dd019, "moc.udiab.tset") || _0x5a0580(_0x1dd019, "nc.gnatnait"))) {      while (_0x1d8312[7]) {          location[_0x1d8312[5]] = location[_0x1d8312[5]] + "?" + Math[_0x1d8312[6]]();      }  }

依然又看到case "3":部分和上面是同样的计俩,替换之,然后_0x1d8312相关对应的数组也替换一下,
替换到_0x5a0580时,本来想继续这个的,到一半发现调用了刚才的_0x10c488反调试的代码,直接跳过,依然是反调试的混淆。
核心代码,最后一行return qs_reverse_str(_0xc98a64).search(_0x503b08) === 0;翻转字符串并搜索。
找到调用参数"moc.udiab.tset"并尝试翻转test.baidu.com, 这是我写的限制域名的,此段代码可以直接删除了。

// case "2":  var _0x1d8312 = ["domain", "split", "reverse", "join", "search", "href", "random", !0];  // case "0":  var qs_domain /*_0x1dd019*/ = document.domain;  // case "4":  var qs_reverse_str /* _0x3f8779 */ = function (qs_str /*_0x2e5797*/) {      return qs_str.split("").reverse().join("");  };  // case "3":  var _0x5a0580 = function (_0xc98a64, _0x503b08) {      var _0xceb034 = function () {          var _0x4236ae = !![];          return function (_0x4e3ff6, _0xc225f7) {              var _0x3d1152 = _0x4236ae ? function () {                  if (_0xc225f7) {                      var _0x23d38f = _0xc225f7.apply(_0x4e3ff6, arguments);                      _0xc225f7 = null;                      return _0x23d38f;                  }              } : function () { };              _0x4236ae = ![];              return _0x3d1152;          };      }();      (function () {          _0xceb034(this, function () {              var _0x66effa = new RegExp("function *( *)");              var _0x5b9f27 = new RegExp("++ *(?:_0x(?:[a-f0-9]){4,6}|(?:b|d)[a-z0-9]{1,4}(?:b|d))", "i");              var _0x2755c6 = _0x10c488("init");              if (!_0x66effa.test((_0x2755c6 + "chain")) || !_0x5b9f27.test((_0x2755c6 + "input"))) {                  _0x2755c6("0");              } else {                  _0x10c488();              }          })();      })();      return qs_reverse_str(_0xc98a64).search(_0x503b08) === 0;  };  // case "1":  if (!(_0x5a0580(qs_domain, "moc.udiab.tset") || _0x5a0580(qs_domain, "nc.gnatnait"))) {      while (!0) {          location["href"] = location["href"] + "?" + Math["random"]();      }  }

然后就剩下一大段的匿名函数,其实这个才是我一开始加密的代码。
依然看到开始定义了对象,和刚才的方法一样,替换之,同时看到了熟悉的"1|2|0|3|4|5|6"和死循环switch
老样子,直接处理掉吧。
这里遇到一个新结构,这个结构折腾了很久,返回了两次对象,最终目的就是为了用参数二进行apply

var _0x4df744 = function () {      var _0x10f531 = !![];      return function (_0x1602b0, _0x1836ce) {          var _0x183ad8 = _0x10f531 ? function () {              if (_0x1836ce) {                  var _0x49cae0 = _0x1836ce.apply(_0x1602b0, arguments);                  _0x1836ce = null;                  return _0x49cae0;              }          } : function () { };          _0x10f531 = ![];          return _0x183ad8;      };  }();

最终结果为

var _0x4df744 = function (_0x1602b0, _0x1836ce) {      _0x1836ce.apply(_0x1602b0, arguments);  }

而定义的_0x4aa006方法主要目的为覆盖系统的console
然后console对象所有的方法失效,主要目的依然为了反调试。
整段代码又可以删除了,(case "4"之前的)
最后结果就只剩下这么几行了

(function (_0x503c1c, _0x5cde2d) {      // case "4":      _0x503c1c.info = "这是一个一系列js操作。";      // case "5":      _0x5cde2d.adinfo = "站长接高级 “JS加密” 和 “JS解密” ,保卫你的 js。";      // case "6":      _0x5cde2d.warning = "如果您的JS里嵌套了PHP,JSP标签,等等其他非JavaScript的代码,请提取出来再加密。这个工具不能加密php、jsp等模版内容";  })(window, document);

后面还有一个不能删除sojson.v5我想你懂得,我就不说了~~~
这就是解析的全部了,由于这次写的匆忙,过程中随心所欲的删除,就没有留下过程文件。
大家随意理解下就好了…
本文也是我一边查资料一边写的,又发现写 JS 真的要上 AST 语法树,破解这玩意真的贼容易。
特别是替换对象属性的那段,很适合,有时间学习下。

下载权限

查看

  • 免费下载
    评论并刷新后下载
    登录后下载

    0″>

  • {{attr.name}}:
您当前的等级为

登录后免费下载登录 小黑屋反思中,不准下载! 评论后刷新页面下载评论 支付以后下载 请先登录 支付积分以后下载立即支付 支付以后下载立即支付 您当前的用户组不允许下载升级会员
您有每天免费下载所有资源次的特权,今日剩余 已取得下载权限

评论 抢沙发

评论前必须登录!