AI摘要

本文介绍利用已知密码片段(前缀/后缀)爆破加密 ZIP/RAR 压缩包的方法。内容涵盖暴力枚举原理、Python 单/多线程实现、字符集优化及 hashcat 工具加速,适用于 CTF 密码破解场景。

概述

CTF MISC / Crypto 题目中常见一类题型:给出一个加密的 ZIP/RAR 压缩包,并提示密码的一部分(如前缀、后缀或已知片段),要求爆破剩余未知位得到完整密码。

全空间爆破 $62^6 \approx 5.6 \times 10^{10}$ 不可行,但已知部分后剩余空间缩小到 $62^n$($n$ 为未知位数),枚举即可。
已知密码格式: ? ? ? ? LiHua
                ↑   ↑
            4个未知字符 (62种可能/位)
            总尝试次数: 62^4 ≈ 1.48 × 10^7

暴力枚举原理

搜索空间计算

未知位数字符集大小尝试次数单线程耗时估算
2623,844< 1 秒
362238,328~2 秒
46214,776,336~2 分钟
562916,132,832~2 小时
662$5.68 \times 10^{10}$不可行
瓶颈不在 CPU 而在 I/O:每尝试一次密码都需要打开压缩包 → 文件 I/O 是主要耗时。

攻击链路

已知部分密码: "LiHua" (后缀)
未知位: 前 4 位
        │
        ▼
┌──────────────────────┐
│  生成候选密码         │
│  itertools.product   │
│  '????' + 'LiHua'   │
└──────────┬───────────┘
           │ 逐条生成
           ▼
┌──────────────────────┐
│  尝试解压             │
│  zipfile.extractall  │
│  / rarfile.extract   │
└──────────┬───────────┘
           ├─ 密码错误 → 下一条
           │  (无异常或 RuntimeError)
           │
           ▼ 解压成功!
┌──────────────────────┐
│  输出正确密码         │
│  文件成功提取         │
└──────────────────────┘

Python 实现

原始脚本分析(MD5 匹配法)

import string
from hashlib import md5

a = string.ascii_letters + string.digits    # 62 个字符: a-z, A-Z, 0-9

for a1 in a:
    for a2 in a:
        for a3 in a:
            for a4 in a:
                pwd = a1 + a2 + a3 + a4 + 'LiHua'
                h = md5(pwd.encode('ascii')).hexdigest()
                if '1a4fb3fb5ee' in h:
                    print(pwd)
                    break

脚本解读:

说明
string.ascii_letters + string.digits字符集 = 52 个字母 + 10 个数字 = 62
四层嵌套for枚举所有 4 位组合,时间复杂度$O(62^4)$
md5(...).hexdigest()对候选密码做 MD5 哈希
'1a4fb3fb5ee' in h比对部分哈希值(CTF 常只给出哈希片段)
break仅跳出最内层循环(Python 的 break 不穿透多层)

此方法的适用场景: 题目只给了密码的 MD5 部分值(而非加密压缩包),只需匹配哈希片段即可。

完整版:ZIP 密码爆破

import zipfile
import itertools
import string

def crack_zip(zip_path: str, known_suffix: str, unknown_len: int,
              charset: str = None, output_dir: str = '.') -> str:
    """
    已知密码后缀,爆破 ZIP 文件的前缀部分

    参数:
        zip_path:    加密 ZIP 文件路径
        known_suffix:  已知的密码后缀(如 'LiHua')
        unknown_len:   未知前缀的位数
        charset:       字符集(默认 a-z, A-Z, 0-9)
        output_dir:    解压输出目录

    返回:
        完整密码,或 None
    """
    if charset is None:
        charset = string.ascii_letters + string.digits  # 62 字符

    total = len(charset) ** unknown_len
    print(f'字符集大小: {len(charset)}, 未知位: {unknown_len}')
    print(f'总尝试次数: {total:,}')

    with zipfile.ZipFile(zip_path, 'r') as zf:
        for idx, prefix in enumerate(
            itertools.product(charset, repeat=unknown_len)
        ):
            pwd = ''.join(prefix) + known_suffix

            if idx % 50000 == 0:
                progress = idx / total * 100
                print(f'[{progress:.2f}%] 尝试中... 当前: {pwd}', end='\r')

            try:
                zf.extractall(path=output_dir, pwd=pwd.encode())
                print(f'\n[成功] 密码: {pwd}  (第 {idx:,} 次尝试)')
                return pwd
            except (RuntimeError, zipfile.BadZipFile):
                continue   # 密码错误,继续

    print(f'\n[失败] 遍历全部 {total:,} 个组合未找到密码')
    return None


