AI摘要

栅栏密码是一种置换密码,分为W型锯齿和分组矩阵转置两种。文章阐述了其原理、Python实现及CTF中的暴力破解方法。

概述

栅栏密码(Rail Fence Cipher)是一种典型的置换密码(transposition cipher):它不改变任何字母本身,只打乱字母的位置。这与凯撒、仿射等"替换密码"(改变字母内容)以及培根密码(隐藏于形式)都属于不同的家族。

中文语境下的"栅栏密码"实际上混指两种做法,务必先分清:

  • W 型(锯齿 / Zigzag)栅栏:把明文沿"之"字形写在 N 条轨道(rail)上,再逐行读出——这是国际标准的 Rail Fence Cipher
  • 分组(栏栅)栅栏:把明文每 N 个字符切成一组,再按"列"的顺序取字重排——本质是矩阵转置,中文 CTF 工具里最常见。
本质:  密文是明文的一个「位置置换」π,字母集合完全不变
       c[i] = p[π(i)]      解密 = 逆置换 π⁻¹

W 型   明文沿 N 条轨道锯齿排列 → 逐行读     (周期 2(N-1))
分组   明文按 N 字一组填入矩阵 → 逐列读     (等价矩阵转置)

密钥   W 型: 轨道数 rails ∈ [2, L-1]
       分组: 每组字数 key(多取 L 的因子)

编码原理

置换而非替换:为什么"字母没变,却读不通"

栅栏密码的加密可以形式化为对下标集合 $\{0,1,\dots,L-1\}$ 的一个置换 $\pi$:密文第 $i$ 位等于明文第 $\pi(i)$ 位。解密只需应用逆置换 $\pi^{-1}$。因为置换是双射,解密天然唯一——这一点与仿射密码"必须 $\gcd(a,26)=1$ 才可逆"形成对照:置换密码的可逆性由结构保证,无需额外条件。

三大古典密码家族的根本差异:

家族代表操作对象字母本身单字母频率
替换凯撒 / 仿射字母的改变整体平移,分布形状保留
置换栅栏 / 列置换字母的位置不变完全不变
隐写培根字母的形式承载于载体——
这正是识别置换密码的关键指纹:密文的单字母频率和正常英文几乎一致,读起来却毫无意义。因为只是换位,E 还是那么多、T 还是那么多——频率分析能一眼看出"这是置换而非替换",但单字母频率对求解密钥毫无帮助,真正被破坏的是双字母 / N-gram 的相邻关系。

W 型栅栏:锯齿排列与周期

设轨道数为 $N$。字符在轨道间上下往返,形成周期为 $\text{cycle} = 2(N-1)$ 的三角波。第 $i$ 个字符所在的轨道号可由下式直接算出:

$$ r = i \bmod 2(N-1), \qquad \text{rail}(i) = \min(r,\ 2(N-1) - r) $$

以明文 FLAGISHERE、$N=3$($\text{cycle}=4$)为例:

       0  1  2  3  4  5  6  7  8  9      ← 明文位置
rail0: F  .  .  .  I  .  .  .  R  .
rail1: .  L  .  G  .  S  .  E  .  E
rail2: .  .  A  .  .  .  H  .  .  .

逐行读出 →  [FIR] [LGSEE] [AH]  =  FIRLGSEEAH

轨道数越多,锯齿越"深",相邻字母被拉得越开。但密钥空间仅 $N \in [2, L-1]$,即 $L-2$ 种,暴力枚举是平凡的。

分组栅栏:矩阵转置视角

分组栅栏等价于:把明文按行填进一个 列数 = key 的矩阵,再按列读出——也就是矩阵转置。以明文 WELCOMETOCTF、$key=3$ 为例:

按行填入 (每行 key=3):          按列读出:
    W  E  L                       列0: W C E C
    C  O  M            →          列1: E O T T
    E  T  O                       列2: L M O F
    C  T  F                       密文: WCECEOTTLMOF

解密时行列互换即可还原。它的密钥(每组字数 $key$)通常取明文长度 $L$ 的因子,否则末组不齐,需引入填充约定——因此分组栅栏的可用密钥比 W 型更少,破解更快


Python 完整实现

W 型栅栏加解密

def rail_fence_encrypt(text: str, rails: int) -> str:
    """W 型栅栏加密:沿 rails 条轨道锯齿排列后逐行读出"""
    if rails < 2:
        return text
    fence = [[] for _ in range(rails)]
    rail, step = 0, 1
    for ch in text:
        fence[rail].append(ch)
        if rail == 0:
            step = 1                 # 触底折返向下
        elif rail == rails - 1:
            step = -1                # 触顶折返向上
        rail += step
    return ''.join(''.join(row) for row in fence)


