Iriton's log

Exploit Tech: Return Address Overwrite 본문

Pwnable/Study

Exploit Tech: Return Address Overwrite

Iriton 2024. 4. 9. 20:14

*본 포스트는 Dreamhack - Systemhacking Lecture 을 참고하여 작성되었습니다.

분석

취약점 분석/트리거

// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie

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

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};

  execve(cmd, args, NULL);
}

int main() {
  char buf[0x28];

  init();

  printf("Input: ");
  scanf("%s", buf);

  return 0;
}

 

해당 프로그램의 취약점은 scanf("%s", buf)에 있다.

%s는 문자열을 입력받을 때 입력의 길이를 제한하지 않으며 공백 문자인 띄어쓰기, 탭, 개행 문자 등이 들어올 때까지 계속 입력을 받는다는 특징이 있다. 따라서 버퍼의 크기보다 큰 데이터를 입력하면 오버플로우가 발생할 수 있다. 정확히 n개의 문자만 입력받는 "%[n]s"의 형태로 사용해야 한다.

 

이외에도 C/C++ 표준 함수 중 버퍼를 다루면서 길이를 입력하지 않는 함수들은 대부분 위험하다.

strcpy, strcat, sprintf 대신에 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직하다.

 

더보기

💡 C/C++의 문자열 종결자(Terminator)와 표준 문자열 함수들의 취약성

C계열 언어에서는 널바이트(“\x00”)로 종료되는 데이터 배열을 문자열로 취급하며, 문자열을 다루는 대부분의 표준 함수는 널바이트를 만날 때까지 연산을 진행한다. 예를 들어, char *strcpy(char *dest, const char *src)은 src배열의 첫 번째 인덱스부터 널바이트가 저장된 인덱스까지 참조하여 dest에 값을 복사한다.

여기서 생각해봐야 할 것은 src에 널바이트가 없을 경우이다. 문자열 함수는 널바이트를 찾을 때까지 배열을 참조하므로, 코드를 작성할 때 정의한 배열의 크기를 넘어서도 계속해서 인덱스를 증가시킨다. 이런 동작으로 인해 참조하려는 인덱스 값이 배열의 크기보다 커지는 현상을 Index Out-Of-Bound라고 부르며, 줄여서 OOB라고도 한다. 그리고 해당 버그를 발생시키는 취약점을 Out-Of-Bound(OOB)취약점이라고 부른다.

OOB는 심각한 보안 취약점 중 하나로, 이를 이용하여 해커는 프로그래머가 의도하지 않은 주소의 데이터를 읽거나, 조작할 수 있고, 몇몇 조건이 만족되면 소프트웨어에 심각한 오동작을 일으킬 수도 있다. 이를 방지하기 위해 개발자는 입력의 길이를 제한하는 문자열 함수를 사용해야 하며, 문자열을 사용할 때는 반드시 해당 문자열이 널바이트로 종결되는지 확인해야 한다.

 

코어 덤프 파일이 생성되지 않아서 강의에서 주어진 자료를 보고 따라가기로 했다...

 

 

코어 파일 분석

스택 최상단 저장 데이터가 입력값의 일부인 AAAAAAAA 라는 것을 볼 수 있다.

실행가능한 메모리 주소가 아니라서 segmentation fault가 발생한 것이다.

이 값이 AAAAAAAAA가 아니라 원하는 코드의 주소가 되도록 입력값을 주입한다면 원하는 코드가 실행되도록 조작할 수 있다.

 

익스플로잇

스택 프레임 구조 파악

스택 버퍼에 오버플로우 발생시켜서 리턴 주소를 덮으려면 해당 버퍼가 스택 프레임 어디에 위치하는지 알아야 한다.

main 어셈블리 코드를 살펴보자.

주목해서 봐야 할 코드는 scanf에 인자를 전달하는 부분이다.

 

nearpc는 디스어셈블된 코드를 가독성 좋게 출력하는 pwndbg 명령어이다. 