# 使用示例
crack_zip('flag.zip', known_suffix='LiHua', unknown_len=4)

多线程加速版

单线程瓶颈在 ZIP 解压 I/O,多线程可并行尝试不同前缀:

import zipfile
import itertools
import string
from concurrent.futures import ThreadPoolExecutor, as_completed

def try_password(zip_path, pwd, output_dir):
    """尝试单个密码,成功返回密码,失败返回 None"""
    try:
        with zipfile.ZipFile(zip_path, 'r') as zf:
            zf.extractall(path=output_dir, pwd=pwd.encode())
        return pwd
    except (RuntimeError, zipfile.BadZipFile):
        return None

def crack_zip_mt(zip_path, known_suffix, unknown_len,
                 charset=None, workers=8):
    if charset is None:
        charset = string.ascii_letters + string.digits

    total = len(charset) ** unknown_len
    print(f'总尝试: {total:,} | 线程: {workers}')

    # 预生成所有候选密码
    passwords = (
        ''.join(p) + known_suffix
        for p in itertools.product(charset, repeat=unknown_len)
    )

    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {
            executor.submit(try_password, zip_path, pwd, '.'): pwd
            for pwd in itertools.islice(passwords, 10000)  # 批次提交
        }
        for future in as_completed(futures):
            result = future.result()
            if result:
                print(f'[成功] 密码: {result}')
                executor.shutdown(wait=False, cancel_futures=True)
                return result
    return None

RAR 文件爆破

import rarfile
import itertools
import string

def crack_rar(rar_path: str, known_part: str, unknown_len: int,
              position: str = 'prefix') -> str:
    """
    RAR 密码爆破 (需要系统安装 unrar)

    position: 'prefix' (未知在前) / 'suffix' (未知在后)
    """
    charset = string.ascii_letters + string.digits

    for combo in itertools.product(charset, repeat=unknown_len):
        extra = ''.join(combo)
        pwd = (extra + known_part) if position == 'prefix' \
              else (known_part + extra)

        try:
            with rarfile.RarFile(rar_path) as rf:
                rf.extractall(pwd=pwd)
            print(f'[成功] 密码: {pwd}')
            return pwd
        except rarfile.BadRarFile:
            continue

    return None

字符集优化

场景自适应

已知信息推荐字符集大小4 位枚举次数
无提示ascii_letters + digits62$1.48 \times 10^7$
仅小写字母ascii_lowercase26456,976
仅数字digits1010,000
仅小写+数字ascii_lowercase + digits36$1.68 \times 10^6$
已知密码为常见词自定义词表~10³极快
# CTF 常见字符集预设
CHARSETS = {
    'lower':        string.ascii_lowercase,                     # 26
    'upper':        string.ascii_uppercase,                     # 26
    'letters':      string.ascii_letters,                       # 52
    'digits':       string.digits,                              # 10
    'lower+digits': string.ascii_lowercase + string.digits,     # 36
    'all':          string.ascii_letters + string.digits,       # 62
    'printable':    string.printable.strip(),                   # 94
}

优化策略对比

