MD5学习和测试

20次阅读
没有评论

共计 5963 个字符,预计需要花费 15 分钟才能阅读完成。

MD5 本质上是 单向哈希(不可逆),所以严格来说无法“解密”原文;具体就两件事:

  1. 生成(“加密”):把任意文本变成 MD5 摘要(hex)。
  2. 尝试还原(字典 / 暴力破解):通过字典匹配或穷举尝试找到能产生该 MD5 的原文(对短 / 简单密码有效,对强密码通常不可行)。

原理

概览 — MD5 在做什么

MD5(Message-Digest Algorithm 5)把任意长度的消息映射到固定的 128-bit(16 字节)哈希值。核心思想是:对分块消息做多轮的简单算术与位运算(加法、与 / 或 / 非、异或、循环左移),并把每块的结果累加到内部状态。设计目标是快速且高度混淆(混合输入位使输出看似随机)。

处理流程(逐步)

  1. 预处理(Padding & Length)
    • 把消息按字节看成比特流,先加一个 1 位(即 0x80),再补若干 0 位,直到消息长度(比特)≡ 448 (mod 512)。
    • 最后附上原始消息长度(以 64 位小端整数表示)。
    • 结果长度是 512 的倍数(即若干个 512-bit 块)。
  2. 初始化 128-bit 状态(四个 32-bit 寄存器) A = 0x67452301 B = 0xEFCDAB89 C = 0x98BADCFE D = 0x10325476 这些是固定的初始常数(小端语义)。
  3. 分块处理(每个 512-bit 块)
    • 把块分成 16 个 32-bit 小端字 M[0..15]。
    • 设 a=A, b=B, c=C, d=D。对这 16 个字进行 64 次操作(4 轮,每轮 16 步)。每步的通用形式:a = b + leftrotate(a + f(b,c,d) + M[k] + T[i], s ) 其中:
      • f 是本轮使用的布尔函数(下文列出)。
      • M[k] 是当前步选择的消息字(k 随步数变化)。
      • T[i] 是第 i 个常数(下面说明)。
      • s 是该步的循环左移位数(固定表)。
      • 运算均在 32 位无符号整数模 2^32 下进行(加法溢出按模处理)。
    • 每轮的 f 分别为:F(X,Y,Z) = (X & Y) | (~X & Z) G(X,Y,Z) = (X & Z) | (Y & ~Z) H(X,Y,Z) = X ^ Y ^ Z I(X,Y,Z) = Y ^ (X | ~Z)
    • T[i] 常数定义为:T[i] = floor(2^32 * abs(sin(i+1)))(i 从 0 到 63,使用弧度的正弦值绝对值)。
    • 每轮有固定的 s(左移量)序列:
      • Round 1: 循环 [7,12,17,22]
      • Round 2: 循环 [5,9,14,20]
      • Round 3: 循环 [4,11,16,23]
      • Round 4: 循环 [6,10,15,21]
    • 64 步结束后,把 a,b,c,d 分别加回 A,B,C,D(模 2^32)。
  4. 输出
    处理完所有块后,输出 A || B || C || D(按小端字节序拼接),得到 128-bit 摘要,通常显示为 32 个十六进制字符。

小端与位运算注意点

  • MD5 按小端(little-endian)方式把字节组合成 32-bit 字;这影响实现和测试向量。
  • 所有加法按无符号 32 位模 (2^{32}) 进行;循环左移(rotate left)是关键的扩散手段(把低位信息带到高位,并保留位数)。

复杂度与性质

  • 时间复杂度:处理消息长度为 n 时为 O(n)(线性),因为每个 512-bit 块做固定次数的操作。
  • 摘要长度固定 128 bit。对理想哈希函数,碰撞复杂度 ~ (2^{64}),预映像复杂度 ~ (2^{128})。
  • 但 MD5 并非理想:碰撞抗性已被破解(见下文)。

