序列密码算法RC4的实现与攻击

RC4的实现

RC4的整个过程可以清晰地分为两个阶段:
密钥调度算法:使用密钥混淆一个内部状态数组(S盒),为生成密钥流做准备
伪随机生成算法:在KSA基础上,不断地生成伪随机的密钥流字节

密钥调度算法

KSA是初始化步骤。接收一个长度可变的密钥(1-256byte),来打乱一个256字节的状态向量 S

from typing import List, Generator

def ksa(key: bytes) -> List[int]:
    """Key-Scheduling Algorithm (KSA)
    返回初始化并经过 KSA 的 S 数组(长度 256)
    """
    key_len = len(key)
    # 1. 初始化 S 数组
    s = list(range(256))
    j = 0
    # 2. 用密钥打乱 S
    for i in range(256):
        j = (j + s[i] + key[i % key_len]) & 0xFF
        s[i], s[j] = s[j], s[i]
    return s

伪随机生成算法

PRGA接收KSA处理过的 S 数组,产生密钥流字节

def prga(s: List[int]) -> Generator[int, None, None]:
    """Pseudo-Random Generation Algorithm (PRGA)
    输入已经运行过 KSA 的 S 数组,返回一个生成器,按需产生字节(0-255)
    """
    # 复制 S 以保证纯函数行为
    s = s.copy()
    i = 0
    j = 0
    while True:
        i = (i + 1) & 0xFF
        j = (j + s[i]) & 0xFF
        s[i], s[j] = s[j], s[i]
        t = (s[i] + s[j]) & 0xFF
        yield s[t]

接口

def keystream(key: bytes, n: int) -> bytes:
    """返回给定 `key` 下的前 `n` 个 RC4 keystream 字节"""
    s = ksa(key)
    g = prga(s)
    return bytes(next(g) for _ in range(n))

def encrypt(key: bytes, plaintext: bytes) -> bytes:
    """使用 RC4 对 `plaintext` 进行加密/解密(异或)并返回 ciphertext
    RC4 是对称流密码,加密和解密相同
    """
    ks = keystream(key, len(plaintext))
    return bytes(p ^ k for p, k in zip(plaintext, ks))

def hexd(b: bytes) -> str:
    return b.hex()

keystream:先用 ksa 准备好 S 数组,然后用 prga 创建一个生成器,最后通过 next(g) 调用 n 次生成器,收集前 n 个字节并打包成 bytes 对象
encrypt:先生成与明文等长的密钥流,然后使用一个生成器表达式将明文和密钥流的每个字节逐个进行异或,最终返回密文

调用演示

key = b"key"
pt = b"Hello, world!"
c = encrypt(key, pt)
m = encrypt(key, ct)

对RC4的攻击

对密钥重用的攻击

如果使用同一个密钥 K 加密两个不同的明文 P1 和 P2,会得到两个密文 C1 和 C2
C1 = P1 ⊕ RC4(K)
C2 = P2 ⊕ RC4(K)
攻击者截获 C1 和 C2 后,可以将它们进行异或:
C1 ⊕ C2 = (P1 ⊕ RC4(K)) ⊕ (P2 ⊕ RC4(K)) = P1 ⊕ P2
密钥流 RC4(K) 被完全消除,攻击者现在得到了 P1 ⊕ P2

def attack_key_reuse_demo():
    # 场景设定
    reused_key = b'HardcodedPassword'
    # 攻击者可能已知的消息 (例如,一个标准协议的问候语)
    known_plaintext1 = b"client hello"
    # 用户想要保密的真实消息
    secret_plaintext2 = b"secret data"
    print(f"重用的密钥: {reused_key.decode()}")
    print(f"已知明文1: {known_plaintext1.decode()}")
    print(f"秘密明文2: {secret_plaintext2.decode()}\n")

    ciphertext1 = encrypt(reused_key, known_plaintext1)
    ciphertext2 = encrypt(reused_key, secret_plaintext2)
    print("服务器发送了两个密文...")
    print(f"密文1: {hexd(ciphertext1)}")
    print(f"密文2: {hexd(ciphertext2)}\n")

    # 攻击者 Eve 的操作
    # 截获 C1 和 C2,并计算它们的异或
    c1_xor_c2 = bytes(a ^ b for a, b in zip(ciphertext1, ciphertext2))
    print(f"计算 C1 ⊕ C2: {hexd(c1_xor_c2)}\n")

    # 攻击者知道 P1,现在可以恢复 P2
    print(f"已知 P1: {known_plaintext1.decode()}")
    recovered_plaintext2 = bytes(a ^ b for a, b in zip(c1_xor_c2, known_plaintext1))
    print(f"恢复的秘密 P2: {recovered_plaintext2.decode()}\n")

对密钥流偏向攻击

研究发现,RC4生成的密钥流并非完全随机,某些字节或字节组合出现的概率会偏离理论值
原理
RC4的密钥流存在多种已知的偏向,其中最著名的是:
第二字节偏向:密钥流的第二个字节是 0x00 的概率约为 1/128,而不是期望的 1/256
初始字节偏向:密钥流的前几个字节(例如前256字节)存在多种统计偏差

def attack_keystream_bias_demo():
    """
    演示RC4密钥流偏斜攻击
    """
    import collections
    import secrets

    # 场景:WEP 风格 key = IV(3 bytes) || secret(1 byte)
    secret = secrets.token_bytes(1)
    print(f"真实 secret (1 byte): 0x{secret.hex()}\n")

    # 捕获若干个使用不同 IV 的第一个 keystream 字节(明文假设为 0x00,密文即 keystream)
    num_captures = 2000
    captures = []  # list of (iv, observed_keystream_byte)
    for _ in range(num_captures):
        iv = secrets.token_bytes(3)
        ks0 = keystream(iv + secret, 1)[0]
        captures.append((iv, ks0))

    # 先展示观察到的分布
    freq = collections.Counter(b for (_, b) in captures)
    print(f"捕获数量: {num_captures}")
    print("观察到的前 10 个最常见 keystream 字节:")
    for byte, c in freq.most_common(10):
        print(f"  0x{byte:02x}: {c} ({c/num_captures:.4f})")

    # 方法 A:严格穷举,要求候选与所有捕获逐一匹配
    def brute_force_recover(captures):
        for candidate in range(256):
            cand = bytes([candidate])
            ok = True
            for iv, observed in captures:
                if keystream(iv + cand, 1)[0] != observed:
                    ok = False
                    break
            if ok:
                return cand
        return None

    recovered = brute_force_recover(captures[:12])  
	# 用12个包尝试严格匹配
    if recovered is not None:
        print(f"恢复到 secret: 0x{recovered.hex()}")
    else:
        print("未找到完全匹配的候选(样本不足或非完全匹配场景)")

    # 方法 B:统计评分(更稳健)——对每个候选统计与观察值相等的次数,得分最高者为猜测
    scores = []
    for candidate in range(256):
        cand = bytes([candidate])
        score = 0
        # 只使用部分样本
        for iv, observed in captures[:500]:
            if keystream(iv + cand, 1)[0] == observed:
                score += 1
        scores.append((candidate, score))

    scores.sort(key=lambda x: x[1], reverse=True)
    print("\n统计评分(前 5 个候选):")
    for cand, sc in scores[:5]:
        print(f"  candidate=0x{cand:02x} score={sc}")

    best = scores[0][0]
    print(f"\n统计评分猜测的 secret: 0x{best:02x} (真实: 0x{secret.hex()})")
posted @ 2025-11-15 16:09  lumiere_cloud  阅读(3)  评论(0)    收藏  举报