MD5
MD5(Message Digest Algorithm 5)是一种单向哈希函数,输出固定 128 位(32 位十六进制字符串)。不可逆,常用于数据签名和完整性校验。 搜索特征常量而非函数名:混淆代码中函数名被替换,但算法的魔法数字(0x67452301、0xefcdab89、0x98badcfe、0x10325476)不会被混淆,全局搜索这 4 个初始化向量可快速定位 MD5 函数。 Hook CryptoJS.MD5 拦截明文:若页面使用 CryptoJS 库,在调用前注入 Hook 打印参数: 区分完整 MD5 和截取变体:目标站点可能只取 MD5
官方文档:https://www.rfc-editor.org/rfc/rfc1321
适用场景:JS 逆向中识别、提取、还原 MD5 加密逻辑
MD5(Message Digest Algorithm 5)是一种单向哈希函数,输出固定 128 位(32 位十六进制字符串)。不可逆,常用于数据签名和完整性校验。
MD5 已不适合密码存储(可被彩虹表攻击),但在 JS 逆向场景中仍大量存在于签名参数生成中。
Python 实现
import hashlib
# 基础用法
def md5(data: str, encoding='utf-8') -> str:
return hashlib.md5(data.encode(encoding)).hexdigest()
# 32位小写
print(md5('hello')) # 5d41402abc4b2a76b9719d911017c592
# 32位大写
print(md5('hello').upper()) # 5D41402ABC4B2A76B9719D911017C592
# 16位(取中间16位)
def md5_16(data: str) -> str:
return md5(data)[8:24]
print(md5_16('hello')) # bc4b2a76b9719d91
# 对字节数据直接哈希
def md5_bytes(data: bytes) -> str:
return hashlib.md5(data).hexdigest()
# MD5 加盐
def md5_salt(data: str, salt: str) -> str:
return md5(salt + data) # 盐前置
# 或 return md5(data + salt) # 盐后置
# 多次 MD5(常见于某些网站)
def md5_double(data: str) -> str:
return md5(md5(data))
# 大文件分块哈希
def md5_file(filepath: str) -> str:
h = hashlib.md5()
with open(filepath, 'rb') as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()
JavaScript 实现
使用 CryptoJS(浏览器/Node.js 通用)
// 安装:npm install crypto-js
const CryptoJS = require('crypto-js');
// 32位小写
const hash = CryptoJS.MD5('hello').toString();
console.log(hash); // 5d41402abc4b2a76b9719d911017c592
// 32位大写
const hashUpper = CryptoJS.MD5('hello').toString().toUpperCase();
// 16位(中间16位)
const hash16 = CryptoJS.MD5('hello').toString().substring(8, 24);
// 加盐
const saltedHash = CryptoJS.MD5('salt' + 'hello').toString();
// 对 WordArray(字节数组)哈希
const wordArray = CryptoJS.enc.Hex.parse('68656c6c6f');
const hashFromBytes = CryptoJS.MD5(wordArray).toString();
使用 Node.js 内置 crypto
const crypto = require('crypto');
// 基础用法
function md5(data, encoding = 'hex') {
return crypto.createHash('md5').update(data).digest(encoding);
}
console.log(md5('hello')); // 5d41402abc4b2a76b9719d911017c592
console.log(md5('hello', 'base64')); // XUFAKrxLKna5cZ2REBfFkg==
// 加盐
function md5Salt(data, salt) {
return md5(salt + data);
}
// 流式(大文件)
const fs = require('fs');
function md5File(path) {
return new Promise((resolve) => {
const hash = crypto.createHash('md5');
fs.createReadStream(path).pipe(hash);
hash.on('finish', () => resolve(hash.digest('hex')));
});
}
使用 Web Crypto API(浏览器原生,异步)
Web Crypto API 不直接支持 MD5,因为 MD5 已被认为不安全。需要使用第三方库或用 SHA-256 替代。
逆向中常见变体
| 变体 | 说明 | Python |
|---|---|---|
| 标准 MD5 | 32位小写 hex | hashlib.md5(data).hexdigest() |
| 大写 MD5 | 32位大写 hex | .hexdigest().upper() |
| 16位 MD5 | 取中间16位 | [8:24] |
| 双重 MD5 | md5(md5(data)) |
嵌套调用 |
| 加盐 MD5 | md5(salt+data) 或 md5(data+salt) |
拼接后哈希 |
| MD5 后 Base64 | 对 MD5 结果再 Base64 | base64.b64encode(bytes.fromhex(md5(data))) |
快速识别
- 结果固定为 32 个十六进制字符(或 16 个,取中间段)
- 搜索关键字:
md5、MD5、CryptoJS.MD5、createHash('md5') - 特征常量:
0x67452301、0xefcdab89(MD5 初始化向量)
最佳实践
搜索特征常量而非函数名:混淆代码中函数名被替换,但算法的魔法数字(0x67452301、0xefcdab89、0x98badcfe、0x10325476)不会被混淆,全局搜索这 4 个初始化向量可快速定位 MD5 函数。
Hook CryptoJS.MD5 拦截明文:若页面使用 CryptoJS 库,在调用前注入 Hook 打印参数:
const _md5 = CryptoJS.MD5
CryptoJS.MD5 = function(msg, ...args) {
console.log('[MD5 input]', typeof msg === 'string' ? msg : JSON.stringify(msg))
return _md5.call(this, msg, ...args)
}
区分完整 MD5 和截取变体:目标站点可能只取 MD5 结果的一部分(如取第 8~24 位共 16 字符),对比 Python hashlib.md5(b'xxx').hexdigest() 验证时要按相同截取方式处理。
自定义 MD5(魔改)先对比标准输出:若标准 MD5 对不上,先输入 "abc" 测试:标准 MD5 结果为 900150983cd24fb0d6963f7d28e17f72,与之对比可判断初始向量、字节序、字符编码哪里被修改。
用 Python 快速验证还原结果:还原 JS 逻辑后,用 Python hashlib.md5(input.encode()).hexdigest() 交叉验证;若编码不同,尝试 encode('latin-1') 或 encode('utf-16-le')。
常见陷阱
陷阱:MD5 输入编码不一致导致结果不匹配
现象: Python hashlib.md5(b'hello').hexdigest() 结果与目标 JS MD5 结果不同。
原因: JS 字符串默认 UTF-16 编码,而 CryptoJS 处理时会先将字符串转为 UTF-8 字节;若 JS 代码将字符串转为 Latin-1 或直接取 charCode,结果会不同。
解决: 检查 JS 中 MD5 调用前的字符串处理逻辑(如 encodeURIComponent、unescape、btoa),在 Python 中用相同编码处理后再 MD5。
陷阱:32 位大小端(字节序)被修改
现象: 特征常量一致但输出完全不同,不像是只修改了初始向量。
原因: 部分魔改版 MD5 修改了内部 32 位整数的字节序(little-endian ↔ big-endian),导致每轮输出不同。
解决: 对比输入 "" 空字符串的 MD5 输出(标准为 d41d8cd98f00b204e9800998ecf8427e),从这一步找到字节序差异所在。
陷阱:将 16 位截取版误认为是不同算法
现象: 签名长度为 16 个字符,查阅算法表找不到对应输出长度的算法。
原因: 这很可能是取了 MD5 的 32 位结果的中间 16 字符(第 8~24 位),而非独立算法。
解决: 用标准 MD5 计算后截取相同范围验证;也可能是截取了前 16 或后 16 位,逐一尝试。