SpiritCTF 2020 – Misc Official Writeup

本博客为 SpiritCTF 2020(吉林大学 CTF 校赛)Misc 部分的官方题解。本次比赛共放出 Misc 题目 8 道,题解按照题目难度从低至高排序。

something so fast

本题题目为快速播放内容的 GIF 图片,可以使用 StegSolve 工具的 Frame Browser 抽取每一帧,之后扫描二维码即可得到 Flag。

YLBNB

本题目为图片隐写,放大后可以观察到部分颜色与背景不同,可以使用画图的填充工具或 StegSolve 的单通道位面查看。

其中,部分字符串难以辨认。实际上,文件头处可以看到文本状态的 Flag。

此为 Photoshop 编辑图片时留存的图层元信息,可以用于数据取证。

锟斤拷

通过检索可知,“锟斤拷” 的成因是 Unicode 的替换字符(Replacement Character,�)于 UTF-8 编码下的结果 EF BF BD 重复,在 GBK 编码中被解释为汉字 “锟斤拷”(EF BF BD EF BF BD)。因此对题目字符串使用 GBK 编码,并以 UTF-8 解释即可。(也就是逆着进行锟斤拷的操作)

之后将结果中的全角字符转换为半角字符即可得到 Flag。

大佬的学习计划表

数据取证题目。题目为一系列日期,结合题干 “日期表” 的提示,在日历中标记给定日期即可得到 Flag。

图片来自 “黑框眼镜” 队 Writeup

搞颜色

