[DCTF 2017] Security CCTV Writeup
Description:
We really need that token. Can you take it? https://security-cctv.dctf-f1nals-2017.def.camp/.
Author:
Lucian Nitescu
Stats:
374 points / 7 solvers
Solution:
The challenge started with this page:
There you can observe the QR code and its reflection on the laptop:
You can also observe the time and date (Date: 2017-11-13 09:42:10.725649), which changes every minute along with the photo itself. On the server side there is a cron job running a Python script that generates a random QR code, which is then inserted into the “frame” image.
Using the following Python script we can extract the QR code from the image and retrieve the token.
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
124
125
126
127
128
129
130
import pyqrcode
import string
import random
import png
import ImageOps
import ImageDraw
import numpy
from PIL import Image, ImageEnhance
import urllib
import qrtools
from qrtools import QR
def transformblit(src_tri, dst_tri, src_img, dst_img):
((x11,x12), (x21,x22), (x31,x32)) = src_tri
((y11,y12), (y21,y22), (y31,y32)) = dst_tri
M = numpy.array([
[y11, y12, 1, 0, 0, 0],
[y21, y22, 1, 0, 0, 0],
[y31, y32, 1, 0, 0, 0],
[0, 0, 0, y11, y12, 1],
[0, 0, 0, y21, y22, 1],
[0, 0, 0, y31, y32, 1]
])
y = numpy.array([x11, x21, x31, x12, x22, x32])
A = numpy.linalg.solve(M, y)
src_copy = src_img.copy()
srcdraw = ImageDraw.Draw(src_copy)
srcdraw.polygon(src_tri)
transformed = src_img.transform(dst_img.size, Image.AFFINE, A)
mask = Image.new('1', dst_img.size)
maskdraw = ImageDraw.Draw(mask)
maskdraw.polygon(dst_tri, fill=0)
dstdraw = ImageDraw.Draw(dst_img)
dstdraw.polygon(dst_tri, fill=(0,0,0,0))
dst_img.paste(transformed, mask=mask)
def randomg(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
# hell gose lose here
# http://13.81.248.25/qr/img/streamframe.png
urllib.urlretrieve("http://13.81.248.25/qr/img/streamframe.png", "streamframe.png" )
qr = Image.open("streamframe.png").convert("RGBA")
blank = Image.new("RGBA", (4608, 2529), (0,0,0,0))
#Start: Part 1 of the QR code
part1 = qr.crop((2089, 1558, 2358, 1723))
contrast = ImageEnhance.Contrast(part1)
part1 = contrast.enhance(1.6)
part1 = part1.rotate(22, resample=Image.BICUBIC, expand=True)
width, height = part1.size
m = -0.62
xshift = abs(m) * width
new_width = width + int(round(xshift))
part1 = part1.transform((new_width, height), Image.AFFINE,
(1, m, -xshift if m > 0 else 0, 0, 1.43, 0), Image.BILINEAR)
part1 = part1.resize((285, 285), Image.ANTIALIAS)
part1 = part1.resize((800, 800), Image.ANTIALIAS)
# junk trimming
tri1 = [(10,10), (20,20), (10,20)]
tri2 = [(10,10), (212,100), (208,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(150,500), (211,280), (600,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(300,420), (470,420), (600,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(470,420), (470,100), (900,600)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(100,140), (470,140), (800,10)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(344,218), (365,0), (800,670)]
transformblit(tri1, tri2, blank, part1)
# final trim
part1 = part1.crop((213, 141, 469, 419))
contrast = ImageEnhance.Contrast(part1)
part1 = contrast.enhance(1.5)
#Start: Part 2 of the QR code
part2 = qr.crop((2031, 1584, 2287, 1742))
contrast = ImageEnhance.Contrast(part2)
part2 = contrast.enhance(2.7)
part2 = ImageOps.mirror(part2)
part2 = part2.rotate(0+20-4, resample=Image.BICUBIC, expand=True)
width, height = part2.size
m = -0.49
xshift = abs(m) * width
new_width = width + int(round(xshift))
part2 = part2.transform((new_width, height), Image.AFFINE,
(1.1, m, -xshift if m > 0 else 0, 0, 1.605, 0), Image.BILINEAR)
part2 = part2.resize((800, 700), Image.ANTIALIAS)
contrast = ImageEnhance.Contrast(part2)
part2 = contrast.enhance(1.2)
part2 = part2.crop((245, 80, 393, 301))
tri1 = [(10,10), (20,20), (10,20)]
tri2 = [(0,0), (0,220), (100,220)]
transformblit(tri1, tri2, blank, part2)
part2 = part2.resize((240, 278), Image.ANTIALIAS)
blank2 = Image.new("RGBA", (278, 278), (0,0,0,0))
contrast = ImageEnhance.Contrast(part2)
part2 = contrast.enhance(1.6)
blank2.paste(part2, (12, 4), part2)
blank2.paste(part1, (0, 0), part1)
blank2.save("./sol.png", "PNG")
part1.save("./part1.png", "PNG")
part2.save("./part2.png", "PNG")
myCode = QR(filename=u"./sol.png")
if myCode.decode():
print myCode.data_to_string()
Example of “phone” QR code retrieval:
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
part1 = qr.crop((2089, 1558, 2358, 1723))
contrast = ImageEnhance.Contrast(part1)
part1 = contrast.enhance(1.6)
part1 = part1.rotate(22, resample=Image.BICUBIC, expand=True)
width, height = part1.size
m = -0.62
xshift = abs(m) * width
new_width = width + int(round(xshift))
part1 = part1.transform((new_width, height), Image.AFFINE,
(1, m, -xshift if m > 0 else 0, 0, 1.43, 0), Image.BILINEAR)
part1 = part1.resize((285, 285), Image.ANTIALIAS)
part1 = part1.resize((800, 800), Image.ANTIALIAS)
# junk trimming
tri1 = [(10,10), (20,20), (10,20)]
tri2 = [(10,10), (212,100), (208,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(150,500), (211,280), (600,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(300,420), (470,420), (600,800)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(470,420), (470,100), (900,600)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(100,140), (470,140), (800,10)]
transformblit(tri1, tri2, blank, part1)
tri2 = [(344,218), (365,0), (800,670)]
transformblit(tri1, tri2, blank, part1)
# final trim
part1 = part1.crop((213, 141, 469, 419))
contrast = ImageEnhance.Contrast(part1)
part1 = contrast.enhance(1.5)
Result:
Example of “laptop” QR code retrieval:
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
part2 = qr.crop((2031, 1584, 2287, 1742))
contrast = ImageEnhance.Contrast(part2)
part2 = contrast.enhance(2.7)
part2 = ImageOps.mirror(part2)
part2 = part2.rotate(0+20-4, resample=Image.BICUBIC, expand=True)
width, height = part2.size
m = -0.49
xshift = abs(m) * width
new_width = width + int(round(xshift))
part2 = part2.transform((new_width, height), Image.AFFINE,
(1.1, m, -xshift if m > 0 else 0, 0, 1.605, 0), Image.BILINEAR)
part2 = part2.resize((800, 700), Image.ANTIALIAS)
contrast = ImageEnhance.Contrast(part2)
part2 = contrast.enhance(1.2)
part2 = part2.crop((245, 80, 393, 301))
tri1 = [(10,10), (20,20), (10,20)]
tri2 = [(0,0), (0,220), (100,220)]
transformblit(tri1, tri2, blank, part2)
part2 = part2.resize((240, 278), Image.ANTIALIAS)
Result:
Final QR:
Execution of the Python script:
1
2
3
lucian@nitescu:~/security-cctv$ python sol.py
BMQQGBMIBKCIAIJLBOKNIKAACQPPKAJGNCKNBNBHOIJQQPHAHCLPJQHIBJIIQCNKIJJBBNKLJGAQJLHKHIALMBMCN
lucian@nitescu:~/security-cctv$
After entering the token in less than a minute, we get the flag.
DCTF{44c5e6a2ef50636b1d5ad1023bb2c63a5ed62d40549d9edb537c54b4cb72b37e}




