[Dreamhack - System Hacking] STAGE 5 : Memory Corruption: Stack Buffer Overflow

eunee22·2023년 8월 5일
0

Dreamhack/SystemHacking

목록 보기
12/12

stack overflow vs stack bufffer overflow

  • stack overflow
    • 스택 영역이 너무 과하게 확장되어 발생하는 버그
    • 스택 영역은 실행 중에 크기가 동적으로 확장이 가능하지만, 메모리의 크기는 한정되어 있으므로 스택이 무한하게 확장되는 것은 불가
  • stack bufffer overflow
    • 스택에 위치한 버퍼의 크기를 넘어선 데이터들이 입력되어 발생하는 버그

버퍼 (buffer)

  • 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소
  • 완충 장치의 역할
    • 데이터의 처리 속도가 다른 두 장치 사이에 오가는 데이터를 임시로 저장함으로써 유실된 데이터가 없이 통신하도록 도움
    • 송신측 → 버퍼 → 수신측
  1. 스택 버퍼 : 스택에 있는 지역 변수
  2. 힙 버퍼 : 힙에 할당된 메모리 영역

오버플로우 (overflow)

  • 넘치는 것
  • 일반적으로 버퍼는 메모리상에 연속해서 할당
    → 버퍼에서 오버플로우 발생 시 메모리가 오염되어 뒤에 있는 버퍼값 조작 위험 존재

스택 버퍼 오버플로우 - 중요 데이터 변조

  • 오버플로우가 발생하는 버퍼 뒤의 데이터를 변조할 수 있다

  • 예제
// Name: sbof_auth.c
// Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector

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

int check_auth(char *password) {
    int auth = 0;
    char temp[16];
    
    strncpy(temp, password, strlen(password));
    
    if(!strcmp(temp, "SECRET_PASSWORD"))
        auth = 1;
    
    return auth;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
        exit(-1);
    }

    
    if (check_auth(argv[1]))
        printf("Hello Admin!\n");
    else
        printf("Access Denied!\n");
}

위의 코드는 입력받은 문자열과 “SECRET_PASSWORD" 문자열을 비교하여 일치하면 1, 불일치하면 0을 리턴하는 check_auth 함수를 이용하여 결과에 따른 문자열을 출력한다.

check_auth 함수에는 버퍼 오버플로우가 발생할 수 있는데, 바로

strncpy(temp, password, strlen(password));

부분이다. temp 배열의 크기는 16 바이트로 선언이 되었는데, strcpy() 함수를 사용할 때, 입력받은 문자열의 길이인 strlen(password)); 만큼 복사하여 temp에 넣으므로 입력받은 문자열이 16바이트를 넘어선다면, temp는 넘치게 된다.