rsi에 rbp-0x30 주소를 넣는데 이건 입력받은 문자열을 저장할 메모리 버퍼의 주소이다.

rdi에는 rip+0xab 주소를 넣는데 이건 %s의 시작을 가리키는 것으로 입력 형식을 나타낸다.

eax에 0을 로드하는 건 scanf 함수의 반환 값을 초기화 하는 것에 해당한다. 

따라서 코드로 표현하면 scanf("%s", (rbp-0x30)); 이다.

 

즉, 오버플로우를 발생시킬 버퍼는 rbp-0x30에 위치한다.

스택프레임 구조를 떠올려 보면 rbp에 SFP가 저장되고 rbp+0x8에는 반환 주소가 저장된다.

따라서 스택 프레임을 그려보면 아래와 같다.

입력할 버퍼와 반환 주소 사이에 0x38만큼의 거리가 있으므로 그만큼 쓰레기 값을 채우고 실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있다.

 

get_shell() 주소 학인/페이로드 구성

이 예제에는 쉘을 실행하는 get_shell() 함수가 있으므로 이 함수 주소로 main 함수의 리턴 주소를 덮어서 쉘을 획득할 수 있다.

주소가 0x4006aa임을 확인했습니다.

 

엔디언 적용

Intel x86-64아키텍처는 리틀엔디언을 사용하기 때문에 get_shell 주소인 0x4006aa는 "\xaa\x06\x40\x00\x00\x00\x00\x00"로 전달돼야 한다.

 

Exploit

(python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00')";cat)| ./rao

해당 명령어는 파이썬을 이용하여 특정 문자열 생성 후 파이프를 통해 ./rao 프로그램에 전달한다.

 

sys.stdout.buffer.write는 파이썬에서 표준 출력 스트림에 바이트 데이터를 쓰기 위해 사용된다. 바이트 데이터를 콘솔에 출력하는 데 사용된다.

 

취약점 패치

 

입력 함수(패턴) 위험도 평가 근거
gets(buf) 매우 위험
  • 입력받는 길이에 제한이 없음.
  • 버퍼의 널 종결을 보장하지 않음: 입력의 끝에 널바이트를 삽입하므로, 버퍼를 꽉채우면 널바이트로 종결되지 않음. 이후 문자열 관련 함수를 사용할 때 버그가 발생하기 쉬움.
     
scanf(“%s”, buf) 매우 위험
  • 입력받는 길이에 제한이 없음.
  • 버퍼의 널 종결을 보장하지 않음: gets와 동일.
scanf(“%[width]s”, buf) 주의 필요
  • width만큼만 입력받음: width를 설정할 때 width <= size(buf) - 1을 만족하지 않으면, 오버플로우가 발생할 수 있음.
  • 버퍼의 널 종결을 보장하지 않음: gets와 동일.
fgets(buf, len, stream) 주의 필요
  • len만큼만 입력받음: len을 설정할 때 len <= size(buf)을 만족하지 않으면, 오버플로우가 발생할 수 있음.
  • 버퍼의 널 종결을 보장함.
    • len보다 적게 입력하면, 입력의 끝에 널바이트 삽입.
    • len만큼 입력하면, 입력의 마지막 바이트를 버리고 널바이트 삽입.
  • 데이터 유실 주의: 버퍼에 담아야 할 데이터가 30바이트인데, 버퍼의 크기와 len을 30으로 작성하면, 29바이트만 저장되고, 마지막 바이트는 유실됨

 

'Pwnable > Study' 카테고리의 다른 글

Exploit Tech: Return to Shellcode  (0) 2024.05.15
Mitigation: Stack Canary  (0) 2024.05.07
Memory Corruption: Stack Buffer Overflow  (0) 2024.04.01
Background: Calling Convention  (0) 2024.04.01
Exploit Tech: Shellcode  (0) 2024.03.27
Comments