# 原始写法:4 层嵌套 — 代码冗余,无法动态调整位数
for a1 in a:
    for a2 in a:
        for a3 in a:
            for a4 in a:
                pwd = a1 + a2 + a3 + a4 + 'LiHua'
                # ...

# 优化写法:itertools.product — 一行搞定任意位数
for prefix in itertools.product(a, repeat=4):
    pwd = ''.join(prefix) + 'LiHua'
    # ...

CTF 实战

常见题目模式

题型特征解法
已知后缀爆破提示"密码后4位是 LiHua"枚举前缀,如本文示例
已知前缀爆破提示"密码以 FLAG 开头"枚举后缀
已知结构爆破提示"密码为 字母+4位数字"先枚举字母,再枚举数字段
已知 MD5 片段给出md5(pwd)[6:16] 的部分值仅做 MD5 比对,无需解压(速度极快)
掩码爆破提示格式????LiHua????前后同时枚举,或分步还原
生日攻击提示"密码是某人生日"生成 1950~2020 所有日期格式
字典 + 规则提示"密码是英文单词+2位数字"加载字典文件 + 枚举数字后缀

MD5 片段匹配 vs 解压尝试

方法每次操作耗时适用场景
MD5 哈希比对~0.5 μs(微秒)仅给出哈希片段,无需实际压缩包
ZIP 解压尝试~5 ms(毫秒)有实际加密 ZIP,需要解密
RAR 解压尝试~20 msRAR 加密包(比 ZIP 慢 4 倍)
MD5 比对比 ZIP 解压快 10,000 倍。如果题目给了哈希片段且明确密码格式,优先用 MD5 匹配效率最高。

利用 hashcat 加速

对于更大搜索空间(如 6 位未知),Python 单线程太慢,可结合 hashcat 掩码模式:

# hashcat 掩码攻击:已知后缀 LiHua,前 4 位为小写字母
# 先提取 ZIP 的 hash
zip2john flag.zip > hash.txt

# 掩码爆破 (?l = 小写字母)
hashcat -m 13600 -a 3 hash.txt '?l?l?l?lLiHua'

# 数字 (?d) + 字母 (?l) 混合
hashcat -m 13600 -a 3 hash.txt '?l?d?l?dLiHua'
工具适用搜索空间速度(4 位未知)
Python (单线程)$< 10^7$~2 分钟
Python (8 线程)$< 10^8$~30 秒
John the Ripper$< 10^9$~10 秒
hashcat (GPU)$< 10^{12}$< 1 秒

常见掩码模式速查

# hashcat 掩码字符
?l  = abcdefghijklmnopqrstuvwxyz
?u  = ABCDEFGHIJKLMNOPQRSTUVWXYZ
?d  = 0123456789
?h  = 0123456789abcdef
?H  = 0123456789ABCDEF
?s  = !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
?a  = ?l?u?d?s    (95 字符全集)
?b  = 0x00 - 0xff

# 常见掩码示例
?a?a?a?a          # 4 位全字符 (95^4)
?d?d?d?d          # 4 位纯数字 (10^4)
?l?l?l?lLiHua     # 4 位小写 + 固定后缀
FLAG?d?d?d?d      # 固定前缀 + 4 位数字
?u?l?l?l?l?d?d    # 1大写 + 4小写 + 2数字

参考文献

  1. Python Software Foundation. itertools — Functions creating iterators for efficient looping [EB/OL]. https://docs.python.org/3/library/itertools.html
  2. Python Software Foundation. zipfile — Work with ZIP archives [EB/OL]. https://docs.python.org/3/library/zipfile.html
  3. hashcat. hashcat — Advanced Password Recovery [EB/OL]. https://hashcat.net/hashcat/
  4. Openwall. John the Ripper password cracker [EB/OL]. https://www.openwall.com/john/
  5. Python Software Foundation. hashlib — Secure hashes and message digests [EB/OL]. https://docs.python.org/3/library/hashlib.html
投币支持一下吧
END