auth는 temp의 버퍼 뒤에 존재하므로 temp에서 오버플로우를 발생시키면 auth의 값을 0이 아닌 임의의 값으로 변경할 수 있다. 그렇게 된다면, 실제 인증 여부와는 무관하게 if (check_auth(argv[1]) 는 참이되어 printf("Hello Admin!\n"); 이 실행된다.

위의 코드를 해석한 것을 기반으로 채워보면,
temp는 어떤 값이 들어가던 무관하므로 아무값으로 채우고, auth영역으로 넘어가는 순간 0이 아닌 임의의 값으로 auth를 변경하게 되므로
if (check_auth(argv[1])) printf("Hello Admin!\n"); 가 실행된다.

main 함수의 매개변수에 대해서 → 참고한 글

공부를 하다보니, argv[1]의 의미와 더불어 평소에는 생략해왔던 main함수의 매개변수에 대한 의문이 생겼다.

대체 argv[1]이 무엇이길래 scanf와 같은 값을 입력받는 함수 없이 입력되는 값을 check_auth 함수로 넘기는 것 일까?

  1. int argc → 메인 함수에 전달되는 정보의 갯수
  2. char* argv[] → 메인 함수에 전달되는 실질적인 정보를 저장하는 문자열 배열
    • argv[0]은 프로그램의 실행 경로로 항상 고정
    • argv[1] 부터 정보를 전달

개인적인 의문점

check_auth 함수는 auth 변수를 리턴하는 함수이다. auth = 0 일 때 값이 리턴된다면, “Access Denied!” 가 출력되어야 맞다고 나는 해석했다. 그런데 auth를 다 0으로 채워도 “Hello Admin”이 뜬다.

→ 해결완료!
read 함수로 입력받는 것은 16진수 값이 아닌 문자열이다. 0은 문자열로 입력받으면 아스키코드로 0x30으로 인식되므로 내가 생각했던 NULL 값인 0x00과는 다르다. 내가 원하는대로 NULL 값을 채우려면 아스키코드가 0x00인 문자열을 넣어주어야한다.

스택 버퍼 오버플로우 - 데이터 유출(메모리 릭)

  • C언어에서, 그리고 표준 문자열 출력 함수들은 널바이트를 문자열의 끝으로 인식
  • 버퍼 오버플로우를 발생시켜서 다른 버퍼와의 사이에 있는 널바이트를 제거한다면 해당 버퍼를 출력시켜서 다른 버퍼의 데이터를 읽어올 수 있음

  • 예제
// Name: sbof_leak.c
// Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector

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

int main(void) {
  char secret[16] = "secret message";
  char barrier[4] = {};
  char name[8] = {};

  memset(barrier, 0, 4); // barrier 배열을 다 NULL로 채우는 함수 memset
  printf("Your name: ");
  read(0, name, 12);
  printf("Your name is %s.", name);
}

위의 코드에서는 3개의 배열이 선언되어 있다.

우리가 메모리 릭을 통해서 얻어낼 내용을 담고 있는 secret, 값을 입력 받는 name, 그 사이에 위치한 널바이트로 채워진 배열 barrier이다. 세 배열은 선언된 순서대로 스택에 쌓이게 된다.

그런데 위의 코드를 보면, read 함수에서 12바이트 만큼 읽어와서 name 배열에 채우게 되어있다. name 배열의 크기는 8바이트로 선언되어 있으므로 12바이트 만큼 읽어서 name 배열에 채우면 그 밑에 위치한 barrier 배열의 값까지 침범하게 된다. barrier 배열을 채우고 있는 널바이트를 모두 제거한다면, name에 입력된 문자열의 끝을 인식하지 못할 것이고, name 배열의 값을 출력하면, name 배열부터 barrier 배열을 거쳐 secret 배열까지 저장된 값을 모두 밷어낼 것을 예상해 볼 수 있다.

위의 코드 해석을 기반으로 값을 채워보면, barrier의 값을 널바이트가 아닌 값으로 채움으로써 저장된 secret 배열의 값까지 출력 시킬 수 있다. namebarrier에 들어가는 값은 중요하지 않다. (내가 처음 보안 공부를 할 때, 왜 A로 채우지? 왜 B지? 와 같은 의문을 가졌었기에 혹시 몰라서 쓴다..ㅎㅎ)

스택 버퍼 오버플로우 - 실행 흐름 조작

  • 함수를 호출하면, 반환 주소를 스택에 쌓아두고 함수가 반환될 때 그 주소를 꺼내어 원래의 실행 흐름으로 돌아감
  • 스택 버퍼 오버플로우로 반환 주소를 조작함으로써 프로세스의 실행 흐름을 바꿀 수 있음

  • 예제
// Name: sbof_ret_overwrite.c
// Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char buf[8];

    printf("Overwrite return address with 0x4141414141414141: ");
    gets(buf);

    return 0;
}

위의 코드를 보면, gets 함수가 buf에 들어갈 문자열을 입력받을 때, buf의 크기인 8바이트 이상으로 값을 받아 채운다면, 오버플로우가 일어날 것이다. 이때, main 함수의 반환 주소를 담는 공간에 내가 흐름을 바꾸길 원하는 공간의 주소를 담는다면, main 함수가 끝날 때, 내가 원하는 곳으로 실행 흐름이 바뀔 것이다.

위의 코드에서는 return address를 0x4141414141414141 로 채우라고 한다. 0x41 은 문자로 A 이다. (아스키코드표 참고) 그러므로 return 주소를 저장하는 ret 공간을 A로 채우면, main의 반환 주소는 우리가 원하는 대로 0x4141414141414141 이 된다.

bufsfp의 값은 무관하다.

profile
보안 공부하는 대학교 4학년 / 시리즈에서 더욱 편하게 글을 찾아보실 수 있습니다:)

0개의 댓글