通过 txt 内容结合检索可以得知,\033[为 VT100 终端控制码的开始标志。因此使用类 UNIX 系统的 Terminal(或其他 VT100 终端)执行指令打印即可:echo -e `cat flag.txt`。若显示不清楚,可以适当调整字体或选择文本。

这么小声还想解压

本题为音频隐写。直接听音频可以明显听出部分音频较刺耳、人声失真。使用 Audition 查看频谱,可以观察到该时段低音部分被替换为其他音频。

图片来自 “黑框眼镜” 队 Writeup

提取该段音频后可发现,该段音频为双音多频信号(DTMF),因此可以对照频谱与查找表,或直接使用识别工具 dtmf-decoder 执行 python dtmf.py -v extract.wav 即可得到数字。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ python dtmf.py -v extract.wav
0:00 11111...#....5..55.5
0:01 .......88888.8......
0:02 ..4444444........777
0:03 7777.......5.55555..
0:04 ......6666666.6....3
0:05 3333333.......222222
0:06 2........2222222....
0:07 .
$ python dtmf.py -v extract.wav 0:00 11111...#....5..55.5 0:01 .......88888.8...... 0:02 ..4444444........777 0:03 7777.......5.55555.. 0:04 ......6666666.6....3 0:05 3333333.......222222 0:06 2........2222222.... 0:07 .
$ python dtmf.py -v extract.wav
0:00 11111...#....5..55.5
0:01 .......88888.8......
0:02 ..4444444........777
0:03 7777.......5.55555..
0:04 ......6666666.6....3
0:05 3333333.......222222
0:06 2........2222222....
0:07 .

根据题目提示,该串数字与解压有关,使用 binwalk 工具(binwalk -e spirit.mp3)可发现文件尾为 Zip 压缩包。使用 DTMF 隐写得到的数字做解压密码即可解答得到 flag.txt。

双重保险

使用相关 Python 逆向工具或在线网页(https://tool.lu/pyc/ 等)可以得到题目的 Python 源码。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def xor(a, b):
return bytes([x[0] ^ x[1] for x in zip(a, b)])
def load_asset():
return open('data', 'rb').read()
def check(data, key1, key2):
key1 = int(key1)
cipher = data[key1: key1 + 26]
return xor(key2.encode(), cipher) == b'Kvbm4aeoZzR5upGgKjqPE39ovM'
if __name__ == '__main__':
data = load_asset()
key1 = input('Plz input password 1:')
key2 = input('Plz input password 2:')
try:
result = check(data, key1, key2)
except Exception as e:
result = False
if result:
print('Correct!')
else:
print('Nope. Try again!')
def xor(a, b): return bytes([x[0] ^ x[1] for x in zip(a, b)]) def load_asset(): return open('data', 'rb').read() def check(data, key1, key2): key1 = int(key1) cipher = data[key1: key1 + 26] return xor(key2.encode(), cipher) == b'Kvbm4aeoZzR5upGgKjqPE39ovM' if __name__ == '__main__': data = load_asset() key1 = input('Plz input password 1:') key2 = input('Plz input password 2:') try: result = check(data, key1, key2) except Exception as e: result = False if result: print('Correct!') else: print('Nope. Try again!')
def xor(a, b):
    return bytes([x[0] ^ x[1] for x in zip(a, b)])


def load_asset():
    return open('data', 'rb').read()


def check(data, key1, key2):
    key1 = int(key1)
    cipher = data[key1: key1 + 26]
    return xor(key2.encode(), cipher) == b'Kvbm4aeoZzR5upGgKjqPE39ovM'


if __name__ == '__main__':
    data = load_asset()
    key1 = input('Plz input password 1:')
    key2 = input('Plz input password 2:')
    try:
        result = check(data, key1, key2)
    except Exception as e:
        result = False
    if result:
        print('Correct!')
    else:
        print('Nope. Try again!')

可以看到,其逻辑为读入两个密码 key1key2,以 key1 做偏移读取一段长度数据,异或 key2 并与结果比较。根据异或运算的性质,key2 可以与结果互换,因此只需要得到 key1 就可以计算结果。考虑到 Flag 有固定格式,因此可以爆破 key1 并以 Flag 格式作为剪枝手段。EXP 代码如下

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def xor(a, b):
return bytes([x[0] ^ x[1] for x in zip(a, b)])
def load_asset():
return open('chuti/data', 'rb').read()
def check_ans(ret):
return ret[:6] == b'Spirit'
def check(data, key1, key2):
key1 = int(key1)
cipher = data[key1: key1 + 26]
return xor(key2.encode(), cipher)
data = load_asset()
key2 = 'Kvbm4aeoZzR5upGgKjqPE39ovM'
mx = len(data)
count = 0
for i in range(mx - 26):
ans = check(data, i, key2)
if check_ans(ans):
count += 1
print(i, ans)
print('done', mx, count)
def xor(a, b): return bytes([x[0] ^ x[1] for x in zip(a, b)]) def load_asset(): return open('chuti/data', 'rb').read() def check_ans(ret): return ret[:6] == b'Spirit' def check(data, key1, key2): key1 = int(key1) cipher = data[key1: key1 + 26] return xor(key2.encode(), cipher) data = load_asset() key2 = 'Kvbm4aeoZzR5upGgKjqPE39ovM' mx = len(data) count = 0 for i in range(mx - 26): ans = check(data, i, key2) if check_ans(ans): count += 1 print(i, ans) print('done', mx, count)
def xor(a, b):
    return bytes([x[0] ^ x[1] for x in zip(a, b)])

def load_asset():
    return open('chuti/data', 'rb').read()

def check_ans(ret):
    return ret[:6] == b'Spirit'

def check(data, key1, key2):
    key1 = int(key1)
    cipher = data[key1: key1 + 26]
    return xor(key2.encode(), cipher)


data = load_asset()
key2 = 'Kvbm4aeoZzR5upGgKjqPE39ovM'
mx = len(data)
count = 0

for i in range(mx - 26):
    ans = check(data, i, key2)
    if check_ans(ans):
        count += 1
        print(i, ans)

print('done', mx, count)

彩蛋

data 实际上为 BMP 格式图片,原图为

baby_png

本题首先为 PNG 格式图片修补。使用 010Editor 打开文件,可以观察到解析错误,文件最后的 IDAT 段长度为 0,显然被篡改。

在之后的数据中,可以发现偏移 36278h 处存在 PNG 块标志 IDAT,因此猜测前 4 字节(36274h)之前为上一数据块内容。故可以计算得到上一 IDAT 块真正的大小为 36274h(块尾)-4(CRC 段)-3000Ch(块首部类型字段后)=25188。修改即可得到正确的图片(CRC 校验也通过)。

可以看到隐藏部分为指令行的一部分。并且修补得到的 PNG 的 36274h 偏移处有一个不自然的 IDAT 段:

  1. 正常 PNG 图片通常填满上一段数据才会产生下一 IDAT
  2. 该段内容全部为可见字符,通常 PNG 图片采用熵编码,全部为可见字符的情况几乎不可能出现

结合指令行中.b64 的提示,此处 IDAT 段应该是隐写了一段 Base64。解码 Base64 后得到一二进制文件,使用 file 指令分析可以发现该文件为 openssl enc 指令产生的文件。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ file secret
secret: openssl enc'd data with salted password
$ file secret secret: openssl enc'd data with salted password
$ file secret
secret: openssl enc'd data with salted password

因此参考指令手册与给出的部分指令,可以写出解密指令:openssl enc -d -des3 -pbkdf2 -in secret -pass pass:sorakwii -out flag.zlib。由后缀名.zlib 猜测该文件采用 Zlib 进行压缩,因此使用 Zlib 进行解压可以得到一串 01,转为数字后以 ASCII 编码解码即可得到 Flag。

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片
  4. 没有图片

评论

  1. 桃花岛来客 2020.10.16 10:53 下午

    学习了!

  2. love 2020.10.23 4:12 下午

    吉大 <b>vpn</b> 账号可以借我用用吗?

  3. 1u1u 2021.01.28 2:17 上午

    请问,有 lcg6 密码的 wp 吗

    • KAAAsS 2021.01.28 11:14 上午

      lcg6 在密码学出题人博客里,可以到比赛群看看

      • pping 2022.05.14 10:38 下午

        请问出题人博客在哪里,或者麻烦师傅给下比赛群。找了好久 wp 都没有找到 QAQ

  4. pping 2022.05.14 10:41 下午

    师傅有密码学出题人的博客地址吗,找了好久 lcg6 的 wp 都没找到 QAQ

在此评论中不能使用 HTML 标签。