스택 버퍼오버플로우(Stack buffer overflow)

noob3er·2023년 1월 25일
0

System Hacking

목록 보기
5/5
post-thumbnail

버퍼 오버플로우(Buffer Overflow)?

버퍼 오버플로우(Buffer Overflow)는 연속된 메모리 공간을 사용하는 프로그램에서 할당된 메모리의 범위를 넘어선 위치에 자료를 읽거나 쓰려고 할 때 발생한다. 버퍼 오버플로우가 발생하게 되면 프로그램의 오작동을 유발시키거나, 악의적인 코드를 실행시킴으로써 공격자 프로그램을 통제할 수 있는 권한을 획득하게 된다.


스택 버퍼 오버플로우 & 힙 버퍼 오버플로우

스택(Stack) 버퍼 오버플로우 : 스택은 함수 처리를 위해 지역변수 및 매개변수가 위치하는 메모리 영역을 말한다. 스택에 할당된 버퍼들이 문자열 계산 등에 의해 정의된 버퍼의 한계치를 넘는 경우 버퍼 오버플로우가 발생하여 복귀주소(Return Address)를 변경하고 공격자가 원하는 임의 코드를 실행한다.

힙(Heap) 버퍼 오버플로우 : 힙은 사용자가 동적으로 할당하는 메모리 영역(malloc()등의 메모리 할당 함수 이용)이다. 힙에 할당된 버퍼들에 문자열 등이 저장되어 질 때, 최초 정의된 힙의 메모리 사이즈를 초과하여 문자열 등이 저장되는 경우 버퍼 오버플로우가 발생하여 데이터와 함수 주소 등을 변경하여 공격자가 원하는 임의 코드를 실행한다.


스택 버퍼 오버플로우가 발생하는 이유

스택 버퍼 오버플로우가 발생하는 이유는 "길이 제한"을 하지 않아서 이다.
대표적으로 취약한 함수들은 아래와 같다.

  • 취약한 함수
    strcat(), strcpy(), gets(), scanf(), sscanf(), vscanf(), vsscanf(), sprinf(), vsprintf(), gethostbyname()
  • 권장함수
    strncat(), strncpy(), fgets(), fscanf(), vfscanf(), snprintf(), vsnprintf()

개념 이해 코드

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

int main(int argc, char** argv) {
   int i, j;
   char buf[100];
   char buf2[] = "ABCD";
   memset(buf, 0x00, sizeof(buf));

   for (i = 0; i < argc; i++) {
      printf("argv[%d] : %s\n", i, argv[i]);
   }
   for (j = 0; j < sizeof(buf2); j++) {
      printf("0x%x\t", buf2[j]);
   }
   printf("\nsizeof(buf) : %d\n", sizeof(buf));
   printf("sizeof(buf2) : %d\n", sizeof(buf2));
   printf("strlen( buf2) : %d\n", strlen(buf2));

}

argc = 전달되는 매개변수의 개수

argv = 전달되는 문자열을 참조하는 변수

"argv[0]"를 통해 첫 번째 매개변수인 프로그램명, "argv[1]'를 통해 두 번째 매개변수인 "1234"를 참조할 수 있다.


실행결과

  • buf2[]의 "ABCD" 문자열을 한자씩 16진수로 출력한 결과를 확인해 보면 마지막에 0x00(null 문자)가 추가된 것을 볼 수 있다.
  • sizeof() 연산자는 버퍼(배열)의 크기를 출력하고 strlen() 함수는 null 문자를 제외한 문자열의 크기를 출력한다.

스택 버퍼 오버플로우 공격 실습

#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)" 부분에서 취약점이 발생한다.
scanf함수의 포맷 스트링 중 '%s'는 문자열을 입력 받을 때 입력의 길이를 제한하지 않으며, 공백 문자인 띄어쓰기, 탭, 개행 문자 등이 들어올 때까지 계속 입력을 받는다는 특징이 있다.
위 코드에서 버퍼의 크기를 0x28로 지정 해주었지만 %s로 입력을 받기 때문에 입력을 0x28보다 길게 준다면 main함수의 반환 주소를 덮을 수 있을 것이다.


실행결과

위처럼 실행하면 "Input : "이라는 문자열이 나오게 되고 5개의 문자만 넣었을 때는 정상적으로 종료 된다.

하지만 위처럼 'A'를 56개 입력해주니 "Segmentation fault (core dumped)"라는 문자열이 나오며 비정상적으로 종료 된다.(실제로 'ls' 명령어를 사용해 파일을 확인해 보면 'core'라는 파일이 생성 되었다.)

