스택침범(Stack Overflow)을 막는 스택보호 (Stack Protection) 컴파일 옵션 : -fstack-protector

경험이 많은 프로그래머이건 적은 프로그래머이건 겪을 때마다 힘든 문제가 하나 있죠? ^^
바로 문제를 발생시키는 코드는 잘 넘어가고 엉뚱한 어디에선가 SIGBUS 를 맞거나 이상한 동작을 하는 경우입니다.

차라리 문제를 발생시키는 코드에서 바로 죽어버리면 문제 해결이 쉬운데 그걸 아무 경고없이 그냥 넘어가니 더 문제입니다. 이러한 대표적인 경우가 stack 을 깨먹었을 때죠.
stack 메모리 영역을 깨먹게 되면 stack 에 선언된 변수가 이상한 값으로 갑자기 셋팅되어 프로그램이 치명적인 오류를 일으키게 됩니다.
stack 침범(Stack Overflow)이 발생하면 아래와 같은 현상이 발생합니다. 이러한 현상을 이용하여 해킹을 하기도 합니다.
아마 프로그래밍 경험이 많으신 분들은 이상하게 함수가 리턴할 때 죽는 경우를 경험하기도 하셨을 겁니다. 이것이 다 스택 오버플로우에 의한 현상일 때가 대부분입니다.

  • 스택 오버플로우에 의해 지역변수 변경
  • 스택 오버플로우에 의해 리턴주소(Return Address) 또는 saved ebp 의 변경

아래의 코드와 메모리 구조의 그림을 볼까요 ?

int function(const char *s)
{
    int local_var;
    // do something
}

void foo (const char * s)
{
    char buf[128];
    strcpy(buf, s);
    function(buf);
    ...
}
위의 코드에 대한 메모리 구조를 살펴보면 다음과 같습니다. 아래 메모리 구조를 보니 무언가 짐작이 가지 않나요 ? ^^
buf[127] 을 넘어가는 오버플로우를 발생시켰을 경우 자칫 function 아래쪽까지 깨먹을 수 있다는 것입니다. 그러니, 함수 주소가 잘못되어 리턴 시 죽을 수도 있고, fuction 내의 stack 변수가 깨질 수도 있겠죠.

그래서, 위와 같은 위험을 막으려고 똑똑한 이들이 스택 침범(Stack Overflow)을 방어할 수 있도록 컴파일 옵션으로 -fstack-protector 을 만들어 놓았습니다. 이는 SSP(Stack-smashing protector) 또는 별칭으로 ProPolice 라고 부르기도 합니다.
그 원리는 아래의 그림으로 압축됩니다.

그림만 봐도 감이 오지 않나요 ?
즉, 스택보호 옵션을 적용시키면 그림과 같이 Stack 의 레이아웃이 변경되는데, function 영역이 stack 변수의 위로 배치되고, stack 변수와 Saved EBP 사이에는 보호값이 셋팅됩니다.
이 보호값 영역을 넘어서면 프로그램을 명시적으로 Down 시켜버립니다.
참고로 Saved EBP 는 현재 Stack 의 마지막을 가리키는 포인터라고 보시면 됩니다.
함수 호출 때마다 Saved EBP 가 쌓이겠죠.

자, 이제 이론을 배웠으니 시험을 해봐야죠 ? ^^
프로그래머는 항상 의심과 호기심으로 가득해야 합니다.
이게 과연 잘 동작하는지 의심부터 해봐야 합니다.
그리고, 그 합리적 의심을 정확하고 깊이 있는 시험 및 분석을 통해 증명해낼 수 있어야 합니다.

아래의 코드를 이용해서 간단하게 시험을 해보도록 하죠.

#include <stdio.h>

int main(int argc, char **argv)
{
    char  buf[8];

    if( argc > 1 )
    {
        int    x = 1;

        printf("before [%d]n", x);

        char * s = argv[1], *d = buf;
        while(*s != '') *d++ = *s++;

        printf("after [%d]n", x);
    }

    return 0;
}