def rail_fence_decrypt(cipher: str, rails: int) -> str:
    """W 型栅栏解密:先复原锯齿骨架,再按列回填"""
    if rails < 2:
        return cipher
    n = len(cipher)
    # 1) 标记每个位置属于哪条轨道
    pattern, rail, step = [], 0, 1
    for _ in range(n):
        pattern.append(rail)
        if rail == 0:
            step = 1
        elif rail == rails - 1:
            step = -1
        rail += step
    # 2) 按轨道顺序把密文依次填回原位
    result, idx = [None] * n, 0
    for r in range(rails):
        for i in range(n):
            if pattern[i] == r:
                result[i] = cipher[idx]
                idx += 1
    return ''.join(result)


# 使用示例
c = rail_fence_encrypt('WEAREDISCOVEREDFLEEATONCE', 3)
print(c)                              # WECRLTEERDSOEEFEAOCAIVDEN
print(rail_fence_decrypt(c, 3))       # WEAREDISCOVEREDFLEEATONCE

分组栅栏加解密

def fence_group_encrypt(text: str, key: int) -> str:
    """分组栅栏加密:每 key 个字符一组,逐列读出(矩阵转置)"""
    groups = [text[i:i + key] for i in range(0, len(text), key)]
    out = []
    for col in range(key):
        for g in groups:
            if col < len(g):          # 兼容长度不整除时的短末组
                out.append(g[col])
    return ''.join(out)


def fence_group_decrypt(cipher: str, key: int) -> str:
    """分组栅栏解密(假设 len 可被 key 整除)"""
    n = len(cipher)
    rows = n // key                   # 组数 = 每列长度
    cols = [cipher[i * rows:(i + 1) * rows] for i in range(key)]
    out = []
    for r in range(rows):
        for c in range(key):
            out.append(cols[c][r])
    return ''.join(out)


# 使用示例
c = fence_group_encrypt('WELCOMETOCTF', 3)
print(c)                              # WCECEOTTLMOF
print(fence_group_decrypt(c, 3))      # WELCOMETOCTF

暴力破解(未知密钥)

栅栏密码的密钥空间极小,直接枚举 + flag 头判分即可:

def crack_rail_fence(cipher: str, hint: str = 'flag') -> list:
    """同时爆破 W 型(枚举轨道数)与分组型(枚举长度因子)"""
    n, results = len(cipher), []

    # W 型:轨道数 2 ~ n-1
    for rails in range(2, n):
        plain = rail_fence_decrypt(cipher, rails)
        if hint.lower() in plain.lower():
            results.append(('W型', rails, plain))

    # 分组型:每组字数取 n 的真因子
    for key in range(2, n):
        if n % key == 0:
            plain = fence_group_decrypt(cipher, key)
            if hint.lower() in plain.lower():
                results.append(('分组', key, plain))

    return results


# 示例:未知参数,自动命中含 flag 的解
cipher = rail_fence_encrypt('flagblrailfenceyes', 4)
for kind, key, plain in crack_rail_fence(cipher, hint='flag'):
    print(f'[{kind} / key={key}] {plain}')
若无 flag 头,可把判分函数换成"英文常见词命中数"或"字母 N-gram 对数似然",对每个候选打分排序,人工确认最高分即可。

CTF 实战

常见题型

题型特征解法
W 型未知栏数字母乱序、无重复规律枚举rails 2 ~ L-1
分组未知组数长度含小因子枚举 L 的因子作key
已知栏数 / 组数题面直接给 N直接解,秒出
带起始偏移锯齿不从顶部起笔额外枚举起点 / 相位
置换 + 替换套娃栅栏 + 凯撒 / Base64先还原位置再解替换,逐层剥离

解题流程

快速判定:若密文的单字母频率接近自然英文、却完全读不通,几乎可锁定为置换密码,栅栏是首选尝试。
步骤操作说明
1 判类统计字母频率分布正常但不可读 → 置换密码
2 分解对长度 L 做质因数分解决定分组型可行的key 集合
3 枚举 Wrails 从 2 试到 L-1空间仅 L-2,毫秒级
4 枚举分组key 取 L 的因子因子很少,极快
5 验证检查 flag 头 / 词频命中flag{ctf{ 即停
6 剥层若仍乱码叠加了替换 / 编码,继续解

与其他古典密码对比

密码类型密钥空间字母是否改变抗频率分析
栅栏(W型)置换$L-2$弱(单频不变,暴露置换特征)
栅栏(分组)置换$L$ 的因子数
列置换密码置换$n!$(n=列数)
凯撒密码替换25极弱
仿射密码替换312
维吉尼亚密码替换$26^n$
栅栏密码的密钥空间随明文长度线性增长($L-2$),看似不小,实则每个密钥都能瞬间验证,暴力破解毫无门槛。它的真正意义在于展示"置换"这一密码学基本操作,而非提供安全性。
投币支持一下吧
END