smashing the stack for fun and profit by aleph...

154
Review of Aleph One’s “ Smashing The Stack For Fun And Profit” by vangelis([email protected]) 888 888 888 888 888 888 888 888 888 .d88b. 888 888 888 88888b. 8888b. .d8888b 888 888 .d88b. 888d888 888 888 888 d88""88b 888 888 888 888 "88b "88b d88P" 888 .88P d8p Y8b 888p 888 888 888 888 888 888 888 888 888 888 .d888888 888 888888K 88888888 888 Y88b 888 d88P Y88..88P Y88b 888 d88P 888 888 888 888 Y88b. 888 "88b Y8b. 888 "Y8888888P" "Y88P" "Y8888888P" 888 888 "Y888888 "Y8888P 888 888 Y8888 888

Upload: others

Post on 26-Jan-2020

3 views

Category:

Documents


0 download

TRANSCRIPT

  • Review of Aleph One’s

    “ Smashing The Stack For Fun And

    Profit”

    by vangelis([email protected])

    888 888

    888 888

    888 888

    888 888 888 .d88b. 888 888 888 88888b. 8888b. .d8888b 888 888 .d88b. 888d888

    888 888 888 d88""88b 888 888 888 888 "88b "88b d88P" 888 .88P d8p Y8b 888p

    888 888 888 888 888 888 888 888 888 888 .d888888 888 888888K 88888888 888

    Y88b 888 d88P Y88..88P Y88b 888 d88P 888 888 888 888 Y88b. 888 "88b Y8b. 888

    "Y8888888P" "Y88P" "Y8888888P" 888 888 "Y888888 "Y8888P 888 888 Y8888 888

    mailto:[email protected]

  • Aleph One의 Smashing the Stack for Fun and Profit는 버퍼 오버플로우(buffer overflow)에

    대한 문서로서는 고전 중의 고전이 된 글이다. 실력 있는 해커가 되기 위해 다방면의 글을

    읽어야 하는데, Aleph One의 이 글을 읽지 않고 시스템 해킹 분야를 공부한다는 것은 상상도 할

    수 없는 일이다. 이 글이 쓰여져 Phrack에 발표된 것이 1996년 11월 8일이지만 여전히 그

    영향력은 무시할 수 없는 무게를 지니고 있다. 거의 모든 오버플로우 관련 글들이 이 문서를

    바탕으로 쓰여졌고, 쓰여지고 있는 것을 생각한다면 이 글의 무게는 미루어 짐작할 수 있다.

    그럼에도 불구하고 아직 이 문서에 대해서 자세한 설명글이 아직 발표되지 않은 것은

    유감스러운 일이다. 고전을 읽는 즐거움은 커지만 그 고전을 완벽하게 이해하고 분석한다는

    것은 그렇게 용이한 일이 아니다. 그 이유는 고전이 가지고 있는 무게를 분석글이

    감당하기에는 너무 버거운 것이기 때문이다.

    그러나 그런 버거운 부담에도 불구하고 이 분석글을 쓰고 있는 첫번 째 이유는 필자

    개인의 지적 호기심을 충족시키기 위한 것이고, 둘 째는 이 글의 독자들이 좀더 쉽게 다가갈

    수 있도록 작은 도움을 주기 위한 것이다.

    이 글이 고전이라면 나름대로의 준비 과정을 거치고 읽는 것이 좋을 것이다. 사전 지식

    없이 이 글을 읽게 된다면 그 결과는 뻔한 것이다. 이 글을 제대로 이해하기 위해서는

    기본적인 어셈블리어1 지식, 가상 메모리에 대한 개념, gdb의 사용법2, 그리고 버퍼(buffer)에

    대한 확실한 이해가 필요하다. 물론 C언어와 유닉스 계열 시스템에 대한 이해도 필수적이다.

    특히 이 글의 모든 테스트들이 대부분 intel x86 CPU 3 와 리눅스에서 이루어 진 것이므로

    리눅스에 대한 이해도 필수적이라 하겠다. 각종 책이나 인터넷 상으로 구할 수 있는 정보를

    이용하여 먼저 공부한 후 이 글을 읽어보는 것이 현명한 선택이라고 생각한다. 빨리 가고

    싶거든 여유 있게 시작하자!

    우선 이 글은 버퍼 오버플로우에 대해 알아보는 글이므로 당연이 버퍼(buffer)가 무엇인지

    알아보고 넘어가야 한다. 버퍼는 같은 데이터 형태를 가진 연속된 컴퓨터 메모리 블록인데,

    오버플로우를 이야기 할 때는 보통 문자 배열(character array)를 말한다. 보통 문자열을

    저장하기 위한 영역을 확보하기 위해 배열이나 malloc() 함수 등을 사용하는 것은 잘 알고

    있을 것이다. 문자 배열에 대해서는 따로 설명할 필요는 없을 것으로 생각한다. C 언어의

    지극히 기초 부분이기 때문에 이에 대한 설명은 하지 않겠다. 하지만 좀더 깊은 공부를 위해서

    1 http://linuxassembly.org 참고

    2 http://sources.redhat.com/gdb/current/onlinedocs/gdb_toc.html 참고

    3 http://www.intel.com/design/Pentium4/manuals 참고

    1

  • 이 글을 읽는 독자는 반드시 배열에 대해서 다시 공부하자. 기초를 다시 공부한다고 해서 흠이

    될 것은 없다.

    배열은 C 언어의 다른 변수들과 마찬가지로 정적(static) 또는 동적(dynamic)으로 선언될

    수 있다. ‘변수가 선언된다’는 것은 메모리에 특정 데이터를 받아들이는데 필요한 공간을

    할당받는다는 것을 의미한다. 변수(variable)라 함은 메모리 내에 독특한 이름을 가지고 있는

    데이터 저장 영역을 말한다는 것을 다들 알고 있을 것이다. 변수는 저장될 데이터가

    초기화되거나 프로그램 실행시 데이터가 입력되는 초기화되지 않은 변수가 있다. 우리가

    다루는 이 글에서는 문제가 되는 것들은 대부분 초기화되지 않은 변수들이며, 선언된 변수를

    위해 할당된 공간에 인수로 데이터를 입력받을 수 있는 형태를 보통 가지고 있다. 변수를

    분류하는 방법에 대해서는 C 언어의 기초를 가지고 있는 사람이라면 잘 알고 있을 것이므로

    변수의 분류에 대해서는 별도 설명은 하지 않겠다. 대신 필요한 부분이 있다면 설명을 하도록

    하겠다.

    정적 변수는 로딩 시에 데이터 세그먼트(segment) 4에 할당된다. 이에 비해 동적 변수는

    실행 시 스택에 할당된다. 즉, 소스코드를 컴파일 한 후 실행파일을 실행시킬 때 인자로

    데이터를 입력받게 되는 것이다. 이 글의 중심은 동적 버퍼의 오버플로우, 즉, 스택 기반의

    오버플로우에 대한 것이다.

    다음은 동적 변수가 사용되고 있는 예로서, 독자들도 한눈에 알 수 있듯이 오버플로우

    취약점을 가지고 있는 소스 코드이다. 여기서 문제가 되는 동적 변수는 char buffer[10];

    부분이다. 변수 char buffer[10]에는 프로그램을 실행할 때 데이터가 입력되게 된다. 그래서

    ‘동적이다’라는 표현을 사용하는 것이다. 그런데 이것이 문제가 되는 것은 strcpy() 함수의

    사용 때문임을 알 수 있다. strcpy() 함수는 메모리에 할당된 크기보다 더 많은 데이터를

    입력할 수 있어 오버플로우 문제를 일으킬 수 있는 대표적인 함수이다. 이에 대해서는 뒤에서

    좀더 자세하게 설명하도록 하겠다. 다음은 소스코드를 작성하고, 컴파일하여 프로그램을

    실행하고, 데이터를 입력하며, 그 결과 오버플로우가 발생하는 과정을 보여준다.

    [vangelis@localhost test]$ vi example.c

  • #include

    main()

    {

    char buffer[10];

    char str;

    printf("Put strings into buffer:");

    scanf("%s",&str);

    strcpy(buffer,str);

    }

    [vangelis@localhost test]$ gcc -o example example.c

  • 먼저 text 영역에 대해 알아보자. text 영역은 인스트럭션(instruction – 프로그램의 기계어

    코드)들을 포함하고 있으며, 읽기 전용이다. 읽기 전용이기 때문에 이 영역에 쓰기를 시도하면

    Segmentation violation 또는 Segmentation fault가 발생한다. Segmentation violation은 특정

    영역에 대한 각종 형태의 침범의 결과로 발생하는 것을 의미한다. 여기서 말한‘각종 형태의

    침범’에 대해서는 오버플로우를 공부할 때마다 자주 접하게 될 것이다.

    이제 data 영역에 대해서 알아보자. 이 영역은 초기화된 데이터와 초기화되지 않은 데이터

    를 포함하고 있다. 정적 변수(static variable) 가 이 영역에 저장되어 있다. 우리가 변수를 몇

    가지 종류로 나누어 공부해야 하는 이유는 변수의 범위가 중요하기 때문이다. 변수의 범위는

    프로그램에서 사용된 각 변수의 유효한 생명력, 메모리에서 변수의 값이 보존되는 기간과 그

    변수를 저장하기 위한 저장 영역의 할당 및 해제에 영향을 미치기 때문이다. 프로그램에서 사

    용되는 각종 함수들은 각종 데이터들을 사용하는데, 이 함수들이 사용할 데이터는 변수에 할당

    되어 있거나 프로그램 실행시 할당되는 것들이다.

    변수는 크게 외부 변수와 지역 변수로 구분할 수 있는데, main() 함수가 시작되기 전에 선

    언된 것이 외부 변수 또는 전역(global) 변수라 하고, 지역(local) 변수는 특정 함수 내에서 선

    언된 것을 말한다. 로컬 변수는 기본적으로 자동변수인데, 이것은 변수가 정의되어 있는 함수가

    호출될 때마다 변수의 값을 보존하지 않는 것을 의미한다. 그러나 함수가 호출될 때마다 변수

    의 값을 보존하고자 한다면 static이란 키워드를 이용하여 정적 변수(static variable)로 정의

    해야 한다. 정적 변수는 함수가 처음 호출될 때 초기화되고, 그 값이 그대로 보존된다. 다음 소

    스를 컴파일하여 실행해보자. 정적 변수에 대해 쉽게 알 수 있을 것이다.

    [vangelis@localhost test]$ vi static.c

    #include

    void func(void);

    main()

    {

    int c;

    for(c=0; c

  • {

    static int a = 0;

    int b = 0;

    printf("a=%d, b=%d\n", a++, b++); }

    [vangelis@localhost test]$ gcc -o static static.c

    [vangelis@localhost test]$ ./static

    c가 0일 때, a=0, b=0

    c가 1일 때, a=1, b=0

    c가 2일 때, a=2, b=0

    c가 3일 때, a=3, b=0

    c가 4일 때, a=4, b=0

    위의 소스에서 변수 a 앞에 static이란 키워드가 붙어 있다. 실행결과를 보면 1씩 더해져 값이

    출력되고 있다. 이것은 처음 초기화된 이후부터 값이 보존되었기 때문이다. 소스와 이

    실행결과를 보고도 이해가 되지 않는 사람은 C 언어에 대해서 먼저 공부해야 할 것이다.

    data 영역을 이야기할 때 함께 이야기 할 것이 data와 bss이다. data와 bss 영역 둘 다

    전역변수에 제공되며, 컴파일 때 할당된다. data 영역은 초기화된 정적(static) 데이터를, bss

    영역은 초기화되지 않은 데이터를 포함하고 있다. 이 영역은 brk 시스템 호출(system call)6에

    의해 크기가 변경될 수 있다. 만약 이 영역들의 사용가능한 메모리가 고갈될 경우 실행중인

    프로세스가 중단되고 재실행되도록 조절된다. 메모리가 부족할 경우 프로세스는 그 임무를 할

    수 없기 때문에 다시 충분한 메모리 공간을 할당받아야 하기 때문이다. 새로운 메모리는

    data와 stack 세그먼트 사이에 추가된다. 리눅스에서 메모리 할당 시스템은 몇 가지가 있다.

    그러나 이 글에서 다룰 내용은 아닌 것 같다. 앞으로도 이 글의 진행을 위해 필요한 부분이

    나올 경우에만 언급하도록 하겠다.

    6 brk은 sbrk와 함께 호출된 프로세스의 데이터 세그먼트을 위해 할당된 공간의 영역을 동적으로 변경하기 위해 사용된다. 이 변경은 프로세스의 break value을 다시 세팅하고, 적절한 양의 공간을

    할당함으로써 이루어진다. break value는 data segment의 끝 넘어 처음으로 할당된 것의 주소이다. 할당된

    공간은 break value가 증가하면서 늘어난다. 새로 할당된 공간은 0으로 설정된다. 하지만, 만약 같은

    메모리 공간이 같은 프로세스에 다시 할당되면 그것의 내용은 정의되어 있지 않다. 다음은 brk의

    시놉시스(synopsis)이다. 시놉시스란 간단한 개요를 의미한다.

    #include

    int brk (void *endds);

    void *sbrk (ssize_t incr);

    5

  • 이제부터 Aleph One의 글이 stack overflow를 다루는 것이므로 스택에 대해 자세하게

    알아보도록 하겠다. 그 전에 앞에서 살펴보았던 프로세스의 메모리 구성도를 하나 추가하도록

    하겠다.

    high address

    env string

    argv string

    env pointer

    argv pointer

    argc

    stack

    heap

    bss

    data

    text

    low adresses

    [표1] 프로세스 메모리 구성도

    위의 도표에서 stack에서는 화살표가 아래로, heap영역에서는 화살표가 위를 가리키고

    있다. 이것은 스택이 함수를 호출할 때 인자나 호출된 함수의 지역변수를 저장하기 위해 아래

    방향으로 크기가 커진다(보통 스택은 가상 주소 0xC0000000로부터 ‘아래로 자란다’(grow

    down, 낮은 메모리 주소로 자란다)는 표현을 사용한다)는 것을 의미하며, 반면 프로그램 수행

    중에 malloc()이나 mfree() 라이브러리 함수를 이용해 동적으로 메모리 공간을 할당받을 수

    6

  • 있는데, 이 공간을 heap 영역이라 한다. 힙 영역은 위의 도표에서도 알 수 있듯이 데이터

    세그먼트의 끝 이후 부분을 차지한다. 힙 영역은 가상 주소 위 방향으로 자라기 때문에 위의

    도표에서 화살표가 위로 향해 있다.

    위의 도표를 좀더 잘 이해하기 위해 다음 프로그램을 보고, 다음 프로그램의 각 요소들이

    어디에 위치하는지 확인해보도록 하자. 도표는 단순화시켰다. 다음 표와 소스는 『 리눅스

    매니아를 위한 커널 프로그래밍 』(조유근 외 2명 지음, 교학사)이라는 책을 참고했다.

    #include

    int a,b;

    int global_variable = 3;

    char buf[100];

    main(int argc, char *argv[])

    {

    int i=1;

    int local_variable;

    a=i+1;

    printf(“a=%d\n”,a);

    }

    커널 공간(kernel space) kernel

    stack

    ↓ argc, argv, i , local_variable

    data a, b, global_variable

    높은

    메모리 주소

    낮은

    메모리 주소

    사용자 공간(user space)

    text a=i+1;

    printf(“a=%d₩n”,a);

    7

  • 스택이란 무엇인가

    스택은 컴퓨터 과학에서 자주 사용되는 추상적인 데이터 타입이다. 추상적인 데이터

    타입이기 때문에 눈을 통해 입체적으로 확인할 수는 없다. 그래서 독자들은 스택의 구조와 그

    용도에 대해 추상성을 전제로 하고 공부를 할 필요가 있다. 스택의 경우, 스택에 위치한 마지막

    오브젝트가 먼저 제거되는 속성을 지니고 있다. 이 속성을 보통 last in first out queue, 또는

    LIFO라고 지칭된다. 몇 가지 오퍼레이션이 스택에 정의되어 있다. 가장 중요한 것 중 두

    가지가 PUSH와 POP이다. PUSH는 스택의 꼭대기에 요소를 더하고, POP은 반대로 스택의

    꼭대기에 있는 마지막 요소를 제거함으로써 스택의 크기를 줄인다. 필수적인 오퍼레이션을

    이해하기 위해 기본적인 어셈블리어 공부가 필요하다. 어셈블리어는 컴퓨터에 대해 직접적인

    통제를 할 수 있도록 해주는 언어이다.

    어셈블리어에 대한 정보는 인터넷 상에서도 많이 구할 수 있으며, 가장 대표적인 사이트가

    http://www.linuxassembly.org이다. 초보자들이 볼만한 책으로는 BOB Neveln이 쓴 Linux

    Assembly Language Programming을 권한다. 번역이 되어 나왔는지 모르겠다. 영어 실력이

    된다면 원서를 볼 것을 권한다.

    왜 스택을 사용하는가?

    현대 컴퓨터들은 높은 수준의 언어들을 염두에 두고 고안되었다. 높은 수준의 언어들에

    의해 도입된 프로그램들을 구조화하기 위한 가장 중요한 테크닉은 프로시저(procedure)7 또는

    함수(function)이다. 하나의 관점에서 보면 프로시저 호출은 jump 8 가 하는 것과 같이 통제

    흐름을 변경할 수 있다. 하지만 jump와는 달리 프로시저가 그것의 임무를 수행하는 것을

    끝마쳤을 때 함수는 그 호출을 뒤따르는 문장(statement) 또는 명령(instruction)에게 통제권을

    리턴한다. 이것은 gdb를 이용해 함수 호출 과정을 살펴보면 쉽게 알 수 있다. 다음 간단한

    소스를 이용해 이 과정을 알아보도록 하자.

    [vangelis@localhost test]$ cat > e.c

    7 프로그래밍에서 procedure란 함수(function)와 거의 유사한 뜻으로 사용된다. 함수는 일정한 동작을 수행하고, 함수를 호출한 프로그램에 리턴 값(결과 값)을 돌려주는 프로그래밍 언어의 독립적인 코드이다.

    C 언어나 다른 종류의 언어를 공부한 사람이라면 굳이 설명할 필요도 없을 것이다. 프로그래밍이 아닌 일

    반적인 상황에서 프로시저는 어떤 임무를 수행하기 위한 일련의 작업 절차를 의미한다.

    8 프로그램 내에서 프로세스를 다른 위치로 전환시키는 것으로써, branch한다고도 말하는데, 어셈블리어에서 대표적인 명령어가 jmp이다. jmp는 C 언어의 함수와는 달리 리턴 정보를 기록하지는 않는다.

    8

    http://www.linuxassembly.org/

  • #include

    int a, b, c;

    int function(int, int);

    main()

    {

    printf("Input a number for a: ");

    scanf("%d",&a);

    printf("Input a number for b: ");

    scanf("%d",&b);

    c=function(a,b);

    printf("The value of a+b is %d", c);

    return 0;

    }

    int function(int a, int b)

    {

    c=a+b;

    }

    [vangelis@localhost test]$ gcc -o e e.c

    [vangelis@localhost test]$ gdb e

    GNU gdb 5.3

    Copyright 2002 Free Software Foundation, Inc.

    GDB is free software, covered by the GNU General Public License, and you are

    welcome to change it and/or distribute copies of it under certain conditions.

    Type "show copying" to see the conditions.

    There is absolutely no warranty for GDB. Type "show warranty" for details.

    This GDB was configured as "i686-pc-linux-gnu"...

    (gdb) disas main

    Dump of assembler code for function main:

    0x8048490 : push %ebp

    0x8048491 : mov %esp,%ebp

    0x8048493 : sub $0x8,%esp

    0x8048496 : sub $0xc,%esp

    9

  • 0x8048499 : push $0x8048598

    0x804849e : call 0x8048370

    0x80484a3 : add $0x10,%esp

    0x80484a6 : sub $0x8,%esp

    0x80484a9 : push $0x8049714

    0x80484ae : push $0x80485af

    0x80484b3 : call 0x8048340

    0x80484b8 : add $0x10,%esp

    0x80484bb : sub $0xc,%esp

    0x80484be : push $0x80485b2

    0x80484c3 : call 0x8048370

    0x80484c8 : add $0x10,%esp

    0x80484cb : sub $0x8,%esp

    0x80484ce : push $0x804970c

    0x80484d3 : push $0x80485af

    0x80484d8 : call 0x8048340

    0x80484dd : add $0x10,%esp

    0x80484e0 : sub $0x8,%esp

    0x80484e3 : pushl 0x804970c

    0x80484e9 : pushl 0x8049714

    0x80484ef : call 0x804851c

    0x80484f4 : add $0x10,%esp

    0x80484f7 : mov %eax,%eax

    0x80484f9 : mov %eax,0x8049710

    0x80484fe : sub $0x8,%esp

    0x8048501 : pushl 0x8049710

    0x8048507 : push $0x80485c9

    0x804850c : call 0x8048370

    0x8048511 : add $0x10,%esp

    0x8048514 : mov $0x0,%eax

    0x8048519 : leave

    10

  • 0x804851a : ret

    0x804851b : nop

    End of assembler dump.

    (gdb) disas function

    Dump of assembler code for function function:

    0x804851c : push %ebp

    0x804851d : mov %esp,%ebp

    0x804851f : mov 0xc(%ebp),%eax

    0x8048522 : add 0x8(%ebp),%eax

    0x8048525 : mov %eax,0x8049710

    0x804852a : pop %ebp

    0x804852b : ret

    0x804852c : nop

    0x804852d : nop

    0x804852e : nop

    0x804852f : nop

    End of assembler dump.

    이 결과를 보면 어떤 특정한 과정과 명령을 통해 각 함수들이 호출되고 있는 것을 알 수 있다.

    이 특정한 과정에 대해서는 “shellcode” 섹션에서 상세하게 알아볼 것이다. 이 높은 수준의

    추상성은 스택이란 개념이 있기 때문에 구현될 수 있는 것이다. 또한 함수에서 사용되는 로컬

    변수를 위한 공간을 동적으로 할당하고, 함수에 파라미터(parameter) 9 를 건네주고, 그

    함수로부터 리턴 값을 돌려주기 위해 사용되기도 한다.

    9 파라미터(parameter – 매개변수 또는 인자)는 함수의 헤더에 포함되는 내용으로, 아규먼트(argument - 인

    수)에 대응하여 영역을 확보하는 역할을 한다. 함수의 파라미터는 고정적인 것이므로 프로그램이 실행되

    는 동안 변하지 않는다. 이에 반해 아규먼트는 함수를 호출하는 프로그램에 의해서 함수로 전달되는 실제

    값이다. 함수가 호출될 때 다른 인수값이 전달될 수 있다. 함수는 인수에 대응하는 파라미터의 이름을 통

    해 값을 받아들인다. 앞에서 제시했던 e.c의 소스코드의 int function(int a, int b)에서 파라미터는 a와

    b이다. 그리고 아규먼트는 프로그램 실행시 입력될 a와 b의 데이터(int형 숫자)이다.

    11

  • 스택 영역

    이제부터 스택 영역에 대해서 좀더 자세히 알아보자. 다시 강조하지만 Aleph One의 글이

    다루는 분야는 오버플로우 중에서도 스택 오버플로우이다. 스택은 데이터를 포함하고 있는

    메모리의 연속된 블록이다. 연속된 블록이라는 것은 스택이 파편적으로 흩어져 있는 것이

    아니라 메모리에서 특정한 위치를 연속적으로 차지하고 있다는 것을 의미한다. 블록(block)은

    주기억 장치의 기억 공간의 물리적 구조와는 관계없이 연속된 정보를 의미하는데, 주로

    입출력시에 사용되는 하나의 입출력 명령에 의해 이동되는 정보의 단위이다. SP(stack

    pointer)라는 레지스터(register)가 스택의 꼭대기를 가리키고 있다. 스택의 바닥은 고정된

    주소에 있다. 스택의 크기는 프로그램 실행시 커널에 의해 동적으로 조정되는데, 이것은

    데이터의 크기와 관련되어 있기 때문이다. 이것에 대해서는 앞에서 메모리 할당 시스템에 대해

    이야기 할 때 언급했었다. 여기서 우리는 레지스터에 대해 먼저 알아볼 필요는 느끼게 된다.

    먼저 레지스터에 대해 간단히 알아본 후 다음 부분으로 넘어가자.

    컴퓨터 시스템에서 제일 중요한 기능을 하는 부분이 바로 CPU이다. 이것은 CPU가 연산을

    포함해 직접적인 역할을 다하기 때문이다. CPU는 프로그램을 수행하는 장치로서

    instruction의 수행 기능 가질뿐만 아니라 instruction들의 수행 순서를 제어하는 기능을

    가지고 있다. 이런한 기능을 수행하기 위해 CPU는 연산 장치(ALU), 레지스터와 플래그(flag),

    내부 버스(internal bus), 제어 장치(CU)와 같은 하드웨어 요소를 가지고 있다. 우리가 지금

    알아보는 것이 레지스터이므로 레지스터와 관련된 것만 집중적으로 알아보도록 하겠다. 다음

    내용의 일부분은 『 컴퓨터 구조화 』(조정완 저)라는 책을 참고했다. 미리 말해둘 것은 모든

    부분을 그대로 옮길 수 필요성을 느끼지 못했기 때문에 약간의 통일성이 떨어지고, 내용이

    빈약할 수 있다. 그러니 이 글을 읽는 독자들은 좀더 자세히 설명되어 있는 책이나 자료를

    참고하길 바란다. 이 글은 레지스터 자체에 대한 설명글이 아니라는 것을 염두에 두길 바란다.

    레지스터(register)는 정보 저장 기능을 가진 요소로서 데이터나 주소를 기억시키는데

    필요하며, 플래그는 연산 결과의 상태를 나타내는데 사용된다. 레지스터는 직렬로 연결된

    플립-플롭(flip-flop)이나 래치(latch)로 구성되어 있다. 간단히 정리하면 다음과 같은 특징을

    레지스터는 가지고 있다.

    • 프로그램의 수행에 필요한 정보나 수행 중에 발생하는 정보를 기억하는 장소이다.

    • 레지스터에 기억된 정보는 주기억 장치에 기억시킨 후에 디스크에 기억시켜야 한다.

    • 정보를 기억시키거나 기억된 정보를 이용하기 위해서는 주소를 사용하여 지정해야 한다.

    레지스터 지정에 사용되는 주소를 레지스터 주소 혹은 레지스터 번호라고 한다.

    12

  • 이제 레지스터의 종류에 대해 알아보자. 레지스터를 분류하는 방법에는 두 가지가 있는데,

    첫 번째는 프로그래머가 레지스터에 기억된 내용을 변경시키거나 기억된 내용을 사용할 수

    있는 가시 레지스터(visible register)와 그렇지 않은 불가시 레지스터(invisible register)로

    나눌 수 있다. 가시 레지스터에는 연산 레지스터, 인덱스 레지스터, 프로그램 주소 카운터

    레지스터가 있고, 불가시 레지스터는 인스트럭션 레지스터(instruction register), 기억 장치

    주소 레지스터(memory address register: MAR), 그리고 기억 장치 버퍼(데이터)

    레지스터(memory buffer(data) register: MBR(MDR))가 있다.

    둘 째 분류 방법은 레지스터에 기억시키는 정보의 종류에 따라 분류하는 방법이다. 이

    분류에 의한 종류는 데이터 레지스터(data register), 주소 레지스터(address register), 그리고

    상태 레지스터(status register)가 있다.

    우리가 이 글을 다루면서 주로 살펴보아야할 것은 데이터 레지스터이다. 데이터

    레지스터는 함수 연산 기능의 instruction의 수행시 사용되는 데이터를 기억시키는

    레지스터이다. 그래서 데이터 레지스터에는 수, 논리값, 문자들이 기억될 수 있다. 데이터

    레지스터에는 AC(accumulator: 연산 전담 레지스터), GPR(general purpose register: 범용

    레지스터), 스택(stack) 등이 있다. 우리는 여기서 스택에 대해서만 알아보기로 한다. 다른

    것들은 관련 책이나 문서들을 참고하길 바란다.

    스택은 기억된 정보를 처리하는 순서가 특수한 구조로, 스택에 기억되는 역순서로

    처리되는 구조이다. 스택을 레지스터로 구현할 때는 적어도 2개 이상의 데이터 레지스터가

    필요한데, 반드시 주소 레지스터인 스택 포인터(stack pointer)가 필요하다. 여기서 주소

    레지스터는 기억된 정보에 접근하는데 필요한 정보인 주소를 기억하는 레지스터를 말한다.

    스택 포인터 레지스터는 스택의 최상단, 즉, 최근 스택에 입력된 데이터 위치의 주소를

    기억하고 있다. 스택에 데이터를 기억시킬 때 스택 포인터 SP를 1 증가시키고 그것이 지정하는

    위치에 기억시킨다. 그리고 스택에 기억된 데이터를 처리하기 위해 접근할 때는 SP가 지정하는

    곳에 기억된 데이터에 접근한 후 스택 포인터는 1 감소된다.

    Aleph One의 글로 다시 돌아가기 전에 우리가 공부하는데 도움이 될 수 있는 부분에 대해

    다음과 같이 레지스터에 대해 간단히 정리해보자.

    레지스트는 크게 4 부분으로 나눌 수 있으며, 각 이름 앞에 붙어 있는 ‘e’는

    ‘extended’를 의미한다. 즉, 16 비트 구조에서 32 비트 구조로 확장되었다는 것을 의미하는 것

    이다. 참고로 여기서는 x86 시스템을 기준으로 하고 있다.

    1. 일반적인 레지스트: data를 주로 다루는 레지스트로, %eax, %ebx, %ecx, %edx 등이 있다.

    2. 세그먼트 레지스트: 메모리 주소의 첫번째 부분을 가지고 있는 레지스트로, 16비

    13

  • 트 %cs, %ds, %ss 등이 있다.

    3. offset 레지스트: 세그먼트 레지스트에 대한 offset을 가리키는 레지스트이다.

    %eip(extended instruction pointer): 다음에 실행될 명령어에 대한 주소

    %ebp(extended base pointer): 함수의 지역변수를 위한 환경이 시작되는 곳

    %esi(extended source index): 메모리 블록을 이용한 연산에서 데이터 소스 offset

    을 가지고 있다.

    %edi(extended destination index): 메모리 블록을 이용한 연산에서 목적지 데이터

    offset을 가지고 있다.

    %esp(extended stack pointer): 스택의 꼭대기를 가리킨다.

    4. 특별한 레지스트: CPU에 의해서 사용되는 레지스트이다.

    Theo Chakkapark는 레지스터와 Intel x86 Assembly OPCode를 다음과 같이 정리10하고

    있으며, 이것은 많은 도움이 되리라 생각된다.

    범용 레지스터(General Purpose Registers)

    Name 32-Bit 16-Bit

    Accumulator EAX AX

    Base Register EBX BX

    Count Register ECX CX

    Data Register EDX DX

    Stack Pointer ESP SP

    Base Pointer EBP BP

    Source Index ESI SI

    Destination Index EDI DI

    Flags Register EFlags Flags

    Instruction Pointer EIP IP

    세그먼트 레지스터(Segment Registers)

    Name Register

    10 http://www.suteki.nu/x86 참고

    14

  • Code Segment CS

    Data Segment DS

    Stack Segment SS

    ES

    FS Extra Segment

    GS

    EFlags Register Bit Flag Desc Bit Flag Desc

    0 CF Carry Flag 16 RF Resume Flag

    1 1 None 17 VM Virtual-8086 Mode

    2 PF Parity Flag 18 AC Alignment Check

    3 0 None 19 VIF Virtual Interrupt Flag

    4 AF Aux. Carry 20 VIP Virtual Interrupt Pending

    5 0 None 21 ID Identification Flag

    6 ZF Zero Flag 22

    7 SF Sign Flag 23

    8 TF Trap Flag 24

    9 IF Interrupt Enable Flag 25

    10 DF Direction Flag 26

    11 OF Overflow Flag 27

    12 28

    13 IOPL I/O Privilege Level

    29

    14 NT Nested Task 30

    15 0 None 31

    0 None

    오퍼랜드 축약(Operand Abbreviations)

    축약 의미

    acc Register AL, AX, or EAX

    dst A register or memory location

    src A register, memory location, or a constant

    reg Any register other than a segment register

    segreg Visible part of CS, DS, SS, ES, FS, or GS

    imm A constant

    mem A memory location

    gdt Global Descriptor Table

    15

  • idt Interrupt Descriptor Table

    port An input or output port

    이동 명령(Movement Instructions)

    OPCode Operation

    MOV dst src dst « src

    reg16 src8

    reg32 src8 MOVZX

    reg32 src16

    reg « zero-extended src

    reg16 src8

    reg32 src8 MOVSX

    reg32 src16

    reg « sign-extended src

    LEA reg32 mem reg32 « offset(mem)

    XCHG dst src temp « dst; dst « src; src « temp

    스택 명령(Stack Instructions)

    OPCode Operation

    BYTE imm8 ESP « ESP - 4; mem32[ESP] « sign-extended imm8

    WORD imm16 ESP « ESP - 2; mem16[ESP] « imm16

    DWORD imm32 ESP « ESP - 4; mem32[ESP] « imm32

    src16

    src32 ESP « ESP - sizeof(src); mem[ESP] « src

    PUSH

    segreg

    src16

    src32 dst« mem[ESP]; ESP « ESP + sizeof(dst)

    POP

    segreg

    PUSHF None ESP « ESP - 4; mem32[ESP] « EFlags

    POPF None EFLAGS « mem32[ESP]; ESP « ESP + 4

    PUSHA None Pushes EAX, ECX, EDX, EBX, orig. ESP, EBP, ESI, EDI

    POPA None Pops EDI, ESI, EBP, ESP (discard), EBX, EDX, ECX, EAX

    Enter imm16, 0 Push EBP, EBP « ESP; ESP « ESP - imm16

    Leave None ESP « EBP; pop EBP

    가감 명령(Addition Instructions)

    OPCode Operation Flags

    ADD dst src dst « dst + src

    ADC dst src dst « dst + src + CF

    OF, SF, ZF, AF, CF, PF

    16

  • SUB dst src dst « dst - src

    SBB dst src dst « dst - src -CF

    CMP dst src dst « src; numeric result discarded

    INC dst dst « dst + 1

    DEC dst dst « dst - 1 OF, SF, ZF, AF, PF

    NEG dst dst « -dst OF, SF, ZF, AF, PF; CF=0 iif dst is 0.

    곱하기 및 나누기 명령(Multiply and Divide Instructions)

    OPCode Operation Comment Flags

    src8 AX « AL x src8

    src16 DX.AX « AX x src16 MUL

    src32 EDX.EAX « EAX x src32

    Use MUL with unsigned operands.

    src8 AX « AL x src8

    src16 DX.AX « AX x src16 IMUL

    src32 EDX.EAX « EAX x src32

    Use IMUL with signed operands.

    Sets CF & 0F if product overflows lower half

    src8 AL« quotient(AX / src8)

    src16 AX « quotient(DX.AX / src16) DIV

    src32 EAX « quotient(EDX.EAX / src32)

    Use DIV with unsigned operands.

    src8 AL« quotient(AX / src8)

    src16 AX « quotient(DX.AX / src16) IDIV

    src32 EAX « quotient(EDX.EAX / src32)

    Use IDIV with signed operands.

    OF, SF, ZF, AF, CF, PF become undefined

    CBW None AX « Sign-extended AL None None

    CWD None DX.AX « Sign-extended AX None None

    CDQ None EDX.EAX « Sign-extended EAX None None

    CWDE None EAX « Sign-extended AX None None

    Bitwise Instructions OPCode Operation Flags

    AND dst src dst « dst & src

    OR dst src dst« dst & src

    XOR dst src dst « dst ^ src

    TEST dst src dst & src; bitwise result discarded

    SF, ZF, PF; (OF, CF are cleared, AF becomes undefined)

    NOT dst dst « ~dst None

    17

  • Jump Instructions OPCode Label Operation Comment Flags

    JMP label Jump to label None

    JA / JNBE label Jump if above / Jump if not below or equal

    JAE / JNB label Jump if above or equal / Jump if not below

    JBE / JNA label Jump if below or equal / Jump if not above

    JB / JNAE label Jump if below / Jump if not above or equal

    Use when comparing unsigned operands.

    JG / JNLE label Jump if greater / Jump if not less or equal

    JGE / JNL label Jump if greater or equal / Jump if not less

    JLE / JNG label Jump if less or equal / Jump if not greater

    JL / JNGE label Jump if less / Jump if not greater or equal

    Use when comparing signed operands

    JE / JZ label Jump if equal / Jump if zero (ZF = 1)

    JNE / JNZ label Jump if not equal / not zero (ZF = 0) Equality comparisons

    JC label Jump if CF = 1 None

    JNC label Jump if CF = 0 None

    JS label Jump if SF = 1 None

    JNS label Jump if SF = 0 None

    None

    이제 다시 Aleph One의 글로 돌아가자. 앞의 첫문단에 이어 두 번째 문단으로부터 시작한다.

    여기서는 앞에서 다루었던 내용이 반복되어 나올 수 있으나 괘념치 말고 보길 바란다.

    스택은 어떤 함수를 호출할 때 push되고, 함수의 역할을 마치고 리턴할 때 pop되는 논리적

    스택 구조로 구성되어 있다. 이것은 앞에서 프로그램을 gdb를 이용했을 때의 결과에서 보았던

    내용이며, 뒤에서도 좀더 자세히 다룰 것이다. 이 스택 프레임은 함수에 대한 파라미터, 로컬

    변수, 그리고 함수 호출시에 IP(instruction pointer)의 값을 포함하여 이전 스택 프레임을

    복구하기 위해 필요한 데이터를 가지고 있다. 이 역시 앞에서 도표를 통해 확인했던 내용이다.

    혹시라도 혼란스럽다면 앞 부분을 다시 보길 바란다.

    스택을 구현하는 방법에 따라 스택이 아래로 자라거나(낮은 메모리 주소쪽으로) 위로 자랄

    수 있는데, Aleph One의 글에서는 Intel x86 CPU와 리눅스 시스템을 테스트용으로 사용하고

    있고, Intel x86 CPU 계열은 아래로 자라는 스택을 구현하고 있다. 이와 같은 구현 방법을

    사용하고 있는 것이 Motorola, SPARC, 그리고 MIPS11 등이다. 필자는 Motorola와 MIPS CPU를

    사용해본 적이 없다. 사용해본 적이 없기 때문에 무책임한 발언은 하지 않겠다. 혹시라도

    11 프랙 56호 “Writing MIPS/IRIX Shellcode”(http://phrack.org/show.php?p=56&a=15) 참고.

    18

  • 관심이 있는 독자라면 개별적으로 확인해보기 바란다. 그리고 SPARC 시스템의 스택과

    레지스터에 대해 좀더 많이 알고 싶은 독자는 “Understanding stacks and registers in the Sparc

    architecture(s)” 12 라는 글을 참고하길 바란다. 스택 포인터(SP, stack pointer) 또한 스택과

    마찬가지로 구현 방법에 따라 아키텍처별로 다를 수 있다. 그러나 이 글은 Intel x86 CPU를

    기준으로 하고 있다고 했기 때문에 스택 상의 마지막 주소를 SP가 가리킨다는 것을 염두에

    두고 글을 읽어야 겠다.

    다음은 프레임 포인터(FP, frame pointer)에 대해 알아보자. 스택의 꼭대기를 가리키는

    레지스터 SP이외에 스택을 구현하기 위해 프레임 포인터(FP, frame pointer)가 사용된다. FP는

    논리적인 스택 프레임에서 고정된 위치를 가리키고 있다. 고정된 위치를 가리키고 있기 때문에

    SP에 비해 또 다른 편이성이 있다. 원리상 지역 변수는 SP로부터 오프셋(offset)13을 줌으로써

    참조될 수 있다. 하지만 word 단위로 각종 데이터가 스택에 push 또는 pop되기 때문에 오프셋이

    변한다. 어떤 경우에는 컴파일러가 스택에 들어가는 데이터의 word 수를 추적할 수 있지만

    그렇지 않을 수도 있다. 그리고 인텔 기반의 프로세서와 같은 몇몇 프로세서에서는 SP로부터

    알려진 거리에 있는 어떤 변수에 접근하는 것은 많은 명령(instruction)들이 필요한다.

    이런 이유들 때문에 지역 변수와 파라미터를 참조하기 위해 많은 컴파일러들은 제 2의

    레지스터 FP를 사용한다. 이것은 FP로부터의 거리가 push와 pop이 되도 변하지 않기 때문이다.

    인텔 CPU에서는 이 FP 기능을 하는 것이 BP(EBP)이다. 이 글이 다루고 있는 인텔 CPU의 경우

    스택이 아래로 자란다고 앞에서 이야기 했는데, 이것 때문에 실제 파라미터는 FP로부터 양수의

    오프셋을 가지고, 지역 변수는 음의 오프셋을 가진다. 이에 대해서는 아래에서 다룰

    function()이라는 함수가 호출될 때 스택의 모양을 보면 알 수 있다. 다시 한번 더 이 부분에

    대해 설명하도록 하겠다.

    어떤 프로시저가 호출될 때 처음으로 하는 것은 이전의 프레임 포인터(FP)를 저장하는

    것이다. 이전 프레임 포인터를 저장하는 것은 프로시저가 exit될 때 원상태를 복원하기

    위해서이다. 프로시저가 exit되었는데 원상태로 복원되지 않는다면 불필요한 메모리 사용으로

    이어질 것이다. 그 다음 단계는 새로운 프레임 포인터를 만들기 위해 스택 포인터(SP)를

    프레임 포인터로 복사하며, 지역 변수를 위한 공간을 확보하기 위해 스택 포인터에서 사용되는

    변수의 크기만큼 뺀다. 이 과정을 보통 procedure prolog라고 부른다. 이에 비해 프로시저가

    12 http://www.sics.se/~psm/sparcstack.html 13 offset은 기준이 되는 주소로부터 또 다른 주소를 만들 때 그 기준이 되는 주소에 더해지는 값을 의미

    한다. 예를 들어, 기준이 되는 주소 a가 100이고, 새로운 주소 b의 값이 150이라고 하자. 그러면 오프셋

    은 50이 될 것이다. 오프셋을 이용하여 주소를 나타내는 것을 상대주소 지정방식이라고 한다.

    19

  • 종료될 때 원래의 상태로 돌아가기 위해 스택이 비워진다. 이때의 과정을 보통 procedure

    epilog라고 부른다. 이에 대한 자세한 내용은 뒤에서 곧 알아볼 것이다. 먼저 원문에 나오는

    내용으로 설명을 하고, 그 다음으로 필자의 시스템에서의 결과를 제시하며 다시 설명하는

    방법을 취하겠다. 다음 소스 코드를 보자.

    ------ example1.c -----------------------------------------------------------------------

    void function(int a, int b, int c){

    char buffer1[5];

    char buffer2[10];

    }

    void main(){

    function(1,2,3);

    }

    ------------------------------------------------------------------------------------

    위의 소스에서 function()이라는 함수를 호출하기 위해 프로그램은 어떤 과정을 거치는지

    알아보기 위해 -S 스위치를 주고 컴파일하면 어셈블리어 코드를 추출할 수 있다. 참고로 Gnu C

    컴파일러의 컴파일 과정은 다음과 같다. 소스코드의 이름을 prog.c라고 하자.

    Source code → Translation Unit → Assembly → Object → Executable File

    prog.c prog.i prog.s prog.o a.out(prog)

    위의 소스를 다음과 같이 컴파일 한다. $ gcc –S –o example1.s example1.c example1.s의 결과는 보면 다음과 같은 부분이 나온다.

    pushl $3

    pushl $2

    pushl $1

    call function

    20

  • 이것을 보면 3개의 아규먼트를 스택 안으로 함수에 대해 push하는데, 그 순서가 역순으로 되어

    있다. 그런 다음 함수를 호출하고 있다. 혹시라도 기초가 부족한 독자들을 위해 아규먼트와

    파라미터를 다시 설명한다. 위의 소스에서 “void function(int a, int b, int c)” 부분에는 a, b, c는

    파라미터이고, 이 파라미터에 직접 대입될 값이 정의되어 있는 “function(1, 2 ,3)”에서 1, 2, 3이

    아규먼트이다. “call function” 부분에서 call이라는 명령은 스택에 IP(instruction pointer)를

    push한다. 이때 스택에 저장된 IP를 리턴 어드레스(RET, return address)라고 부를 것이다.

    함수 호출이 있은 후 처음으로 이루어지는 것은 앞에서 잠시 이야기했던 procedure prolog이다.

    다음을 보자.

    push %ebp

    mov %esp,%ebp

    sub $20,%esp

    먼저 프레임 포인터로 사용되는 ebp를 스택에 push한다(push %ebp). 왜 프레임 포인터를

    사용하는지에 대해서는 앞에서도 이미 언급했지만, 고정된 위치를 가지는 프레임 포인터가

    지역 변수와 파라미터 등을 참조하기에 유용하기 때문이다. 그런 다음 현재 sp를 ebp 위로

    복사하고, 복사된 sp를 새로운 프레임 포인터로 만든다(mov %esp,%ebp). 앞으로도 복사되어

    새로운 프레임 포인터가 된 것을 ‘저장된 FP’(SFP, Saved Frame Pointer)라고 부를 것이다.

    그런 다음 SP로부터 크기를 빼 지역 변수를 위한 공간을 할당한다(sub $20,%esp). 이것은

    어떤 함수를 처리하기 위해 그 함수 내에서 사용되는 지역 변수가 들어갈 공간이 필요하기

    때문이다. 이상과 같이 procedure prolog라고 불리는 과정을 알아보았다.

    그런데 procedure prolog에서 지역 변수를 위해 20 바이트를 할당하고 있는데, 이것은

    메모리가 word 단위로 정렬되기 때문이다. 1 word는 4 바이트(32 비트) 크기이며, 그래서

    buffer1[5]은 실제 8 바이트, buffer2[10]는 12 바이트의 메모리를 차지하게 된다. 따라서 20

    바이트를 SP에서 빼게 된다. 간단히 표를 통해 알아보자.

    [ buffer1 ]

    1 word → buffer1[0] buffer1[1] buffer1[2] buffer1[3]

    2 word → buffer1[4]

    char형은 데이트는 1바이트의 메모리를 차지한다는 것은 잘 알고 있을 것이다. buffer1에 5

    21

  • 바이트가 할당되므로 2개의 word를 사용해야 한다. 2개의 word에서 실제 사용되는 5바이트

    이외의 나머지 3바이트는 데이터 처리와 상관없이 메모리 낭비가 되는 것이다. 결국 buffer1은

    2개의 word를 사용하므로 8바이트의 메모리가 할당되었다. 참고로 위의 표에서 왜

    buffer1[0]으로 시작되는지 이해가 되지 않는 사람은 이 글을 읽을 자격이 없다.

    [ buffer2 ]

    1 word → buffer2[0] buffer2[1] buffer2[2] buffer2[3]

    2 word → buffer2[4] buffer2[5] buffer2[6] buffer2[7]

    3 word → buffer2[8] buffer2[9]

    10바이트를 실제 사용하게 되는 buffer2[10]의 경우 3개의 word가 사용되어야 한다. 결국

    12바이트의 메모리가 할당되는 것이다.

    앞에서 언급한 것을 염두에 두고 function()이라는 함수가 호출될 때 스택의 모양을

    살펴보면 다음과 같다.

    낮은

    메모리 주소 높은

    메모리 주소

    buffer2 buffer1 SFP ret a b c ←

    [12 byte] [8 byte] [4 byte] [4 byte] [4 byte] [4 byte] [4 byte]

    스택의 꼭대기 스택의

    바닥

    위의 표에서 화살표가 ← 쪽으로 나와 있는데, 스택의 꼭대기로 데이터가 쌓이고, 대신

    메모리 주소는 낮아지는 것이다. 앞에서도 잠시 언급했듯이 프레임 포인터(SFP)를 기준으로

    보면 지역 변수의 오프셋은 음수이고, 파라미터들은 양수임을 알 수 있다. 이것은 스택이

    아래로 자라기 때문이다. 초등학교 수학 시간에 배운 것을 생각해보자. 기준점이 되는 SFP가

    0이고, 좌측에 있는 지역 변수는 음수의 오프셋을 가지게 되고, 우측에 있는 파라미터들은

    양수의 오프셋을 가지게 될 것이다. 유치할지도 모르는 비유를 하나 하자. 나무가 한 그루

    자란다. 줄기는 위로 자라고, 뿌리는 아래로 자란다. 메모리의 입장에서는 줄기쪽으로 커가고,

    22

  • 스택은 뿌리쪽으로 커간다. 정상적인 우리 인간의 사고로 보면 줄기쪽으로 생각한다. 그러나

    스택을 이야기할 때는 거꾸로 생각하자.

    이제 앞에서 살펴보았던 내용을 현재 우리가 주로 사용하고 있는 시스템에서의 결과를

    알아보도록 하겠다. 필자가 테스트 용으로 사용하고 있는 시스템은 Red Hat 8.0버전이다. 그리고

    먼저 언급해야 할 것은 gcc 버전에 따른 결과값이 다르다는 것이다. Aleph One의 글에서 사용된

    gcc 버전은 2.96 이하 버전이다. 바로 앞에서도 살펴보았지만 지역 변수를 위해 할당하는

    공간의 크기를 우리는 쉽게 이해할 수 있었다. 그러나 보안상의 이유인지 모르겠으나 gcc 2.96

    이후 버전부터는 결과값을 분석하기가 그만큼 어려워졌다. 하지만 포기하면 해커의 자질이

    없는 것이다. 뭔가 실마리를 찾아야 한다. 그래서 gcc 버전별 차이가 어떤지 확인을 해볼

    것이다. 먼저 다음은 필자의 시스템에서 나온 결과이다.

    [vangelis@localhost test]$ uname -a

    Linux localhost.localdomain 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux

    [vangelis@localhost test]$ gcc -v

    gcc version 3.2 20020903 (Red Hat Linux 8.0 3.2-7)

    [vangelis@localhost test]$ vi example1.c

    /* example1.c by Aleph One */

    void function(int a, int b, int c){

    char buffer1[5];

    char buffer2[10];

    }

    void main(){

    function(1,2,3);

    }

    ~

    ~

    [vangelis@localhost test]$ gcc -o ex ex.c

    ex.c: In function `main':

    ex.c:6: warning: return type of `main' is not `int'

    23

  • [vangelis@localhost test]$ gdb ex

    GNU gdb Red Hat Linux (5.2.1-4)

    Copyright 2002 Free Software Foundation, Inc.

    GDB is free software, covered by the GNU General Public License, and you are

    welcome to change it and/or distribute copies of it under certain conditions.

    Type "show copying" to see the conditions.

    There is absolutely no warranty for GDB. Type "show warranty" for details.

    This GDB was configured as "i386-redhat-linux"...

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482fc : push %ebp

    0x80482fd : mov %esp,%ebp

    0x80482ff : sub $0x8,%esp

    0x8048302 : and $0xfffffff0,%esp

    0x8048305 : mov $0x0,%eax

    0x804830a : sub %eax,%esp

    0x804830c : sub $0x4,%esp

    0x804830f : push $0x3

    0x8048311 : push $0x2

    0x8048313 : push $0x1

    0x8048315 : call 0x80482f4

    0x804831a : add $0x10,%esp

    0x804831d : leave

    0x804831e : ret

    0x804831f : nop

    End of assembler dump.

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    24

  • 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    (gdb) q

    [vangelis@localhost test]$

    gdb를 사용하여 추출한 function 함수 부분을 보면 다음과 같은 결과가 나왔다.

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    그런데 -S 스위치를 사용하여 추출한 아래의 경우는 다음과 같은 결과가 나왔다.

    function:

    pushl %ebp

    movl %esp, %ebp

    subl $40, %esp

    둘을 비교해보면 뭔가 차이점을 볼 수 있는데, gdb를 이용했을 때는 16진수로 값($0x28)으로

    표시되어 있고, -S 스위치를 사용하여 추출한 어셈블리어 코드에는 십진수($40)로 되어 있다는

    것을 알 수 있다. 이상할 것 없다 16진수 0x28은 십진수 40이기 때문이다. 단지 표현하는

    방식이 다를 뿐이다. 그리고 다른 차이점은 gdb를 이용했을 때는 ‘push’라고 되어 있지만

    아래의 어셈블리어 코드에서는 ‘pushl’ 로 표현되어 있다. 이 역시 표현의 차이이지 같은

    명령어이므로 초보자들은 헷갈리지 않도록 하자. 아래는 -S 스위치를 사용하여 추출한

    어셈블리어 코드이다.

    [vangelis@localhost test]$ gcc -S -o ex.s ex.c

    ex.c: In function `main':

    ex.c:6: warning: return type of `main' is not `int'

    [vangelis@localhost test]$ cat ex.s

    .file "ex.c"

    .text

    .align 2

    .globl function

    25

  • .type function,@function

    function:

    pushl %ebp

    movl %esp, %ebp

    subl $40, %esp

    leave

    ret

    .Lfe1:

    .size function,.Lfe1-function

    .align 2

    .globl main

    .type main,@function

    main:

    pushl %ebp

    movl %esp, %ebp

    subl $8, %esp

    andl $-16, %esp

    movl $0, %eax

    subl %eax, %esp

    subl $4, %esp

    pushl $3

    pushl $2

    pushl $1

    call function

    addl $16, %esp

    leave

    ret

    .Lfe2:

    .size main,.Lfe2-main

    .ident "GCC: (GNU) 3.2 20020903 (Red Hat Linux 8.0 3.2-7)"

    [vangelis@localhost test]$

    26

  • 위의 결과를 보면 Aleph One의 테스트 결과와 필자의 시스템에서 나온 결과는 다르다. 표를

    통해 알아보도록 하자.

    아마도 초보자들이 느끼는 가장 큰 어려움 중의 하나가 시스템의 차이 때문에 참고하는 문서의

    테스트 결과와 자신의 테스트 결과가 다르게 나와 혼란을 겪는 것일 것이다. 그러면 왜 이런

    차이가 날까?

    이제 gcc 2.96버전 이전과 이후의 차이점에 대해서 간단히 알아보도록 하자. gcc 2.96

    버전이 채택된 것은 Red Hat 7.0부터이다. 7.0 버전에 설치된 gcc 2.96버전에 버그가 있어 Red

    Hat Linux 7.1 - i386에서 수정된 gcc 2.96버전이 설치되었다. 그러나 2.96과 2.97 버전은 공식

    릴리즈가 아니라 개발버전이라는 것을 염두에 두자.14 필자가 이 글을 위해 사용하고 있는 Red

    Hat 8.0에서는 3.2 버전이 사용되고 있다. 그러면 우선 다음 테스트 결과를 보자.

    [vangelis@localhost gdb]$ vi e1.c

    void function(int a, int b, int c){

    char buffer1[5];

    14 http://www.gnu.org/software/gcc/gcc-2.96.html

    구분 소스 결과 차이점

    Aleph One

    (gcc 2.96 이전)

    push %ebp

    mov %esp,%ebp

    sub $20,%esp

    필자의 시스템

    (gcc 2.96 이후)

    void function(int a, int b, int c)

    {

    char buffer1[5];

    char buffer2[10];

    }

    void main(){

    function(1,2,3);

    }

    push %ebp

    mov %esp,%ebp

    sub $40,%esp

    지역 변수를 위한

    메모리 할당량이

    달라짐

    27

  • char buffer2[10];

    }

    void main(){

    function(1,2,3);

    }

    ~

    ~

    [vangelis@localhost gdb]$ gcc -o e1 e1.c

    e1.c: In function `main':

    e1.c:6: warning: return type of `main' is not `int'

    [vangelis@localhost gdb]$ gdb e1

    GNU gdb Red Hat Linux (5.2.1-4)

    Copyright 2002 Free Software Foundation, Inc.

    GDB is free software, covered by the GNU General Public License, and you are

    welcome to change it and/or distribute copies of it under certain conditions.

    Type "show copying" to see the conditions.

    There is absolutely no warranty for GDB. Type "show warranty" for details.

    This GDB was configured as "i386-redhat-linux"...

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482fc : push %ebp

    0x80482fd : mov %esp,%ebp

    0x80482ff : sub $0x8,%esp

    0x8048302 : and $0xfffffff0,%esp

    0x8048305 : mov $0x0,%eax

    0x804830a : sub %eax,%esp

    0x804830c : sub $0x4,%esp

    0x804830f : push $0x3

    0x8048311 : push $0x2

    0x8048313 : push $0x1

    0x8048315 : call 0x80482f4

    0x804831a : add $0x10,%esp

    0x804831d : leave

    0x804831e : ret

    0x804831f : nop

    End of assembler dump.

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    28

  • (gdb) q

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e2.c

    void function(int a, int b, int c){

    char buffer1[1];

    char buffer2[12];

    }

    void main(){

    function(1,2,3);

    }

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e3.c

    void function(int a, int b, int c){

    char buffer1[16];

    char buffer2[16];

    }

    void main(){

    function(1,2,3);

    }

    ~

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : leave

    29

  • 0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e4.c

    void function(int a, int b, int c){

    char buffer1[17];

    char buffer2[16];

    }

    void main(){

    function(1,2,3);

    }

    ~

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x38,%esp 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e5.c

    void function(int a, int b, int c){

    char buffer1[17];

    char buffer2[17];

    }

    void main(){

    function(1,2,3);

    }

    ~

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    30

  • 0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x48,%esp 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e6.c

    void function(int a, int b, int c){

    char buffer1[33];

    char buffer2[25];

    }

    void main(){

    function(1,2,3);

    }

    ~

    (gdb) disas function

    Dump of assembler code for function function:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x58,%esp 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    [vangelis@localhost gdb]$ vi e7.c

    void function(int a, int b, int c){

    char buffer1[49];

    char buffer2[25];

    }

    void main(){

    function(1,2,3);

    }

    ~

    (gdb) disas function

    Dump of assembler code for function function:

    31

  • 0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x68,%esp 0x80482fa : leave

    0x80482fb : ret

    End of assembler dump.

    --------------------------------------------------------------

    결과를 살펴보면 조금 복잡해 보이지만 뭔가 규칙성이 보인다. 그 규칙성만 찾아내면 우리의

    공부가 한층 쉬워질 수 있을 것이다. 이제 그 규칙성을 찾아내기 위해 다음과 같은 간단한

    소스를 이용해보자.

    ----------------------------------------------------------------------------------------

    void main()

    {

    char buffer[1];

    }

    ----------------------------------------------------------------------------------------

    char buffer[1] 부분은 그 데이터의 양을 4 바이트씩 증가시키면서 테스트를 할 것이다. 이렇게

    하는 이유는 word 단위로 데이터가 메모리에 들어가기 때문이다. 아래의 테스트에서 실행

    파일의 이름은 데이터의 양과 일치한다는 것을 참고로 이야기 한다. 그리고 각각의 소스는

    생략한다.

    [vangelis@localhost gdb]$ vi 1.c

    void main()

    {

    char buffer[1];

    }

    ~

    ~

    [vangelis@localhost gdb]$ gcc -o 1 1.c

    [vangelis@localhost gdb]$ gdb 1

    GNU gdb Red Hat Linux (5.2.1-4)

    Copyright 2002 Free Software Foundation, Inc.

    GDB is free software, covered by the GNU General Public License, and you are

    32

  • welcome to change it and/or distribute copies of it under certain conditions.

    Type "show copying" to see the conditions.

    There is absolutely no warranty for GDB. Type "show warranty" for details.

    This GDB was configured as "i386-redhat-linux"...

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x8,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 4

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x8,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 5

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x18,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    33

  • 0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 8

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x18,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 12

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x18,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 16

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x18,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    34

  • 0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 17

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 20

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 24

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    35

  • 0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 28

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 32

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x28,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 33

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    36

  • 0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x38,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    -- 중략 --

    [vangelis@localhost gdb]$ gdb 48

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x38,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 49

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x48,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    37

  • [vangelis@localhost gdb]$ gdb 64

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x48,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 65

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x58,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 80

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x58,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    38

  • 0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 81

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x68,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 96

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x68,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 97

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x78,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    39

  • 0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 112

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x78,%esp

    0x80482fa : and $0xfffffff0,%esp

    0x80482fd : mov $0x0,%eax

    0x8048302 : sub %eax,%esp

    0x8048304 : leave

    0x8048305 : ret

    0x8048306 : nop

    0x8048307 : nop

    End of assembler dump.

    [vangelis@localhost gdb]$ gdb 113

    (gdb) disas main

    Dump of assembler code for function main:

    0x80482f4 : push %ebp

    0x80482f5 : mov %esp,%ebp

    0x80482f7 : sub $0x88,%esp

    0x80482fd : and $0xfffffff0,%esp

    0x8048300 : mov $0x0,%eax

    0x8048305 : sub %eax,%esp

    0x8048307 : leave

    0x8048308 : ret

    0x8048309 : nop

    0x804830a : nop

    0x804830b : nop

    End of assembler dump.

    (gdb) q

    [vangelis@localhost gdb]$

    편의를 위해 위의 결과를 표로 작성해보자.

    40

  • 데이터 량 메모리 할당값 데이터 량 메모리 할당값

    (buffer의 크기) 16진수 10진수 (buffer의 크기) 16진수 10진수

    1 0x8 8 23 0x28 40

    2 0x8 8 24 0x28 40

    3 0x18 24 25 0x28 40

    4 0x8 8 26 0x28 40

    5 0x18 24 27 0x28 40

    6 0x18 24 28 0x28 40

    7 0x18 24 29 0x28 40

    8 0x8 8 30 0x28 40

    9 0x18 24 31 0x28 40

    10 0x18 24 32 0x28 40

    11 0x18 24 33 0x38 56

    12 0x18 24 48 0x38 56

    13 0x18 24 49 0x48 72

    14 0x18 24 64 0x48 72

    15 0x18 24 65 0x58 88

    16 0x18 24 80 0x58 88

    17 0x28 40 81 0x68 104

    18 0x28 40 96 0x68 104

    19 0x28 40 97 0x78 120

    20 0x28 40 112 0x78 120

    21 0x28 40 113 0x88 136

    22 0x28 40

    위를 보면 1부터 16까지 8바이트와 24바이트가 불규칙하게 분포되어 있지만, 대부분 24

    바이트가 할당되어 있다. 그리고 초록색 음영이 들어가 있는 부분을 보면 한가지 공통점이

    41

  • 있다. 드디어 우리가 어려워 했던 부분이 해결됨 셈이다. 위의 표를 보고 정리를 하면 다음과

    같다.

    어떤 함수가 호출될 때 지역 변수를 위해 공간을 할당하는 방식은 데이트의 양과 상관없이

    16 바이트를 다 채운다는 것이다. 즉, 예를 들어 지역 변수 buffer[5]에게는 gcc 2.96 이전

    버전에서는 word 단위로 메모리에 데이터가 적재되기 때문에 8 바이트가 할당되지만, 2.96 버전

    이후의 경우 16 바이트가 할당된다는 것이다. 그래서 buffer[17]의 경우 2.96 이전 버전에서는

    20 바이트가 할당되지만 이후 버전에서는 32 바이트가 할당된다. 오버플로우 공격시 이 점을

    염두에 두고 덮어쓰기를 해야한다.

    다시 Aleph One의 example1.c의 경우에 대해 마지막으로 간단히 설명해보자. 앞에서

    정리했던 표를 다시 보자.

    구분 소스 결과 차이점

    Aleph One

    (gcc 2.96 이전)

    push %ebp

    mov %esp,%ebp

    sub $20,%esp

    필자의 시스템

    (gcc 2.96 이후)

    void function(int a, int b, int c)

    {

    char buffer1[5];

    char buffer2[10];

    }

    void main(){

    function(1,2,3);

    }

    push %ebp

    mov %esp,%ebp

    sub $40,%esp

    지역 변수를 위한

    메모리 할당량이

    달라짐

    위의 표를 보면 gcc 2.96 이전 버전을 보면 20 바이트가 할당되어 있는데, 이것은 앞에서 설명한

    것처럼 다음과 같다.

    char buffer1[5](8 바이트) + char buffer2[10](12 바이트) = 20 바이트

    42

  • 그런데 gcc 2.96 이후 버전을 보면 40 바이트가 할당되어 있으며, 이것은 다음과 같이 설명할 수

    있다.

    char buffer1[5](16 바이트) + char buffer2[10](16 바이트) + dummy(8 바이트) = 40 바이트

    위의 내용을 보면, char buffer1[5]의 경우 데이터 값이 5이므로 16 바이트 이하이다.

    그래서 16 바이트 전체가 할당되고, char buffer2[10]의 경우 역시 데이터 값이 10이므로 16

    바이트 이하이다. 그래서 16 바이트가 할당된다. 이것만 계산하면 32 바이트가 되겠지만 테스트

    결과는 40 바이트였다. 이것은 dummy값 8 바이트가 할당되기 때문이다. 그래서 우리가 앞에서

    살펴보았던 원문에 나오는 표는 다음과 같이 수정해야 한다.

    buffer2 buffer1 dummy SFP ret a b c

    16 byte 16 byte 8 byte 4 byte 4 byte 4 byte 4 byte 4 byte

    여기서 dummy값 8 바이트가 왜 생겼는지에 대해 간단히 알아보자. 위의 표를 보면

    buffer2와 buffer1에도 각각 16 바이트가 할당되어 있다. gcc 2.96 이전 버전의 경우 12

    바이트와 8 바이트가 할당되어야 하지만, gcc 2.96 이후 버전에서는 스택의 정렬(alignment)을

    16 바이트씩 유지한다는 원칙에 의해 각각 16 바이트씩 할당된 것이다. 그런데 위의 표에서

    ret에서 buffer1까지의 거리가 dummy값을 제외한다면 8 바이트밖에 안된다. 그러면 스택의 기본

    정렬 규칙이 깨지게 되므로 스택 정렬 원칙을 유지하기 위해 buffer1과 sfp 사이에 dummy값 8

    바이트가 들어간 것이다.

    이제 지루한 기초 지식을 다 알아본 셈이다. 지금부터 본격적으로 버퍼 오버플로우 공격에

    대해 알아보자.

    Buffer Overflows 버퍼 오버플로우는 버퍼 안에 다룰 수 있는 것보다 더 많은 데이터를 집어넣고자할 때

    발생하는 결과이다. 이것은 프로그래머의 잘못된 코딩에 기인하고, 이 취약점을 공격자로

    하여금 공격자가 원하는 코드를 실행할 수 있게 해준다. 버퍼가 수용할 수 있는 것보다 더

    많은 데이터를 넣을 수 있는 상황은 바운드 체킹을 하지 않는 strcpy()와 같은 함수를 사용할

    43

  • 때 대부분 발생한다. 예를 들면, 버퍼가 수용할 수 있는 데이터의 양을 50으로 설정해두었고,

    버퍼에 데이터를 넣어주는 함수를 strcpy()를 사용한다면, strcpy() 함수의 경우 바운드 체킹을

    하지 않는 함수이기 때문에 버퍼에 데이터를 입력할 때 지정된 데이터의 양보다 더 많은

    데이터를 입력하는 것이 가능하고, 그 결과 리턴 어드레스도 조작할 수 있게 된다. 이 때 쉘을

    실행하는 쉘코드를 같이 입력하고, 조작된 리턴 어드레스가 쉘코드를 가리키도록 한다면

    함수가 자신의 일을 마치고 리턴할 때 쉘을 실행하게 될 것이고, 만약 취약한 프로그램이

    setuid 0으로 설정되어 있다면 루트쉘을 실행하게 될 것이다.

    앞에서 리턴 어드레스가 어떻게 조작될 수 있는지에 대해서 이미 알아보았다. 이제부터

    본격적인 버퍼 오버플로우 기법에 대해 알아보도록 한다. 물론 Aleph One의 글에 한정지어서 할

    것이다. 그 이유는 수 많은 기법들이 발표되었기 때문에 그 기법들을 모두 알아보기 위해서는

    별도의 글이 필요할 것이다. 다음 소스를 보자.

    ------ example2.c -----------------------------------------------------------------------

    void function(char *str){

    char buffer[16];

    strcpy(buffer, str);

    }

    void main(){

    char large_string[256];

    int;

    for(i=0; i

  • 같이 코딩을 할 프로그래머는 없을 것이다. 어디까지나 오버플로우 공부를 위해 제시한

    프로그램일뿐이다. 그래서 오버플로우 취약점을 가진 프로그램을 분석해서 취약점을 찾아내고,

    그 프로그램을 공략하기까지는 많은 열정이 필요할 것이다. 위의 프로그램은 독자들도 잘 알고

    있듯이 strcpy() 함수를 사용하여 바운드 체킹을 하지 않아 오버플로우 문제가 생긴다. 만약

    strncpy() 15 함수를 사용한다면 바운드 체킹을 하게 되어 오버플로우 문제는 발생하지 않을

    것이다. 다음 간단한 예를 보자.

    ----------------------------------------------------------------------------------------

    #include

    #include

    int main ()

    {

    char buf1[]= "wowhacker";

    char buf2[6];

    strncpy (buf2,buf1,5); /* 복사되어야 할 문자가 5로 설정되어 있음 */

    buf2[5]='₩0';

    puts (buf2);

    return 0;

    }

    ----------------------------------------------------------------------------------------

    위의 프로그램을 실행하면 결과는 ‘wowha’로 나올 것이다. 버퍼에 들어갈 데이터의 양을

    지정해두었기 때문에 오버플로우의 위험이 없다.

    다시 Aleph One의 소스로 돌아가면, 이 프로그램을 실행하게 되면 세그멘테이션 오류를

    일으키게 될 것이다. 함수를 호출하게 될 때 스택의 모양은 대략 다음과 같을 것이다. 물론

    요즘 환경에서는 더미값이 buffer와 sfp사이에 들어갈 것이다. 이것은 앞에서 살펴본 바다.

    15 strncpy() 함수의 시놉시스는 다음과 같다.

    char * strncpy ( char * dest, const char * src, sizet_t num );

    45

  • 낮은

    메모리 주소 높은

    메모리 주소

    ← buffer

    [ ]

    sfp

    [ ]

    ret

    [ ]

    *str

    [ ]

    스택의 꼭대기 스택의

    바닥

    이제 좀더 자세히 앞의 소스를 살펴보자. 먼저 왜 세그멘테이션 오류가 발생하는가? 이미

    수차례 살펴보았듯이 strcpy() 함수는 널문자가 스트링에서 발견될 때까지 buffer[]에

    *str(large_string[])의 내용을 복사한다. 소스에서도 볼 수 있듯이 buffer[]의 크기는

    *str보다 휠씬 더 작다. buffer[]의 크기는 16 바이트이고, buffer[]에 256 바이트를 넣으려고

    한다. 이것은 스택의 버퍼 다음에 250 바이트 전체가 덮어쓰인다는 것을 의미한다. 그래서 sfp,

    ret, 그리고 심지어 *str까지 덮어쓰게 된다. 결국 ‘A’라는 문자로 large_string을 가득

    채우게 된다. ‘A’의 16진수값은 0x41이므로, ‘A’로 덮어쓰여진 리턴 어드레스는

    0x41414141이 된다. 이것은 리턴 어드레스가 4 바이트이기 때문이다. 이제 함수가 리턴할 때

    세그멘테이션 오류를 일으킨 주소 0x41414141로부터 다음 instruction을 읽으려고 시도할

    것이고, 이것 때문에 세그멘테이션 오류가 나는 것의 이유이다.

    여태까지 알아본 것을 통해 우리는 버퍼 오버플로우를 통해 함수의 리턴 어드레스를

    변경시킬 수 있다는 것을 알았다. 리턴 어드레스가 변경되면 당연히 프로그램의 실행 흐름도

    역시 변경된다. 여기서 우리가 처음 보았던 example1.c의 경우로 다시 돌아가보자. 이 예를

    통해 리턴 어드레스를 조작하는 것과 임의의 코드를 실행할 수 있는 방법에 대해 알아보도록

    한다. example1.c의 소스코드는 다음과 같다.

    ------ example1.c -----------------------------------------------------------------------

    void function(int a, int b, int c){

    char buffer1[5];

    char buffer2[10];

    }

    void main(){

    function(1,2,3);

    46

  • }

    ----------------------------------------------------------------------------------------

    function()이라는 함수가 호출될 때 스택의 모양을 살펴보면 다음과 같다.

    낮은

    메모리 주소 높은

    메모리 주소

    buffer2 buffer1 SFP ret a b c ←

    [12 byte] [8 byte] [4 byte] [4 byte] [4 byte] [4 byte] [4 byte]

    스택의 꼭대기 스택의

    바닥

    스택 상에서 buffer1 앞에 SFP가 있고, SFP 앞에 ret이 있다. 그것은 buffer1[]의 끝에서 4

    바이트 거리이다. 하지만 buffer1[]은 실제 2 word이며, 그래서 8 바이트이다. 결국 리턴

    어드레스는 buffer1[]의 시작 부분부터 12 바이트가 된다. 리턴 값을 변경하기 위해 Aleph

    One은 함수 호출이 jump한 후 할당식 ‘x=1;’을 이용하는 방식을 사용하고 있다. 이를 위해

    리턴 어드레스에 8 바이트를 추가하고, 코드는 다음과 같다.

    ------ example3.c -----------------------------------------------------------------------

    void function(int a, int b, int c){

    char buffer1[5];

    char buffer2[10];

    int *ret;

    ret = buffer1+12;

    (*ret)+=8;

    }

    void main(){

    int x;

    x=0;

    47

  • function(1,2,3);

    x=1;

    printf(“%d\n”,x);

    }

    ----------------------------------------------------------------------------------------

    example1.c과 example3.c와 다른 점은 example3.c에서는 buffer1[]의 주소에 12를 더한

    것이다(ret = buffer1+12;). 이 새로운 어드레스는 리턴 어드레스가 저장되어 있는 곳이기도

    하다. buffer1에서 ret까지의 거리가 12가 된다는 것을 앞에서도 살펴본 바다. 그리고 우리는

    printf 콜에 대한 할당을 지나 스킵하기를 원한다. 그런데 리턴 어드레스에 8을 더하는 것을

    어떻게 알았는가? 이것은 gdb를 이용해 알아낼 수 있다.

    --------------------------------------------------------------------------

    [aleph1]$ gdb example3

    GDB is free software and you are welcome to distribute copies of it under certain conditions;

    type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show

    warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation,

    Inc...

    (no debugging symbols found)...

    (gdb) disassemble main

    Dump of assembler code for function main:

    0x8000490 : pushl %ebp /* procedure prolog */

    0x8000491 : movl %esp,%ebp

    0x8000493 : subl $0x4,%esp /* 정수형 변수 x를 위한 공간 확보 */

    0x8000496 : movl $0x0,0xfffffffc(%ebp) /* x=0; */

    0x800049d : pushl $0x3 /* function(1,2,3); 파라미터 push */

    0x800049f : pushl $0x2

    0x80004a1 : pushl $0x1

    0x80004a3 : call 0x8000470 /* function(); 호출 */

    0x80004a8 : addl $0xc,%esp /* function(int a, int b, int c) */

    0x80004ab : movl $0x1,0xfffffffc(%ebp) /* x=1; ebp-4(x)에 1 복사 */

    48

  • 0x80004b2 : movl 0xfffffffc(%ebp),%eax /* x=1; ebp-4(x)을 eax로 복사 */

    0x80004b5 : pushl %eax /* x=1; 스택에 복사 */

    0x80004b6 : pushl $0x80004f8

    0x80004bb : call 0x8000378 /* printf(); 호출 */

    0x80004c0 : addl $0x8,%esp

    0x80004c3 : movl %ebp,%esp /* procedure epilog */

    0x80004c5 : popl %ebp

    0x80004c6 : ret

    0x80004c7 : nop

    ------------------------------------------------------------------------------

    function()을 호출할 때 RET이 0x8004a8이 된다는 것을 알 수 있으며, 0x80004ab에 있는

    할당식을 지나 jump하기를 원한다. 우리가 실행하고자 원하는 다음 instruction은

    0x80004b2이다. 이 둘 사이의 거리는 ‘0x80004b2 – 0x80004ab = 7’처럼 간단한 계산을 통해

    8이라는 것을 알 수 있다.

    49

  • Shell Code

    앞장에서 살펴본 것은 오버플로우 취약점을 이용해 리턴 어드레스를 변조하여 우리가

    원하는 코드를 실행하고, 이것을 통해 프로그램의 실행 흐름을 바꿀 수 있다는 것이었다. 그럼

    여기서 ‘우리가 실행하고자 원하는 코드’는 무엇인가? 대부분 쉘을 실행하는 쉘코드이다.

    쉘을 우리가 획득하게 되면, 특히 루트 쉘을 획득하게 되면 우리가 원하는 작업은 무엇이든 할

    수 있게 된다. 그럼 우리에게 남은 것은 어