Running GDB
우리는 프로그램을 디버거에서 실행하면 메모리에서 무슨 일이 일어나는지 정확히 알 수 있습니다. 우리 우분투 머신은 GDB와 함께 제공되므로, 여기에 나와 있는 것처럼 디버거에서 프로그램을 열고 다섯 글자크기의 버퍼를 오버플로하면 메모리에서 어떤 일이 일어나는지 지켜봅시다.
georgia@ubuntu:~$ gdb overflowtest
(gdb)
프로그램을 실행하기 전에 프로그램의 특정 지점에서 실행을 일시 중지하도록 몇 가지 중단점을 설정하고 그 시점에서 메모리의 상태를 볼 수 있도록 할 것입니다. -g 플래그로 프로그램을 컴파일했기 때문에 아래 예제와 같이 소스 코드를 직접 보고 일시 중지할 라인에 중단점을 설정할 수 있습니다.
(gdb) list 1,16
1 #include <string.h>
2 #include <stdio.h>
3
4 void overflowed() {
5 printf("%s\n", "Execution Hijacked");
6 }
7
8 void function(char *str){
9 char buffer[5];
10 strcpy(buffer, str); ❶
11 } ❷
12 void main(int argc, char *argv[])
13 {
14 function(argv[1]); ❸
15 printf("%s\n", "Executed normally");
16 }
(gdb)
❸ => 메인 함수가 function1을 호출하기 직전에 프로그램을 잠깐 멈추도록 하자. 추가적으로 function1 함수안에 2개의 브레이크포인트를 설정하도록 한다. 하나는 strcpy가 실행되기 직전인 ❶, 다른 하나는 실행된 직후인 ❷.
GDB에서 중단점을 설정하는 방법은 아래 예제와 같습니다. GDB command break을 이용하여 14, 10, 11번 행에 중단점을 설정합니다.
(gdb) break 14
Breakpoint 1 at 0x8048433: file overflowtest.c, line 14.
(gdb) break 10
Breakpoint 2 at 0x804840e: file overflowtest.c, line 10.
(gdb) break 11
Breakpoint 3 at 0x8048420: file overflowtest.c, line 11.
(gdb)
버퍼 오버플로우가 발생하여 프로그램이 크래시되기 전에, 여기에 나와 있는 것처럼 단 4개의 A로 실행하고, 프로그램이 정상적으로 실행되는 것을 메모리로 지켜보도록 하겠습니다.
(gdb) run AAAA
Starting program: /home/georgia/overflowtest AAAA
Breakpoint 1, main (argc=2, argv=0xbffff5e4) at overflowtest.c:14
14 function(argv[1]);
우리는 GDB 명령어 run과 바로 뒤에 이어지는 인수를 사용하여 디버거에서 프로그램을 시작합니다. 여기서는 4개의 A를 인수로 사용하여 프로그램을 실행합니다. function1이 호출되기 직전에 첫 번째 중단점을 맞았는데, 이때 GDB 명령어 x를 사용하여 프로그램의 메모리를 검사할 수 있습니다.
GDB는 우리가 보고 싶은 메모리의 어떤 부분을 어떻게 표시해야 하는지 알아야 합니다. 메모리 내용은 8진수, 16진수, 10진수, 2진수 형식으로 표시할 수 있습니다. 우리는 개발 과정을 통해 여정에서 많은 16진수를 볼 수 있을 것이므로 x 플래그를 사용하여 GDB에게 우리의 메모리를 16진수 형식으로 표시하도록 지시합니다.
또한 메모리를 1바이트, 2바이트 하프 워드, 4바이트 워드, 8바이트 자이언트 단위로 출력할 수 있습니다. 아래 예제에서 볼 수 있듯이 ESP 레지스터에서 시작하는 16개의 16진수 형식 워드를 x/16xw $esp 명령으로 살펴보겠습니다.
(gdb) x/16xw $esp
0xbffff540: 0xb7ff0f50 0xbffff560 0xbffff5b8 0xb7e8c685
0xbffff550: 0x08048470 0x08048340 0xbffff5b8 0xb7e8c685
0xbffff560: 0x00000002 0xbffff5e4 0xbffff5f0 0xb7fe2b38
0xbffff570: 0x00000001 0x00000001 0x00000000 0x08048249
x/16xw $esp 명령어는 ESP를 시작으로 16개의 4바이트 워드를 16진수 형식으로 출력합니다. ESP는 스택에서 가장 낮은 메모리 주소를 표시한다는 것을 장 앞에서 기억하십시오. function1로 호출되기 직전에 첫 번째 중단점에서 실행이 일시 중지되었기 때문에 ESP는 메인 스택 프레임의 맨 위에 있습니다.
바로 위 명령어 결과에서 GDB의 메모리 출력은 처음에는 조금 혼란스러울 수 있으므로 이를 나누어 보겠습니다. 맨 왼쪽에는 메모리 주소가 16바이트 단위로 표시되어 있고 그 다음에 해당 주소의 메모리 내용이 표시되어 있습니다. 이 경우 처음 4바이트는 ESP에서 시작하여 스택을 계속하여 추가 메모리의 내용이 됩니다.
우리는 x/1xw $ebp 명령으로 여기와 같은 EBP를 조사함으로써 메인 스택 프레임의 하단(또는 가장 높은 주소)을 가리키는 EBP를 찾을 수 있습니다.
(gdb) x/1xw $ebp
0xbffff548: 0xbffff5b8
(gdb)
이 명령어를 사용하면 EBP에서 하나의 16진수 단어를 검사하여 EBP 레지스터의 메모리 위치와 내용을 찾을 수 있습니다. 출력에 따라 main의 스택 프레임은 다음과 같습니다:
0xbffff540: 0xb7ff0f50 0xbffff560 0xbffff5b8
보시다시피, 많은 것이 없지만, 다시 말하지만, main이 하는 일은 다른 함수를 호출한 다음 텍스트 한 줄을 화면에 인쇄하는 것입니다. 과도한 처리가 필요하지 않습니다.
스택에 대해 알고 있는 바에 따르면 프로그램을 계속 진행하고 function1을 호출할 때 메인에 대한 리턴 어드레스와 function1에 대한 스택 프레임이 스택에 푸시되리라는 것을 예상할 수 있습니다. 스택이 증가하여 메모리 어드레스가 낮아지므로 function1의 다음 중단점을 맞았을 때 스택의 상단은 메모리 어드레스가 낮아진다는 것을 기억하십시오. 다음 중단점은 strcpy 명령이 실행되기 직전에 함수1의 내부에 있다는 것을 기억하십시오. 아래 예제에서와 같이 다음 중단점까지 프로그램이 계속 실행되도록 명령을 사용하여 아래 예제와 같이 다음 중단점까지 프로그램을 계속 실행하십시오.
(gdb) continue
Continuing.
Breakpoint 2, function (str=0xbffff74c "AAAA") at overflowtest.c:10
10 strcpy(buffer, str);
(gdb) x/16xw $esp❶
0xbffff520: 0xb7f93849 0x08049ff4 0xbffff538 0x080482e8
0xbffff530: 0xb7fcfff4 0x08049ff4 0xbffff548 0x08048443
0xbffff540: 0xbffff74f 0xbffff560 0xbffff5b8 0xb7e8c685
0xbffff550: 0x08048470 0x08048340 0xbffff5b8 0xb7e8c685
(gdb) x/1xw $ebp❷
0xbffff538: 0xbffff548
continue 명령어를 사용하여 다음 breakpoint까지 프로그램을 실행한 후 function1의 stack frame의 내용은 ❶에서 ESP, ❷에서 EBP를 조사하여 function1의 stack frame의 내용을 확인합니다.
0xbffff520: 0xb7f93849 0x08049ff4 0xbffff538 0x080482e8
0xbffff530: 0xb7fcfff4 0x08049ff4 0xbffff548
function1의 스택 프레임은 메인보다 약간 큽니다. 로컬 변수 버퍼에 할당된 메모리가 있고 strcpy가 작동할 수 있는 약간의 여유 공간이 있지만 30개 혹은 40개의 A값 입력을 받기ㅇ 위한 공간이 확실히 부족합니다. 이번 말고 지난 중단점에서 메인함수의 스택 프레임은 메모리 주소 0xbff540에서 시작되었음을 기억하십시오. 스택에 대한 지식에 기초하여 function1의 스택 프레임과 메인의 스택 프레임 사이의 4바이트 메모리 주소인 0x08048443이 메인의 반환 주소가 되어야 합니다. 아래 예제와 같이 disass 명령으로 메인을 분해하여 0x08048443이 어디로 들어오는지 알아보겠습니다.
(gdb) disass main
Dump of assembler code for function main:
0x08048422 <main+0>: lea 0x4(%esp),%ecx
0x08048426 <main+4>: and $0xfffffff0,%esp
0x08048429 <main+7>: pushl -0x4(%ecx)
0x0804842c <main+10>: push %ebp
0x0804842d <main+11>: mov %esp,%ebp
0x0804842f <main+13>: push %ecx
0x08048430 <main+14>: sub $0x4,%esp
0x08048433 <main+17>: mov 0x4(%ecx),%eax
0x08048436 <main+20>: add $0x4,%eax
0x08048439 <main+23>: mov (%eax),%eax
0x0804843b <main+25>: mov %eax,(%esp)
0x0804843e <main+28>: call 0x8048408 <function1> ❶
0x08048443 <main+33>: movl $0x8048533,(%esp) ❷
0x0804844a <main+40>: call 0x804832c <puts@plt>
0x0804844f <main+45>: add $0x4,%esp
0x08048452 <main+48>: pop %ecx
0x08048453 <main+49>: pop %ebp
0x08048454 <main+50>: lea -0x4(%ecx),%esp
0x08048457 <main+53>: ret
End of assembler dump.
당신이 어셈블리 코드에 능숙하지 못하더라도 걱정하지 마십시오. 우리가 찾고 있는 명령어가 간단한 영어로 우리에게 불쑥 나타납니다. ❶ 0x0804843e에서 메인은 function1의 메모리 주소를 호출합니다. function1이 종료될 때(따라서 우리의 리턴 주소) 실행되는 다음 명령어가 목록의 다음 명령어가 됨이 타당합니다. 물론 다음줄 ❷는 우리가 스택에서 발견한 반환 주소를 보여줍니다. 모든 것이 이론이 말하는 것과 똑같이 보입니다.
프로그램을 계속 진행하여 4개의 A를 버퍼에 복사할 때 메모리에서 어떤 일이 일어나는지 알아보겠습니다. 세 번째 중단점에서 프로그램이 일시 중지된 후 아래 예제와 같은 일반적인 방식으로 메모리를 검사합니다.
(gdb) continue
Continuing.
Breakpoint 3, function (str=0xbffff74c "AAAA") at overflowtest.c:11
11 }
(gdb) x/16xw $esp
0xbffff520: 0xbffff533 0xbffff74c 0xbffff538 0x080482e8
0xbffff530: 0x41fcfff4 0x00414141❶ 0xbffff500 0x08048443
0xbffff540: 0xbffff74c 0xbffff560 0xbffff5b8 0xb7e8c685
0xbffff550: 0x08048470 0x08048340 0xbffff5b8 0xb7e8c685
(gdb) x/1xw $ebp
0xbffff538: 0xbffff500
보여지는 것처럼 우리는 여전히 function1 내부에 있으므로 스택 프레임 위치는 동일합니다. function1의 스택 프레임 안에서 우리는 16진수로 표시된 4개의 A가 ❶가 41로 표시되고 그 뒤에 끝나는 null 바이트가 00으로 표시되는 것을 볼 수 있습니다. 이들은 우리의 5글자 크기의 버퍼에 잘 맞으므로 반환 주소는 여전히 온전하며 아래 예제와 같이 프로그램을 계속 진행할 때 모든 것이 예상대로 작동합니다.
(gdb) continue
Continuing.
Executed normally
Program exited with code 022.
(gdb)
“Executed normally” 글자들이 화면에 인쇄됩니다.
이제 프로그램을 다시 실행해 보겠습니다. 이번에는 버퍼가 너무 많은 문자로 넘쳐나고 메모리에서 어떤 일이 일어나는지 보겠습니다.
Crashing the Program in GDB
우리는 긴 A 문자열을 입력할 수도 있고, 아래 예제에서와 같이 Perl 스크립트 언어가 해당 문자열을 생성하도록 할 수도 있습니다. (Perl은 나중에 프로그램을 중단하는 대신 실제 메모리 주소를 사용하여 실행을 가로채려고 할 때 유용할 것입니다.)
Example 16-9. Running the program with 30 As as an argument
(gdb) run $(perl -e 'print "A" x 30') ❶
Starting program: /home/georgia/overflowtest $(perl -e 'print "A" x 30')
Breakpoint 1, main (argc=2, argv=0xbffff5c4) at overflowtest.c:14
14 function(argv[1]);
(gdb) x/16xw $esp
0xbffff520: 0xb7ff0f50 0xbffff540 0xbffff598 0xb7e8c685
0xbffff530: 0x08048470 0x08048340 0xbffff598 0xb7e8c685
0xbffff540: 0x00000002 0xbffff5c4 0xbffff5d0 0xb7fe2b38
0xbffff550: 0x00000001 0x00000001 0x00000000 0x08048249
(gdb) x/1xw $ebp
0xbffff528: 0xbffff598
(gdb) continue
여기서는 Perl에게 명령어 프린트를 실행하여 30개의 A의 문자열을 만들고 overflowtest의 ❶ 인수로 결과를 입력하라고 명령합니다. strcpy가 5글자 크기의버퍼에 이러한 긴 문자열을 넣으려고 하면 스택의 일부가 A로 덮어쓰는 것을 볼 수 있습니다. 첫 번째 중단점에 도달했을 때 여전히 메인 상태이며 지금까지 모든 것이 정상적으로 보입니다. strcpy가 너무 많은 A로 실행된 후 세 번째 중단점까지 문제가 시작되지 않아야 합니다.
정말 흥미로운 부분으로 넘어가기 전에 예제 16-10의 두 번째 중단점에서 한 가지만 주목해 보겠습니다.
Example 16-10. Examining memory at breakpoint 2
Breakpoint 2, function (str=0xbffff735 'A' <repeats 30 times>)
at overflowtest.c:10
10 strcpy(buffer, str);
(gdb) x/16xw $esp
0xbffff500: 0xb7f93849 0x08049ff4 0xbffff518 0x080482e8
0xbffff510: 0xb7fcfff4 0x08049ff4 0xbffff528 0x08048443❶
0xbffff520: 0xbffff735 0xbffff540 0xbffff598 0xb7e8c685
0xbffff530: 0x08048470 0x08048340 0xbffff598 0xb7e8c685
(gdb) x/1xw $ebp
0xbffff518: 0xbffff528
(gdb) continue
Continuing.
function1의 스택 프레임 또한 32바이트만큼 이동했습니다. 또한 반환 주소는 여전히 메모리 주소 0x08048443 ❶를 유지하고 있습니다. 스택 프레임이 조금 이동했지만 실행할 메모리의 명령어는 같은 위치에 있습니다.
continue 명령을 다시 사용하여 세 번째 중단점으로 이동합니다. 예제 16-11에서 볼 수 있듯이, 여기서 일이 재미있게 진행됩니다.
Example 16-11. Return address overwritten by As
Breakpoint 3, function (str=0x41414141 <Address 0x41414141 out of bounds>)
at overflowtest.c:11
11 }
(gdb) x/16xw $esp
0xbffff500: 0xbffff513 0xbffff733 0xbffff518 0x080482e8
0xbffff510: 0x41fcfff4 0x41414141 0x41414141 0x41414141
0xbffff520: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff530: 0x08040041 0x08048340 0xbffff598 0xb7e8c685
(gdb) continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)
strcpy 직후에 function1이 main으로 돌아오기 전에 세 번째 중단점에서 메모리를 다시 살펴보자. 이번에는 리턴 어드레스가 ❶에서 As에 의해 덮어쓰기 될 뿐만 아니라 main의 스택 프레임의 일부도 덮어쓰기 되었다. 이 시점에서 프로그램이 복구될 희망은 없습니다.
function1이 반환될 때 프로그램은 반환 주소에서 명령을 메인으로 실행하려고 시도하지만 반환 주소가 A로 덮어쓰게 되어 메모리 주소 41414141에서 명령을 실행하려고 할 때 예상되는 분할 오류segmentation fault가 발생합니다. (다음 섹션에서는 반환 주소를 크래시하는 대신 프로그램을 자체 코드로 리디렉션하는 것으로 대체하는 것에 대해 설명합니다.)
'Security > 보안기초' 카테고리의 다른 글
| 웹 보안 기초(02) (0) | 2023.11.15 |
|---|---|
| 웹보안 기초(01) (0) | 2023.11.15 |
| Memory Theory(01) (0) | 2023.11.14 |
| Operation Aurora (0) | 2023.11.12 |
| 취약점 찾기 (0) | 2023.11.12 |