- 🏅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 | --> 하위 주소
그러나 여기서 의문이 들었다. 내가 임의의 입력을 통해 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바이트)]
| flag_buf[56] | <-- /flag 내용을 저장 | my_page.name[16]| <-- 사용자 입력 저장 | my_page.age (4) | <-- 나이 저장
[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가 출력되었다.
다음글이 없습니다.이전글이 없습니다.댓글