// 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;
}
}
}
모든 보호 기법이 적용되어 있습니다.
이럴 때는 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해볼 수 있습니다.
코드 분석
human_func 함수와 robot_func 함수를 살펴보면, 구조체 변수를 위한 메모리 영역을 할당할 때, 할당한 메모리 영역을 초기화하지 않습니다. Human 구조체와 Robot 구조체의 크기는 같으므로, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있는, Use After Free가 발생합니다.
robot_func는 생성한 Robot 변수의 fptr이 NULL이 아니면 이를 호출해주므로, UAF로 이 변수에 원하는 값을 남겨놓을 수 다면, 실행 흐름을 조작할 수 있습니다.
익스플로잇 설계
Robot.fptr의 값을 one_gadget의 주소로 덮어서 셸을 획득하겠습니다. 이를 위해 libc가 매핑된 주소를 먼저 구해야합니다.
1. 라이브러리 릭
코드에 있는 취약점은 UAF밖에 없으므로, 이 취약점을 이용하여 libc가 매핑된 주소를 구해야 합니다. 이를 위해 unsorted bin의 특징을 이용하겠습니다.
Unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성합니다. 다시 말해, 처음 unsorted bin에 연결되는 청크의 fd와 bk에는 libc 내부의 주소가 쓰입니다. 따라서 unsorted bin에 연결된 청크를 재할당하고, fd나 bk의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.
코드에 custom_func 함수는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수입니다. 0x410 이하의 크기를 갖는 청크는 tcache에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있을 것입니다.
여기서 주의할 점은, 해제할 청크가 탑 청크와 맞닿으면 안 된다는 것입니다. unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합됩니다. 이를 피하려면 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 합니다.
추가로 1050 크기를 free 하게 되면 unsortedbin에 먼저 들어가고 largebin에 들어갑니다.
다음 1050을 할당 요청이 들어오면 unsortedbin에서 먼저 찾고 largebin을 찾습니다.
라이브러리 릭
offset을 알기위해 attach를 한다.
fd와 bk에 특정 libc 주소가 들어가 있다. bk에 있는 값을 확인해보면 main_arena+96 위치이다.
main_arena는 libc 안에 존재하기 때문에 offset를 구해 base 주소를 알 수 있다.
libc 주소와 bk값을 빼면 main_arena+96의 offset 주소가 출력된다.
bk 값 - offset을 하면 현재 libc base 주소가 출력이 된다.
[그림 1-6] 코드에서 출력되는 값을 fd이다.
[그림 1-3] fd 값을 보면 사용자가 입력한 "B"(0x42) 문자가 main_arena + 96 주소를 덮었다.
따라서 fd 값에서 libc base 주소를 구하려면 offset을 0x3ebc42로 바꾸면 된다.
42로 바꾼이유는 사용자가 "B"(0x42)를 입력했기 때문이다. 만약 "C"(0x43)을 입력하면 43으로 바꿔야 한다.
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42 코드를 추가하여 실행하면 libc 주소를 잘 받아온다.
2libc-2.27.so 버전은 local이랑 remote랑 같은데 왜? one_gadget offset은 다르게 나올까..
'Dreamhack - pwnable' 카테고리의 다른 글
tcache_dup (0) | 2023.02.01 |
---|---|
tcache_poison (0) | 2023.01.30 |
ptmalloc2 (0) | 2023.01.28 |
basic_exploitation_003 (0) | 2023.01.24 |
basic_exploitation_002 (0) | 2023.01.24 |