Qubic Rube
300 points
Please continue to solve Rubic’s Cube and read QR code.
http://qubicrube.pwn.seccon.jp:33654/
One of the reasons I love CTFs is because they force me to learn new technologies quickly. This challenge was awesome because I learned image manipulation and QR code scanning in Python - something I wanted to try for a while.
When we open the referenced site we are shown a spinning Rubics Cube with sides containing QR codes:
The images can be loaded directly and analyzed. As we decode the codes we see that one of them contains a reference to the next page in sequence (50 of them total):
However, as we progress from page to page the images get more and more mangled:
It’s clear that we have to reconstruct proper sides from separate pieces. Here is the sequence of steps that we have to follow:
- Load all images on a page
- Split each in 9 pieces (3x3)
- For each piece determine the color side it belongs to
- “Normalize” pieces by rotating corner pieces to be in the top-left position, and sides to be in the top-middle position
- Join pieces for each side in all possible combinations to see if we can extract the text
- If the text is a flag - show it
- If the text is the reference to the next page - follow it and start again
Let’s put this algorithm into a script:
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import qrtools
import urllib2
from PIL import Image
import os
SIZE = 82
TEST_IMG = "test.png"
color_map = {}
color_map[(196, 30, 58)] = 0
color_map[(255, 88, 0)] = 1
color_map[(255, 255, 255)] = 2
color_map[(0, 81, 186)] = 3
color_map[(0, 158, 96)] = 4
color_map[(255, 213, 0)] = 5
permutations = [[1,2,3,4], [1,2,4,3], [1,3,2,4], [1,3,4,2], [1,4,2,3], [1,4,3,2],
[2,1,3,4], [2,1,4,3], [2,3,1,4], [2,3,4,1], [2,4,1,3], [2,4,3,1],
[3,1,2,4], [3,1,4,2], [3,2,1,4], [3,2,4,1], [3,4,1,2], [3,4,2,1],
[4,1,2,3], [4,1,3,2], [4,2,1,3], [4,2,3,1], [4,3,1,2], [4,3,2,1]
]
def process(file, center, corners, sides):
orig_img = Image.open(file)
# cut out 9 pieces
for x in range(3):
for y in range(3):
img = orig_img.copy()
img = img.crop((x*SIZE, y*SIZE, x*SIZE+SIZE, y*SIZE+SIZE))
colors = img.getcolors(256) #put a higher value if there are many colors in your image
# determine piece color
for c in colors:
if c[1] in color_map:
colorid = color_map[c[1]]
# normalize by rotation
if x == 0 and y > 0: img = img.rotate(270)
if x == 2 and y < 2: img = img.rotate(90)
if x > 0 and y == 2: img = img.rotate(180)
# store piece in proper bucket
if x == 1 and y == 1: center[colorid] = img
else:
if x == 1 or y == 1: sides[colorid].append(img)
else: corners[colorid].append(img)
def combine(center, corners, sides):
global permutations
# recombine pieces all possible ways
for colorid in range(6):
for corn in range(len(permutations)):
for side in range(len(permutations)):
img = Image.new("RGB",(SIZE*3,SIZE*3))
img.paste(center[colorid],(SIZE,SIZE))
# paste and rotate corners
for x in range(4):
pos = permutations[corn][x]
if pos == 1: img.paste(corners[colorid][x],(0,0))
if pos == 2: img.paste(corners[colorid][x].rotate(-90),(SIZE*2,0))
if pos == 3: img.paste(corners[colorid][x].rotate(-180),(SIZE*2,SIZE*2))
if pos == 4: img.paste(corners[colorid][x].rotate(-270),(0,SIZE*2))
# paste and rotate sides
for x in range(4):
pos = permutations[side][x]
if pos == 1: img.paste(sides[colorid][x],(SIZE,0))
if pos == 2: img.paste(sides[colorid][x].rotate(-90),(SIZE*2,SIZE))
if pos == 3: img.paste(sides[colorid][x].rotate(-180),(SIZE,SIZE*2))
if pos == 4: img.paste(sides[colorid][x].rotate(-270),(0,SIZE))
img.save(TEST_IMG)
qr = qrtools.QR()
qr.decode(TEST_IMG)
os.remove(TEST_IMG)
# see if we found the link to the next page
if "seccon.jp" in qr.data:
return qr.data[qr.data.rfind("/")+1:]
# print the flag if found
if "SECCON" in qr.data:
print qr.data
return None
# starting image prefix
pref = "01000000000000000000"
while True:
print "---"
corners = []
sides = []
center = []
for x in range(6):
corners.append([])
sides.append([])
center.append(None)
# download all sides
for x in "RLUDFB":
file = "%s_%s.png" % (pref,x)
open(file,"wb").write(urllib2.urlopen('http://qubicrube.pwn.seccon.jp:33654/images/' + file).read())
process(file, center, corners, sides)
# find the next link or the flag
pref = combine(center, corners, sides)
if pref == None:
print "Not found"
break
else:
print "Found " + pref
After running for a while the script gets us the answer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
Found 4882153b757d0af86d97
---
Found 49d06dfaeaefaa612e72
---
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
Found 504ded069e4db4e3bef9
---
SECCON{Thanks to Denso Wave for inventing the QR code}
SECCON{Thanks to Denso Wave for inventing the QR code}
SECCON{Thanks to Denso Wave for inventing the QR code}
SECCON{Thanks to Denso Wave for inventing the QR code}
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
SECCON 2017 Online CTF
Not found
The flag is SECCON{Thanks to Denso Wave for inventing the QR code}
.