安全性

  • 自 2004 年起,研究者(如 Wang 等)提出了 MD5 碰撞攻击方法,实践上能在远低于 (2^{64}) 的成本下构造碰撞(即找出两个不同消息有相同 MD5)。随后实务上出现了实际滥用(伪造证书、签名等)。
  • 结论:不再适合任何安全敏感用途(数字签名、证书、代码签名等禁用)。
  • 对于存密码,应使用带盐和成本因子的算法(例如 bcrypt、scrypt、argon2)。对于通用哈希完整性检查,使用 SHA-256 或更强(SHA-3、BLAKE2 等)。

一个简化的伪代码

MD5(message):
  msg = preprocess(message)      # padding, append length (64-bit little-endian)
  A,B,C,D = init_constants()
  for each 512-bit chunk in msg:
    M[0..15] = split_chunk_little_endian(chunk)
    a,b,c,d = A,B,C,D
    for i in 0..63:
      if 0 <= i <= 15:
        f = F(b,c,d); k = i; s = s_table_round1[i mod 4]
      elif 16 <= i <= 31:
        f = G(b,c,d); k = (5*i + 1) mod 16; s = s_table_round2[i mod 4]
      elif 32 <= i <= 47:
        f = H(b,c,d); k = (3*i + 5) mod 16; s = s_table_round3[i mod 4]
      else:
        f = I(b,c,d); k = (7*i) mod 16; s = s_table_round4[i mod 4]
      temp = (a + f + M[k] + T[i]) mod 2^32
      a = b + leftrotate(temp, s) mod 2^32
      # rotate a,b,c,d for next step
      a,b,c,d = d,a,b,c
    A = (A + a) mod 2^32   # 累加回全局状态
    B = (B + b) mod 2^32
    C = (C + c) mod 2^32
    D = (D + d) mod 2^32
  return little_endian_bytes(A) || little_endian_bytes(B) || ...

直观理解

想像把消息切成等长片,送入一个四室搅拌机。每片消息在 64 个小步骤里通过位运算、加法和循环移位被“搅拌”进四个寄存器。每处理一片,搅拌机的当前状态又与整体状态相加,进入下一片。若输入有微小差异,经过多轮旋转与异或,输出应产生大幅变化(理想情况下)。

案例:计算 MD5,或者用字典或可配置的暴力穷举来“尝试破解”目标 MD5。

以下是脚本文件:

#!/usr/bin/env python3
"""
md5_tool.py
功能:- 计算字符串的 MD5
 - 使用字典文件尝试破解 MD5
 - 使用简单暴力穷举(指定字符集与最大长度)尝试破解 MD5

注意:MD5 是单向哈希。真正强密码通常无法被破解。此脚本仅用于学习 / 合法测试。python
import hashlib
import argparse
import itertools
import sys
import time

def md5_hash(s: str) -> str:
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def check_md5(candidate: str, target_hash: str) -> bool:
    return md5_hash(candidate) == target_hash.lower()

def dict_attack(target_hash: str, dict_path: str, encoding='utf-8'):
    """逐行读取字典文件,寻找匹配的原文"""
    target_hash = target_hash.lower()
    with open(dict_path, 'r', encoding=encoding, errors='ignore') as f:
        for lineno, line in enumerate(f, 1):
            word = line.rstrip('\n\r')
            if not word:
                continue
            if md5_hash(word) == target_hash:
                return word, lineno
    return None, None

def brute_force_attack(target_hash: str, charset: str, max_len: int, show_progress=True):
    """用给定字符集做逐长度穷举(注意:复杂度随 max_len 指数增长)"""
    target_hash = target_hash.lower()
    start = time.time()
    tried = 0
    for length in range(1, max_len + 1):
        if show_progress:
            print(f"尝试长度 {length} ...", flush=True)
        # itertools.product 生成笛卡尔积(等于所有可能的长度为 length 的字符串)for tup in itertools.product(charset, repeat=length):
            tried += 1
            cand = ''.join(tup)
            if md5_hash(cand) == target_hash:
                elapsed = time.time() - start
                return cand, length, tried, elapsed
            # 少量输出避免太慢:每 200000 次打印一次
            if show_progress and (tried % 200000 == 0):
                elapsed = time.time() - start
                print(f"已尝试 {tried} 个候选,耗时 {elapsed:.1f}s", flush=True)
    return None, None, tried, time.time() - start

def main():
    parser = argparse.ArgumentParser(description="MD5 生成与(字典 / 暴力)尝试还原工具")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--hash', '-H', help="计算给定文本的 MD5(搭配 --text)")
    group.add_argument('--crack', '-c', help="要破解 / 匹配的目标 MD5(hex)")

    parser.add_argument('--text', '-t', help="与 --hash 一起使用:要计算 MD5 的文本")
    parser.add_argument('--dict', '-d', help="字典文件路径:逐行尝试匹配 MD5")
    parser.add_argument('--bruteforce', '-b', action='store_true', help="启用暴力穷举(需指定 --charset 和 --maxlen)")
    parser.add_argument('--charset', default='abcdefghijklmnopqrstuvwxyz0123456789', help="暴力穷举字符集(默认:小写字母 + 数字)")
    parser.add_argument('--maxlen', type=int, default=4, help="暴力穷举最大长度(默认 4)")
    parser.add_argument('--no-progress', action='store_true', help="禁止打印长进度信息(暴力模式)")
    args = parser.parse_args()

    if args.hash:
        if args.text is None:
            print("使用 --hash 时请同时提供 --text 要计算的文本。", file=sys.stderr)
            sys.exit(2)
        print(f"文本: {args.text}")
        print(f"MD5 : {md5_hash(args.text)}")
        return

    # 下面处理破解流程
    target = args.crack.lower()
    if len(target) != 32:
        print("目标 MD5 长度应为 32 个 hex 字符。", file=sys.stderr)
        sys.exit(2)

    # 优先字典攻击(如果提供)if args.dict:
        print(f"开始字典攻击,字典文件:{args.dict}")
        start = time.time()
        found, lineno = dict_attack(target, args.dict)
        elapsed = time.time() - start
        if found:
            print(f"匹配成功!原文 ='{found}'(字典第 {lineno} 行),耗时 {elapsed:.2f}s")
            return
        else:
            print(f"字典未命中(耗时 {elapsed:.2f}s)。")

    # 如果指定暴力
    if args.bruteforce:
        print(f"开始暴力穷举(charset 长度 ={len(args.charset)}, maxlen={args.maxlen})")
        cand, length, tried, elapsed = brute_force_attack(target, args.charset, args.maxlen, show_progress=not args.no_progress)
        if cand:
            print(f"匹配成功!原文 ='{cand}'(长度 {length}),尝试次数 {tried},耗时 {elapsed:.2f}s")
            return
        else:
            print(f"暴力穷举未命中(尝试 {tried} 个候选,耗时 {elapsed:.2f}s)。请增大 charset 或 maxlen 或使用字典。")
            return

    print("未指定破解方法:既未提供字典也未启用 --bruteforce。请使用 --dict 或 --bruteforce。", file=sys.stderr)
    sys.exit(2)

if __name__ == '__main__':
    main()

使用示例

  1. 计算 MD5:
python md5_tool.py --hash --text "password123"
# 输出示例:# 文本: password123
# MD5 : 482c811da5d5b4bc6d497ffa98491e38
  1. 字典攻击:
python md5_tool.py --crack 482c811da5d5b4bc6d497ffa98491e38 --dict ./common_passwords.txt
  1. 暴力穷举(小写字母 + 数字,最大长度 5):
python md5_tool.py --crack < 目标 md5> --bruteforce --charset abcdefghijklmnopqrstuvwxyz0123456789 --maxlen 5

总结:

  • MD5 不安全:不要把它用于保护密码或关键数据。现代应用应使用 bcrypt, scrypt, argon2 等带盐(salt)和成本因子的方案。
  • 时间成本:暴力穷举复杂度是 |charset|^length,很快变得不可行。字典攻击在真实世界更实用(如果密码是常见词)。
  • 合法性:只对你有权测试的哈希 / 系统使用本脚本。未经授权的破解是违法的。
  • 优化方向:可并行化、使用 GPU、或引入彩虹表、预计算表或更智能的规则(变体、大小写替换、数字后缀等)来提高命中率。

正文完
 0
一诺
版权声明:本站原创文章,由 一诺 于2025-09-29发表,共计5963字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码