2020 CSICTF WriteUp
“csictf team” 是印度 VIT 大学(Vellore Institute of Technology)计算机学会(CSI)学生分部所运营的团队
Misc
Gradient sky(图片隐写)

查看字符串拿到 flag
1 | strings sky.jpg |
unseen(通道隐写 + Steghide 隐写 + Whitespace)
对 nyc.png 做 LSB 位平面分析,红色通道第 0 位平面左上角能看到数字

解 morse.wav 的摩斯电码,得到
1 | SEARCH-SOMEWHERE-ELSE |
这个提示说明还要继续在音频里找隐藏内容。用图片得到的数字作为 steghide 密码
1 | steghide extract -sf morse.wav -p 42845193 |
提取出 flag.txt。该文件表面上是空白,实际是 Whitespace 语言代码,解码后得到 flag
1 | csictf{7h47_15_h0w_y0u_c4n_83c0m3_1nv151813} |
Panda(ZIP 爆破)

爆破出密码是 2611,提取出两张图片
逐字节比较两张图拿到 flag
1 | from pathlib import Path |
1 | csictf{kung_fu_p4nd4} |
In Your Eyes(QuickStego + 盲文)

使用 QuickStego 工具拿到一段十六进制

再盲文进行解码,即可得到 flag:csictf{ucbr4ill3}
Archenemy(LSB 隐写)

Steghide 在图像中找到了一个嵌入的 flag.zip 文件
爆破密码是 kathmandu,解压得到 flag:csictf{1_h0pe_y0u_don't_s33_m3_here}
Crypto
little RSA(分解 n)
题目给了
1 | c=32949 |
分解 n 得到 p,q

1 | from Crypto.Util.number import long_to_bytes |
解出为 18429 解压密码
Rivest Shamir Adleman(分解 n)
和上题一样的方法

1 | from math import isqrt |
Modern Clueless Child(XOR)

题目给的是
1 | 52f41f58f51f47f57f49f48f5df46f6ef53f43f57f6cf50f6df53f53f40f58f51f6ef42f56f43f41f5ef5cf4e |
按 f 分割得到
1 | 52 41 58 51 47 57 49 48 5d 46 6e 53 43 57 6c 50 6d 53 53 40 58 51 6e 42 56 43 41 5e 5c 4e |
转成 ASCII 得到一段密文
1 | RAXQGWIH]FnSCWlPmSS@XQnBVCA^\N |
将题目给的 Key 转为 ASCII 字符
1 | '1' = 0x31 |
然后对密文循环进行 XOR
1 | cipher_hex = "52f41f58f51f47f57f49f48f5df46f6ef53f43f57f6cf50f6df53f53f40f58f51f6ef42f56f43f41f5ef5cf4e" |
得到 csictf{you_are_a_basic_person}
Quick Math(低指数广播攻击)

1 | from math import prod, gcd |
Mein Kampf(Enigma M4 参数残缺恢复)

题目给的文件内容核心是
1 | M4 UKW $ Gamma 2 4 $ 5 9 $ 14 3 $ 5 20 fv cd hu ik es op yl wq jm |
题目描述说:
1 | some data was corrupted during transmission |
所以里面的 $ 就是传输损坏的字段,需要我们恢复
根据内容基本可以判断是德国 Enigma M4
1 | M4 -> 四转子 Enigma |
Enigma M4 的薄反射器一般是
1 | UKW B |
普通转子可以从 I 到 VIII 里选 3 个,且顺序有影响
1 | I II III IV V VI VII VIII |
非常小,直接暴力破解即可
1 | from itertools import permutations |
得到 flag:csictf{no_shit_sherlock}
The Climb(希尔密码)

