拿到“[安洵杯 2019]吹着贝斯扫二维码”这道杂项题时,我的第一反应是:这题目名怕不是来逗我笑的。“吹着贝斯”能是什么?贝斯不就是 Base 的谐音吗,再配上一个“扫二维码”,Base64 这个关键词几乎是把答案贴脸上了。但等真正上手做的时候,才发现这道题没那么简单,二维码扫描、Base 家族辨识、加密层判断,每一步都可能让人卡住。这篇文章就围绕这道题,把这套“图 -> 字符串 -> Base -> 加密 -> flag”的完整链路拆开讲清楚,里面会用到的二维码修复、编码识别、加密特征判断这些技能,在其它杂项题里同样能复用。想入门 CTF Misc 的师傅可以把它当一条主线来练,老手也能顺便看看我踩过的坑。
1. 先看题面:贝斯梗、二维码和“先别急着扫码”
1.1 题目名称就是第一层提示
安洵杯的杂项题一向喜欢在题目名上做文章,尤其是这种带谐音梗的。“吹着贝斯扫二维码”,拆开看就是两个关键词:贝斯、二维码。贝斯谐音 Base,这在 CTF 里几乎是默认语感了——Base 家族编码,包括 Base16、Base32、Base64、Base58 这些;二维码则是题目真正要给你的信息载体,信息就藏在那张 QR 码图片里。
很多新人看到“贝斯”两个字就直接冲去 Base64 解码,结果发现手头连字符串都没有,这其实是顺序搞反了。这题的完整逻辑大概率是一个链路:二维码图片 -> 扫描/识别出字符串 -> 字符串经过某种 Base 编码 -> 解码后再进入下一层处理。题目名里的“贝斯”可能提示的是第一步或中间某一步,但不一定是终点。
所以正确的打开方式是先想清楚这条链路的入口在哪。入口就是那张二维码图,而拿到图之后,第一步反而不是扫码,而是先检查附件本身。
1.2 别急着扫,先拿 file、binwalk、strings 过一遍
CTF 附件往往不会太老实。表面上是一张二维码 PNG,但文件尾部可能拼了一个压缩包,或者是图片里嵌了隐写信息。拿到附件我习惯先跑三件套:
file qr.png binwalk qr.png strings qr.png | head -50file 能告诉我们真实文件类型,binwalk 能看到有没有嵌入式文件,strings 则能快速扫出是否有可疑可见字符串。实测中不少二维码题目会在文件尾部塞一段十六进制文本或一个 zip,如果直接扫码就会错过重要线索。
不过对于“吹着贝斯扫二维码”这类题,通常走到 file 和 binwalk 就能判断出没有额外隐写,主要战场就在 QR 码本身。即使如此,也不要跳过这一步——万一哪天遇到带压缩包的变体,这个习惯能救你一命。确认附件干净之后,才进入真正的扫码环节。
2. 二维码扫不出?先确认是不是二维码“坏了”
2.1 手机扫码失败的五种常见情况
很多人做题的习惯是掏出手机对准屏幕,扫不出来就说“题目有问题”。但实际上,二维码读不出来往往不是题坏了,而是题目故意设置了读取障碍。我见过的 CTF 二维码题目里,常见故障基本是这几种:
| 故障类型 | 特征 | 典型原因 |
|---|---|---|
| 遮挡/污损 | 二维码部分区域被图案或色块覆盖 | 贴了 logo、加马赛克、盖了文字 |
| 反色 | 黑底白条,手机识别困难 | 出题人做了颜色反转 |
| 定位角缺失 | 三个角上的回字方块不完整 | 截图裁剪、局部破坏 |
| 噪点干扰 | 黑白模块之间有大量随机色点 | 加噪声增加识别难度 |
| 版本过高 | 模块又小又密,手机很难对上焦 | 高版本 QR 码信息量大 |
关键点是:QR 码本身有容错机制。它的纠错等级分 L、M、Q、H 四档,最高 H 档大约能容忍 30% 的码字损坏。所以一些轻微的污损和遮挡,理论上不靠人眼修图也能被专业工具救回来,关键是要用对工具。
2.2 用 QRazyBox 和 pyzbar 做“二维码手术”
遇到手机扫不出来的二维码,我的第一个选择是 QRazyBox。这是一款专门用来分析、修复、读取 QR 码的网页工具,在杂项圈里算是标配。它能把二维码的掩码、纠错级别、版本信息都解析出来,甚至可以手动抹掉干扰块后再解码。
基本操作流程是:
- 打开 QRazyBox,把二维码图片拖进去。
- 点 Decode,看是否能直接读出内容。
- 读不出来时,用工具面板里的 Mask、Extract QR Information 等功能检查二维码是否被反转或改动。
- 如果只是局部被遮挡,可以通过 Repair 相关功能把误判模块改回来,再重新解码。
如果手头没有 QRazyBox,也可以用 Python 的 pyzbar 库快速批量识别,尤其是二维码比较规整但手机扫不出的版本,比如 25 版本以上的高密度二维码,手机摄像头经常对不上焦,换成 pyzbar 反而一次就能解出来。代码很简单:
from PIL import Image import pyzbar.pyzbar as pyzbar img = Image.open("qr.png") results = pyzbar.decode(img) for r in results: print(r.data.decode())这套组合拳基本能应付大多数“扫不出来”的二维码。但要注意:如果二维码被恶意破坏得太严重,比如定位角直接缺了一个,QRazyBox 不一定能自动恢复,这时需要手动补全,这个操作有点费眼,但确实可行。
2.3 反色二维码:不用眼睛折腾,交给代码
如果二维码是反色形态,即黑底白条,肉眼看得清,但扫码工具不一定认。处理方式很简单——反色之后再识别。
import cv2 img = cv2.imread("qr.png") inv = cv2.bitwise_not(img) cv2.imwrite("qr_inv.png", inv)用 OpenCV 的 bitwise_not 对每个像素取反,存成新图再拿去识别。同样,有些二维码识别库直接提供 Invert 选项,不需要额外处理图片。掌握这个操作很有用,因为我见过不少题把反色当成一种“防直接扫码”的障眼法,实际上代码一转就露馅了。
这道题如果二维码本身是正常的,那就跳过修复环节,直接进入识别读串阶段。如果识别后得到了一串看起来像 Base64 的字符串,恭喜你,终于进入“贝斯”主场了。
3. 贝斯不只有贝斯——Base64 解码的第一步与连环坑
3.1 先确认这串字符是不是“Base64 脸”
二维码扫出来的内容形式很多,可能是 URL,可能是中文,也可能是乱码。对于“吹着贝斯扫二维码”来说,题目名已经暗示了 Base 编码,所以大概率扫出来的是一串符合 Base64 特征的文本:包含大写字母 A-Z、小写字母 a-z、数字 0-9,可能还有+、/,结尾跟着一个或两个=填充符。
例如这样的字符串:
ZmxhZ3tUaGlzX2lzX25vdF9yZWFsX2ZsYWd9看到这种格式,第一件事就是直接解码。用 Python 一行搞定:
import base64 s = "ZmxhZ3tUaGlzX2lzX25vdF9yZWFsX2ZsYWd9" print(base64.b64decode(s))如果解出来是正常可读文本,那就结束了;但真实比赛里往往不会这么顺利。很多时候你解出来的不是明文,而是另一串看起来更奇怪的字符,甚至是一堆不可打印的乱码。这时候大多数新手会怀疑自己解码错了,然后反复确认 Base64 没写错。实际上,这个流程没有错,乱码可能只是链路还没走完。
3.2 “解出来是乱码”不代表解错:可能是加密密文
这就是 Base64 题最经典的坑:Base64 是编码,不是加密。它可以把任意二进制数据转成可见字符,所以 Base64 解码后完全可能是某种加密算法产出的密文,也就是“非人话”。
怎么判断这是不是密文?一个很明显的特征是开头。如果 Base64 解出来的内容开头是U2FsdGVkX1,那么恭喜,这基本可以确定为 OpenSSL 标准 salted 格式的密文,后面跟着的是一段加密后的二进制数据。这类密文在 Rabbit、AES-128-CBC 等加密结果里非常常见。
举个例子,如果扫描得到的字符串是:
U2FsdGVkX1//ZyB9oDhkCbFu9c9T8C3hZ2nr3xkPYGDhFp262ZcZEELEgSzkB2b3rHGif3DKviQ==先用 Base64 解,得到的是以“Salted__”开头的字节串,但直接看仍然不是 flag。到这里,真正的工作才刚开始:你需要识别这层加密叫什么,然后解密。
3.3 贝斯家族:不止有 Base64,还有 Base32、Base58、Base85
题目名虽然只写了“贝斯”,但出题人可以在一道题里玩好几次谐音。我曾经见过一串字符先用 Base64 解出来,结果内容是一段 Base32,再解才是 flag。Base 家族的成员特征要能迅速分辨:
| 编码 | 字符集特征 | 常见开头/结尾特征 | 典型用途 |
|---|---|---|---|
| Base16 | 只有 0-9 和 A-F | 全是十六进制字符 | hex 转字符串 |
| Base32 | A-Z 和 2-7 | 结尾常有一串=,数量较多 | 短链接、OTP 密钥 |
| Base58 | 去掉 0、O、I、l 等易混字符 | 没有=,字符集范围小 | 比特币地址 |
| Base64 | A-Z、a-z、0-9、+、/ | 最长出现 2 个= | 通用编码 |
| Base85 | ASCII 可打印字符 | 几乎见不到= | 二进制紧凑编码 |
实际判断时,最快的方式不是人眼逐个看,而是用 CyberChef 的 Magic 模块。Magic 可以自动识别输入字符串最可能的编码方式,并尝试解码。把二维码扫出来的字符串丢进去,如果它有编码层,通常几秒钟就能看到可读结果。但 Magic 不是万能的,它擅长处理编码,不擅长处理加密,一旦遇到需要密钥的加密算法,Magic 只会卡在“看起来像乱码”这一步。
4. 从一串特征字符串到最终 flag——识别那一层“加密”
4.1 常见“伪乱码”的特征速查
当字符串经过 Base64 解码后仍然不是人话,很多人会陷入“不知道该往哪搜”的状态。我的经验是先从特征入手,用特征去匹配算法。这里列一个简化版的速查表,直接能定位大多数常见情况:
| 特征 | 大概率方向 | 处理方式 |
|---|---|---|
以U2FsdGVkX1开头的 Base64 | Rabbit / OpenSSL salted 加密 | Base64 解码后用 Rabbit 解密 |
| 全是十六进制字符且长度为偶数 | hex 编码 | bytes.fromhex() 转字节流再判断 |
| 大量 0 和 1 | 二进制编码 | 每 8 位转 ASCII |
| 所有字母相对于原字母有固定位移 | 凯撒密码 | 尝试 0-25 位移或词频分析 |
| 字符按列读或按固定长度分组有规律 | 栅栏密码 | 按行数/栏数尝试重组 |
肉眼可见的=较多 | Base32 | 先按 Base32 解 |
| 解出来又是 Base64 特征串 | 可能是多层 Base64 嵌套 | 连续解码直到出现非 Base64 特征 |
这表的目的不是让你把所有算法背下来,而是在解到“乱码”时有一个大概的方向,能快速决定下一步用什么工具。CTF 杂项里最怕的不是不会算法,而是对着乱码发愣,连搜索的关键词都想不到。
4.2 Rabbit 与 OpenSSL:那一串“U2FsdGVkX1”背后的东西
先解释一下为什么U2FsdGVkX1这么有辨识度。OpenSSL 在加密时,会在密文前加 8 字节的盐值,并在开头写入固定的魔数Salted__,对应的十六进制是53 61 6c 74 65 64 5f 5f。这段加上随机盐值再经过 Base64 编码后,固定以U2FsdGVkX1开头。所以以后只要看到这个前缀,就能确定用的是 OpenSSL 系列工具加密,常见对应算法包括 Rabbit、AES-128-CBC、AES-256-CBC 等。
解密思路是先区分算法,再找密钥。算法可以通过密文长度、题目提示或尝试常用几种来判断;密钥则往往隐藏在题目名、文件名、图片里的备注信息中。比如这里“贝斯”除了指 Base,也可以直接当作密钥候选字,试着用base或bass解密一次,成本极低,万一中了就直接省掉一大部分时间。
实操中可以优先用在线 Rabbit 解密工具,把 Base64 解码后的内容放进密文框,密钥填候选词,点解密。如果解出来是flag{...}或类似可读文本,就成功了。Python 脚本同样能做,但不同环境对 Rabbit 算法的封装支持不同,使用在线工具反而更通用。重点在于:不要猜完密钥就放弃,多试几个题目上下文里的单词。
4.3 逐步验证:最终 flag 应该长什么样
解密得到结果后,别急着提交,先做三层自检:
- 字符串是否可读且语义通顺。flag 通常是完整的英文短语或带下划线的单词组合,不会是一串无意义乱码。
- 是否包含比赛指定的花括号格式。安洵杯这类比赛大多标准为
flag{...},如果解出来是安洵杯{...},也不用奇怪,以题目页面公示的提交格式为准。 - 提交前保留完整解密链记录。从二维码原文到每层解码结果都存下来,万一哪一步理解错了,可以快速回溯,不用从头再扫一遍二维码。
我在实际做题时经常遇到一种情况:解密出来确实是人话,但总感觉缺了后半段。这时候回头看,往往是中间某层 Base64 解码时多了或少了填充字符,导致后半段密文被截断。遇到这种“半人话”结果,优先考虑编码填充问题,而不是算法问题。
5. 这题教会我的“编码解码链”套路与 CTF 杂项工具箱
5.1 通用流程:从图片到 flag 的判断链路
做完这道题,我把整个流程抽象成了一条可复用的链路:
- 拿到附件,先做文件类型与隐写检测(file、binwalk、strings)。
- 如果附件是二维码,先尝试常规识别;失败则进入修复流程。
- 识别得到的字符串,先判断编码类型,优先用 CyberChef Magic 自动探测。
- 解码后如果可读,直接拿 flag;如果不可读,判断是否为加密特征。
- 依据特征匹配算法,从题目名、文件名中找密钥线索,解密得到最终 flag。
这套流程对大量“图片 -> 字符串 -> 编码/加密”类杂项题都成立。把链路记熟之后,做题的速度会快很多,因为每一步你都知道“接下来该干嘛”,而不是瞎试工具。
5.2 工具清单:真正用得上的就这些
杂项工具五花八门,但我实际只用这几样就把这道题和同类题跑通了:
| 工具 | 用途 | 使用时机 |
|---|---|---|
| file / binwalk / strings | 文件探测、隐写发现 | 拿到附件第一步 |
| QRazyBox | QR 码分析和修复 | 二维码扫描失败或损坏 |
| pyzbar + OpenCV | 二维码识别、反色处理 | 需要自动化批量识别时 |
| CyberChef | 编码、解码、Magic 自动识别 | 识别 Base 家族和常见编码 |
| Python3 | 手写解码/解密脚本 | 工具覆盖不到时的高度自由方案 |
| 在线解密工具 | Rabbit / 古典密码解密 | 识别出加密特征之后 |
这里多说一句,工具不在多,在于你知道每个工具解决什么问题。很多新人收藏了十几个工具包,遇到题却每个都打开点一遍,反而浪费时间。
5.3 谐音题出题规律:题目名里的每个字都可能是线索
CTF 杂项出题人很喜欢用谐音梗来“善意提醒”解题方向。贝斯指 Base,栅栏可能指栅栏密码,猪圈可能指猪圈密码,“摩斯”指摩斯电码,“凯撒”指凯撒密码。看到一个奇怪题目名,先把所有可能的谐音和双关写在纸上,再对照附件内容,往往能提前锁定思路。
这道“吹着贝斯扫二维码”最典型:名字里就写着“Base”和“二维码”,所以整个解题主线几乎是被题目名牵着走的。如果你拿到题名先懵了一会儿,再对着二维码研究半天才想起来 Base,那说明下次要先在题目名上多花三十秒。
我在实际做这道题时,印象最深的不是二维码本身,而是“Base64 解码后得到乱码”那一步。那一步卡了我将近二十分钟,反复确认自己没有解码错,直到看到U2FsdGVkX1前缀才反应过来这已经进入加密层了。从那以后,我养成了一个习惯:任何 Base64 解码结果都不急着下结论,先看一眼开头特征,再决定下一步。现在把这个习惯和整套链路整理出来,希望你能少走我走过的这段弯路。