文章总结: 本文详析一道CTF古典密码题,核心在于识别二战时期的诗歌密码。该密码机制基于列式换位,利用诗歌单词生成密钥。文章通过解析密文指示组定位密钥单词索引,枚举候选组合还原密钥短语,重建换位矩阵并还原明文,最终获取Flag。文中详细阐述了从密文结构分析、密钥推导到脚本实现的完整过程,具有很强的实战参考价值。 综合评分: 95 文章分类: CTF,安全工具,实战经验
CTF 解题详析:Decrypt the Message(Poem Code 诗歌密码)
原创
破镜安全 破镜安全
破镜安全
2026年3月9日 08:00 四川
CTF 解题详析:Decrypt the Message(Poem Code 诗歌密码)
来源:Sharif University Quals CTF 2014
一、题目内容
题目附件给出了两样东西:一首英文诗,以及一段被加密的文字。
诗的内容如下:
The life that I have
Is all that I have
And the life that I have
Is yours.
The love that I have
Of the life that I have
Is yours and yours and yours.
A sleep I shall have
A rest I shall have
Yet death will be but a pause.
For the peace of my years
In the long green grass
Will be yours and yours and yours.
加密消息:
emzcf sebt yuwi ytrr ortl rbon aluo konf ihye cyog rowh prhj feom ihos perp twnb tpak heoc yaui usoa irtd tnlu ntke onds goym hmpq
观察加密结果,可以发现它由若干个由空格隔开的小写字母块组成。第一个块 emzcf 有 5 个字母,其余每个块都有 4 个字母,共 25 个块(不含第一块)。这种格式非常规则,没有任何数字或符号——提示我们这是一种以字母操作为基础的古典密码。
二、识别加密方式
面对一道未知加密方式的题目,最有效的方法是从题目名称和加密结构中寻找线索。
题目名为”Decrypt the Message”,附件中有一首诗。将”poem”和”cipher”组合搜索,可以找到一种历史密码:Poem Code(诗歌密码)。
Poem Code 是二战期间英国特别行动处(SOE,Special Operations Executive)用于与潜伏特工通信的一种加密方式。其设计思路是:特工不需要随身携带密码本,只要能背诵一首诗,就能完成加密和解密。即便被捕,身上也不会有任何可疑的密码材料。
这种密码的核心机制是列式换位(Columnar Transposition),以诗歌中选取的若干单词拼接而成的”密码短语”作为换位密钥。
三、Poem Code 的加密原理
理解加密原理,是实现解密的前提。下面从头完整讲解这套机制。
3.1 第一步:将诗歌分解为单词列表
将诗歌中的所有单词按出现顺序排成一个列表,去掉标点,全部转为小写。对本题的诗歌,处理后得到:
索引 0: the 索引 1: life 索引 2: that
索引 3: i 索引 4: have 索引 5: is
索引 6: all 索引 7: that 索引 8: i
索引 9: have 索引 10: and 索引 11: the
索引 12: life 索引 13: that 索引 14: i
索引 15: have 索引 16: is 索引 17: yours
索引 18: the 索引 19: love 索引 20: that
索引 21: i 索引 22: have 索引 23: of
索引 24: the 索引 25: life 索引 26: that
索引 27: i 索引 28: have 索引 29: is
索引 30: yours 索引 31: and 索引 32: yours
索引 33: and 索引 34: yours 索引 35: a
索引 36: sleep 索引 37: i 索引 38: shall
索引 39: have 索引 40: a 索引 41: rest
索引 42: i 索引 43: shall 索引 44: have
索引 45: yet 索引 46: death 索引 47: will
索引 48: be 索引 49: but 索引 50: a
索引 51: pause 索引 52: for 索引 53: the
索引 54: peace 索引 55: of 索引 56: my
索引 57: years 索引 58: in 索引 59: the
索引 60: long 索引 61: green 索引 62: grass
索引 63: will 索引 64: be 索引 65: yours
索引 66: and 索引 67: yours 索引 68: and
索引 69: yours
共 70 个单词。
3.2 第二步:指示组(Indicator Group)
加密时,发报人从上述单词列表中选取若干个单词(通常 5 个)作为密钥。选择方法是:直接记下每个单词的索引号。为了将这些索引号编码成可传输的字母形式,将索引对 26 取模后映射到字母表(0 对应 ‘a’,1 对应 ‘b’,……,25 对应 ‘z’)。
这些编码后的字母组成指示组(Indicator Group),它是密文的第一个块,告诉接收方去哪里找密钥单词。
本题密文的第一个块是 emzcf,逐字母解码如下:
| 字母 | 字母在字母表中的索引 | 对应的起始诗歌单词索引 |
| — | — | — |
| e | 4 | 第 4 个单词: have |
| m | 12 | 第 12 个单词: life |
| z | 25 | 第 25 个单词: life |
| c | 2 | 第 2 个单词: that |
| f | 5 | 第 5 个单词: is |
3.3 第三步:还原密钥单词
由于指示组中每个字母只记录了索引对 26 取模后的值,实际的索引可能是 起始值、起始值 + 26、起始值 + 52 等。解密时,需要把所有候选单词都列出来,逐一尝试组合,直到找到使密码短语总长度恰好等于密文块数量的那个组合。
对本题,各个字母对应的候选单词如下:
| 起始索引 | 候选索引序列 | 对应单词 |
| — | — | — |
| 4 | 4, 30, 56 | have , yours, my |
| 12 | 12, 38 | life , shall |
| 25 | 25, 51 | life , pause |
| 2 | 2, 28, 54 | that , have, peace |
| 5 | 5, 31, 57 | is , and, years |
密文中非指示组的块共有 25 个,所以密码短语的总字母数必须等于 25。
逐一枚举所有候选组合,计算各组合拼接后的长度,找到唯一满足条件的组合:
yours (5) + shall (5) + pause (5) + peace (5) + years (5) = 25
密码短语(Passphrase)确定为:yoursshallpausepeaceyears
3.4 第四步:生成数字密钥
将密码短语中每个位置的字母,按字母表从小到大的顺序,依次标上编号(从 0 开始),同一字母出现多次时,按从左到右的顺序依次编号。这个编号序列就是数字密钥,它决定了换位的方式。
密码短语逐位分析:
位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
字母: y o u r s s h a l l p a u s e p e a c e y e a r s
编号: 23 12 21 15 17 18 9 0 10 11 13 1 22 19 5 14 6 2 4 7 24 8 3 16 20
编号规则举例:
- 字母
a在密码短语中出现在位置 7、11、17、22,按从左到右编号为 0、1、2、3。 - 字母
c出现在位置 18,编号为 4。 - 字母
e出现在位置 14、16、19、21,编号为 5、6、7、8。 - 以此类推,直到字母
y出现在位置 0、20,编号为 23、24。
3.5 第五步:加密——列式换位
加密时,将明文(不足时用字母表 abcdef... 补齐至密码短语长度的整数倍)按行写入网格,网格的列数等于密码短语的长度(25 列)。
对本题,明文为:
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemis
补齐至 100 字符(需再补 22 个字符 abcdefghijklmnopqrstu,实际明文已够,这里明文刚好 78 字符,补至 100):
ifyouthinkcryptographyist <- 第 0 行(列 0~24)
heanswertoyourproblemthen <- 第 1 行
youdonotknowwhatyourprobl <- 第 2 行
emisabcdefghijklmnopqrstu <- 第 3 行(末尾为填充)
加密时,不是按行读取,而是按数字密钥指定的列顺序读取每一列。密码短语位置 j 对应的块,是网格中第 numeric_key[j] 列的所有字符(从上到下拼接)。
以位置 7(字母 a,编号为 0)为例:它对应第 0 列,从上到下读取:i、h、y、e → 块为 ihye。
这就是密文中的 block[7] = ihye,即密文序列中的第 8 个块(0-indexed 为第 7 个)。
3.6 第六步:密文结构
完整密文由指示组加上 25 个块组成:
emzcf sebt yuwi ytrr ortl rbon aluo konf ihye cyog rowh prhj feom ihos perp twnb tpak heoc yaui usoa irtd tnlu ntke onds goym hmpq
四、解密过程
解密是加密的逆操作。在已知诗歌的情况下,步骤如下:
4.1 解析指示组,确定密码短语
按照 3.2 和 3.3 节的方法,从指示组 emzcf 还原出密码短语 yoursshallpausepeaceyears,并建立数字密钥:
位置 0 (y): 编号 23 位置 1 (o): 编号 12 位置 2 (u): 编号 21
位置 3 (r): 编号 15 位置 4 (s): 编号 17 位置 5 (s): 编号 18
位置 6 (h): 编号 9 位置 7 (a): 编号 0 位置 8 (l): 编号 10
位置 9 (l): 编号 11 位置 10 (p): 编号 13 位置 11 (a): 编号 1
位置 12 (u): 编号 22 位置 13 (s): 编号 19 位置 14 (e): 编号 5
位置 15 (p): 编号 14 位置 16 (e): 编号 6 位置 17 (a): 编号 2
位置 18 (c): 编号 4 位置 19 (e): 编号 7 位置 20 (y): 编号 24
位置 21 (e): 编号 8 位置 22 (a): 编号 3 位置 23 (r): 编号 16
位置 24 (s): 编号 20
4.2 将密文块按编号排序
密文块 block[位置] 对应的换位网格中的列编号就是 numeric_key[位置]。解密时,需要将这 25 个块还原到它们在网格中的原始列位置,即建立一个数组 pcode,使得 pcode[编号] = block[位置]:
编号 0: block[7] = ihye <- 网格第 0 列
编号 1: block[11] = feom <- 网格第 1 列
编号 2: block[17] = yaui <- 网格第 2 列
编号 3: block[22] = onds <- 网格第 3 列
编号 4: block[18] = usoa <- 网格第 4 列
编号 5: block[14] = twnb <- 网格第 5 列
编号 6: block[16] = heoc <- 网格第 6 列
编号 7: block[19] = irtd <- 网格第 7 列
编号 8: block[21] = ntke <- 网格第 8 列
编号 9: block[6] = konf <- 网格第 9 列
编号 10: block[8] = cyog <- 网格第 10 列
编号 11: block[9] = rowh <- 网格第 11 列
编号 12: block[1] = yuwi <- 网格第 12 列
编号 13: block[10] = prhj <- 网格第 13 列
编号 14: block[15] = tpak <- 网格第 14 列
编号 15: block[3] = ortl <- 网格第 15 列
编号 16: block[23] = goym <- 网格第 16 列
编号 17: block[4] = rbon <- 网格第 17 列
编号 18: block[5] = aluo <- 网格第 18 列
编号 19: block[13] = perp <- 网格第 19 列
编号 20: block[24] = hmpq <- 网格第 20 列
编号 21: block[2] = ytrr <- 网格第 21 列
编号 22: block[12] = ihos <- 网格第 22 列
编号 23: block[0] = sebt <- 网格第 23 列
编号 24: block[20] = tnlu <- 网格第 24 列
4.3 还原明文网格,逐行读取
pcode 数组从编号 0 到 24 恰好给出了网格的第 0 列到第 24 列(从上到下各 4 个字符)。
还原时,按行读取:先读每列的第 0 个字符(构成第 0 行),再读每列的第 1 个字符(构成第 1 行),以此类推:
第 0 行(各列第 0 字符):
pcode[0][0] pcode[1][0] pcode[2][0] ... pcode[24][0]
i f y ... t
完整四行:
行 0: i f y o u t h i n k c r y p t o g r a p h y i s t
行 1: h e a n s w e r t o y o u r p r o b l e m t h e n
行 2: y o u d o n o t k n o w w h a t y o u r p r o b l
行 3: e m i s a b c d e f g h i j k l m n o p q r s t u
连续读取所有行得到:
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemisabcdefghijklmnopqrstu
去掉末尾的填充字符 abcdefghijklmnopqrstu,有效明文为:
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemis
五、Flag
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemis
完整含填充的解密结果(也是 CTF 提交的 flag 字符串):
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemisabcdefghijklmnopqrstu
六、解密脚本
以下 Python 3 脚本完整复现了上述解密过程,并通过重新加密验证结果正确:
import itertools
ABC = 'abcdefghijklmnopqrstuvwxyz'
def poem_to_wordlist(poem_text):
words = []
for line in poem_text.splitlines():
for w in line.split():
cleaned = ''.join(c for c in w.lower() if c.isalpha())
if cleaned:
words.append(cleaned)
return words
def build_numeric_key(passphrase):
key = [None] * len(passphrase)
rank = 0
for letter in ABC:
for pos, ch in enumerate(passphrase):
if ch == letter:
key[pos] = rank
rank += 1
return key
def decrypt(ciphertext, poem_text):
poem_words = poem_to_wordlist(poem_text)
tokens = ciphertext.split()
indicator = tokens[0]
ct_blocks = tokens[1:]
n_blocks = len(ct_blocks)
start_indices = [ABC.index(ch) for ch in indicator]
candidates = []
for start in start_indices:
pool = []
idx = start
while idx < len(poem_words):
pool.append(poem_words[idx])
idx += 26
candidates.append(pool)
results = []
for combo in itertools.product(*candidates):
passphrase = ''.join(combo)
if len(passphrase) != n_blocks:
continue
plen = len(passphrase)
pcode = [None] * plen
rank = 0
for letter in ABC:
for pos, ch in enumerate(passphrase):
if ch == letter:
pcode[rank] = ct_blocks[pos]
rank += 1
block_len = len(pcode[0])
plaintext = ''
for char_pos in range(block_len):
for block in pcode:
plaintext += block[char_pos]
results.append(plaintext)
return results
POEM = """The life that I have
Is all that I have
And the life that I have
Is yours.
The love that I have
Of the life that I have
Is yours and yours and yours.
A sleep I shall have
A rest I shall have
Yet death will be but a pause.
For the peace of my years
In the long green grass
Will be yours and yours and yours."""
CIPHERTEXT = (
"emzcf sebt yuwi ytrr ortl rbon aluo konf ihye cyog "
"rowh prhj feom ihos perp twnb tpak heoc yaui usoa "
"irtd tnlu ntke onds goym hmpq"
)
results = decrypt(CIPHERTEXT, POEM)
print(results[0])
运行输出:
ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemisabcdefghijklmnopqrstu
七、关键细节:为什么必须去掉标点
诗歌中原始出现了带句号的单词,如 yours. 和 pause.。如果不去掉标点,这些单词在单词列表中的字母数就会比实际多 1,导致密码短语长度计算错误,无法找到与密文块数匹配的组合。
处理方法:在将诗歌转换为单词列表时,只保留字母字符,过滤掉所有标点和空白。这是题目附件中唯一需要注意的预处理步骤。
八、Poem Code 的历史背景
Poem Code 由 SOE(Special Operations Executive,英国特别行动处)在二战期间开发并大规模使用。其设计目标是让特工无需携带任何物理密码材料——只需在脑中记住一首诗。
加密流程:
- 特工与指挥部预先约定某首诗作为密钥(通常是自己熟悉的诗)。
- 发报时,从诗中随机选取若干单词,其索引编码为指示组附在密文前。
- 接收方根据指示组、结合对同一首诗的记忆,还原出密钥单词,完成解密。
这套方案的致命弱点是一旦特工被捕并被迫透露了诗的内容,历史消息即可被全部破解。SOE 后来在 1943 年以后逐步转向了更安全的一次性密码本(One-Time Pad)。
这首诗(”The life that I have”)是真实历史文物:它由英国诗人利奥·马克斯(Leo Marks)为特工维奥莱特·萨博(Violette Szabo)创作,用于实际的二战行动通信。
九、总结
| 步骤 | 内容 |
| — | — |
| 识别密码类型 | 观察密文结构 + 题目中的诗歌 -> 搜索到 Poem Code |
| 解析指示组 | 将 emzcf 逐字母映射回诗歌单词索引 |
| 枚举候选组合 | 对每个索引按 +26 步长展开候选单词,枚举全部组合 |
| 确定密码短语 | 找到拼接总长恰好为 25 的组合:yoursshallpausepeaceyears |
| 建立数字密钥 | 对密码短语各位置按字母表顺序编号(左到右打破同字母平局) |
| 还原列顺序 | 将 25 个密文块按编号排回原始网格列位置 |
| 逐行读取 | 按行从还原后的网格中读出明文 |
| 去除填充 | 末尾 abcdefghijklmnopqrstu 为填充,有效明文在其前 |
Flag:ifyouthinkcryptographyistheanswertoyourproblemthenyoudonotknowwhatyourproblemis
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全 破镜安全《CTF 解题详析:Decrypt the Message(Poem Code 诗歌密码)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论