A를 56개 입력해준 이유 :

0x28만큼의 버퍼 생성, 즉 40 만큼의 버퍼가 생기고 스택 프레임에는 버퍼 이외에 sfpret가 존재하는데 x64 아키텍처에서는 각각 8바이트로 40 + 8 + 8 = 56크기를 전부 덮어준 것이다.

sfp가 필요한 이유

rbp레지스터는 한 개이기 때문에 함수가 시작할 때마다 rbp값이 바뀌는데 그 전의 rbp값을 스택에다가 저장해야 하기 때문이다

ret가 필요한 이유

다음에 실행해야 하는 명령이 위치한 메모리 주소값이 ret이기 때문이다. 그렇기 때문에 ret부분을 자기가 원하는 명령이 있는 곳의 메모리 주소로 덮어 쓴다면 자기가 원하는 명령을 실행시켜 버릴 수도있다

core dump란?

컴퓨팅에서, 코어 덤프, 메모리 덤프, 또는 시스템 덤프는 컴퓨터 프로그램이 특정 시점에 작업 중이던 메모리 상태를 기록한 것으로, 보통 프로그램이 비정상적으로 종료했을 때 만들어진다.


페이로드

우선 스택 프레임의 구조를 파악해야 한다.
GDB를 사용해 main함수의 디스어셈블리된 코드를 보면 다음과 같다.

위에서 확인 할 수 있는 점은 버퍼의 위치는 rbp-0x30에 위치해 있고, sfp가 저장되고, rbp+0x8에는 반환 주소가 저장된다.
따라서 이를 바탕으로 스택 프레임을 그려보면 다음과 같다.

위 코드에서는 셸을 실행해주는 get_shell()함수가 있기에 get_shell()의 함수 주소로 main함수의 반환 주소를 덮어서 셸을 획득 할 수 있다.


익스플로잇

파이썬의 pwntools를 활용하여 익스플로잇 할 수 있다.

코드 설명

  • from pwn import를 통해 pwntools를 import해준다.
  • context.arch = 'amd64'를 통해 자신의 아키텍처(amd64)로 설정해준다.
  • addr = 0x4011d, addr라는 변수에 get_shell()함수의 주소를 저장해준다.
  • p = process('./bof'), 로컬 바이너리 'bof'를 대상으로 익스플로잇을 수행시켜준다.
  • p.recvuntil(b': ')를 통해 파일 실행시 나오는 'Input: '에 콜론( : )뒤 공백까지 즉 'Input: '를 받아온다.(꼭 필요 x )
  • payload = b'A'0x30 + b'B'0x08 + p64(addr), byte형식으로 A라는 문자를 0x30만큼 보내 버퍼를 채워주고 byte형식의 B라는 문자를 0x08만큼 보내 sfp를 채워주고 ret 부분에 get_shell함수의 주소를 리틀엔디언의 바이트 배열로 변경하기 위해 p64방식으로 패킹 해주어 ret함수의 주소를 변경해준다.
  • p.send(payload) 를 통해 작성한 페이로드를 로컬 바이너리 'bof'에 보낸다.
  • p.interactive()를 통해 프로세스 데이터 입력, 출력을 확인 할 수 있다.

실행 결과

위처럼 실행시켜주면 성공적으로 셸을 획득한걸 확인 할 수 있다.


취약점 패치

#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);' 부분을 scanf("%[n]s", buf) 형식으로 바꿔주면 buf의 크기와 n이 동일할 경우, 버퍼의 뒤에 널바이트가 삽입된다.


스택 버퍼 오버플로우 대응기술

스택 가드(Stack Guard) : 메모리상에서 프로그램의 복귀 주소(Return Address)와 변수 사이에 특정 값(카나리)을 저장해 두었다가 그 값이 변경되었을 경우를 오버플로우로 가정하여 프로그램 실행을 중단하는 기술을 말한다.

스택 쉴드(Stack Shield) : 함수 시작 시 복귀 주소(Return Address)를 Global RET라는 특수 스택에 저장해 두었다가 함수 종료 시 저장된 값과 스택의 RET값을 비교해 다를 경우 오버플로우로 가정하여 프로그램 실행을 중단시키는 기술을 말한다.

ASLR(Address Space Layout Randomization) : 메모리 공격을 방어하기 위해 주소 공간 배치를 난수화 하는 기법이다. 실행 시 마다 메모리 주소를 변경시켜 악성코드에 의한 특정주소 호출을 방지한다.


profile
"Hard work beats talent when talent doesn't work hard."

0개의 댓글