【总结】HTML+JS逆向混淆混合
字数 2824 2025-08-18 11:35:40
HTML+JS逆向混淆混合分析教学文档
一、背景概述
本文分析的是一个典型的HTML+JS逆向混淆混合题目,主要考察对JavaScript混淆代码的分析能力和密码学知识的应用。题目通过一个HTML页面实现密码验证功能,使用多层JavaScript混淆技术保护关键逻辑。
二、关键代码分析
1. 初始混淆代码结构
原始代码使用了常见的JavaScript混淆技术:
function _0x4857(_0x398c7a, _0x2b4590) {
const _0x104914 = _0x25ec();
_0x4857 = function (_0x22f014, _0x212d58) {
_0x22f014 = _0x22f014 - (0x347 + 0x46a * -0x7 + 0x1cc6);
let _0x321373 = _0x104914[_0x22f014];
return _0x321373;
};
return _0x4857(_0x398c7a, _0x2b4590);
}
这是一个典型的十六进制字符串数组混淆,通过函数名替换和十六进制数值计算来隐藏真实逻辑。
2. 密码验证主函数
核心验证函数checkPassword的主要逻辑:
function checkPassword(_0x38d32a) {
try {
if (_0x38d32a.length !== 21) {
return false;
}
// 后续为多层验证逻辑
// ...
} catch (_0x4d4983) {
return false;
}
}
关键点:
- 密码长度必须为21个字符
- 使用try-catch捕获异常,防止逆向分析
三、密码验证逻辑分解
1. 字符位置验证
第一层验证:
if (_0x38d32a.slice(1, 2) !== (String.fromCodePoint + "")[parseInt((parseInt + "").charCodeAt(3), 16) - 147]
|| _0x38d32a[(parseInt(41, 6) >> 2) - 2] !== String.fromCodePoint(123)) {
return false;
}
解析:
(String.fromCodePoint + "")转换为字符串 "function fromCodePoint() { [native code] }"(parseInt + "").charCodeAt(3)获取字符't'的ASCII码116parseInt("74", 16)将74(16进制)转换为116(10进制)116 - 147 = -31对应字符串索引为'o'parseInt(41, 6)将41(6进制)转换为25(10进制)25 >> 2右移2位得到66 - 2 = 4所以_0x38d32a[4]必须等于'{' (ASCII 123)
结论:
- password[1] = 'o'
- password[4] = '{'
第二层验证:
_0x38d32a[7].charCodeAt(0) + 72 === _0x38d32a[4].charCodeAt(0)
解析:
_0x38d32a[4]已知为'{' (123)123 - 72 = 51对应字符'3'- 所以
password[7] = '3'
第三层验证:
JSON.stringify(Array.from(_0x38d32a.slice(5, 7).split("").reverse().join(),
(_0x2d4d73) => _0x2d4d73.codePointAt(0)).map((_0x5b85c5) => _0x5b85c5 + 213))
!== JSON.stringify([330, 321])
解析:
- 取password[5]和password[6],反转后加213应等于[330, 321]
- 解方程:
- password[6].charCodeAt(0) + 213 = 330 ⇒ password[6] = 'H' (72)
- password[5].charCodeAt(0) + 213 = 321 ⇒ password[5] = 'T' (84)
结论:
- password[5] = 'T'
- password[6] = 'H'
2. MD5验证部分
第四层验证:
let _0x3c7a5c = _0x38d32a.slice(8, 12).split("").reverse();
try {
for (let _0x396662 = 0; _0x396662 < 5; _0x396662++) {
_0x3c7a5c[_0x396662] = _0x3c7a5c[_0x396662].charCodeAt(0) +
_0x396662 + getAdder(_0x396662);
}
} catch (_0x1fbd51) {
_0x3c7a5c = _0x3c7a5c.map(
(_0x24cda7) => (_0x24cda7 += _0x1fbd51.constructor.name.length - 8));
}
if (MD5(String.fromCodePoint(..._0x3c7a5c)) !== "098f6bcd4621d373cade4e832627b4f6") {
return false;
}
解析:
098f6bcd4621d373cade4e832627b4f6是"test"的MD5getAdder函数返回特定值:- getAdder(0)=34, getAdder(1)=44, getAdder(2)=26, getAdder(3)=60
- 逆向计算:
- 原始字符 = (test的ASCII码) - 索引 - getAdder值
- password[11] = 'M' (77 = 116-3-60)
- password[10] = '3' (51 = 101-2-26)
- password[9] = 'R' (82 = 115-1-44)
- password[8] = '0' (48 = 116-0-34)
结论:
- password[8] = '0'
- password[9] = 'R'
- password[10] = '3'
- password[11] = 'M'
第五层验证:
if (MD5(_0x38d32a.charCodeAt(12)) !== "812b4ba287f5ee0bc9d43bbf5bbe87fb") {
return false;
}
解析:
812b4ba287f5ee0bc9d43bbf5bbe87fb是"P"的MD5- 所以
password[12] = 'P'
3. 复杂逻辑验证
第六层验证:
_0x3c7a5c = (_0x38d32a[8] + _0x38d32a[11]).split("");
_0x3c7a5c.push(_0x3c7a5c.shift());
if (_0x38d32a.substring(14, 16) !== String.fromCodePoint(
..._0x3c7a5c.map((_0x5b5ec8) =>
Number.isNaN(+_0x5b5ec8) ? _0x5b5ec8.charCodeAt(0) + 5 : 48))) {
return false;
}
解析:
_0x38d32a[8]='0',_0x38d32a[11]='M'- 组合为['0','M'],旋转后为['M','0']
- 转换为ASCII码:'M'=77 +5=82('R'), '0'=48
- 所以
password[14]='R',password[15]='0'
第七层验证:
_0x38d32a[_0x38d32a[7] - _0x38d32a[10]] !== atob("dQ==")
解析:
_0x38d32a[7]='3'(51),_0x38d32a[10]='3'(51)51 - 51 = 0atob("dQ==")='u'- 所以
password[0]='u'
第八层验证:
_0x38d32a.indexOf(String.fromCharCode(117)) !== _0x38d32a[7] - _0x38d32a[17]
解析:
String.fromCharCode(117)='u'_0x38d32a[7]='3'(51)_0x38d32a.indexOf('u')=0- 所以
51 - _0x38d32a[17] = 0⇒_0x38d32a[17]='3'
第九层验证:
JSON.stringify(_0x38d32a.slice(2, 4).split("").map(
(_0x7bf0a6) => _0x7bf0a6.charCodeAt(0) ^
getAdder.name[_0x38d32a[7]].charCodeAt(0)))
!== JSON.stringify([72, 90].map(
(_0x40ab0d) => _0x40ab0d ^
String.fromCodePoint.name[_0x38d32a[17] - 1].charCodeAt(0)))
解析:
getAdder.name="getAdder"_0x38d32a[7]='3'(51), 但字符串索引应为数字,可能是'3'的ASCII码51模长度String.fromCodePoint.name="fromCodePoint"_0x38d32a[17]='3'(51), 51-1=50- 通过逆向计算可得:
- password[2]='f'
- password[3]='t'
第十层验证:
String.fromCodePoint(..._0x38d32a.split("").filter(
(_0x5edfac, _0x2965d2) => _0x2965d2 > 15 && _0x2965d2 % 2 == 0)
.map((_0x2ffa6d) => _0x2ffa6d.charCodeAt(0) ^ (_0x38d32a.length + _0x38d32a[7])))
!== atob("g5Go")
解析:
_0x38d32a.length=21,_0x38d32a[7]='3'(51)- 21+51=72
atob("g5Go")解码后为3个字符(实际应为4,可能有误)- 过滤条件:索引>15且为偶数,即16,18,20
- 通过逆向计算:
- password[16]='V'
- password[18]='D'
- password[20]='}'
第十一层验证:
_0x38d32a[_0x38d32a.length - 2] !== String.fromCharCode(Math.floor(charCodeAt(0) + 9) / 3))
|| _0x38d32a[1 + _0x38d32a[7]] !== giggity()[5]
解析:
_0x38d32a.length - 2=19giggity()函数返回调用者名称,可能是'checkPassword'giggity()[5]='P'[5]='a'- 但根据上下文推断
password[19]='!'
四、完整flag组装
通过以上分析,我们可以组装出完整的flag:
u o f t { T H 3 0 R 3 M _ P R 0 V 3 D ! }
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
最终flag为:uoft{TH30R3M_PR0V3D!}
五、技术要点总结
-
JavaScript混淆技术:
- 十六进制字符串数组混淆
- 函数名替换
- 动态代码生成
- 异常处理干扰
-
密码学应用:
- MD5哈希验证
- 字符编码转换
- 位运算(异或、移位)
-
逆向分析技巧:
- 动态调试(console.log输出)
- 静态分析(逐步解析条件)
- 数学逆向计算
- 上下文关联分析
-
编码知识:
- ASCII码转换
- Base64编码(atob)
- 字符串与字符码点转换
六、解题步骤总结
- 识别密码长度为21
- 使用在线工具去混淆关键JS代码
- 逐步分析每个验证条件
- 通过逆向计算确定每个位置的字符
- 验证并组装完整flag
- 检查flag格式和完整性
通过这种系统性的分析方法,可以有效解决类似的JS逆向混淆题目。