위의 코드를 간단히 설명하면, 프로그램 실행 시 인자로 받은 string 을 지역 변수 buf 에 한문자씩 복사하는데, 지역변수 buf 의 길이보다 인자의 문자열 길이가 더 길어서 buf 영역을 넘어서게 복사하는 경우입니다.
제가 일부러 x 라는 integer 형 local 변수도 하나 선언해 두었습니다.
이 값도 영향을 받는 것을 보여주기 위함입니다.

일단 -fstack-protector 옵션없이 컴파일하여 실행해봅니다.

sh> gcc x.c

sh> ./a.out 123456789012345678901234567890

before [1]
after [909456435]

local 변수인 x 의 값이 쓰레기(garbage)값으로 변경이 되었습니다. 보이시죠 ?
이 변수값을 어디선가 유의미하게 사용하게 된다면 뼈아픈 장애를 맛볼 수도 있겠습니다.

이번에는 -fstack-protector 를 이용하여 컴파일한 후 수행해 보겠습니다.

sh> gcc -fstack-protector x.c

sh> ./a.out 123456789012345678901234567890

before [1]
after [1]

*** stack smashing detected ***: ./a.out terminated
Segmentation fault (core dumped)

"stack smashing detected" 라는 메시지와 함께 segv 로 죽어버리는군요.
아주 좋습니다.
x 의 값이 알아채지 못하게 변경되는 경우를 막았으니 이는 시스템의 엄청난 버그를 찾아준 것이나 다름없네요. ^^
복잡한 소프트웨어에서는 이러한 버그를 찾기가 정말 어렵습니다.

이제 위의 옵션 사용 후 gdb 를 통해 수행해보죠.

sh> gdb --args ./a.out 123456789012345678901234567890

...

(gdb) r

Starting program: /home/bitmyer/tmp/a.out 123456789012345678901234567890
before [1]
after [1]

*** stack smashing detected ***: /home/bitmyer/tmp/a.out terminated

Program received signal SIGSEGV, Segmentation fault.
0x0000003b6ba0f807 in ?? () from /lib64/libgcc_s.so.1
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.80.el6_3.6.x86_64 libgcc-4.4.7-3.el6.x86_64 libstdc++-4.4.7-3.el6.x86_64
(gdb) bt
#0  0x0000003b6ba0f807 in ?? () from /lib64/libgcc_s.so.1
#1  0x0000003b6ba100b9 in _Unwind_Backtrace () from /lib64/libgcc_s.so.1
#2  0x000000301bafe13e in backtrace () from /lib64/libc.so.6
#3  0x000000301ba6fffb in __libc_message () from /lib64/libc.so.6
#4  0x000000301bb01d47 in __fortify_fail () from /lib64/libc.so.6
#5  0x000000301bb01d10 in __stack_chk_fail () from /lib64/libc.so.6
#6  0x00000000004006c0 in main (argc=2, argv=0x7fffffffde38) at x.cpp:20

위의 backtrace 결과에서 __stack_chk_fail 이 나오면 이 옵션이 잘 동작한 것입니다.
해당 함수는 glibc 에 포함되어 있는 함수로, "stack smashing detected" 라는 메시지를 뿌려주고 종료하는 함수입니다.

자, 참 좋은 기능이죠 ?  저도 마음에 듭니다.

그런데, 한가지 좀 따져봐야할 것이 하나 있습니다. 이에 대한 증명은 여러분의 몫입니다.
"이 옵션을 사용하면 성능하락은 없을까 ?"

이 합리적 의심을 여러분이 증명해 보십시오. ^^
만약 이 옵션이 자신의 시스템에서 허용할 수 없는 범위 이상의 성능하락을 보인다면, 릴리즈 버전에는 이 옵션을 사용하지 않고 개발 단계에서 디버깅 용도로만 사용해도 유용하지 않을까 생각합니다.

참고로 이 옵션은 default 로는 8 byte 이상의 char, signed char, unsigned char 배열을 할당한 함수와 alloca 를 호출하는 함수만을 보호합니다.
만약 byte 수를 변경하거나 모든 함수를 보호하고 싶을 때는 옵션을 좀 더 명시해주어야 합니다.

gcc -fstack-protector --param ssp-buffer-size=N xx.c   ==> byte 변경
gcc -fstack-protector-all xx.c                                     ==> 모든 함수 보호

You may also like...

5 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x