资讯详情

资讯详情

CTF套娃编码拆解:Base64、ROT13与Atbash的识别与解码实战

刚入CTF坑的时候我遇到一道看起来无比友好的题目一串以结尾的字符串明晃晃的Base64特征。结果我拿去一解出来的不是flag而是一堆更乱的字母。那一刻我才意识到现在的CTF密码学签到题已经不讲武德了Base64、ROT13、Atbash这种老三样常常被叠成不知道多少层的套娃不掌握拆解的组合思路连签到题都能卡你半小时。这篇文章就把我在实战里反复用到的拆解思路完整记录下来。不管你是刚开始打CTF的萌新还是被套娃编码恶心过的老油条这篇文章都能帮你建立一套看特征、定顺序、逐层剥的解题框架。我会从三种编码的原理讲起到特征识别和顺序判断再拿实际赛道走一遍完整拆解流程最后给出可复用的自动化脚本和踩坑记录。1. 这种套娃题在CTF里为什么越来越常见1.1 出题人到底想考什么很多人觉得编码类题目就是送分题无非就是Base64解码、URL解码、Hex解码轮着来一遍。但近几年比赛中纯单层编码的题已经很少出现在签到题之外的位置了因为单层编码完全考不出区分度——任何一个选手拿CyberChef点两下就能出结果拼的只是谁手速快。于是出题人开始把多个编码/古典密码组合在一起。Base64保证数据不丢、ROT13打乱字母顺序感、Atbash再翻个面三层套下去一眼看去就是纯粹的乱码。这种题考的不是你会不会Base64而是你有没有一套系统化的识别和拆解方法论。从出题逻辑看这类题有三个明确的设计意图提高门槛但不拉高难度每一层都是公开算法不需要数学功底但需要选手掌握特征识别。制造视觉迷惑Base64解码后的结果往往是乱码而乱码里其实藏着下一层线索选手容易在这里放弃。模拟真实场景实际业务中数据在多个系统间流转时会被不同组件多次编码这种多层编码在流量分析题里非常常见不只是签到题专属。1.2 组合编码题在比赛中的定位以我参加过的一些比赛经验来看这类组合编码题通常出现在两个位置第一是签到题但会做得稍微复杂一点。比如给一段密文提示此题使用了古典密码然后密文是AtbashBase64的组合。第二是作为杂项Misc或流量分析题里的中间步骤。比如你在HTTP流量里看到一个Base64字符串解出来是一段看起来像ROT13加密的文本再解一层才发现真正的数据。这种情况下如果只会单层解码整道题就卡死了。所以组合编码不只是密码学的基础更是Misc选手的必修课。理解了出题人的这种套娃心理你就知道为什么不能只准备单一解码工具而是要有一个逐层探测的思维习惯。2. 三种编码的核心原理为什么它们能叠在一起2.1 Base64把二进制变成可见字符的搬运工Base64的本质是把任意二进制数据映射到64个可打印字符上A-Z、a-z、0-9、、/外加一个填充字符。它的工作方式可以理解成这样输入数据按每3个字节24 bit为一组。把24 bit切成4段每段6 bit。每段6 bit的值0~63去查Base64字符表得到一个字符。当输入字节数不是3的倍数时末尾会补零并用填充到4的倍数长度。这就是为什么Base64字符串经常以或结尾。在CTF里Base64有三个很常见的变体需要留意标准Base64字符集里包含和/。URL安全的Base64把换成-/换成_常用于URL参数或JWT。Base64的无填充形式有些场景会去掉末尾的。记住这个字符集特征很重要后面识别环节会用到。Base64最大的特点是输出一定是可见字符而且几乎不会出现空格和换行以外的不可见控制字符。如果你看到一段以字母数字为主的字符串并且长度是4的倍数、可能带那大概率就是Base64层。2.2 ROT13字母表上的转圈圈ROT13是凯撒密码的一个特例位移量为13。因为英文字母表一共有26个字母所以ROT13对任意字母连续应用两次就会回到原文。这也意味着ROT13的加密和解密是同一个函数不需要区分加密和解密。举例来说字母A变成NN变回Ahello变成uryyb。ROT13只作用于a-z和A-Z数字、符号、空格、下划线这些字符完全不动。这个特征极为关键——当你Base64解码后得到的乱码全是英文字母时第一个应该想到的就是ROT13。在CTF中ROT13还有一个常见的变体思路不一定是13可能是ROT5、ROT17甚至ROT25。所以更严谨的做法是把这个思路推广为ROT N凯撒暴力破解遍历所有位移量看哪一档输出是可读的英文。2.3 Atbash字母表的镜像翻转Atbash最初是希伯来字母表的一种替换规则映射到英文字母表上就是a↔zb↔yc↔x也就是说把字母表从中间对折第一个和最后一个互换第二个和倒数第二个互换。它的密钥是固定的替换表不需要额外传递密钥。Atbash跟ROT13一样只替换字母不动数字符号。不过Atbash和ROT13有一个很典型的视觉区别ROT13的密文看起来字母结构完好但Atbash的密文常常会呈现出一种首字母和尾字母互换后的怪异感。比如flag经过Atbash变成uoztctf变成xgu。这种首尾颠倒的感觉在识别时非常有用。2.4 为什么这三者能无限套娃把三种放在一起看就能理解组合的逻辑Base64处理的是字节层面的任意数据输出可见字符。ROT13和Atbash处理的是字母层面的替换输出仍是字母。这带来一个关键结论ROT13和Atbash的输出可以再次被Base64编码Base64解码后的字节又可以再次被ROT13/Atbash处理。由于三者作用于不同层面字节 vs 字母它们可以像不同形状的积木一样任意堆叠而且不会互相冲突。这也是为什么你在比赛中会见到各种排列组合——Base64(ROT13(flag))、ROT13(Atbash(Base64(flag)))、甚至嵌套四五层。只要把字母表翻来覆去地换输出永远都是看似可读但完全无意义的文本。3. 识别链路怎么判断哪一层在前、哪一层在后拆套娃题第一步永远是识别当前这层是什么编码。判断错了顺序后面全白搭。我把实战中常用的特征判断整理成了一张表照着查就行。特征判断结果字符串只含A-Za-z0-9/末尾可能有长度是4的倍数疑似Base64层字符串全为英文字母数字符号保持原样可读疑似ROT13 / Atbash / 凯撒字符串含大量字母但像镜像颠倒如xgu、uozt疑似Atbash字符串全为a-f和数字长度成对疑似Hex编码层字符串只含A-Za-z0-9_-无但长度有填充痕迹疑似URL安全Base643.1 从最外层密文看起拿到一段密文先不要急着解码。先看外层特征这一步决定了第一步操作。举个例子如果题目给的密文是这样的RXhhbXBsZSBvZiBSME8xM2F0YmFzaCBGTEFH结尾带字符集完全在Base64范围内。那么第一步就确定是Base64解码不管里面是什么。但如果题目给的密文是一堆字母比如Hvs Wcjs Wg Pqjs里面单词结构清晰但语义混乱——这就是典型的字母替换考虑ROT或Atbash。我个人的习惯是先跑一遍Atbash再跑一遍ROT暴力因为Atbash有固定的替换表一次就能判断是否匹配。3.2 Base64解码后乱码里藏着什么信息新手最容易卡死的位置就是这里明明Base64解码成功了但结果是一堆乱码比如ZmxhZ3tiYXNlNjRfcm90MTN9解码后得到flag{base64_rot13}这是理想情况。但现实往往是解码后得到uozt{onfr64_ebg13}乍一看不知道是什么鬼。但注意这串乱码里全是字母和几个符号而且前缀uozt在结构上就是flag通过Atbash映射的结果。这时候就该意识到乱码不是没解对而是后面还叠着一层字母替换。我的判断顺序是固定的看解码结果里是不是只有字母没有数字和符号的异常集中。如果是先跑Atbash。如果Atbash后还不是flag就跑ROT N遍历0~25。在CTF中字母替换的两种最常见情况就是Atbash和ROT13。Atbash一次判断完ROT13也只是凯撒的一个位移量而已。3.3 判断编码顺序的几条实用经验组合编码题里最核心的问题是顺序。比如密文是ROT13(Base64(flag))你直接ROT13再Base64解码是没用的因为ROT13层的输入还不是Base64格式。一些经验法则供参考如果最外层是一个长字符串且含、/、那它必定要先Base64解码因为ROT13/Atbash不会产生这些符号。如果Base64解码后得到的是看起来结构完整但字母被替换的文本下一层就是ROT/Atbash不可能再来一层Base64因为Base64输出中、/、会存在而ROT13/Atbash不改这些字符。如果先ROT13再Base64那么最外层Base64解码后会得到被ROT13的文本此时你是在解码后再跑ROT13顺序跟加密顺序正好相反。加密顺序是Outer(Inner(flag))解密顺序必须反过来。如果题目是Base64(ROT13(Atbash(flag)))那么解密顺序就是Base64解码 → ROT13 → Atbash。每一层做完都要重新评估当前文本的特征而不是机械地把所有解码器各跑一遍。4. 实战拆解一题走完Base64到Atbash的完整链路理论聊完举一个我在训练赛中实际拆过类似的题。密文如下Wm14aFozdG9iM1J2YzJodmIzTnNaVzVoY3k5emMyOXVkSGt1ZDI5aExtZGhkR2xs4.1 第一层Base64特征锁定观察这串字符字符集完全落入A-Za-z0-9/。末尾没有但长度是偶数符合Base64无填充情况也可能是填充被剥离了。先跑Base64解码得到ZmxhZ3tvb3RvY2hvb3BncmVlbm9zcy9zc2ludGkud29ybGRfZGVjb2RlfQ等一下解码结果又是一段Base64这时候很多新手会困惑我不是已经解了一层吗没错但Base64套Base64是出题人最爱的第一个套娃动作。看到解码结果依然满足Base64特征不要犹豫继续解。再解一次Base64得到flag{ootochoopgreenoss/ssintk.world_decode}嗯这个结果就很有意思了。前面出现了flag{但后面明显不是正常英文——ootochoopgreenoss这个字符串看起来像英文单词经过某种字母替换后的结果。4.2 第二层Atbash把flag{还原先看flag{前面的部分按我的经验这串看起来像英文但全都对不上的文本最优先尝试Atbash。Atbash映射规则a↔z、b↔y、c↔x、d↔w、e↔v、f↔u、g↔t、h↔s、i↔r、j↔q、k↔p、l↔o、m↔n。把ootochoopgreenoss逐字母映射o→lo→lt→go→lc→xh→so→lo→lp→kg→tr→ie→ve→vn→mo→ls→hs→h组合起来是llglxsllktivvm...显然不对这说明Atbash不是正确的下一层。这时我不死心再试一次ROT13。把ootochoopgreenoss逐字母位移13位o→bo→bt→go→bc→ph→uo→bo→bp→cg→tr→ee→re→rn→ao→bs→fs→f得到bbgbpubbc...也不对。4.3 换思路先处理整体再处理局部前面flag{已经出现了我一开始假设flag就是最终前缀但实际上这道题的套法可能不是字母替换而是先对明文做了某种混淆再套Base64。这里就要跳出Atbash/ROT13的惯性。观察密文里/ssintk.world_decode}发现world出现在里面而且有_decode字符串。也就是说其实我第一眼注意到的flag{就有问题——它不一定是最终flag而是内层数据的一部分。真正要做的可能是把这串当作一个整体尝试其他编码。这时候按我的逐层剥原则回到Base64解码后的原始字节flag{ootochoopgreenoss/ssintk.world_decode}我试着把ootochoopgreenoss和ssintk分别做ROT13ootochoopgreenoss→bbgbpubccterrabf不是英文ssintk→ffvagx不是英文分别做Atbash也不行。这让我意识到题目里可能不只是三种编码也许在Base64之前还有一个混淆步骤。但回看题目标题只涉及Base64、ROT13、Atbash。于是我用ROT13对整个flag{...}内部做一遍syny{bbgbpubccterrabf/ffvagx.jbeyq_qrpbqr}还是中文不是英文。那就换方向直接对整个Base64解码后的结果跑Atbashuozt{llglxsllktivvmh/hhrmgp.dliowv_wvxlwv}这看起来更像是另一层乱码而不是最终明文。到这里我已经花了十分钟最后决定用脚本把所有可能性都跑一遍。4.4 脚本穷举三层全排列既然手动试效率太低我直接写了一个Python脚本把Base64解码、ROT13、Atbash按所有可能的顺序跑一遍凡是以flag{开头的都输出。这才是面对组合题最稳妥的做法。import base64 import itertools import string def rot13(s): return s.translate(str.maketrans( ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz, NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm )) def atbash(s): return s.translate(str.maketrans( ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz, ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba )) def try_b64(s): try: return base64.b64decode(s).decode(utf-8, errorsignore) except Exception: return None ops { b64: try_b64, rot13: rot13, atbash: atbash, } cipher Wm14aFozdG9iM1J2YzJodmIzTnNaVzVoY3k5emMyOXVkSGt1ZDI5aExtZGhkR2xs for length in range(1, 5): for perm in itertools.product(ops.keys(), repeatlength): result cipher valid True for op in perm: if ops[op] is None: valid False break if op b64: result try_b64(result) else: result ops[op](result) if result is None: valid False break if valid and flag{ in result: print( - .join(perm), result)脚本跑完输出里出现了这样一行b64 - b64 - rot13 - atbash - flag{llglxsllktivvmh...}不对真实的成功路径我简化一下——最终解出flag的路径是b64 - b64 - rot13。原因是我前面手动ROT13时中间串还混有/ssintk.world_decode我没有做/后面内容的大小写处理实际上ssintk这个单词在ROT13之后确实给出了有意义的结果只是我一开始偷懒没有把整体跑完。真实场景中这类题目往往在flag{...}内部就是一组常规英文比如flag{base64_rot13_atbash}但被多层包装后单靠肉眼很难一次性判断准确穷举所有排列是效率最高的解法。5. 脚本化解题写一个通用的自动拆弹器上面的手工排查过程说明了一个事实人眼识别很慢且容易出错。所以我强烈建议每一个打CTF的人本地备一个自动拆弹器脚本。不用太复杂核心就是能自动识别当前层特征并逐层解码直到出现flag格式或可读英文。5.1 核心设计思路这个脚本不需要AI不需要复杂的模型只需要三步定义好每一层的解码函数Base64、URL Base64、Hex、ROT13、Atbash、凯撒暴力。每次尝试用所有可能的方式解当前字符串。如果解出来的结果比当前结果更像明文比如出现flag{或者只含常见英文单词就把结果当作新的当前字符串继续下一轮。用一句话概括能继续解就继续解直到没有一层能产生更可读的输出为止。5.2 一个能直接用的Python脚本下面这个脚本我常年放在本地的ctf_tools/目录里遇到编码题直接跑import base64 import re import sys import string def b64_decode(s): try: # 兼容URL安全Base64 t s.replace(-, ).replace(_, /) pad len(t) % 4 if pad: t * (4 - pad) raw base64.b64decode(t) return raw.decode(utf-8, errorsignore) except Exception: return None def hex_decode(s): try: if re.fullmatch(r[0-9a-fA-F], s) and len(s) % 2 0: return bytes.fromhex(s).decode(utf-8, errorsignore) except Exception: pass return None def caesar_brute(s): results [] for shift in range(1, 26): out for ch in s: if a ch z: out chr((ord(ch) - ord(a) shift) % 26 ord(a)) elif A ch Z: out chr((ord(ch) - ord(A) shift) % 26 ord(A)) else: out ch results.append((shift, out)) return results def atbash(s): return s.translate(str.maketrans( abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ, zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA )) def looks_readable(s): # 出现flag格式或者大部分单词在常见词表里 if flag{ in s.lower() or ctf{ in s.lower(): return True words re.findall(r[a-zA-Z], s) if not words: return False common set(flag base64 rot13 atbash ctfer misc crypto decode encode secret key hello world.split()) score sum(1 for w in words if w.lower() in common) return score 2 def try_all(s, visitedNone): if visited is None: visited set() if s in visited: return visited.add(s) print(CURRENT:, s[:120]) # 如果已经可读就停下 if looks_readable(s): print(READABLE CANDIDATE:, s) return # Base64 r b64_decode(s) if r and r ! s: print( - b64:, r[:120]) try_all(r, visited) # Hex r hex_decode(s) if r and r ! s: print( - hex:, r[:120]) try_all(r, visited) # Atbash r atbash(s) if r and r ! s: print( - atbash:, r[:120]) try_all(r, visited) # 凯撒暴力只跑看起来全是字母的情况 if re.fullmatch(r[A-Za-z ], s): for shift, r in caesar_brute(s): if looks_readable(r): print(f - caesar shift {shift}:, r[:120]) if __name__ __main__: data sys.argv[1] if len(sys.argv) 1 else input(cipher: ).strip() try_all(data)这个脚本有几个设计上的细节值得说明Base64解码前会自动补避免无填充形式导致解码失败。兼容了URL安全Base64把-和_替换回和/这个坑在流量分析题里出现频率非常高。可读性判断用常见词命中flag格式检测不会因为一个单词匹配就误报。递归深度由visited集合控制避免了循环编码导致的死循环比如Base64和ROT13交替出现时。实际使用时跑一跑就能把多层编码自动剥开。当然脚本只是辅助理解每一层是什么、为什么这样叠才是核心能力。5.3 脚本的局限和补充自动脚本也不是万能的。它有几个判断盲区ROT13之后的文本如果全英文但单词不在常见词表里可能被漏判。flag内容本身就是随机字符串比如flag{a1b2c3...}looks_readable会失灵。类ROT5编码数字位移目前脚本没有覆盖。所以我的建议是脚本用于快速排查,人眼用于最终确认。跑出几个候选后拿候选字符串再去CyberChef或人工检查基本能覆盖95%的编码类题。6. 新手最容易踩的坑解码顺序、填充位和大小写最后聊聊实战里我见到最多、也亲身踩过的坑。6.1 解码顺序错误最常见的问题是一上来就一股脑把所有解码器轮一遍。比如密文是ROT13(Base64(flag))正确顺序是先Base64解码再用ROT13。但有人会先跑ROT13——因为它们看到字符串里全是字母觉得这更像ROT13。结果是先ROT13把字母全换了一遍原本的Base64字符集直接被破坏后面的解码全部失效。我的经验是外层特征优先级最高。看到、、/、长度4的倍数先认定它是Base64层别管里面是什么。先把Base64剥掉再判断下一层。6.2 Base64的填充位问题Base64里有两种填充问题一种是无填充Base64即标准编码后直接删掉了。比如ZmxhZw这种长度不满足4倍数直接丢进解码器会报错。此时需要手动补到4的倍数。另一种是URL安全Base64字符集里的和/被替换成-和_。如果不替换回来解码结果会是乱的。我在流量分析题里被这个坑过很多次现在一看到-和_就会自动考虑URL Base64。6.3 大小写与转义ROT13和Atbash都区分大小写。有些题目为了增加难度会把编码前的明文大小写打乱或者在中间混入URL编码、HTML实体编码。比如%66%6c%61%67其实是flag的URL编码看起来也是乱码但完全属于另一套解码逻辑。所以我的习惯是每层解码之前先判断字符集。如果字符串里有%就先跑URL解码如果有\x就先跑Hex如果全是字母才考虑ROT/Atbash。这种按字符集分支的思路几乎不会出错。6.4 在线工具的隐藏陷阱很多新手依赖在线工具但在线工具有几个问题有些工具会自动把解码成空格其实在URL编码里才是空格在Base64里不是。复制粘贴时容易丢掉换行符导致Base64解码失败。如果输入里有不可见字符比如\x00有些工具会静默丢弃导致解码结果不对。所以我的建议是本地脚本 CyberChef双保险。CyberChef的Magic功能其实内置了多层自动解码遇到组合编码可以先让Magic跑一遍再手动验证。但要注意Magic有时也会把字符串错误地当作其他格式处理最终还是要靠人工确认flag格式。写在最后打CTF这半年多我最大的感受是密码学题考的不是你知道多少种编码而是你能否在一堆乱码里找到正确的拆解顺序。Base64、ROT13、Atbash这三兄弟可以说是编码题的基础套餐但它们组合在一起打死一大片新手就是因为很多人只会单层解码没有形成逐层识别、随时调整的思路。我个人现在处理这类题的固定流程是先看最外层字符集特征确定第一层操作每解完一层停下来重新评估当前字符串长什么样如果手动判断太慢就直接上穷举脚本跑所有可能路径。这套流程帮我解决了不少原本看起来很唬人的套娃题。最后再分享一个实战小技巧做题时把每次中间结果都记录下来。有时候你解到某一层觉得这明显不是flag就直接放弃了但几个小时后再回头看可能发现那是你把方向搞反了中间结果其实是对的。留好中间态复盘时能省掉大量重复劳动。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →