从调试某Electron应用看JS混淆

0x01 解包

首先对Electron应用进行解包:asar extract app.asar app,解包文件会释放到当前目录下的app文件夹里。通过package.json可以看到入口js文件以及相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"name": "******",
"version": "1.2.2",
"author": "*****",
"description": "*****",
"main": "main/index.js",
"dependencies": {
"@babel/generator": "^7.18.2",
"@babel/parser": "^7.18.5",
"@babel/traverse": "^7.18.5",
"@nestjs/common": "^8.2.4",
"@nestjs/core": "^8.2.4",
"@nestjs/microservices": "^8.2.4",
"bytenode": "^1.3.6",
"electron-log": "^4.4.6",
"electron-store": "^8.0.1",
"electron-updater": "^4.6.5",
"express": "^4.17.3",
"fs-extra": "^10.1.0",
"glob": "^7.1.7",
"iconv-lite": "^0.6.3",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.7",
"protobufjs": "^6.11.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.1",
"semver": "^7.3.7",
"systeminformation": "^5.11.9",
"tcp-port-used": "^1.0.2",
"url-parse": "^1.5.10",
"uuid": "^8.3.2"
}
}

通过"main":"main/index.js"可知入口JS文件在main子目录下的index.js文件。打开文件得到源码:

1
2
(function(_0x2c2b00,_0x266f24){function _0x495d9e(_0x5c6e5d,_0x2e5990,_0x92ffbf,_0xd034a8,_0x357357){return _0x2428(_0x357357- -0x87,_0x2e5990);}function _0x245656(_0x3c14a2,_0xf20f43,_0x684d2f,_0xbe1a9a,_0x4c1a31){return _0x2428(_0xbe1a9a-0x39e,_0x4c1a31);}function _0xf3f8ba(_0x3bb4fe,_0x3abbcb,_0x5b1533,_0x12f9ce,_0x428add){return _0x2428(_0x12f9ce-0x38e,_0x3abbcb);}var _0x17d610=_0x2c2b00();function _0x3d68b3(_0x2076a7,_0x106168,_0x234a88,_0x360ccc,_0x461c10){return _0x2428(_0x234a88- -0x1b4,_0x360ccc);}while(!![]){try{var _0x277bc2=parseInt(_0x495d9e(0x1d5,'\x5a\x37\x47\x6d',0x267,0x1b1,0x231))/0x1+parseInt(_0x495d9e(0x224,'\x24\x6a\x38\x33',0x18c,0x17e,0x211))/0x2+parseInt(_0x495d9e(0x255,'\x40\x77\x78\x28',0x201,0x279,0x1ff))/0x3*(-parseInt(_0x495d9e(0x29a,'\x34\x64\x67\x33',0x2fc,0x2a5,0x253))/0x4)+parseInt(_0x3d68b3(0xcb,0x2b,0xb6,'\x6c\x40\x4a\x75',0x14a))/0x5+parseInt(_0x495d9e(0x158,'\x67\x52\x72\x4f',0xdc,0x1ee,0x133))/0x6+parseInt(_0x495d9e(0xe6,'\x41\x38\x39\x68',0x130,0xbc,0x14d))/0x7*(parseInt(_0x245656(0x52c,0x52c,0x62d,0x5e3,'\x79\x41\x4a\x78'))/0x8)+-parseInt(_0xf3f8ba(0x5cc,'\x41\x66\x4d\x28',0x60f,0x609,0x575))/0x9*(parseInt(_0x495d9e(0x21e,'\x6c\x7a\x29\x30',0x1e0,0xd5,0x172))/0xa);if(_0x277bc2===_0x266f24)break;else _0x17d610['push'](_0x17d610['shift']());}catch(_0x546e2a){_0x17d610['push'](_0x17d610['shift']());}}}(_0x4a83,0x213df));function _0x2428(_0x222817,_0x44b30d){var _0x30921e=_0x4a83();return _0x2428=function(_0xb30ec8,_0x2748e6){_0xb30ec8=_0xb30ec8-0x175;var _0x4a83e9=_0x30921e[_0xb30ec8];if(_0x2428['\x6f\x4e\x59\x48\x4d\x4f']===undefined){var _0x2428bb=function(_0xdad3f3){var _0x322eb2='\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d';var _0x42cc1a='',_0x3bd1d2='',_0x20b060=_0x42cc1a+_0x2428bb;for(var _0x140781=0x0,_0x3b67a1,_0x2ed931,_0x43927d=0x0;_0x2ed931=_0xdad3f3['\x63\x68\x61\x72\x41\x74'](_0x43927d++);~_0x2ed931&&(_0x3b67a1=_0x140781%0x4?_0x3b67a1*0x40+_0x2ed931:_0x2ed931,_0x140781++%0x4)?_0x42cc1a+=_0x20b060['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x43927d+0xa)-0xa!==0x0?String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](0xff&_0x3b67a1>>(-0x2*_0x140781&0x6)):_0x140781:0x0){_0x2ed931=_0x322eb2['\x69\x6e\x64\x65\x78\x4f\x66'](_0x2ed931);}for(var _0x4d79cc=0x0,_0x149750=_0x42cc1a['\x6c\x65\x6e\x67\x74\x68'];_0x4d79cc<_0x149750;_0x4d79cc++){_0x3bd1d2+='\x25'+('\x30\x30'+_0x42cc1a['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x4d79cc)['\x74\x6f\x53\x74\x72\x69\x6e\x67'](0x10))['\x73\x6c\x69\x63\x65'](-0x2);}return decodeURIComponent(_0x3bd1d2);};var _0x3ee60f=function(_0x56923f,_0x4dd766){var _0x1c8920=[],_0x3cd08d=0x0,_0x1a10c0,_0x43d367='';_0x56923f=_0x2428bb(_0x56923f)...
//代码太长了就不完全展示了...

0x02 混淆的简单理解

混淆的原理,按我个人的理解,就是把简单的代码复杂化。比如var a=1这个表达式,混淆之后可以是var a=99;a=a-98;a=a+2-3;++a;--a;++a,实际上,变量a的值(或者说a的语义)并没有发生改变。但是,数组混淆之后,其序列内原本的成员顺序改变了,语义怎么保持和原来一致呢?所以,混淆后的JS代码可能会存在数组排序的问题。

数组排序

以解混淆后的数组排序代码为例:

1
2
3
4
5
6
7
8
9
10
  while (!![]) {
try {
var _0x277bc2 = parseInt(_0x495d9e(469, "Z7Gm", 615, 433, 561)) / 1 + parseInt(_0x495d9e(548, "$j83", 396, 382, 529)) / 2 + parseInt(_0x495d9e(597, "@wx(", 513, 633, 511)) / 3 * (-parseInt(_0x495d9e(666, "4dg3", 764, 677, 595)) / 4) + parseInt(_0x3d68b3(203, 43, 182, "l@Ju", 330)) / 5 + parseInt(_0x495d9e(344, "gRrO", 220, 494, 307)) / 6 + parseInt(_0x495d9e(230, "A89h", 304, 188, 333)) / 7 * (parseInt(_0x245656(1324, 1324, 1581, 1507, "yAJx")) / 8) + -parseInt(_0xf3f8ba(1484, "AfM(", 1551, 1545, 1397)) / 9 * (parseInt(_0x495d9e(542, "lz)0", 480, 213, 370)) / 10);

if (_0x277bc2 === _0x266f24) break;else _0x17d610["push"](_0x17d610["shift"]());
} catch (_0x546e2a) {
_0x17d610["push"](_0x17d610["shift"]());
}
}
})(_0x4a83, 136159);

可以看到当(_0x277bc2_0x266f24全等时,循环才会跳出。如果不全等,则会执行pushshift操作,在数组末尾添加成员,或移除数组成员。以后在混淆的JS代码中,看到循环里对数组的pushshift,可以先尝试理解成数组排序。

0x03 解混淆

在这里,先用JS NICE: Statistical renaming, Type inference and Deobfuscation工具解混淆。这个工具可以将混淆的代码格式化,来变的更可读。但实际在调试过程中,如果遇到函数码表这类反调试措施可能无法让JS文件正常调试下去,具体细节下文会详细说明。

通过工具得到反混淆后代码。此处省略,因为代码太多了笔记会卡就不贴上了。

AST树解混淆

如果是手动反混淆的话,**抽象语法树(Abstract Syntax Tree,AST)**是一个很好的工具。它把JS文件中的每个语句都拆成类似JSON这种树状结构的数据类型,以树状的形式表示JS文件的语法结构,树上的每个结点都表示源代码的一种结构。具体进行分析时,我们可以使用AST explorer对语句进行分析。

在使用ASTexplorer之前,需要把Parser设置为@bebel/parser,这是因为在第一节解包的时候发现该应用依赖于@bebel/parser,所以要设置成这个格式。

混淆的源码中,存在着大量类似\x63\x64\x65\x660xff这类十六进制数据,我们可以直接对其进行解析:

同样的,AST树中也是直接把这些十六进制的数据给出来了:

可以观察到,最后的值value的父节点是StringLiteralNumericLiteral。我们可以使用@babel/generator生成混淆代码的AST树,通过@babel/traverse遍历每个节点,找到从@babel/types引用的数据类型后,提取value替换即可达到一个简单的反混淆效果。

下面是一个替换十六进制字符串的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let fs = require('fs') //导入fs包,读写文件

var parser = require('@babel/parser') //用于解析AST树
var traverse = require('@babel/traverse').default //遍历AST树
var generator = require('@babel/generator').default //生成AST树

const {StringLiteral,NumericLiteral} = require('@babel/types') //导入数据类型

let t_code = fs.readFileSync('index.js',{encoding:"utf-8"}) //读混淆代码
let ast = parser.parse(t_code) //将读取的代码解析成AST树

traverse(ast,{ //对指定的数据类型结点进行遍历,生成操作后的代码ast
StringLiteral(path){
let value = path.node.value //获得StringLiteral节点的value值
path.replaceWith(StringLiteral(value)) //对当前路径的value值进行替换
path.skip() //跳往下个节点
},

NumericLiteral(path){
let value = path.node.value
path.replaceWith(NumericLiteral(value))
path.skip()

}

})

let code = generator(ast).code //生成处理后的ast代码
fs.writeFileSync('2.js',code,{encoding:"utf-8"})

替换之后的2.js文件内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(function (_0x2c2b00, _0x266f24) {
function _0x495d9e(_0x5c6e5d, _0x2e5990, _0x92ffbf, _0xd034a8, _0x357357) {
return _0x2428(_0x357357 - -135, _0x2e5990);
}

function _0x245656(_0x3c14a2, _0xf20f43, _0x684d2f, _0xbe1a9a, _0x4c1a31) {
return _0x2428(_0xbe1a9a - 926, _0x4c1a31);
}

function _0xf3f8ba(_0x3bb4fe, _0x3abbcb, _0x5b1533, _0x12f9ce, _0x428add) {
return _0x2428(_0x12f9ce - 910, _0x3abbcb);
}

var _0x17d610 = _0x2c2b00();

function _0x3d68b3(_0x2076a7, _0x106168, _0x234a88, _0x360ccc, _0x461c10) {
return _0x2428(_0x234a88 - -436, _0x360ccc);
}

while (!![]) {
try {
var _0x277bc2 = parseInt(_0x495d9e(469, "Z7Gm", 615, 433, 561)) / 1 + parseInt(_0x495d9e(548, "$j83", 396, 382, 529)) / 2 + parseInt(_0x495d9e(597, "@wx(", 513, 633, 511)) / 3 * (-parseInt(_0x495d9e(666, "4dg3", 764, 677, 595)) / 4) + parseInt(_0x3d68b3(203, 43, 182, "l@Ju", 330)) / 5 + parseInt(_0x495d9e(344, "gRrO", 220, 494, 307)) / 6 + parseInt(_0x495d9e(230, "A89h", 304, 188, 333)) / 7 * (parseInt(_0x245656(1324, 1324, 1581, 1507, "yAJx")) / 8) + -parseInt(_0xf3f8ba(1484, "AfM(", 1551, 1545, 1397)) / 9 * (parseInt(_0x495d9e(542, "lz)0", 480, 213, 370)) / 10);

if (_0x277bc2 === _0x266f24) break;else _0x17d610["push"](_0x17d610["shift"]());
} catch (_0x546e2a) {
_0x17d610["push"](_0x17d610["shift"]());
}
}
})(_0x4a83, 136159);

//.......
//以下部分省略,具体结果上手操作可以得到

0x04 绕过反调试机制

通过上节方法解混淆之后,可以得到一个可读性稍强的代码,这时候就可以开始调试了。但是程序设计者肯定不会让我们这么顺利的调试的,往往在混淆的过程中加一些反调试机制,也就是暗桩。

这时候就需要秉持着一个原则:

  1. 大函数之间下断点
  2. 函数内,代码多的函数先步过(一口气走完跳过)
  3. 直到出现异常情况(程序崩溃、大量循环),开始分析暗桩

第一点还是比较好理解的,因为在程序中,往往是从上向下执行。如果一个JS文件中有四个函数,可以先在AB函数之间写个console.log,看是否输出,如果程序异常且无输出,说明A函数存在暗桩。

注:JavaScript存在声明提升机制,更多说明与利用见龙哥博客:【Javascript】声明提升 - SomebodyNoStation

第二、三点,就是常规调试操作,没出现异常就步过,出现异常就步进,直至确认异常出现在哪行代码中。在这里,先从_0x2428函数之前加个console.log,投石问路:

看图可知,程序执行完毕跳出且没有打印控制台命令。也就是说程序并没有执行完所有的语句就退出了,可能存在反调试机制……

内存爆破

var _0x277bc2处打断点,程序运行到此处暂停,步过后停止。也就是说反调试存在此行子函数中。继续调试,暂停到此行后步入,发现以下代码较为可疑:

首先_0x24ecd3匹配了两个正则表达式,具体匹配的什么先不管~~(其实是看不懂)~~,看下面的103行:

1
_0x32c41e = _0x24ecd3["test"](this["myBoDO"]["toString"]()) ? --this["RfnWOy"][1] : --this["RfnWOy"][0];

这里对this["myBoDO"]["toString"]()进行了匹配正则,也就是对function () {return "newState";的格式化后的字符串进行了正则匹配,如果匹配成功则返回-1,匹配不成功则返回0。最后该函数带参数返回this["CugJBF"](_0x32c41e)

this["myBoDO"]["toString"]()也可以写成this["myBoDO"]+''的形式,语义不会发生改变。

而在下面的函数中:

1
2
3
4
_0x3b755e["prototype"]["CugJBF"] = function (_0x893c54) {
if (!Boolean(~_0x893c54)) return _0x893c54;
return this["RzzIoo"](this["xdQAnv"]);
},

已知传入参数只有-1和0,这样我们可以直接从控制台中打印最后的结果:

当传入值为-1时,也就是正则匹配成功时才会直接返回-1,否则不成功的话会接着运行return this["RzzIoo"](this["xdQAnv"])

现在看一下这个函数究竟是个什么东西,这里的代码使用JSNICE反混淆后的代码,更容易阅读。

1
2
3
4
5
6
7
8
9
WMCacheControl["prototype"]["RzzIoo"] = function(saveNotifs) {
var fp = 0;
var len = this["RfnWOy"]["length"];
for (; fp < len; fp++) {
this["RfnWOy"]["push"](Math["round"](Math["random"]())); //push--判断数组
len = this["RfnWOy"]["length"]; //一直循环,内存爆破
}
return saveNotifs(this["RfnWOy"][0]);
};

这里简单来说就是不断生成随机数,并加入到数组中;虽然fp是每次循环都+1,但是数组的长度也是不断增长的,所以这个循环根本就不会停止。而储存在内存里的数组元素也是不断增长的,所以就会实现一个内存爆破的效果。

更直观的图片可以直接看下图:

至于绕过方法,有很多种,其实原理都是一样的,不要让他爆破就行了:

  1. 105行直接返回值改true;
  2. 调换103行-1和0的结果值;
  3. 直接删除掉内存爆破函数;
  4. ……

函数编码表

解决完内存爆破的问题后,在调试过程中不会异常退出了,但会出现程序在很长时间都一直运行的问题……说明程序陷入了某个循环或调用链中,需要再次进行调试。通过打断点发现,出问题的存在_0x2428(row, value)函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function _0x2428(row, value) {
var custom_filters = _0x4a83();
return _0x2428 = function(i, value) {
i = i - 373;
var text = custom_filters[i];
if (_0x2428["oNYHMO"] === undefined) {
var getOwnPropertyNames = function(o) {
var listeners = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";
var PL$13 = "";
var urn = "";
var data = PL$13 + getOwnPropertyNames;
var bc = 0;
var bs;
var buffer;
var n = 0;
for (; buffer = o["charAt"](n++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? PL$13 = PL$13 + (data["charCodeAt"](n + 10) - 10 !== 0 ? String["fromCharCode"](255 & bs >> (-2 * bc & 6)) : bc) : 0) {
buffer = listeners["indexOf"](buffer);
}
var PL$19 = 0;
var PL$15 = PL$13["length"];
for (; PL$19 < PL$15; PL$19++) {
urn = urn + ("%" + ("00" + PL$13["charCodeAt"](PL$19)["toString"](16))["slice"](-2));
}
return decodeURIComponent(urn);
};
//........
}

需要注意的一点,在var data=PL$13 + getOwnPropertyNames;这个语句中,实际上就是var data=getOwnPropertyNames+''。它会把getOwnPropertyNames这个函数以字符串的形式储存在data中。而在(data["charCodeAt"](n + 10) - 10 !== 0 ? String["fromCharCode"](255 & bs >> (-2 * bc & 6)) : bc) : 0),发现了程序在对data进行数据操作,可以初步确定该函数是以其字符串化的数据为码表,进行编解码的。

但这里需要注意:经过测试,使用JSNICE解混淆后的代码是没法过函数码表调试的,因为解码后的函数和变量被重新命名,语义发生了改变。

所以这里需要使用AST树反混淆后的代码进行绕过:

1
2
3
4
5
6
7
8
9
10
11
12
if (_0x2428["oNYHMO"] === undefined) {
var _0x2428bb = function (_0xdad3f3) {var _0x322eb2="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";var _0x42cc1a="",_0x3bd1d2="",_0x20b060=_0x42cc1a+_0x2428bb;
for (var _0x140781 = 0, _0x3b67a1, _0x2ed931, _0x43927d = 0; _0x2ed931 = _0xdad3f3["charAt"](_0x43927d++); ~_0x2ed931 && (_0x3b67a1 = _0x140781 % 4 ? _0x3b67a1 * 64 + _0x2ed931 : _0x2ed931, _0x140781++ % 4) ? _0x42cc1a += _0x20b060["charCodeAt"](_0x43927d + 10) - 10 !== 0 ? String["fromCharCode"](255 & _0x3b67a1 >> (-2 * _0x140781 & 6)) : _0x140781 : 0) {
_0x2ed931 = _0x322eb2["indexOf"](_0x2ed931);
}

for (var _0x4d79cc = 0, _0x149750 = _0x42cc1a["length"]; _0x4d79cc < _0x149750; _0x4d79cc++) {
_0x3bd1d2 += "%" + ("00" + _0x42cc1a["charCodeAt"](_0x4d79cc)["toString"](16))["slice"](-2);
}

return decodeURIComponent(_0x3bd1d2);
};

因为在JS代码未格式化之前,所有的语句都是尽量连在一起的,所以在这里我们需要去掉语句之间的空格。再次调试后会发现已经绕过了函数码表,面对我们的是最后一个障碍……

拒绝服务

绕过函数码表后,还是不断打断点进行调试,发现程序运行到_0x3cd8b0()后卡死。继续在_0x3cd8b0()打断点,F11步入到var _0x5a2da4中。按照调试的原则,直到出异常后重新打断点步入。发现在下面代码中无响应:

1
var _0x223952 = _0x4ad3cf[_0x1b6697("Q[uh", 338, 440, 475, 346)](_0x425e28, arguments);

打断点,停止程序,重新调试。运行到该步时,对该行三个子函数全打断点后步过,发现前两个顺利步过,由此可知_0x425e28处存在暗桩:

之后f11步入到var _0x377802,步过一次,发现到了return,一堆子函数,依然是全打断点步过:

调试到_0x377802处卡死,这时候猛地发现:这个_0x3cd8b0()在return处也有……继续分析_0x3cd8b0()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  _0x3cd8b0 = _0x384e83(this, function () {
function _0x1db718(_0x5b392e, _0x43b334, _0x2e2186, _0x51608b, _0x1e40ec) {
return _0x2428(_0x51608b - 418, _0x2e2186);
}

function _0x5852f4(_0x879a90, _0x1557ee, _0xa232dd, _0x533bbc, _0x32f796) {
return _0x2428(_0x879a90 - 467, _0x32f796);
}

var _0x377802 = {
"uawfM": _0x5bc29b(-299, -136, -166, -154, "pf7m") + _0x5bc29b(-337, -265, -385, -263, "tR1%") + "+$"
};

function _0x2129cf(_0xb35e3c, _0x58b231, _0x3ea525, _0x3dfb26, _0x54f224) {
return _0x2428(_0x54f224 - -299, _0x58b231);
}

function _0x5bc29b(_0x29383b, _0x2783c2, _0xab345a, _0x222ff3, _0x444bd3) {
return _0x2428(_0x222ff3 - -662, _0x444bd3);
}

return _0x3cd8b0[_0x5bc29b(-310, -327, -203, -271, "PVAS") + _0x2129cf(526, "IVdS", 443, 392, 439)]()[_0x5852f4(989, 1150, 955, 1088, "l@Ju") + "h"](_0x377802[_0x5bc29b(84, 1, -69, 79, "u4os")])[_0x2129cf(279, "uiYA", 254, 247, 428) + _0x5bc29b(-229, 25, -135, -55, "PVAS")]()[_0x5852f4(1087, 1013, 946, 1105, "lz)0") + _0x5bc29b(208, 101, 98, 21, "lz)0") + "r"](_0x3cd8b0)[_0x2129cf(285, "b#3e", 379, 555, 433) + "h"](_0x377802[_0x5bc29b(-93, -192, -239, -277, ")Ua$")]);
});

发现存在四个子函数,但是最后都返回调用了_0x2428,而_0x2428恰恰是上步我们研究码表时的函数,猜测主要起解码的作用。这里也可以大致猜到,_0x3cd8b0不断嵌套调用解码函数,起到了一个拒绝服务的作用。但具体表示什么含义呢?不妨先将方括号里的参数用控制台打印输出一下:

1
2
3
4
5
6
7
_0x5bc29b(-310, -327, -203, -271, "PVAS") + _0x2129cf(526, "IVdS", 443, 392, 439) ==> 'toString'
_0x5852f4(989, 1150, 955, 1088, "l@Ju") + "h" ==> 'search'
_0x5bc29b(84, 1, -69, 79, "u4os") ==> 'uawfM'
_0x2129cf(279, "uiYA", 254, 247, 428) + _0x5bc29b(-229, 25, -135, -55, "PVAS") ==> 'toString'
_0x5852f4(1087, 1013, 946, 1105, "lz)0") + _0x5bc29b(208, 101, 98, 21, "lz)0") + "r" ==> 'constructor'
_0x2129cf(285, "b#3e", 379, 555, 433) + "h" ==> 'search'
_0x5bc29b(-93, -192, -239, -277, ")Ua$") ==> 'uawfM'

综合起来就是:

1
_0x3cd8b0['toString']()['search'](_0x377802['uawfM'])['toString']()['constructor'](_0x3cd8b0)['search'](_0x377802['uawfM']);

因为_0x377802这个函数在上面有体现,还可以进一步替换:_0x3cd8b0.toString().search('(((.+)+)+)+$').toString()

这个表达式的语义是,_0x377802.toString()把函数字符串输出,然后正则匹配(((.+)+)+)+$规则,之后输出字符串。但是通过我们调试可以发现,这个字符串是肯定输出不出来的,因为卡住了……

经过测试,发现这个语句是一个基于正则表达式的拒绝服务,如果字符串里有回车,第一行正文长度大于14个字符的情况下,就拒绝响应。

直接注释掉这个函数,就可以看到最后的曙光:

这里错误信息表示我没有electron模块,是因为我这里没有进行环境配置(使用混淆代码直接进行调试会发现相同的错误)。如果配置好electron环境后,就可以发现应用打开了。


从调试某Electron应用看JS混淆
https://k1nm0.com/2022/06/28/某Electron应用的JS调试过程/
作者
K1nm0
发布于
2022年6月28日
许可协议