题目描述:Let's see if you're a QuickR soldier as you pretend to be.
解题参考:https://sequr.be/blog/2020/05/quickr/
解题参考:https://0awawa0.medium.com/htb-quick-r-425b2012567c
本题为远程环境题目,在终端中通过nc进行访问。
如果直接扫描二维码的话,可以识别到结果为:
14.747533572766981 / 108.60719112210555 / 9.512112839301214 =
我们接下来需要做的就是计算这个数学等式的结果,并将结果输入提交即可拿到flag。
但是我们发现提交时间太晚了,结合前面的提示 you got only 3 seconds!
,我们只有3秒钟的时间去提交结果,而且如果提交错误的结果也是无法通过的。
综上,我们只有3秒钟的时间去完成这一系列的操作才可以拿到flag,大致流程就是:
(1)打开连接,读取服务端响应的数据
(2)提取二维码
(3)识别二维码
(4)计算数学等式
(5)提交正确的计算结果
(6)接收flag
其中最复杂的一步就是提取二维码。下面我们逐步完成该题的解题步骤。
1.打开连接,读取服务端响应
我们可以通过 pwntools 模块与服务器进行交互,读取服务器传回的数据。
首先,我们使用 nc 测试与服务器交互的内容:
➜ 2-QuickR nc 144.126.230.162 32190
___ _ __ _______
.' `. (_) [ | _ |_ __ \
/ .-. \ __ _ __ .---. | | / ] | |__) |
| | | | [ | | | [ | / /'`\] | '' < | __ /
\ `-' \_ | \_/ |, | | | \__. | |`\ \ _| | \ \_
`.___.\__|'.__.'_/[___]'.___.'[__| \_]|____| |___|
[*] Hello there! Let's see if you are an QuickR soldier, you got only 3 seconds!
...
QR code content...
...
[+] It's important to realise that this is, in a real sense, an illusion: you simply need the true machine value.
[!] Decoded string:
我们可以使用下面的代码来接收二维码部分的内容:
1
2
3
4
5
6
7
8
9
10
11
12
|
#python3 code : exp.py
#代码中字符串前面的b"",表示处理的该数据是字节流类型的数据
from pwn import *
p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
print(data) #ANSI转义字符表示的二维码
print(data.decode()) #decode()函数可以解码字节流数据为字符串格式,可以实现解析ANSI转义字符
p.close()
|
执行后的效果:
1
2
3
4
5
6
|
➜ python3 exp.py
[+] Opening connection to 144.126.230.162 on port 32190: Done
#(1)显示data变量存放的ANSI转义字符表示的二维码数据
b'\t\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\n\t\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1b[0m\x1b[7m \x1
#(2)显示解析ANSI转义字符后显示的data变量存放的二维码
...有颜色的二维码图...
|
我们可以看到 data 变量中有很多的ANSI转义字符,通过对ANSI转义字符的解析得到了我们在终端中看到的二维码。
为了查看 data 变量中存放数据的规律,我们先将data变量存放到文件data.txt
中,使用如下代码即可创建包含完整 data 变量数据的文件data.txt
。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from pwn import *
p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n") #存放的ANSI转义字符表示的二维码数据
lines = data.split(b"\n") #以"\n"为分隔符分割每一行
f = open("data.txt", "wb")
for line in lines:
f.write(line+b"\n")
p.recvuntil(b"Decoded string:")
f.close()
p.close()
|
我们使用 sublime 打开data.txt
进行查看(建议先关闭 sublime 的自动换行)。
特征1:可以看到这部分数据一共有 53 行,实际包含数据的行数只有 51 行。
特征2:代码中有特别多的双空格(两个空格字符)。
特征3:每个双空格左右两边都有一个ANSI转义字符。两个ANSI转义字符实现对一个双空格的包裹。
我们知道ANSI转义字符是可以对字符串设置颜色的。基本颜色表示如下:
详细信息请查看:https://ss64.com/nt/syntax-ansi.html
其中,这里的Esc
字符对应的的十六进制值就是我们看到的data.txt
中大量出现的<0x1b>
。
也就是说在data.txt
中出现了大量的Esc[7m
、Esc[0m
、Esc[41m
。
对应上表中实现的效果就是:Esc[7m
:反转当前终端前景色(文字)的颜色、Esc[0m
:重置终端为默认颜色、Esc[41m
:设置背景色(字体下面的背景颜色)为红色。
可能只看描述的话感觉有点绕。下面我们在 python 环境中显示这些控制字符的效果。
①默认情况下,当前的演示终端颜色为黑底白字(黑色背景,白色字体)
②接下来,我们使用Esc[7m
反转当前前景色(文字)的颜色。也就是把前景色(文字)的颜色和背景色进行互换。也就是黑底白字反转为白底黑字。
可以看到,此时已经实现了黑底白字变为白底黑字。
③继续,我们使用Esc[0m
重置终端为默认颜色,也就是这里演示终端的黑底白字(黑色背景,白色字体)
④继续,我们使用Esc[41m
设置背景色(字体下面的背景颜色)为红色。
上面的演示中,我们通过ANSI转义字符实现了对终端前景色和背景色的修改。改完一次之后,后续当前终端会一直使用这种配色方案。这是一种使用ANSI转义字符的用途。
⑤ANSI转义字符还可以实现为输出的字符串设置颜色。比如我们可以给字符串"hello"设置为红色字体。
我们设置完前景色红色Esc[31m
后,紧接着设置了要显示的内容hello
,注意字符串hello
左右没有空格,最后,为了不影响后续的终端配色方案,我们还需要使用Esc[0m
还原之前的终端配色方案。这种用法也是很常用的。
⑥回到题目里,我们可以看到,我们接收到的二维码数据部分第一行是白色的。
那么这是怎么实现的呢?
查看我们保存的data.txt
文件,我们可以看到第一行开始就是持续的重复的<0x1b>[7m <0x1b>[0m
,也就是\x1b[7m \x1b[0m
。注意两个ANSI转义字符之间的两个空格,本质上就是通过这两个ANSI转义字符为这个两个空格上色。因为当前演示的终端默认配色方案为黑底白字,那么这里的上色效果就是:先反转颜色,实现白底黑字,接着还原终端默认配色,最终实现了将这个双空格上色为白色的效果。
我们将第一行中的复制到新的记事本中,搜索关键字<0x1b>[7m <0x1b>[0m
。
可以看到一共包含51组,也就是说二维码的第一行白色就是由这51组双空格设置为白色实现的。
相应的,对应data.txt
的51行数据,那么就可以理解为这个二维码就是51*51
像素块的数据。
我们还可以观察到,文件data.txt
的前3行,后3行以及左侧3组和右侧组的都是相同的内容(<0x1b>[7m <0x1b>[0m
),也就是都是白色的格子。
刚好对应了二维码的显示效果。
除了<0x1b>[7m <0x1b>[0m
显示为白格子之外,还有一组显示为:<0x1b>[41m <0x1b>[0m
,这组的显示效果就是渲染红格子。data.txt
通过白格子和红格子的拼接,最终呈现出了一个完整的二维码。
2.提取二维码
明白了上面二维码实现的原理之后,我们就可以按照像素块的方式提取还原二维码了。
白色格子:<0x1b>[7m <0x1b>[0m
红色格子:<0x1b>[41m <0x1b>[0m
我们可以对每一行的内容进行匹配替换。匹配白色格子并替换为-
,表示二维码的空白部分;匹配红色格子并替换为*
,表示二维码的数据部分。
1
2
3
4
5
6
7
8
9
|
#code test : test.py
f = open("data.txt", "rb")
lines = f.readlines()
for line in lines:
line = line.decode("utf-8") #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
line = line.replace("\t", "").replace("\n", "") #去除每一行中的"\t"和"\n"
line = line.replace("\x1b[7m \x1b[0m","-") #替换白色格子为"-"
line = line.replace("\x1b[41m \x1b[0m","*") #替换红色格子为"*"
print(line)
|
输出效果:
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
|
➜ 2-QuickR python3 test.py
---------------------------------------------------
---------------------------------------------------
---------------------------------------------------
---*******-*--*-*---**-**-**-*-*-*--*--*-*******---
---*-----*-***--*-------*--*-*--*---*-*--*-----*---
---*-***-*-*-****-*--**--**-***-***-*-*--*-***-*---
---*-***-*---******-----*-----**----*-**-*-***-*---
---*-***-*--********-********-**-**--***-*-***-*---
---*-----*-*-*--*-*--***---****-*--------*-----*---
---*******-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*******---
-----------*****-***---*---*-*----***--*-----------
-----***-*-**----***--******-**-**--*-*-***--***---
----*--**-***-*-*--**-**-*-*-*---*-**--*---***-*---
------**-**--*--***-*---*---**-*-----*-****--------
---*--*-*--*-**---**-****-****-******-**---*-***---
-----*****--*--**-*-****-*-*******--*-*-*-*-*-**---
---**-**--**--*****-**-*****-----**-*--*---*****---
---*-**-***-*-------***-****---**-*--*--**--**-----
---*-**----****--*-*-*-*****--*-*-***-**---*-**----
----*-*--*-----**--*--***-*-*--*----**---**-*-*----
---***--*---**-**--*-***-***---**--**--*---*-***---
-----*---*---*---*---*-**---**-*-***-*-*-*---------
-------*--*-****--**--******-----***-----*-*-*-*---
----********-**-****-*******-*****--*********-*----
---**-**---*----***-**-*---*-**--**-*-**---*****---
-------*-*-*-*------*--*-*-**-*-**---*-*-*-*-------
---*-*-*---*-***-*-**-**---*--**-*---*-*---*-*-----
----********-*-**-*-*-******-**---**---******-**---
----**-*----*****--*-*--**-*---*---**-*-*-*--***---
----**--**--***--*-*--*-----**********-*--*--*-----
---***-**-*---**-**---*-**-**-*-*-**--****---*-----
-----*****-*-*--**--***-*-**-*---------*---**--*---
---**-**--*--*-----*-***--**-****---*-*---*--*-*---
-----******-*****--*-***-**--*--*-*--*----**-*-----
---***-*---*--*-********--**-*-*--**--**-*-*-*-----
-----**-**--***-*--******---***-*---**-*---**------
----**--*-*----**---**--*--**----*--*-*--**---**---
-------*-*--**----*---*----*-****--*-**-----**-----
----****---*-*-*--*----*-*--*-****-**-******-**----
---*--**-*-*------*-**-******-*-*----*-*****---*---
-----------***-*---*--**---****---*-*--*---*****---
---*******------****--**-*-*-*-------***-*-*-*-----
---*-----*----***-*-*--*---*----*-**---*---*-**----
---*-***-*-**-*****-********---*-----*-******-*----
---*-***-*-*-*-*-**-**---*--*-*-*--**-*-*--*--**---
---*-***-*-**---**----*--**-******---***-**-**-----
---*-----*--****--*-***-*--**--*-*--*--*---*-*-----
---*******--------**--*-*--*--*-----*--****-*-*----
---------------------------------------------------
---------------------------------------------------
---------------------------------------------------
|
继续修改下上面的代码,我们使用 Pillow 模块,将用*
和-
表示的二维码,按照像素点填充的方式,将其转换为一张51*51
像素的黑白图片。
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
|
#code test : test.py
from PIL import Image
from PIL import ImageDraw
f = open("data.txt", "rb")
lines = f.readlines()
img = Image.new("1", (51,51), 1) #创建了一个新的图像对象img
#Image.new(mode, size, color)
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img) #创建一个用于在图像上进行绘制的对象draw
y = 0 #初始行数为0
for line in lines:
line = line.decode("utf-8") #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
line = line.replace("\t", "").replace("\n", "") #去除每一行中的"\t"和"\n"
line = line.replace("\x1b[7m \x1b[0m","-") #替换白色格子为"-"
line = line.replace("\x1b[41m \x1b[0m","*") #替换红色格子为"*"
#print(line)
x = 0 #初始列数为0,每次开始新的行数,列数都重新从0开始
for char in line:
if char == "*":
draw.point((x, y), 0) #填充黑色像素块
elif char == "-":
draw.point((x, y), 1) #填充白色像素块
else:
print("character error.")
x += 1
y += 1
img.save("qr.png") #保存生成的二维码
f.close()
|
执行后,我们就可以在当前文件夹下找到新生成的二维码文件qr.png
。
1
|
➜ 2-QuickR python3 test.py
|
可以看到,我们已经成功的还原了二维码。
接下来我们只需要将上面的代码进行稍微的调整下,就可以用在解题脚本exp.py
中了。修改后的exp.py
脚本内容如下:
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
|
from PIL import Image
from PIL import ImageDraw
from pwn import *
p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
lines = data.split(b"\n") #以"\n"为分隔符分割每一行
#Image.new(mode, size, color)
img = Image.new("1", (51,51), 1) #创建一个新的图像对象img
#Image.new(mode, size, color)
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img) #创建一个用于在图像上进行绘制的对象draw
y = 0 #初始行数为0
for line in lines:
line = line.decode("utf-8") #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
line = line.replace("\t", "").replace("\n", "") #去除每一行中的"\t"和"\n"
line = line.replace("\x1b[7m \x1b[0m","-") #替换白色格子为"-"
line = line.replace("\x1b[41m \x1b[0m","*") #替换红色格子为"*"
#print(line)
x = 0 #初始列数为0,每次开始新的行数,列数都重新从0开始
for char in line:
if char == "*":
draw.point((x, y), 0) #填充黑色像素块
elif char == "-":
draw.point((x, y), 1) #填充白色像素块
else:
print("character error.")
x += 1
y += 1
img.save("qr.png") #保存生成的二维码
p.recvuntil(b"Decoded string:")
p.close()
|
运行上面的脚本,即可直接从服务端获取二维码数据,然后还原二维码到文件qr.png
中。
1
2
3
|
➜ 2-QuickR python3 exp.py
[+] Opening connection to 144.126.230.162 on port 32190: Done
[*] Closed connection to 144.126.230.162 port 32190
|
3.识别二维码
接下来我们需要使用 pyzbar 模块识别图片qr.png
中的二维码读取出要计算的数学等式。
macos安装pyzbar模块:
1
2
3
|
brew install zbar
pip install pyqrcode
pip install pyzbar
|
识别qr.png
的代码:
1
2
3
4
5
6
7
8
9
|
#code test : test.py
from PIL import Image
from pyzbar.pyzbar import decode
equation = decode(Image.open("./qr.png"))
equation = equation[0].data.decode() #将字节流类型的数据转为字符串类型
equation = equation.replace("=","").replace("x","*") #去除等号,替换乘号
print(equation)
|
执行效果:
1
2
|
➜ 2-QuickR py test.py
2.095213484743085 / 23.013727493106604 / 171.06434387764517
|
4.剩余步骤
剩下还有几步比较简单的操作步骤:计算数学等式、提交正确的计算结果、接收flag。
完整的解题脚本整理如下:
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
|
from PIL import Image
from PIL import ImageDraw
from pyzbar.pyzbar import decode
from pwn import *
p = remote('144.126.230.162', 32190)
p.recvuntil(b"you got only 3 seconds!")
p.recvuntil(b"\n\n\n")
data = p.recvuntil(b"\n\n").strip(b"\n\n")
lines = data.split(b"\n") #以"\n"为分隔符分割每一行
#Image.new(mode, size, color)
img = Image.new("1", (51,51), 1) #创建一个新的图像对象img
#Image.new(mode, size, color)
#mode: 二值图像,只能是黑色(0)或白色(1)
#size: 51*51像素
#color: 指定了图像的初始颜色,1表示初始时图像中的所有像素块都是白色
draw = ImageDraw.Draw(img) #创建一个用于在图像上进行绘制的对象draw
y = 0 #初始行数为0
for line in lines:
line = line.decode("utf-8") #先将字节流转换为字符串类型,否则对数据进行后续的替换处理
line = line.replace("\t", "").replace("\n", "") #去除每一行中的"\t"和"\n"
line = line.replace("\x1b[7m \x1b[0m","-") #替换白色格子为"-"
line = line.replace("\x1b[41m \x1b[0m","*") #替换红色格子为"*"
#print(line)
x = 0 #初始列数为0,每次开始新的行数,列数都重新从0开始
for char in line:
if char == "*":
draw.point((x, y), 0) #填充黑色像素块
elif char == "-":
draw.point((x, y), 1) #填充白色像素块
else:
print("character error.")
x += 1
y += 1
img.save("qr.png") #保存生成的二维码
equation = decode(Image.open("./qr.png"))
equation = equation[0].data.decode() #将字节流类型的数据转为字符串类型
equation = equation.replace("=","").replace("x","*") #去除等号,替换乘号
result = eval(equation) #计算数学等式
p.recvuntil(b"Decoded string:")
p.sendline(str(result).encode()) #发送等式计算的结果给服务器
p.recv()
flag = p.recv().decode().split("flag:")[1] #提取服务器返回的flag
print(flag)
p.close()
|
脚本运行结果: