ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从杰斐逊圆盘到CTF密码学:古典密码原理与Python实战破解

从杰斐逊圆盘到CTF密码学:古典密码原理与Python实战破解 1. 从“托马斯.杰斐逊”到Crypto一场古典密码与现代CTF的碰撞最近在Bugku的Crypto密码学题目里看到一个名字叫“托马斯.杰斐逊”的题目挺有意思的。乍一看这名字跟密码学八竿子打不着但稍微了解点密码史的朋友就知道这背后藏着一个经典的密码装置——杰斐逊圆盘。这其实也是很多CTFCapture The Flag夺旗赛密码学题目的典型风格给你一个看似无关的线索或者一个具体的错误提示比如最近常看到的typeerror: crypto$2.getrandomvalues is not a function让你去挖掘背后的算法、协议或者历史密码的实现。今天我就结合这个题目以及处理这类Crypto题目的通用思路来聊聊怎么从零开始一步步拆解这类“名不副实”的密码题。首先得明确像Bugku这类平台上的“Game1”或者任何一道Crypto题目其核心考察点很少是让你去写一个完整的、生产级的加密系统。它更多是考察你对密码学原理的理解、对已知经典密码的识别能力以及将理论转化为代码通常是Python的实操能力。题目“托马斯.杰斐逊”就是一个绝佳的例子。它没有直接给你密文和算法描述而是给了你一个历史人名作为提示。你的第一个任务就是建立这个“提示”与“考点”之间的连接。托马斯·杰斐逊除了是美国第三任总统还发明了一种称为“杰斐逊圆盘”的密码器械。所以这道题九成九就是考察杰斐逊圆盘密码的加密、解密或者其变种。那么接下来的问题就是我们知道了考点该如何下手尤其是当你面对的可能是一段残缺的代码、一个奇怪的报错或者像网络热词里提到的crypto.getRandomValues is not a function这样的前端JavaScript环境问题这通常出现在浏览器中尝试使用Web Crypto API时环境不支持或用法错误。在CTF中我们通常是在本地用Python解题所以这个错误提示本身可能不是题目的核心但它提醒我们环境配置和工具链是解题的第一步。你需要一个稳定的、包含丰富密码学库如pycryptodome,gmpy2,sympy的Python环境。对于杰斐逊圆盘这种古典密码我们甚至不需要这些重型库用纯Python的字符串操作就足够了。关键在于理解原理然后翻译成代码。2. 杰斐逊圆盘密码原理深度拆解在直接动手写代码前我们必须彻底吃透杰斐逊圆盘的工作原理。这不是为了炫技而是因为CTF题目往往不会考原封不动的标准实现总会加一些“料”比如改变圆盘顺序、使用自定义字母表、或者结合其他编码如Base64、十六进制进行二次包装。如果你只背了一个解密脚本而不懂原理题目稍作变化你就束手无策了。杰斐逊圆盘本质上是一种多表替代密码由多个同心圆盘组成。每个圆盘边缘被等分为26格假设是英文字母每一格随机刻印一个字母。所有圆盘的字母排列顺序各不相同。加解密时需要两个完全相同的圆盘组。2.1 标准加密流程假设我们有36个圆盘历史上杰斐逊用了36个每个圆盘有26个字母。发送方Alice将明文消息比如“ATTACKATDAWN”的每个字母依次写在每个圆盘对应的“基准线”位置。通常我们会把圆盘排列成一个“圆柱”每一“行”就是一个圆盘基准线对齐。密钥圆盘的排列顺序就是密钥。发送方和接收方必须约定好这36个圆盘的排列顺序例如顺序是圆盘#5, #12, #30, ...。生成密文行固定好圆盘顺序和对齐的基准线明文行后你可以转动整个圆柱让基准线对准任何其他位置。从这个新的对齐线上读取每一行的字母就得到了一行“乱码”。你可以选择任意一行除了明文行本身作为密文发送。实际上为了增加安全性杰斐逊的方法是从所有可能的行25行因为除开明文行中随机选一行作为密文。传输发送方将密文行一串字母和所使用的“行索引”即转动了多少格告知接收方。行索引可以公开因为不知道圆盘顺序密钥依然无法解密。2.2 标准解密流程接收方Bob拥有相同的一套圆盘并且知道密钥圆盘排列顺序。对齐密文Bob按照密钥顺序排列好自己的圆盘。然后他将收到的密文行字母分别对齐到每个圆盘对应的位置上。寻找明文此时Bob转动整个圆盘组依次检查每一行字母。当转到某一行时如果这一行字母构成了有意义的单词或句子即明文那么解密就成功了。因为圆盘顺序是固定的明文必然出现在某个特定的行上。从计算机的角度看我们可以这样抽象圆盘就是一个长度为26的字符串代表该圆盘上字母的排列。例如一个圆盘可能是“ZKMSHGPXQJYFTRDCLVUOAIWBNE”。圆盘组密钥就是一个列表包含了所有圆盘的字符串并且顺序固定。disks [disk1_str, disk2_str, disk3_str, ...]明文也是一个字符串长度通常等于圆盘的数量或者其倍数。加密将明文的第i个字母视为在第i个圆盘按密钥顺序上“查找”。但注意查找的目标不是得到密文字母而是确定一个“偏移量”。更简单的实现方式是将每个圆盘看作一个“字母映射表”。加密时我们选择一行“密文行索引”offset0-25。那么密文的第i个字母 第i个圆盘字符串中位于(pos_of_plaintext_i offset) % 26位置的字母。其中pos_of_plaintext_i是明文字母在该圆盘字符串中的索引。解密知道密文和offset。对于密文的第i个字母在第i个圆盘字符串中找到它的位置pos_of_cipher_i。那么明文的第i个字母 第i个圆盘字符串中位于(pos_of_cipher_i - offset) % 26位置的字母。注意这是最直观的理解。实际上因为圆盘可以任意旋转加解密中的offset是相对的。在CTF题目中为了简化经常假设“明文行”就是圆盘字符串的原始顺序即索引0的那一行然后密文是偏移了offset格的那一行。我们解题时要紧扣题目给出的具体描述。2.3 CTF中的常见变体与突破口已知明文攻击这是杰斐逊圆盘的一个弱点也是CTF常考的点。如果攻击者知道一部分明文和对应的密文即使不知道圆盘顺序和偏移他就有可能推导出圆盘的部分信息甚至完全破解。题目可能会给你一个密文并暗示明文是某个常见的单词或句子如“flag{”开头。圆盘顺序未知但可穷举如果圆盘数量不多比如10个以内并且字母表就是26个字母那么圆盘本身的所有可能排列26!是天文数字不可行。但圆盘之间的相对顺序即密钥如果是可枚举的就可以暴力破解。例如给你36个圆盘的字符串但不知道顺序。如果明文有特定格式如包含“flag”你可以尝试所有圆盘排列36! 仍然太大但通常题目会简化比如圆盘数量减少或者圆盘顺序存在规律如按字符串排序。偏移量未知偏移量只有26种可能完全可以暴力尝试。与编码结合密文可能以Base64、十六进制等形式给出需要先解码得到真正的字母串。“Bugku Game1”风格这类题目往往不会直接说“这是杰斐逊圆盘”。它可能给你一个文本文件里面是几十行乱七八糟的字符串每行26个字符。这很可能就是代表了各个圆盘。另一行或者文件名提示了偏移量。你的任务就是识别出这个数据结构并应用解密算法。3. 构建通用解题脚本从原理到代码理解了原理我们就可以着手编写一个通用的解题脚本框架。这个框架要足够灵活能处理圆盘顺序未知、偏移未知、需要尝试已知明文等多种情况。我们以Python为例。3.1 数据准备与清洗假设题目给了一个文件disks.txt内容如下DMTWSILRUYQNKFEJCAZBPGXOHV KPQZNRAYFWBIGMUSCVETLOHJXD ... 共36行以及一个密文“HXJMVQZR...”首先我们需要读取并处理这些圆盘。def load_disks(filepath): with open(filepath, r) as f: lines f.readlines() # 清洗去除换行符只保留大写字母确保每行长度26 disks [line.strip().upper() for line in lines if len(line.strip()) 26] # 验证确保每个字符串都是26个不重复的字母古典密码通常如此 for i, d in enumerate(disks): if len(set(d)) ! 26: print(f警告: 第{i}行圆盘包含重复字符或长度不为26: {d}) return disks3.2 核心加解密函数实现我们实现两个基础函数encrypt_jefferson和decrypt_jefferson。这里假设加解密时圆盘顺序disk_order和偏移量offset是已知的。def encrypt_jefferson(plaintext, disks, disk_order, offset): 使用杰斐逊圆盘加密。 plaintext: 明文字符串长度应等于len(disk_order) disks: 所有圆盘列表disks[i]是第i个圆盘的字符串 disk_order: 加密时使用的圆盘顺序列表元素是圆盘在disks中的索引 offset: 旋转偏移量 (0-25) ciphertext [] plaintext plaintext.upper() if len(plaintext) ! len(disk_order): raise ValueError(f明文长度{len(plaintext)}与圆盘顺序长度{len(disk_order)}不符) for i, disk_idx in enumerate(disk_order): disk disks[disk_idx] plain_char plaintext[i] if plain_char not in disk: raise ValueError(f字符 {plain_char} 不在圆盘 {disk_idx} 中) pos disk.index(plain_char) cipher_pos (pos offset) % 26 ciphertext.append(disk[cipher_pos]) return .join(ciphertext) def decrypt_jefferson(ciphertext, disks, disk_order, offset): 使用杰斐逊圆盘解密。 plaintext [] ciphertext ciphertext.upper() for i, disk_idx in enumerate(disk_order): disk disks[disk_idx] cipher_char ciphertext[i] if cipher_char not in disk: raise ValueError(f字符 {cipher_char} 不在圆盘 {disk_idx} 中) pos disk.index(cipher_char) plain_pos (pos - offset) % 26 plaintext.append(disk[plain_pos]) return .join(plaintext)3.3 应对未知情况暴力破解与已知明文攻击在CTF中disk_order和offset往往是未知的。我们需要编写搜索函数。情况一偏移量未知圆盘顺序已知。这很简单偏移量只有26种可能我们遍历即可。def brute_force_offset(ciphertext, disks, disk_order, known_plaintext_fragmentNone): 暴力尝试所有偏移量。 如果提供了已知明文片段如FLAG则只打印匹配的结果。 results [] for offset in range(26): plain decrypt_jefferson(ciphertext, disks, disk_order, offset) if known_plaintext_fragment: if known_plaintext_fragment.upper() in plain: print(f找到匹配偏移量: {offset}, 明文: {plain}) return plain, offset else: results.append((offset, plain)) # 如果没有提供已知明文打印所有结果供人工检查 if not known_plaintext_fragment: for offset, plain in results: print(fOffset {offset:2d}: {plain}) return None, None情况二圆盘顺序未知偏移量可能已知或未知。这是难点。如果圆盘数量是N那么圆盘顺序有 N! 种可能。当N10时10! 3,628,800勉强可接受暴力破解。当N36时完全不可行。因此题目一定会给出限制条件圆盘顺序是固定的某种排列比如题目可能暗示圆盘是按字符串字典序排列的。那么disk_order就是sorted(range(len(disks)), keylambda i: disks[i])。已知明文足够长如果你知道明文或部分明文你可以利用它来推导或验证圆盘顺序。例如你知道密文前5个字母是“ABCDE”对应的明文是“HELLO”并且知道偏移量。那么对于每个位置i你都可以列出一个方程disk[i]中H的位置 offset disk[i]中A的位置 (mod 26)。这可以用于校验候选的圆盘顺序。圆盘顺序可枚举题目可能只给了6-8个圆盘。下面是一个结合已知明文和暴力破解圆盘顺序的示例框架假设圆盘数量较少例如8个from itertools import permutations def brute_force_order_and_offset(ciphertext, disks, known_plaintext): 暴力破解圆盘顺序和偏移量。 警告仅适用于圆盘数量很少的情况如 8。 ciphertext: 密文 disks: 圆盘列表 known_plaintext: 已知的明文片段长度必须等于圆盘数量或密文长度 n len(disks) if len(ciphertext) ! n or len(known_plaintext) ! n: print(密文或已知明文长度与圆盘数量不符) return known_plaintext known_plaintext.upper() ciphertext ciphertext.upper() # 遍历所有圆盘排列 for disk_order in permutations(range(n)): # 对于每种排列遍历所有偏移量 for offset in range(26): # 尝试解密 decrypted decrypt_jefferson(ciphertext, disks, disk_order, offset) # 检查解密结果的前n位是否与已知明文匹配 if decrypted known_plaintext: print(f成功破解) print(f圆盘顺序: {disk_order}) print(f偏移量: {offset}) print(f完整明文假设: {decrypted}) # 通常知道了顺序和偏移我们可以解密整个消息如果密文更长 return disk_order, offset print(未找到匹配的组合。) return None, None4. 实战演练模拟“Bugku Game1”式题目解题全流程现在让我们模拟一个完整的解题过程假设我们拿到的题目文件如下cipher.txt: 内容为一行密文“VSPRXW...”(假设长度36)wheels.txt: 内容为36行每行26个不重复的大写字母代表36个圆盘。题目描述只有一句话“托马斯·杰斐逊的发明”。4.1 第一步分析题目结构密文长度36圆盘数量36。这强烈暗示明文长度也是36每个圆盘用于加密一个明文字母。没有给出圆盘顺序和偏移量。这是我们需要破解的。我们需要一个已知明文片段。在CTF中flag通常有固定格式如“flag{”或“bugku{”。但这里明文长度36“flag{”只有5个字符。我们需要更多信息。假设flag格式是“bugku{xxxxxxxxxxxxxxxxxxxxxxxxxxx}”长度可能为36这不确定。另一种思路题目可能隐含了已知明文。例如密文解密后的结果可能是一句英文名言或者包含“JEFFERSON”等单词。我们可以尝试使用词频分析或字典攻击但这在36个字符的短文本中效果有限。关键突破口杰斐逊圆盘在CTF中有时会设置圆盘顺序就是其在文件中出现的顺序即disk_order [0,1,2,...,35]或者按圆盘字符串排序的顺序。偏移量则可能是一个小数字如0,1,13等。我们可以优先尝试这些简单情况。4.2 第二步编写并运行试探脚本我们先尝试最简单的假设圆盘顺序就是文件中的顺序list(range(36))然后暴力尝试所有26个偏移量观察输出中是否有可读的英文单词或flag格式。# 假设已经加载了 disks 和 ciphertext ciphertext VSPRXW... # 从cipher.txt读取 disks load_disks(wheels.txt) simple_order list(range(len(disks))) print(尝试圆盘顺序为[0,1,2,...]暴力所有偏移量) brute_force_offset(ciphertext, disks, simple_order)运行后我们会得到26行输出。我们需要人工浏览寻找像英文的句子。如果找到了比如某一行是“THEQUICKBROWNFOXJUMPSOVERTHELAZYDOG”一个著名的全字母短句那么很可能就破解了。如果没找到说明圆盘顺序不是默认顺序。4.3 第三步尝试有序的圆盘顺序接下来尝试圆盘按字符串字典序排列。sorted_order sorted(range(len(disks)), keylambda i: disks[i]) print(f按圆盘字符串排序后的顺序: {sorted_order}) print(尝试此顺序暴力所有偏移量) brute_force_offset(ciphertext, disks, sorted_order)4.4 第四步利用已知明文片段进行攻击如果以上都不行我们必须寻找已知明文。仔细观察密文和圆盘。有时题目会在圆盘字符串中隐藏信息。例如每个圆盘字符串的第一个字母连起来可能是一句话。我们可以检查一下first_letters .join([d[0] for d in disks]) print(f所有圆盘第一个字母: {first_letters})如果first_letters看起来像“THISISTHEKEYORDERREADVERTICALLY”那么这就是在提示圆盘顺序你需要按照这个提示的单词顺序去排列圆盘。例如提示是“READTHEWHEELSVERTICALLY”你可能需要去理解是“垂直阅读圆盘”还是别的意思。假设我们通过某种方式可能是题目附带的提示文本或者别的挑战环节知道了明文开头是“CONGRATULATIONSYOUHAVEFOUNDTHEFLAG”。那么我们就可以用这个已知明文去反推圆盘顺序和偏移量。但由于36!太大我们不能暴力所有顺序。这时我们可以利用已知明文和密文对每个位置单独计算“相对关系”从而校验或推导顺序。对于每个位置i已知明文P[i]和密文C[i]。对于任意一个圆盘disk如果它被用在位置i那么必须满足在disk字符串中P[i]的位置和C[i]的位置之差模26是一个常数即偏移量offset并且这个常数对所有位置i都相同。 我们可以编写一个函数来寻找满足这个条件的圆盘分配方案def find_consistent_order(disks, plaintext, ciphertext): 尝试为每个位置分配一个圆盘使得所有位置的偏移量一致。 返回所有可能的 (disk_order, offset) 组合列表。 这是一个搜索问题但可以利用约束大大缩小搜索空间。 n len(plaintext) candidates [] # 存储可能的顺序偏移量 # 预计算对于每个圆盘计算 plain-cipher 需要的偏移量如果可能 # 但这需要假设每个位置用的圆盘不同且每个圆盘只能用一次。 # 这是一个典型的精确覆盖或回溯问题。 # 由于CTF题目通常有唯一解我们可以用递归回溯实现。 used_disks set() current_order [-1] * n # 我们从第一个位置开始尝试 def backtrack(pos, possible_offset): if pos n: # 所有位置都分配完毕且偏移量一致 candidates.append((list(current_order), possible_offset)) return for disk_idx, disk in enumerate(disks): if disk_idx in used_disks: continue # 检查这个圆盘放在当前位置其偏移量是否与之前的一致 p_char plaintext[pos] c_char ciphertext[pos] if p_char not in disk or c_char not in disk: continue offset_candidate (disk.index(c_char) - disk.index(p_char)) % 26 if pos 0: # 第一个位置任何偏移量都可以记录下来 current_order[pos] disk_idx used_disks.add(disk_idx) backtrack(pos1, offset_candidate) used_disks.remove(disk_idx) current_order[pos] -1 else: if offset_candidate possible_offset: current_order[pos] disk_idx used_disks.add(disk_idx) backtrack(pos1, possible_offset) used_disks.remove(disk_idx) current_order[pos] -1 backtrack(0, None) return candidates这个回溯算法在圆盘数量不多比如36且已知明文足够长时实际上仍然可能很慢因为搜索空间是排列数。但在CTF中已知明文可能很长比如36个字符全知道这会产生极强的约束可能让回溯快速收敛。如果已知明文只有几个字符这个函数可能返回很多候选解需要进一步筛选。4.5 第五步综合与验证在实际解题中我们往往需要结合多种手段先尝试最简单假设默认顺序、排序顺序、常见偏移量。观察数据特征如第一个字母连成的字符串。利用已知的flag格式或上下文猜测部分明文。编写针对性的脚本进行搜索或约束求解。一旦通过某种方法得到了一个看似合理的明文例如包含“bugku{”我们需要验证得到的圆盘顺序是否唯一是否还有其他顺序能产生同样的明文用得到的圆盘顺序和偏移量能否解密其他由相同圆盘组加密的密文如果有明文是否具有意义符合英文语法或题目语境5. 进阶思考从“杰斐逊”到更广的Crypto题型通过“托马斯.杰斐逊”这道题我们可以提炼出解决古典密码题乃至许多Crypto题目的通用方法论5.1 识别与归类看到题目名、描述或文件第一时间联想可能的密码体系。历史人名/名词如“杰斐逊”、“维吉尼亚”、“栅栏”、“凯撒” - 对应古典密码。数学名词如“RSA”、“椭圆曲线”、“离散对数” - 对应现代公钥密码。错误提示如“typeerror: crypto$2.getrandomvalues is not a function”这很可能是一个Web前端Crypto API的题目。虽然Bugku平台是后端判题但这个错误提示可能出现在题目附带的源码HTML/JS中。你需要分析前端JavaScript代码看它如何生成或处理密钥/密文然后可能在Python中复现逻辑。这类题目的核心是代码审计与算法移植。5.2 环境与工具准备Python环境安装pip install pycryptodome gmpy2 sympy是标配。在线工具熟悉CyberChef、dcode.fr等在线密码学工具用于快速尝试Base64、ROT13、字频分析等。调试技巧对于复现算法善于使用print或日志逐步跟踪变量状态确保每一步都与原算法如JS代码一致。5.3 数据处理与转换密文或密钥经常不是“干净”的。它们可能是编码形式Hex, Base64, Base32, ASCII码值甚至莫尔斯电码。第一步永远是尝试各种解码。嵌入在奇怪格式中藏在图片元数据Exif、音频频谱图、文件末尾附加数据中。需要binwalk,strings,xxd等工具检查。需要预处理去除空格、标点转换大小写。5.4 暴力破解与优化当密钥空间不大时暴力破解是终极武器。密钥空间评估凯撒密码26种、单表替换26! 巨大但可结合词频分析、维吉尼亚密码密钥长度有限时可暴力。并行与剪枝使用itertools.product生成组合利用已知明文片段crib在早期就淘汰大量无效密钥大幅减少计算量。上面的find_consistent_order函数就是利用已知明文进行剪枝的典范。资源利用对于计算量大的暴力破解考虑使用multiprocessing库进行多进程并行。5.5 脚本的健壮性与复用性不要写“一次性”脚本。将核心算法如杰斐逊加解密封装成函数将数据读取、清洗、尝试逻辑分开。这样当题目条件变化时比如圆盘字母表变成36个字符包含数字你只需要修改少数几个地方而不是重写整个脚本。这也是处理bugku game1这类系列题的关键——第一道题的经验和代码稍作修改就能用于第二、第三道题。回到我们最初的题目“托马斯.杰斐逊”不仅仅是一道古典密码题它更像一个引子引导你去学习密码学史、理解多表替代的原理、并掌握一套从分析、假设、编码到验证的完整解题流程。当你再遇到“typeerror: crypto$2.getrandomvalues is not a function”时你不会只把它看作一个错误而会意识到这背后可能是一个考察Web Crypto API用法和随机数生成的前端密码题然后你知道该去检查浏览器环境、对比Node.js/Python的随机数实现差异。这才是玩转Crypto挑战的真正乐趣所在——不断建立新的知识连接并将它们转化为可靠的、可复现的代码逻辑。
返回列表