本博客为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。
搞颜色
通过txt内容结合检索可以得知,\033[为VT100终端控制码的开始标志。因此使用类UNIX系统的Terminal(或其他VT100终端)执行指令打印即可:echo -e `cat flag.txt`。若显示不清楚,可以适当调整字体或选择文本。
这么小声还想解压
本题为音频隐写。直接听音频可以明显听出部分音频较刺耳、人声失真。使用Audition查看频谱,可以观察到该时段低音部分被替换为其他音频。
提取该段音频后可发现,该段音频为双音多频信号(DTMF),因此可以对照频谱与查找表,或直接使用识别工具dtmf-decoder执行python dtmf.py -v extract.wav即可得到数字。
$ 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源码。
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!')
可以看到,其逻辑为读入两个密码key1、key2,以key1做偏移读取一段长度数据,异或key2并与结果比较。根据异或运算的性质,key2可以与结果互换,因此只需要得到key1就可以计算结果。考虑到Flag有固定格式,因此可以爆破key1并以Flag格式作为剪枝手段。EXP代码如下
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段:
- 正常PNG图片通常填满上一段数据才会产生下一
IDAT段 - 该段内容全部为可见字符,通常PNG图片采用熵编码,全部为可见字符的情况几乎不可能出现
结合指令行中.b64的提示,此处IDAT段应该是隐写了一段Base64。解码Base64后得到一二进制文件,使用file指令分析可以发现该文件为openssl enc指令产生的文件。
$ 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。














学习了!
吉大 <b>vpn</b> 账号可以借我用用吗?
抱歉不行~
请问,有lcg6密码的wp吗
lcg6在密码学出题人博客里,可以到比赛群看看
请问出题人博客在哪里,或者麻烦师傅给下比赛群。找了好久wp都没有找到QAQ
师傅有密码学出题人的博客地址吗,找了好久lcg6的wp都没找到QAQ
在这:https://badmonkey.site/archives/spirit-ctf-2020
不过其实也没有 lcg6(小声)
好的,谢谢师傅啦!