JS混淆学习及实战
简介
JavaScript 压缩即去除 JavaScript 代码中的不必要的空格、换行等内容或者把一些可能公用的代码进行处理实现共享,最后输出的结果都被压缩为几行内容,代码可读性变得很差,同时也能提高网站加载速度。
如果仅仅是去除空格换行这样的压缩方式,其实几乎是没有任何防护作用的,因为这种压缩方式仅仅是降低了代码的直接可读性。如果我们有一些格式化工具可以轻松将 JavaScript 代码变得易读,比如利用 IDE、在线工具或 Chrome 浏览器都能还原格式化的代码。
目前主流的前端开发技术大多都会利用 Webpack 进行打包,Webpack 会对源代码进行编译和压缩,输出几个打包好的 JavaScript 文件,其中我们可以看到输出的 JavaScript 文件名带有一些不规则字符串,同时文件内容可能只有几行内容,变量名都是一些简单字母表示。这其中就包含 JavaScript 压缩技术,比如一些公共的库输出成 bundle 文件,一些调用逻辑压缩和转义成几行代码,这些都属于 JavaScript 压缩。另外其中也包含了一些很基础的 JavaScript 混淆技术,比如把变量名、方法名替换成一些简单字符,降低代码可读性。
JavaScript 加密基本思路是将一些核心逻辑使用诸如 C/C++ 语言来编写,并通过 JavaScript 调用执行,从而起到防护作用。其加密的方式现在有 Emscripten 和 WebAssembly 等,其中后者越来越成为主流。
Emscripten: Emscripten编译器可以将 C/C++ 代码编译成 asm.js 的 JavaScript 变体,再由 JavaScript 调用执行。因此某些 JavaScript 的核心功能可以使用 C/C++ 语言实现,这可以算是一种前端加密技术。
WebAssembly: WebAssembly 的技术也能将 C/C++ 转成 JavaScript 引擎可以运行的代码,但转出来的代码是二进制字节码,而asm.js 是文本,
因此运行速度更快、体积更小。得到的字节码具有和 JavaScript 相同的功能,在语法上完全脱离 JavaScript,同时具有沙盒化的执行环境。
利用 WebAssembly 技术,我们可以将一些核心的功能利用 C/C++ 语言实现,形成浏览器字节码的形式。然后在 JavaScript 中通过类似如下的方式调用:
WebAssembly.compile(new Uint8Array(` 00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01 7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61 64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02 08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c 0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16)) )).then(module => { const instance = new WebAssembly.Instance(module) const { add, square } = instance.exports console.log('2 + 4 =', add(2, 4)) console.log('3^2 =', square(3)) console.log('(2 + 5)^2 =', square(add(2 + 5))) })
这种加密方式更加安全,想要逆向或破解那得需要逆向 WebAssembly,难度极大。
但整体来说,JavaScript 压缩技术只能在很小的程度上起到防护作用,要想真正提高防护效果还得依靠 JavaScript 混淆和加密技术。原文链接「小小明-代码实体」
混淆主要涉及两种思路:
a.通过正则替换实现的混淆器;
b.通过语法树替换实现的混淆器。
第一种实现成本低,但是效果也一般,适合对混淆要求不高的场景。第二种实现成本较高,但是更灵活,而且更安全,更适合对抗场景。
JS混淆简单参考:几个实用的JS混淆工具。
主要混淆技术:
-
变量混淆
将带有含意的变量名、方法名、常量名随机变为无意义的类乱码字符串,降低代码可读性,如转成单个字符或十六进制字符串。 -
字符串混淆
将字符串阵列化集中放置、并可进行MD5
或Base64
加密存储,使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口点。 -
属性加密
针对 JavaScript 对象的属性进行加密转化,隐藏代码之间的调用关系。 -
控制流平坦化
打乱函数原有代码执行流程及函数调用关系,使代码逻变得混乱无序。 -
僵尸代码
随机在代码中插入无用的僵尸代码、僵尸函数,进一步使代码混乱。 -
调试保护
基于调试器特性,对当前运行环境进行检验,加入一些强制调试debugger
语句,使其在调试模式下难以顺利执行 JavaScript 代码。 -
多态变异
使 JavaScript 代码每次被调用时,将代码自身即立刻自动发生变异,变化为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析调试。 -
锁定域名
使 JavaScript 代码只能在指定域名下执行。 -
反格式化
如果对 JavaScript 代码进行格式化,则无法执行,导致浏览器假死。 -
特殊编码
将 JavaScript 完全编码为人不可读的代码,如表情符号、特殊表示内容等等。
总之,以上方案都是 JavaScript 混淆的实现方式,可以在不同程度上保护 JavaScript 代码。
混淆工具有UglifyJS,JScrambler,jsbeautifier,JSDetox,obfuscator等
此次介绍OB混淆,即obfuscator
OB混淆
介绍
OB混淆官网:JavaScript Obfuscator Tool
来自官方的混淆示例:
// Paste your JavaScript code here
function hi() {
console.log("Hello World!");
}
hi();
混淆后(已格式化):
(function (_0x5e3669, _0x797fa0) {
var _0x4222ee = _0x4119, _0x40a199 = _0x5e3669();
while (!![]) {
try {
var _0xf23fb6 = parseInt(_0x4222ee(0xd9)) / 0x1 + parseInt(_0x4222ee(0xdc)) / 0x2 + parseInt(_0x4222ee(0xd3)) / 0x3 + -parseInt(_0x4222ee(0xda)) / 0x4 + parseInt(_0x4222ee(0xd7)) / 0x5 + parseInt(_0x4222ee(0xd6)) / 0x6 * (-parseInt(_0x4222ee(0xd8)) / 0x7) + -parseInt(_0x4222ee(0xd5)) / 0x8;
if (_0xf23fb6 === _0x797fa0) break;
else _0x40a199['push'](_0x40a199['shift']());
} catch (_0x2d2067) {
_0x40a199['push'](_0x40a199['shift']());
}
}
}(_0x5ac2, 0xa50c3));
function hi() {
var _0x543b06 = _0x4119;
console[_0x543b06(0xdb)](_0x543b06(0xd4));
}
function _0x4119(_0x13dc26, _0xc31dd5) {
var _0x5ac260 = _0x5ac2();
return _0x4119 = function (_0x4119c2, _0xbffe6d) {
_0x4119c2 = _0x4119c2 - 0xd3;
var _0x42ea54 = _0x5ac260[_0x4119c2];
return _0x42ea54;
},
_0x4119(_0x13dc26, _0xc31dd5);
}
hi();
function _0x5ac2() {
var _0x3de06f = ['Hello\x20World!', '3046168LiXzXu', '6JNGVAy', '4550545CwPJCt', '5231807GgAdWc', '847568YVXvAQ', '2733296cTWkBb', 'log', '879906prswui', '869703bTiBZl'];
_0x5ac2 = function () {
return _0x3de06f;
};
return _0x5ac2();
}
OB 混淆特点
- 一般由一个大数组或者含有大数组的函数、自执行函数、解密函数和加密后函数四部分组成
- 函数名和变量名通常以
_0x
或者0x
开头,后接1~6位数字或字母组合 - 自执行函数进行移位操作有明显的
push
、shift
关键字
实战
逆向
目标:极简壁纸
该网站有无限debugger,在首页加载完毕后,可以用将以下代码保存到片段里并运行来绕过(或直接在控制台运行):
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
if (a == 'debugger') {
return function () {};
}
return Function.prototype.constructor_(a);
}
同时本网站有滑块(并不是每次请求都有),滑块不在讨论范围,因此以没有进行滑块验证后的请求作为研究目标
可以看到表单中无加密(请求头中也无)
而响应结果加密:
通过XHR断点或者发起程序开找,找到以下疑似处(可以多下断点广撒网)
可以发现基本就是这了
以下代码便是解密代码:
_0xa60097['a']['decipher'](_0x3bb7ad[_0x430ddf(0x1f7, 'JJcn')]['result'])
而_0x3bb7ad[_0x430ddf(0x1f7, 'JJcn')]['result']
便是响应结果中的result
,因此只需要扣_0xa60097['a']['decipher']
即可
跳转代码如下(已封装至本地):
function get_data(result){
return _0x1372a9(_0x230351(_0x43f8fa(result)));
}
开始慢扣三个函数,发现运行无结果,于是在已扣出_0x1372a9
、_0x230351
和_0x43f8fa
的情况下(因为这三个函数包含在一个对象中,有点像webpack),直接全盘复制,不过要注意去掉涉及到无限debugger
的代码(不要和老夫说什么半扣优雅,老夫上来就是一个全扣,省时省力),运行,结果正常,这样就拿到了图片的ID(但是想获取图片还需要逆向对应的API,这里点到为止),逆向结束