STUDY.md
  • 🏅Dreamhack - memory_leakage 문제풀이
    2024년 11월 18일 01시 07분 03초에 업로드 된 글입니다.
    작성자: 방세연

     

     

     

     


    코드
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    #include <string.h>
    
    FILE *fp;
    
    struct my_page {
    	char name[16];
    	int age;
    };
    
    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()
    {
    	struct my_page my_page;
    	char flag_buf[56];
    	int idx;
    
    	memset(flag_buf, 0, sizeof(flag_buf));
    	
    	initialize();
    
    	while(1) {
    		printf("1. Join\n");
    		printf("2. Print information\n");
    		printf("3. GIVE ME FLAG!\n");
    		printf("> ");
    		scanf("%d", &idx);
    		switch(idx) {
    			case 1:
    				printf("Name: ");
    				read(0, my_page.name, sizeof(my_page.name));
    
    				printf("Age: ");
    				scanf("%d", &my_page.age);
    				break;
    			case 2:
    				printf("Name: %s\n", my_page.name);
    				printf("Age: %d\n", my_page.age);
    				break;
    			case 3:
    				fp = fopen("/flag", "r");
    				fread(flag_buf, 1, 56, fp);
    				break;
    			default:
    				break;
    		}
    	}
    
    }​

    코드를 확인해보면, 다음과 같은 핵심 구조를 확인할 수 있다.

    1. my_page 구조체에서 name, age 두 개의 필드를 저장하고 switch문을 통해 case1, case2에서 입력받을 수  있도록 한다.
    2. initialize()함수가 main함수에서 호출되는데 alarm(30)을 통해 타임아웃을 30초로 설정하고, 이 안에 작업을 수행하지 못하면 프로그램이 종료된다.
    3. case3에는 fp를 통해 flag파일을 오픈할 수 있는 함수가 마련되어있는데, memory leakage를 통해 접근해봐야할 것 같다.


    버퍼 오버플로우 취약점을 활용하기 위해서는 배열에 집중해야 한다.

    read(0, my_page.name, sizeof(my_page.name));

     

    name버퍼는 16바이트로 제한되어 있다.

    struct my_page {
    	char name[16];
    	int age;
    };

    그러나 read 함수에서 입력 길이를 제한하지 않았고, 이는 즉 16바이트를 초과해 my_page.name에서 벗어나 my_page.age까지 넘어가 원치 않은 행동을 나타나게 할 수 있다.

    char flag_buf[56];

    메인함수를 보면 flag_buf 상위 주소는 56바이트를 가지고 있는데, flag_buf, my_page 상위주소를 포함한 하위 주소까지 나타낸 구조는 다음과 같은 것이다.
    ⬇️

    | flag_buf[56]      | --> 상위 주소
    | my_page           |
    |   ├── name[16]    |
    |   └── age         | --> 하위 주소

     

    fread(flag_buf, 1, 56, fp);

    그런데 이 코드에서는 flag_buf를 fread할 수 있는 코드가 나타나있다.

    1. 한번 읽을 때 데이터 크기 1 바이트를 읽는다.
    2. 1바이트 크기의 데이터를 최대 56개 읽을 수 있다.
    3. fp를 통해 /flag 파일로부터 데이터를 읽는다.


    | flag_buf[56]      | --> 상위 주소
    | my_page           |
    |   ├── name[16]    |
    |   └── age         | --> 하위 주소

     

    다시 이 구조를 확인해보면 답이 나온다. 만약 my_page.name에 버퍼를 초과한 내용을 입력하면, flag_buf라는 스택 상의 연속된 메모리 공간까지 침범하게 된다.

    그러나 여기서 의문이 들었다. 내가 임의의 입력을 통해 flag_buf 영역을 덮어쓰면, 단순히 flag 값이 바뀌는 것 아닌가? 그렇다면 /flag 파일을 읽는 동작과 어떤 관계가 있는 것일까?


    메모리 관련 내용들을 찾아본 결과, 결론은 다음과 같았다:

    아무리 flag_buf 메모리 내용을 덮어쓰더라도, 이는 fread 실행 전이기 때문에 /flag 파일을 읽는 동작에 영향을 주지 않는다.fread 호출 시 /flag 파일에서 읽은 데이터가 다시 flag_buf에 복사되므로, 덮어쓴 데이터는 덮어쓰기 전의 데이터를 무효화한다.
    따라서: fread 실행 후에는 flag_buf의 내용을 조작해서는 안 된다.대신, my_page.name을 통해 프로그램의 흐름을 조작하여 flag_buf의 데이터를 출력하도록 유도해야 한다. (예: printf("Flag: %s\n", flag_buf);)


    만약 my_page.name에 초과 입력을 하면, fread 호출 후 /flag 파일 내용이 다시 flag_buf에 복사되므로, 초과 입력으로 덮어쓴 내용은 의미가 없어지는 것 아닌가?

    이에 대한 해결책은 다음과 같았다:
    flag_buf에 내용을 덮어쓰는 것이 목적이 아니라, flag_buf에 저장된 /flag 파일 내용을 출력하는 것이 목적이다.flag_buf는 /flag 파일 내용을 저장하기 위해 사용되며, 프로그램은 이 데이터를 다른 함수(printf 등)를 통해 출력할 수 있다.다른 메모리 주소를 덮어써 프로그램의 흐름을 조작해야 한다.
    my_page.name에 초과 입력을 통해 flag_buf를 가리키는 포인터를 변경하거나, 프로그램이 printf("%s", flag_buf)를 호출하도록 만들어야 한다. 이를 통해 /flag 파일 내용(flag_buf)을 정상적으로 출력할 수 있다.

    결론적으로 flag_buf에 직접적인 조작은 불필요하며, my_page.name의 초과 입력으로 printf 등에서 flag_buf를 참조하도록 조작하는 것이 핵심이다!

    그럼 어떤 주소를 덮어씌워야 하는걸까?
    name 버퍼는 16바이트고 age 버퍼는 4바이트 아닌가? 어떻게 출력하게 만들고 왜 굳이 오버플로우 취약점을 이용해야 하는거지? 메모리 취약점을 이용하지 않고 그냥 처음에 스크립트를 실행할 때 name에 print문을 입력해버리면 안되나?

    | flag_buf[56]    |  <-- 플래그 파일 내용 저장
    | my_page.name[16]|  <-- 사용자 입력 저장
    | my_page.age (4) |

    my_page.name는 그냥 문자열 데이터일 뿐이기 때문에 취약점을 입력하지 않고 그냥 Name에다가 명령어를 입력하면 문자열만 출력되고 끝날 것이다.

    프로그램이 flag_buf를 출력하도록 만들려면, 

    [16바이트 데이터] + [flag_buf 주소(4~8바이트)]

     

    이런 식으로 접근해서 my_page.name이 스택 상에서 flag_buf를 가리키도록 조작해야 한다.
    | flag_buf[56]    | <-- /flag 내용을 저장
    | my_page.name[16]| <-- 사용자 입력 저장
    | my_page.age (4) | <-- 나이 저장

     

    그럼 최종적으로는 20바이트를 초과하는 곳에 /flag 내용을 호출하는 내용을 저장하고 flag_buf를 호출하면 flag 내용을 볼 수 있지 않을까?

    [16바이트 입력] + [4바이트 덮어쓰기] + [flag_buf 주소]

    첫 16바이트에 my_page.name을 채우고 다음 의미없는 4 바이트에 my_page.age를 덮어쓰고, 그 다음에 my_page.name이 flag_buf를 참조하도록 flag_buf 주소를 삽입해서 name이 침범하게 하면 된다.

     

     

     

    코드를 너무 오래봤는데 gdb도 한번 보자

    (gdb) disas main
    Dump of assembler code for function main:
       0x080486eb <+0>:     lea    0x4(%esp),%ecx
       0x080486ef <+4>:     and    $0xfffffff0,%esp
       0x080486f2 <+7>:     push   -0x4(%ecx)
       0x080486f5 <+10>:    push   %ebp
       0x080486f6 <+11>:    mov    %esp,%ebp
       0x080486f8 <+13>:    push   %ecx
       0x080486f9 <+14>:    sub    $0x64,%esp
       0x080486fc <+17>:    mov    %gs:0x14,%eax
       0x08048702 <+23>:    mov    %eax,-0xc(%ebp)
       0x08048705 <+26>:    xor    %eax,%eax
       0x08048707 <+28>:    sub    $0x4,%esp
       0x0804870a <+31>:    push   $0x38
       0x0804870c <+33>:    push   $0x0
       0x0804870e <+35>:    lea    -0x44(%ebp),%eax
       0x08048711 <+38>:    push   %eax
       0x08048712 <+39>:    call   0x8048550 <memset@plt>
       0x08048717 <+44>:    add    $0x10,%esp
       0x0804871a <+47>:    call   0x804869b <initialize>
       0x0804871f <+52>:    sub    $0xc,%esp
       0x08048722 <+55>:    push   $0x80488c9
       0x08048727 <+60>:    call   0x8048500 <puts@plt>
       0x0804872c <+65>:    add    $0x10,%esp
       0x0804872f <+68>:    sub    $0xc,%esp
       0x08048732 <+71>:    push   $0x80488d1
       0x08048737 <+76>:    call   0x8048500 <puts@plt>
       0x0804873c <+81>:    add    $0x10,%esp
       0x0804873f <+84>:    sub    $0xc,%esp
       0x08048742 <+87>:    push   $0x80488e6
       0x08048747 <+92>:    call   0x8048500 <puts@plt>
       0x0804874c <+97>:    add    $0x10,%esp
       0x0804874f <+100>:   sub    $0xc,%esp
       0x08048752 <+103>:   push   $0x80488f7
       0x08048757 <+108>:   call   0x80484c0 <printf@plt>
       0x0804875c <+113>:   add    $0x10,%esp
       0x0804875f <+116>:   sub    $0x8,%esp
       0x08048762 <+119>:   lea    -0x5c(%ebp),%eax
       0x08048765 <+122>:   push   %eax
       0x08048766 <+123>:   push   $0x80488fa
       0x0804876b <+128>:   call   0x8048560 <__isoc99_scanf@plt>
       0x08048770 <+133>:   add    $0x10,%esp
       0x08048773 <+136>:   mov    -0x5c(%ebp),%eax
       0x08048776 <+139>:   cmp    $0x2,%eax
       0x08048779 <+142>:   je     0x80487da <main+239>
       0x0804877b <+144>:   cmp    $0x3,%eax
       0x0804877e <+147>:   je     0x8048804 <main+281>
       0x08048784 <+153>:   cmp    $0x1,%eax
       0x08048787 <+156>:   je     0x804878e <main+163>
       0x08048789 <+158>:   jmp    0x8048835 <main+330>
       0x0804878e <+163>:   sub    $0xc,%esp
       0x08048791 <+166>:   push   $0x80488fd
       0x08048796 <+171>:   call   0x80484c0 <printf@plt>
       0x0804879b <+176>:   add    $0x10,%esp
       0x0804879e <+179>:   sub    $0x4,%esp
       0x080487a1 <+182>:   push   $0x10
       0x080487a3 <+184>:   lea    -0x58(%ebp),%eax
       0x080487a6 <+187>:   push   %eax
       0x080487a7 <+188>:   push   $0x0
       0x080487a9 <+190>:   call   0x80484b0 <read@plt>
       0x080487ae <+195>:   add    $0x10,%esp
       0x080487b1 <+198>:   sub    $0xc,%esp
       0x080487b4 <+201>:   push   $0x8048904
       0x080487b9 <+206>:   call   0x80484c0 <printf@plt>
       0x080487be <+211>:   add    $0x10,%esp
       0x080487c1 <+214>:   sub    $0x8,%esp
       0x080487c4 <+217>:   lea    -0x58(%ebp),%eax
       0x080487c7 <+220>:   add    $0x10,%eax
    --Type <RET> for more, q to quit, c to continue without paging--
       0x080487ca <+223>:   push   %eax
       0x080487cb <+224>:   push   $0x80488fa
       0x080487d0 <+229>:   call   0x8048560 <__isoc99_scanf@plt>
       0x080487d5 <+234>:   add    $0x10,%esp
       0x080487d8 <+237>:   jmp    0x8048835 <main+330>
       0x080487da <+239>:   sub    $0x8,%esp
       0x080487dd <+242>:   lea    -0x58(%ebp),%eax
       0x080487e0 <+245>:   push   %eax
       0x080487e1 <+246>:   push   $0x804890a
       0x080487e6 <+251>:   call   0x80484c0 <printf@plt>
       0x080487eb <+256>:   add    $0x10,%esp
       0x080487ee <+259>:   mov    -0x48(%ebp),%eax
       0x080487f1 <+262>:   sub    $0x8,%esp
       0x080487f4 <+265>:   push   %eax
       0x080487f5 <+266>:   push   $0x8048914
       0x080487fa <+271>:   call   0x80484c0 <printf@plt>
       0x080487ff <+276>:   add    $0x10,%esp
       0x08048802 <+279>:   jmp    0x8048835 <main+330>
       0x08048804 <+281>:   sub    $0x8,%esp
       0x08048807 <+284>:   push   $0x804891d
       0x0804880c <+289>:   push   $0x804891f
       0x08048811 <+294>:   call   0x8048540 <fopen@plt>
       0x08048816 <+299>:   add    $0x10,%esp
       0x08048819 <+302>:   mov    %eax,0x804a06c
       0x0804881e <+307>:   mov    0x804a06c,%eax
       0x08048823 <+312>:   push   %eax
       0x08048824 <+313>:   push   $0x38
       0x08048826 <+315>:   push   $0x1
       0x08048828 <+317>:   lea    -0x44(%ebp),%eax
       0x0804882b <+320>:   push   %eax
       0x0804882c <+321>:   call   0x80484f0 <fread@plt>
       0x08048831 <+326>:   add    $0x10,%esp
       0x08048834 <+329>:   nop
       0x08048835 <+330>:   jmp    0x804871f <main+52>
    End of assembler dump.​


    read 함수가 호출되는 부분
    0x080487a3 <+184>:   lea    -0x58(%ebp),%eax
    0x080487a6 <+187>:   push   %eax
    0x080487a7 <+188>:   push   $0x0
    0x080487a9 <+190>:   call   0x80484b0 <read@plt>

    0x080487a9에서 read 함수가 호출=> my_page.name에 데이터 저장하는 부분이다.
    여기서 입력 길이 제한이 없는데 이게 문제다.

    0x0804880c <+294>:   call   0x8048540 <fopen@plt>
    0x0804882c <+321>:   call   0x80484f0 <fread@plt>
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);


    fopen으로 /flag를 열고 fread로 읽어오는 과정이고, 이 과정에서 flag가 스택의 특정 버퍼에 저장된다.
    여기를 조작해서 데이터를 참조하도록 해보자.



    -0x44(%ebp)가 플래그 데이터 위치라는 것을 추론하기

    0x08048828 <+317>:   lea    -0x44(%ebp),%eax  ; 플래그를 저장할 버퍼 주소
    0x0804882b <+320>:   push   %eax              ; `ptr`로 전달
    0x0804882c <+321>:   call   0x80484f0 <fread@plt>

    fread의 첫번째 인수는 lea -0x44(%ebp), %eax인데, ebp 기준에서 44바이트 아래의 주소를 계산해 eax에 저장한다는 뜻이다.
    그 이후 push를 통해 eax를 fread의 첫 번째 인수 ptr로 전달하고, fread를 호출(call)한 뒤에 /flag 데이터를 첫 번째 인수에 저장한다.

    레지스터의 내용도 확인해보면

    my_page.name ($ebp-0x58)

    0xffffd2a0:     0x00000000      0xffffd52b      0x00000000      0xffffd2d8

    16바이트의 초기화된 내용이 나타나며 여기에 데이터를 모두 입력하면 my_page.age 이후 공간을 덮어쓸 수 있다.

    flag_buf ($ebp-0x44)

    0xffffd2b4:     0x00000000      0x00000014      0x00000000      0xf7fc6570
    0xffffd2c4:     0xf7fc6000      0x00000000      0x00000000      0x00000000
    0xffffd2d4:     0x00000000      0xffffffff      0xf7c11994      0xf7fc0400
    0xffffd2e4:     0x00000000      0x00000000

    flag_buf의 내용인데, 여기도 초기화 상태이다.
    my_page.name를 오버플로우 하면 flag_buf의 주소를 덮어씌울 수 있는 가능성이 높아보인다.

    즉,

    my_page.name: 0xffffd2a0 | ebp - 0x58
    my_page.age:  0xffffd2b0 | ebp - 0x48
    flag_buf:     0xffffd2b4 | ebp - 0x44

    로컬 바이너리의 주소는 이렇게 보여진다.
    그런데 로컬 바이너리 주소와 원격 서버의 주소는 다르게 나타나서 해당 정보로 exploit 코드를 작성하기에는 좀 어려운 점이 있어서 밑의 방법을 사용해서 최종적인 플래그를 구했다.

     

     

    플래그 구하기

    exploit 코드를 직접 짜서 해보려다가 계속 안되어서 그냥 nc 명령어를 통해 플래그를 도출했다.

    도출한 원리는 다음과 같다.

     

    1. (>3)

    /flag의 내용이 flag_buf에 저장될 수 있도록 먼저 case 3번을 호출한다.

    이제 flag_buf에 flag에 대한 값이 저장되었을거고 이 값을 memory leakage를 통해서 읽는 방법을 찾는게 핵심이다.

     

    2. (>1)

    Name과 Age를 직접 입력했다.

    -> Name에는 16개의(그 이상의) 'A'를 입력했고, 원래 name을 printf 해야했을 경우(2.Print information)가 flag_buf를 가리키는 데이터로 참조하게 되고 flag를 드러낼 것이라고 생각했다. 특히 16개의 A만 입력해도 my_page.name과 flag_buf는 메모리 상에서 연결된 상태로 동작하기 때문에 flag_buf에 간섭하는 행위가 가능해진다.

    -> my_page.name에  Age에는 -1을 입력했다. 이유는 age에는 4바이트(int 형 타입)이기 때문에 4바이트로 꽉 채워진 값을 넣어야 했고, 이때 여러가지 경우의 수가 있겠지만 나는 -1을 통해 0xFFFFFFFF 값을 입력해도 괜찮을 거라고 판단했다.

     

    + printf 내용

    printf("Name: %s\n", my_page.name);

     

     

     

    3. (>2)

    print information을 통해 원래였으면 Name이 출력되었어야 할 상황에서 flag 값도 함께 출력되게 만들어줬다.

    따라서 flag가 출력되었다.

     

    댓글