【总结】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码116
  • parseInt("74", 16) 将74(16进制)转换为116(10进制)
  • 116 - 147 = -31 对应字符串索引为'o'
  • parseInt(41, 6) 将41(6进制)转换为25(10进制)
  • 25 >> 2 右移2位得到6
  • 6 - 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"的MD5
  • getAdder函数返回特定值:
    • 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 = 0
  • atob("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=19
  • giggity()函数返回调用者名称,可能是'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!}

五、技术要点总结

  1. JavaScript混淆技术

    • 十六进制字符串数组混淆
    • 函数名替换
    • 动态代码生成
    • 异常处理干扰
  2. 密码学应用

    • MD5哈希验证
    • 字符编码转换
    • 位运算(异或、移位)
  3. 逆向分析技巧

    • 动态调试(console.log输出)
    • 静态分析(逐步解析条件)
    • 数学逆向计算
    • 上下文关联分析
  4. 编码知识

    • ASCII码转换
    • Base64编码(atob)
    • 字符串与字符码点转换

六、解题步骤总结

  1. 识别密码长度为21
  2. 使用在线工具去混淆关键JS代码
  3. 逐步分析每个验证条件
  4. 通过逆向计算确定每个位置的字符
  5. 验证并组装完整flag
  6. 检查flag格式和完整性

通过这种系统性的分析方法,可以有效解决类似的JS逆向混淆题目。

HTML+JS逆向混淆混合分析教学文档 一、背景概述 本文分析的是一个典型的HTML+JS逆向混淆混合题目,主要考察对JavaScript混淆代码的分析能力和密码学知识的应用。题目通过一个HTML页面实现密码验证功能,使用多层JavaScript混淆技术保护关键逻辑。 二、关键代码分析 1. 初始混淆代码结构 原始代码使用了常见的JavaScript混淆技术: 这是一个典型的十六进制字符串数组混淆,通过函数名替换和十六进制数值计算来隐藏真实逻辑。 2. 密码验证主函数 核心验证函数 checkPassword 的主要逻辑: 关键点: 密码长度必须为21个字符 使用try-catch捕获异常,防止逆向分析 三、密码验证逻辑分解 1. 字符位置验证 第一层验证: 解析: (String.fromCodePoint + "") 转换为字符串 "function fromCodePoint() { [ native code ] }" (parseInt + "").charCodeAt(3) 获取字符't'的ASCII码116 parseInt("74", 16) 将74(16进制)转换为116(10进制) 116 - 147 = -31 对应字符串索引为'o' parseInt(41, 6) 将41(6进制)转换为25(10进制) 25 >> 2 右移2位得到6 6 - 2 = 4 所以 _0x38d32a[4] 必须等于'{' (ASCII 123) 结论: password[ 1 ] = 'o' password[ 4 ] = '{' 第二层验证: 解析: _0x38d32a[4] 已知为'{' (123) 123 - 72 = 51 对应字符'3' 所以 password[7] = '3' 第三层验证: 解析: 取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验证部分 第四层验证: 解析: 098f6bcd4621d373cade4e832627b4f6 是"test"的MD5 getAdder 函数返回特定值: 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' 第五层验证: 解析: 812b4ba287f5ee0bc9d43bbf5bbe87fb 是"P"的MD5 所以 password[12] = 'P' 3. 复杂逻辑验证 第六层验证: 解析: _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[7] ='3'(51), _0x38d32a[10] ='3'(51) 51 - 51 = 0 atob("dQ==") ='u' 所以 password[0]='u' 第八层验证: 解析: String.fromCharCode(117) ='u' _0x38d32a[7] ='3'(51) _0x38d32a.indexOf('u') =0 所以 51 - _0x38d32a[17] = 0 ⇒ _0x38d32a[17]='3' 第九层验证: 解析: 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' 第十层验证: 解析: _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.length - 2 =19 giggity() 函数返回调用者名称,可能是'checkPassword' giggity()[5] ='P'[ 5 ]='a' 但根据上下文推断 password[19]='!' 四、完整flag组装 通过以上分析,我们可以组装出完整的flag: 最终flag为: uoft{TH30R3M_PR0V3D!} 五、技术要点总结 JavaScript混淆技术 : 十六进制字符串数组混淆 函数名替换 动态代码生成 异常处理干扰 密码学应用 : MD5哈希验证 字符编码转换 位运算(异或、移位) 逆向分析技巧 : 动态调试(console.log输出) 静态分析(逐步解析条件) 数学逆向计算 上下文关联分析 编码知识 : ASCII码转换 Base64编码(atob) 字符串与字符码点转换 六、解题步骤总结 识别密码长度为21 使用在线工具去混淆关键JS代码 逐步分析每个验证条件 通过逆向计算确定每个位置的字符 验证并组装完整flag 检查flag格式和完整性 通过这种系统性的分析方法,可以有效解决类似的JS逆向混淆题目。