#SECCON2017 Writeup
この記事はIS17erアドベントカレンダー9日目の記事として(あとから)書かれたものです。
12/9 - 12/10の期間にSECCONという大会があったのでTSGのメンバーとして出場しました。それまでCTFは自分でちょっとだけやったことがあったので雰囲気は知っていましたが、大会に出場するの初めてでした。
限定公開だし雰囲気が伝わるようにいろいろ具体的に書いていきたいです。
イントロ
CTFとは
Capture the Flagの略で、問題の中に含まれるSECCON{hoge-fuga-piyo}
みたいな文字列を探し出します。Crypto, Pwn, Web, Programming, Binaryなどの分野があり、自分はPwn(リバーシングなどを伴うやつ)が全くわからないのでWebを中心に、ドカタ作業などをやっていました。
SECCONとは
これぐらい量の問題が出ます。問題ははじめ6問ぐらいでしたが、進行に従って追加されていき、12時間が経過したぐらいにすべてが公開されます。
Writeup
自分がわりと関わった問題について書いていきたいです。
Qubic Rube
六面にQRコードが貼られた立方体が回転しています。
とりあえず静止した状態で見たいので、コンソールから画像の直リンクを取り出します。
http://qubicrube.pwn.seccon.jp:33654/images/01000000000000000000_{U|D|L|R|F|B}.png
という6枚の画像が得られます。一つは次のようなやつです。
とりあえず最初はスマホのカメラを使ってデコードしていたのですが、いずれはプログラムでデコードする必要が生じそうだったのでライブラリを探しました。
Pythonのpyqrcodeというのを見つけたので、pip install pyqrcode
していきます。
→結局zbarlightというのを使いました。
6面のQRコードのうち、一つは、デコードするとURLが現れます。それにアクセスすると次の画面に進みます。
もう雰囲気はわかったと思います。はい、ルービックキューブの画像が与えられるので、解いて、どんどん次の画面に進んでいきます。一番最初のルービックキューブについて、デコードした6つの文字列のうち、ひとつに1/50
というのがあったので、おそらく50回繰り返せばいいことが推測できます。
アルゴリズムは以下のようになります。
0. 画像linkを初期化
- 画像を6枚取得してくる。
- 画像を9つのパートに分割し、各面の角を揃える
- 続いて各面のエッジを揃える
- 各面の中心は4通り全部試し、アクセス可能だったもののみを取り出す
- アクセス可能だったもののURLでlinkを更新し、1へ戻る
自分ははじめ、四隅を揃えて、他は滑らかにつながるようにランダムに配置するようなコードを書いていましたが、わりとQRコードのデコードに時間がかかったり、画像処理に時間がかかったりして、一秒間に1000ケースぐらいしか試せなかったので、絶望していたのですが、リアルルービックキューブのプロであるhakatashiさんが、ルービックキューブソルバーを書いてくれたので、上のようなアルゴリズムで可能となりました(多分、境界の滑らかさを判定する関数を書いて、1/9ずつ確実に当てはめていけばルービックキューブの解法を知らなくても可能だったとは思います)。
以下コードになります。
from PIL import Image
from collections import Counter
import zbarlight
import urllib
colors = [
(255, 255, 255), # U
(0, 81, 186), # D
(0, 158, 96), # F
(255, 213, 0), # B
(196, 30, 58), # R
(255, 88, 0), # L
]
names = ['U', 'D', 'F', 'B', 'R', 'L']
def solve(name):
u_img = Image.open("{}_U.png".format(name), 'r')
d_img = Image.open("{}_D.png".format(name), 'r')
f_img = Image.open("{}_F.png".format(name), 'r')
b_img = Image.open("{}_B.png".format(name), 'r')
r_img = Image.open("{}_R.png".format(name), 'r')
l_img = Image.open("{}_L.png".format(name), 'r')
images = [u_img, d_img, f_img, b_img, r_img, l_img]
new_images = [
Image.new(u_img.mode, (246, 246)),
Image.new(u_img.mode, (246, 246)),
Image.new(u_img.mode, (246, 246)),
Image.new(u_img.mode, (246, 246)),
Image.new(u_img.mode, (246, 246)),
Image.new(u_img.mode, (246, 246)),
]
faces = []
face_colors = []
for image in images:
face = []
for y in range(3):
for x in range(3):
counter = Counter()
for dy in range(82):
for dx in range(82):
pixel = image.getpixel((x * 82 + dx, y * 82 + dy))
try:
index = colors.index(pixel)
counter[index] += 1
except:
pass
color_index, count = counter.most_common(1)[0]
face.append(color_index)
if x == 1 and y == 1:
face_colors.append(color_index)
faces.append(face)
rev_face_colors = [0,0,0,0,0,0]
for index, color in enumerate(face_colors):
rev_face_colors[color] = index
corners = [
((faces[0][6], face_colors[0], 6), (faces[2][0], face_colors[2], 0), (faces[5][2], face_colors[5], 2)),
((faces[0][8], face_colors[0], 8), (faces[4][0], face_colors[4], 0), (faces[2][2], face_colors[2], 2)),
((faces[0][2], face_colors[0], 2), (faces[3][0], face_colors[3], 0), (faces[4][2], face_colors[4], 2)),
((faces[0][0], face_colors[0], 0), (faces[5][0], face_colors[5], 0), (faces[3][2], face_colors[3], 2)),
((faces[1][6], face_colors[1], 6), (faces[3][8], face_colors[3], 8), (faces[5][6], face_colors[5], 6)),
((faces[1][8], face_colors[1], 8), (faces[4][8], face_colors[4], 8), (faces[3][6], face_colors[3], 6)),
((faces[1][2], face_colors[1], 2), (faces[2][8], face_colors[2], 8), (faces[4][6], face_colors[4], 6)),
((faces[1][0], face_colors[1], 0), (faces[5][8], face_colors[5], 8), (faces[2][6], face_colors[2], 6)),
]
for corner in corners:
colorset = list(map(lambda x: x[1], list(corner)))
colorset_rotations = [
(colorset[0], colorset[1], colorset[2]),
(colorset[1], colorset[2], colorset[0]),
(colorset[2], colorset[0], colorset[1]),
]
for index, rotation in enumerate(colorset_rotations):
for corner0 in corners:
colorset0 = tuple(map(lambda x: x[0], list(corner0)))
if colorset0 == rotation:
# print(colorset, colorset0, index, corner, corner0)
for part_index, part in enumerate(corner):
area = images[rev_face_colors[corner0[(part_index - index) % 3][1]]].crop((
(corner0[(part_index - index) % 3][2] % 3) * 82,
(corner0[(part_index - index) % 3][2] // 3) * 82,
(corner0[(part_index - index) % 3][2] % 3) * 82 + 82,
(corner0[(part_index - index) % 3][2] // 3) * 82 + 82
))
copy_area = area.copy()
if corner0[(part_index - index) % 3][2] == 0:
rotate_from = 0
elif corner0[(part_index - index) % 3][2] == 2:
rotate_from = 1
elif corner0[(part_index - index) % 3][2] == 8:
rotate_from = 2
elif corner0[(part_index - index) % 3][2] == 6:
rotate_from = 3
if part[2] == 0:
rotate_to = 0
elif part[2] == 2:
rotate_to = 1
elif part[2] == 8:
rotate_to = 2
elif part[2] == 6:
rotate_to = 3
if (rotate_to - rotate_from + 4) % 4 == 3:
copy_area = copy_area.transpose(Image.ROTATE_90)
elif (rotate_to - rotate_from + 4) % 4 == 2:
copy_area = copy_area.transpose(Image.ROTATE_180)
elif (rotate_to - rotate_from + 4) % 4 == 1:
copy_area = copy_area.transpose(Image.ROTATE_270)
new_images[rev_face_colors[part[1]]].paste(copy_area, ((
(part[2] % 3) * 82,
(part[2] // 3) * 82,
(part[2] % 3) * 82 + 82,
(part[2] // 3) * 82 + 82
)))
edges = [
((faces[0][7], face_colors[0], 7), (faces[2][1], face_colors[2], 1)),
((faces[0][5], face_colors[0], 5), (faces[4][1], face_colors[4], 1)),
((faces[0][1], face_colors[0], 1), (faces[3][1], face_colors[3], 1)),
((faces[0][3], face_colors[0], 3), (faces[5][1], face_colors[5], 1)),
((faces[2][5], face_colors[2], 5), (faces[4][3], face_colors[4], 3)),
((faces[4][5], face_colors[4], 5), (faces[3][3], face_colors[3], 3)),
((faces[3][5], face_colors[3], 5), (faces[5][3], face_colors[5], 3)),
((faces[5][5], face_colors[5], 5), (faces[2][3], face_colors[2], 3)),
((faces[1][7], face_colors[1], 7), (faces[3][7], face_colors[3], 7)),
((faces[1][5], face_colors[1], 5), (faces[4][7], face_colors[4], 7)),
((faces[1][1], face_colors[1], 1), (faces[2][7], face_colors[2], 7)),
((faces[1][3], face_colors[1], 3), (faces[5][7], face_colors[5], 7)),
]
for edge in edges:
colorset = list(map(lambda x: x[1], list(edge)))
colorset_rotations = [
(colorset[0], colorset[1]),
(colorset[1], colorset[0]),
]
for index, rotation in enumerate(colorset_rotations):
for edge0 in edges:
colorset0 = tuple(map(lambda x: x[0], list(edge0)))
if colorset0 == rotation:
# print(colorset, colorset0, index, edge, edge0)
for part_index, part in enumerate(edge):
area = images[rev_face_colors[edge0[(part_index - index) % 2][1]]].crop((
(edge0[(part_index - index) % 2][2] % 3) * 82,
(edge0[(part_index - index) % 2][2] // 3) * 82,
(edge0[(part_index - index) % 2][2] % 3) * 82 + 82,
(edge0[(part_index - index) % 2][2] // 3) * 82 + 82
))
copy_area = area.copy()
if edge0[(part_index - index) % 2][2] == 1:
rotate_from = 0
elif edge0[(part_index - index) % 2][2] == 5:
rotate_from = 1
elif edge0[(part_index - index) % 2][2] == 7:
rotate_from = 2
elif edge0[(part_index - index) % 2][2] == 3:
rotate_from = 3
if part[2] == 1:
rotate_to = 0
elif part[2] == 5:
rotate_to = 1
elif part[2] == 7:
rotate_to = 2
elif part[2] == 3:
rotate_to = 3
if (rotate_to - rotate_from + 4) % 4 == 3:
copy_area = copy_area.transpose(Image.ROTATE_90)
elif (rotate_to - rotate_from + 4) % 4 == 2:
copy_area = copy_area.transpose(Image.ROTATE_180)
elif (rotate_to - rotate_from + 4) % 4 == 1:
copy_area = copy_area.transpose(Image.ROTATE_270)
new_images[rev_face_colors[part[1]]].paste(copy_area, ((
(part[2] % 3) * 82,
(part[2] // 3) * 82,
(part[2] % 3) * 82 + 82,
(part[2] // 3) * 82 + 82
)))
for face in range(6):
for rotate in range(4):
new_image = new_images[face].copy()
center = images[face].crop((82, 82, 82 * 2, 82 * 2))
center = center.copy()
if rotate == 1:
center = center.transpose(Image.ROTATE_90)
elif rotate == 2:
center = center.transpose(Image.ROTATE_180)
elif rotate == 3:
center = center.transpose(Image.ROTATE_270)
new_image.paste(center, (82, 82, 82 * 2, 82 * 2))
new_image.save("{}_{}.png".format(names[face], rotate), 'png')
return u_img;
def get_file(f):
return f[37:-1] + f[-1]
def get_file_name(f):
return f[44:-1] + f[-1]
def generate_png_url(url):
suffix = ['_U', '_D', '_L', '_R', '_F', '_B']
return map(lambda s: url + s + '.png', suffix)
if __name__=="__main__":
answers = []
for i in ['U', 'D', 'L', 'R', 'F', 'B']:
for j in [0,1,2,3]:
answers.append("%s_%d.png" % (i,j))
link = '01000000000000000000'
for _ in range(50):
solve(link)
for i in range(24):
with open(answers[i], 'rb') as f:
img = Image.open(f)
img.load()
try:
code = zbarlight.scan_codes('qrcode', img)[0]
print(code)
if code[0:4] == 'http':
link = get_file(code)
for p in generate_png_url('http://qubicrube.pwn.seccon.jp:33654/images/' + link):
urllib.urlretrieve(p, get_file_name(p))
except TypeError:
continue
何がクソだったかというと、もともとルービックキューブの配色が日本配色だったのですが、30回ぐらい解くといきなり国際配色に変わり、もともとのプログラムが停止してしまうなどがありました(変更は容易でしたが)。
こうしてフラグSECCON{Thanks to Denso Wave for inventing the QR code}
を得ました。
ちなみに、プログラムを書かずに実際にルービックキューブを組み立てた班もあったようです笑
SECCONはQubic Rube作って遊んでました pic.twitter.com/VoR9iyEXWf
— さわ (@ywkw1717) December 10, 2017
Log Search
この問題は本当にほげで、この記事にあるように、解いた時刻によって難易度がかなり違いました。
ページにアクセスすると以下のように大量のログが表示されます。
結論から言えば、/flag-hoge-fuga-piyo.txt
みたいなファイルに200
でアクセスできるようなログを見つけ出せば勝ちです。
問題文にElastic Searchを使えと書いているのでElastic Searchで検索してみます。Elastic Searchはquery string queryで、クエリ文字列を投げることで、対応する結果を返してくれます。
response:200 AND request:/flag/
と検索欄に入力します。
出てきたファイルにアクセスするとSECCON{N0SQL_1njection_for_Elasticsearch!}
を獲得します。
すごくシンプルな問題ですが、そもそもどうやったらフラグにアクセスできるかがはじめ全然わからなく、タイムスタンプを遡ったり、他にどういうフィールドが存在するかをElastic Searchで検索したり(flag
みたいなフィールドが存在すると思った)、いろいろ苦戦しました。