theclimb.java 里核心逻辑是矩阵加密,这是典型的希尔密码
1 | rmatrix[i] += kmatrix[i][j] * tmatrix[j]; |
源码里给了 key:gybnqkurp
长度是 9,所以是 3×3 矩阵
1 | g y b |
转换成数字,也就是 a=0, b=1, ..., z=25
1 | K = [ |
因为源码是加密程序,所以我们要反过来做解密
1 | C = K × P mod 26 |
求出矩阵 K 在模 26 下的逆矩阵
1 | K^-1 = [ |
然后把密文每 3 个字符一组解密,解出来是
1 | hillshaveeyesxx |
最后的 xx 是源码里自动补齐分组长度时加的 padding
得到 flag
1 | csictf{hillshaveeyes} |
Reverse
Esrever(算法破解)

esrever.py 里有四个加密函数
1 | enc1(text) # 随机 Caesar 偏移 |
加密流程是
1 | Encrypted Key = enc4(enc4(encryptedKey)) |
所以解密时要完全倒过来
1 | Encrypted Key -> inv_enc4 -> inv_enc4 -> 爆破 enc1 得到 key |
1 | from string import ascii_lowercase |
因为 enc1 会把 { 和 } 也映射成小写字母
在对应 Caesar 偏移下
1 | { -> a |
所以得到 flag
1 | csictfaesreverisjustreverseinreverserightc |
pydis2ctf(Python 反汇编)

C1cipher.txt 里面核心反汇编是
1 | LOAD_CONST 1 ('') |
翻译成看的 Python 代码就是
1 | def C1(text): |
所以 C1 的加密公式是
1 | cipher_char = chr(2 * ord(plain_char) - len(text)) |
反过来就是
1 | plain_char = chr((ord(cipher_char) + len(text)) / 2) |
C2cipher.txt 还原出来是
1 | def C2(inpString): |
也就是每个字符和 'S' 异或
但是实际解题时,对 encodedflag.txt 直接反 C1 就能得到 flag;C2 更像是干扰项
1 | import ast |
得到 flag:csictf{T#a+_wA5_g0oD_d155aSe^^bLy}
Scrambled Eggs(算法破解)

flag 预处理如下
1 | flag=flag.replace('{','a') |
第一层加密是循环左移,sys.maxsize 是 Python 中一个系统相关的常量,表示当前 Python 解释器所能支持的最大整数索引值
64 位系统上通常等于 2 ^ 63 - 1,再 % 28 就等于 7 了,所以范围是 0~7,可枚举
1 | def enc1(text): |
源码中
1 | key1 = 'ashikka_is_mine' |
但 ashikka_is_mine 长度是 15,替换 _ 并不会改变长度,所以源码中的 key1 是假的,真正的 key1 要从密文中恢复
key2 完全由 key1 决定
1 | key2 = enc2(enc2(key1)) |
xtfsyhhlizoiyx 有可能是 key1,也有可能是 key2,因为:
1 | # 从列表 [key1, key2] 中随机抽取 2 个元素,并以随机顺序返回,实际上就是随机打乱 key1 和 key2 的顺序 |
附件中给出了 key2 是 eudlqgluduggdluqmocgyukhbqkx,源码中 key2 是前 14 位随机和后 14 位真实 key2 混合
1 | key2 = k + key2 # 将随机字符串 k (14位) 拼到 key2 前面 |
enc2 的规则是:map[字母序] → 映射字符
1 | def enc2(text): |
逆向得到 xpsotfopspffsoptmihfvpyqjtyw
1 | MAP = "vrtpwgncobafmiluhzdqjyxeks" |
1 | k:xpsotfopspffso |
源码中
1 | # 生成 14 位随机字符串 k |
因为 k 只有 14 位,而 key2 原字符也一定在 a~z 范围内,所以我们可以穷举所有可能的原字符:
对每个位置
i:取已知的
k[i](通过之前的逆 enc2 拆解得到)枚举 26 个字母
c ∈ a~z用加密公式计算出混合后的字符
再次应用
enc2映射(因为加密最后还有一次enc2)检查是否与最终密文
key2对应位置的字符一致
筛选出所有匹配的候选
1 | from itertools import product |
运行后得到四组结果
1 | [+] candidates: |
我们之前恢复出的 key2 实际上是加密流程中间状态的 key2
在生成最终 key2 前,key1/key2 已经经过了两轮交换
因此,现在得到的是交换后的 key2
核心加密流程如下
1 | # 用 key1 和 key2 的字母值作为索引,交叉交换 flag 和密钥的字符位置,打乱顺序 |
编写解密脚本
1 | MAP = "vrtpwgncobafmiluhzdqjyxeks" |

flag 的加密过程是 flag = enc1(enc1(enc1(enc2(flag))))
附件中的 flag 是 lvvrafwgtocdrdzfdqotiwvrcqnd
通过前面的解题脚本恢复为 oaabklefcihsbsrlsticneabhtgs
然后是枚举移位
1 | def ror(s, n): |
第 11 次得到 csictfaallbthebkingsbhorsesa,但它仍然经过了交换处理,继续编写解密脚本
恢复 flag 时必须逆向整个交叉置换过程,同步恢复 key1/key2 的中间状态,而不能直接用最终的 key 去解 flag
1 | def undo_all(flag, key1, key2): |
得到 csictfaallbthebkingsbhorsesa
源码最开始有对 flag 进行预处理
1 | flag.replace('{','a') |
最终 flag 是 csictf{all_the_kings_horses}