1. 什么是“围栏”
摘要:本文从“围栏”这一抽象概念出发,说明栅栏密码的本质是“写入顺序 ≠ 读出顺序”。围绕围栏形状与读出方向两个特质,分别讲解普通栅栏(按行写入、按列读出)与 W 型栅栏(之字形写入、按行读出)的加密原理与解密思路,并用 Markdown 表格从六个维度对比两者。随后给出自动尝试所有栏数、用英文评分排序的完整 Python 解码脚本与运行示例,最后说明栅栏密码属于古典密码中的置换密码,只换位置不换字符,密钥空间小,仅适合教学与 CTF 入门。
在讲栅栏密码之前,先理解“围栏”这个概念本身。
想象你手里有一串字符,需要把它们按某种规则“摆”到一个看不见的网格里——这个网格就是围栏。围栏决定了字符的写入位置,而解密要做的,就是根据同样的围栏规则,把字符按另一种顺序读出来。
所以栅栏密码的本质可以概括为一句话:
写入顺序 ≠ 读出顺序。
只要写入和读出用的是两套不同的遍历方式,信息就被“打乱”了;而只要知道围栏的形状(栏数、走向),就能把顺序还原回来。
在围栏这个框架下,有两个关键特质决定了密码的具体形态:
围绕这两个特质,就衍生出两种最常见的栅栏密码:普通栅栏(简单栅栏) 和 W 型栅栏(之字形栅栏)。下面分别说明它们的原理与破解思路。
2. 特质一:普通栅栏(简单栅栏)
2.1 加密原理
普通栅栏的围栏是一个规整的矩形:把明文按行依次写入,然后按列依次读出。
以明文 HELLOWORLD、栏数 3 为例,按行写入:
H E L L
O W O R
L D
按列读出(从上到下、从左到右):
H O L | E W D | L O | L R
得到密文 HOLEWDLOLR。
2.2 解密思路
解密是加密的逆过程,难点在于:我们只知道密文和栏数,不知道每一行原本有多少个字符。
关键观察是:总长度 L 除以栏数 rails,商 base 是每行的基础长度,余数 extra 表示前 extra 行会多出一个字符。于是每行的长度可以确定:
base = L // rails
extra = L % rails
row_lens = [base + 1 if i < extra else base for i in range(rails)]
有了每行长度,就能反推出每一列有多少个字符(某一列的长度 = 该列上所有行中“这一行还有字符”的数量):
col_lens = []
for j in range(max(row_lens)):
col_lens.append(sum(1 for rl in row_lens if rl > j))
接着把密文按列长度切分,还原出每一列的字符:
cols = []
idx = 0
for cl in col_lens:
cols.append(list(cipher[idx:idx + cl]))
idx += cl
最后按“行优先”的顺序,从各列中依次取出字符,就得到了明文:
result = []
for r in range(rails):
for j in range(row_lens[r]):
result.append(cols[j][r])
return ''.join(result)
完整函数如下:
def rail_fence_decrypt(cipher, rails):
"""简单栅栏解密"""
L = len(cipher)
if rails <= 1 or rails >= L:
return cipher
base = L // rails
extra = L % rails
row_lens = [base + 1 if i < extra else base for i in range(rails)]
col_lens = []
for j in range(max(row_lens)):
col_lens.append(sum(1 for rl in row_lens if rl > j))
cols = []
idx = 0
for cl in col_lens:
cols.append(list(cipher[idx:idx + cl]))
idx += cl
result = []
for r in range(rails):
for j in range(row_lens[r]):
result.append(cols[j][r])
return ''.join(result)
核心思想:先根据长度关系“重建围栏的形状”,再把密文填回围栏,最后按写入顺序读出来。
3. 特质二:W 型栅栏(之字形栅栏)
3.1 加密原理
W 型栅栏的围栏不再是矩形,而是一条上下起伏的折线:字符沿着 0 → 1 → 2 → … → rails-1 → rails-2 → … → 0 的轨迹写入,像画一个“W”(或连续的锯齿)。
以明文 HELLOWORLD、栏数 3 为例,写入轨迹是:
H . . . O . . . L .
. E . L . W . R . D
. . L . . . O . . .
按行读出:第 0 行 HOL,第 1 行 ELWRD,第 2 行 LO,得到密文 HOLELWRDLO。
3.2 解密思路
W 型栅栏的解密比普通栅栏更“聪明”一些,因为它不需要猜每行长度——轨迹是确定的。
第一步,先根据长度和栏数,把每个位置属于哪一行算出来(这就是写入轨迹):
pattern = []
r = 0
direction = 1
for _ in range(L):
pattern.append(r)
if r == 0:
direction = 1
elif r == rails – 1:
direction = –1
r += direction
第二步,统计每一行有多少个字符,然后按行把密文切分回去:
row_counts = [0] * rails
for r in pattern:
row_counts[r] += 1
rows = []
idx = 0
for rc in row_counts:
rows.append(list(cipher[idx:idx + rc]))
idx += rc
第三步,沿着同一条轨迹,从对应的行里依次取字符,就还原出明文:
result = []
row_idx = [0] * rails
for r in pattern:
result.append(rows[r][row_idx[r]])
row_idx[r] += 1
return ''.join(result)
完整函数如下:
def zigzag_rail_fence_decrypt(cipher, rails):
"""W 型栅栏解密"""
L = len(cipher)
if rails <= 1 or rails >= L:
return cipher
pattern = []
r = 0
direction = 1
for _ in range(L):
pattern.append(r)
if r == 0:
direction = 1
elif r == rails – 1:
direction = –1
r += direction
row_counts = [0] * rails
for r in pattern:
row_counts[r] += 1
rows = []
idx = 0
for rc in row_counts:
rows.append(list(cipher[idx:idx + rc]))
idx += rc
result = []
row_idx = [0] * rails
for r in pattern:
result.append(rows[r][row_idx[r]])
row_idx[r] += 1
return ''.join(result)
核心思想:轨迹是公开的,所以只要知道栏数,就能精确还原每个字符的归属行,再按轨迹回填即可。
3.3 两种栅栏对比
把两种栅栏放在一起看,差异其实都源于“围栏形状”这一个源头:
| 围栏形状 | 规整矩形,行数固定为栏数 | 上下起伏的折线,形如连续的“W” |
| 写入轨迹 | 按行从左到右依次填充,填满一行换下一行 | 沿 0 → 1 → … → rails-1 → rails-2 → … → 0 往返移动 |
| 读出方式 | 按列从上到下、从左到右读出 | 按行读出,每行内部保持写入时的先后顺序 |
| 解密关键 | 先由总长度和栏数推出每行、每列的长度,重建矩形再回填 | 先由长度和栏数生成确定的轨迹 pattern,统计各行字符数后按轨迹回填 |
| 时间复杂度 | O(L),需要额外计算行列长度 | O(L),需要额外生成轨迹数组 |
| 典型密文特征 | 末行可能不满,密文尾部字符来自较短的列 | 首尾行字符数明显少于中间行,密文中段更“密集” |
选择建议:如果题目只说是“栅栏密码”、没有额外提示,优先尝试普通栅栏,因为它是教材和入门题里最常见的默认形态;如果普通栅栏各栏数都解不出可读文本,或者密文长度与栏数组合下首尾行字符数明显偏少,就应重点尝试 W 型栅栏。实际做题时最稳妥的做法仍是两种都跑一遍,再用英文评分排序挑出最像人话的结果。
4. 自动尝试所有栏数:用英文评分排序
实际做题时,我们往往不知道栏数,也不知道是普通栅栏还是 W 型栅栏。最直接的办法是:把所有可能的栏数、两种类型全部试一遍,然后按“像不像正常英文”来排序。
4.1 英文评分函数
评分从三个维度累加:
- 常见单词命中:the、and、flag、ctf 等词出现一次加 10 分;
- 空格数量:正常英文有空格,空格越多越可能是明文;
- 字母频率:按英文字母出现频率表累加,越接近自然语言得分越高。
def score_english(text):
common = [
'the', 'and', 'that', 'have', 'for', 'not', 'with', 'you',
'this', 'but', 'his', 'from', 'they', 'say', 'her', 'she',
'will', 'one', 'all', 'would', 'there', 'their', 'what',
'out', 'about', 'who', 'get', 'which', 'when', 'make',
'can', 'like', 'time', 'just', 'him', 'know', 'take',
'people', 'into', 'year', 'your', 'good', 'some', 'could',
'them', 'see', 'other', 'than', 'then', 'now', 'look',
'only', 'come', 'its', 'over', 'think', 'also', 'back',
'after', 'use', 'two', 'how', 'our', 'work', 'first',
'well', 'way', 'even', 'new', 'want', 'because', 'any',
'these', 'give', 'day', 'most', 'us', 'flag', 'ctf'
]
freq = {
'a': 8.17, 'b': 1.49, 'c': 2.78, 'd': 4.25, 'e': 12.70,
'f': 2.23, 'g': 2.02, 'h': 6.09, 'i': 6.97, 'j': 0.15,
'k': 0.77, 'l': 4.03, 'm': 2.41, 'n': 6.75, 'o': 7.51,
'p': 1.93, 'q': 0.10, 'r': 5.99, 's': 6.33, 't': 9.06,
'u': 2.76, 'v': 0.98, 'w': 2.36, 'x': 0.15, 'y': 1.97,
'z': 0.07
}
s = text.lower()
score = 0.0
padded = ' ' + s + ' '
for w in common:
score += padded.count(' ' + w + ' ') * 10
score += s.count(' ')
for ch in s:
if 'a' <= ch <= 'z':
score += freq.get(ch, 0)
return score
4.2 主程序:遍历 + 去重 + 排序
主程序先读入密文,再对 2 ~ max_rails 的每个栏数分别跑两种解密,收集结果后去重、按评分降序输出前 15 条:
def main():
cipher = input("请输入栅栏密码密文: ").strip()
if not cipher:
print("密文不能为空")
return
L = len(cipher)
if L < 4:
print("密文太短,无法尝试栅栏")
return
max_rails_str = input(f"请输入最大栏数(直接回车默认 {L // 2}): ").strip()
if max_rails_str:
try:
max_rails = int(max_rails_str)
except ValueError:
print("栏数必须是整数")
return
else:
max_rails = L // 2
max_rails = min(max_rails, L – 1)
if max_rails < 2:
print("最大栏数必须至少为 2")
return
results = []
for rails in range(2, max_rails + 1):
plain1 = rail_fence_decrypt(cipher, rails)
plain2 = zigzag_rail_fence_decrypt(cipher, rails)
results.append(('简单栅栏', rails, plain1, score_english(plain1)))
results.append(('W型栅栏', rails, plain2, score_english(plain2)))
seen = set()
unique = []
for typ, rails, plain, score in results:
if plain in seen:
continue
seen.add(plain)
unique.append((typ, rails, plain, score))
unique.sort(key=lambda x: –x[3])
print("\\n===== 解码结果(按评分排序) =====")
top_n = 15
for typ, rails, plain, score in unique[:top_n]:
print(f"[{typ} 栏数={rails}] 评分={score:.1f}")
print(f" {plain}")
if len(unique) > top_n:
print(f"\\n… 还有 {len(unique) – top_n} 个结果未显示")
if __name__ == "__main__":
main()
4.3 运行示例
请输入栅栏密码密文: HOLEWDLOLR
请输入最大栏数(直接回车默认 5):
===== 解码结果(按评分排序) =====
[简单栅栏 栏数=3] 评分=68.4
HELLOWORLD
[W型栅栏 栏数=3] 评分=52.1
HLOWOLDLER
…
评分最高的那条,通常就是正确答案。
5. 栅栏密码在古典密码中的位置
栅栏密码属于古典密码中的置换密码(Transposition Cipher) 一类。
古典密码大致可以分为两大分支:
- 替换密码(Substitution):字符本身被换成别的字符,比如凯撒密码、维吉尼亚密码、仿射密码;
- 置换密码(Transposition):字符本身不变,只是改变排列顺序,比如栅栏密码、列置换密码、矩阵转置。
栅栏密码的典型特征正是“只换位置、不换字符”——密文和明文的字符集合完全一致,只是顺序被打乱了。这也意味着:
- 它不改变字母频率分布,所以单靠频率分析无法破解,必须结合结构(栏数、轨迹)来还原;
- 它的密钥空间很小(本质上只有栏数一个参数),现代计算条件下暴力枚举所有栏数几乎瞬间完成,这也是为什么它只能作为教学和 CTF 入门题出现,而不能用于真实加密。
理解了“围栏”这个抽象概念,再看普通栅栏和 W 型栅栏,就会发现它们只是同一思想下的两种围栏形状;而自动解码脚本,本质上就是把“所有可能的围栏形状都试一遍,再用语言模型打分挑出最像人话的那个”。
网硕互联帮助中心





评论前必须登录!
注册