반응형

출처: https://bpsecblog.wordpress.com/2016/06/10/memory_protect_linux_4/


이번 편은 linux 환경에서의 메모리 보호 기법 알아보기의 마지막 편입니다!!

3편에서 살펴봤던 PIC(Position Independent Code)는 공유 라이브러리에서 이용되지만 이번 편에서 살펴볼 PIE(Position Independent Executable)를 이용하면 실행 파일도 위치 독립적으로 생성할 수 있습니다. PIE에 대해 자세히 알아보겠습니다.

* 실습환경 Ubuntu 15.10 32bits


1. PIE(Position Independent Executable) 만들기

PIE(Position Independent Executable)이란, 전체가 위치 독립 코드로 이루어진 실행 가능한 바이너리입니다. 직접 PIE를 만들어보면서 PIE에 대해 자세히 알아보겠습니다.

먼저 PIC 공유 라이브러리를 만들고, 라이브러리를 우리가 만들 PIE에 링킹합니다.

$ vi mysum.c

linux_protect_4_01

위 소스를 다음과 같이 PIC 공유 라이브러리로 컴파일합니다.

$ gcc -c -fPIC mysum.c
// 독립적인 코드로 만들기 위해 -fPIC 옵션을 이용하여 컴파일

$ gcc -shared -o libmysum.so mysum.o
// 공유 라이브러리 즉, libmysum.so 파일 생성

linux_protect_4_02.png

$ vi test.c           // 만든 라이브러리 함수를 호출하는 프로그램 생성

linux_protect_4_03.png

위 소스를 공유 라이브러리와 함께 컴파일하여 PIE를 만들어보겠습니다.

$ gcc -fPIE -pie test.c -o test_pie -lmysum -L.
$ ./test_pie

linux_protect_4_04

test_pie를 실행하면 위와 같이 잘 동작하며, test_pie 프로그램은 앞에서 만들어줬던libmysum.so 공유 라이브러리를 아래와 같이 참조하고 있습니다.

$ ldd test_pie

linux_protect_4_05.png

그리고 checksec.sh을 이용하여 정말로 PIE가 걸렸는지 확인해 보면 다음과 같습니다.

checksec.sh –file test_pie

linux_protect_4_06.png

이렇게 checksec.sh 스크립트를 이용하여 실행파일이 PIE인지 아닌지를 알 수 있습니다.


2. non-PIE vs PIE

그렇다면 이제 본격적으로 PIE가 아닌 일반 실행파일과 PIE의 차이점을 알아보겠습니다.

예제는 간단히 buf에 저장된 값과 buf의 주소를 출력해주는 소스입니다.

linux_protect_4_07.png

이 소스를 non-PIE버전과 PIE버전으로 컴파일을 합니다.

$ gcc -o address address.c                                           // 일반 실행파일 생성
$ gcc -fPIE -pie -o address_pie address.c             // PIE 생성

linux_protect_4_08.png

그리고 아래와 같이 checksec.sh 스크립트를 실행해보면 일반 실행파일은 “No PIE”, PIE는 ”PIE enabled”라고 표시됩니다.

checksec.sh –file address
checksec.sh –file address_pie

linux_protect_4_09.png

파일의 타입을 확인할 수 있는 file 명령어를 일반 실행파일과 PIE에 대해 실행해 보면 아래와 같이 일반 실행파일은 “executable”이고, PIE는 “shared object”입니다.

$ file address
$ file address_pie

linux_protect_4_10.png


다음으로 다른 예제를 통해 함수 호출시 non-PIE와 PIE의 차이에 대해 알아보겠습니다.

linux_protect_4_11.png

위 예제에서는 사용자 정의 함수, puts, printf, system함수를 호출합니다. 이제 위 예제를 non-PIE, PIE 두 버전으로 컴파일하여 gdb로 디스어셈블 해보겠습니다.

$ gcc -o got got.c                                              // 일반 실행파일 생성
$ gcc -fPIE -pie -o got_pie got.c                // PIE 생성

linux_protect_4_12.png

# gdb -q got

linux_protect_4_13.png

일반 실행파일인 경우 정해진 주소에 코드들이 위치합니다. 또한 puts, printf, system함수의 plt도 절대주소로 고정되어 있으며 사용자정의함수인 func함수의 경우도 절대주소인 0x804847b에서부터 위치합니다.

# gdb -q got_pie

linux_protect_4_14.png

PIE인 경우 일반 실행파일과는 다르게 매우 작은 주소에 코드들이 위치합니다. 이는 PIC 공유 라이브러리처럼 주소공간의 어느 위치에 매핑되어도 작동하도록 상대주소로 되어있는 것입니다. 동적 링커는 PIE를 실행할 때 PIC 공유 라이브러리에 대해 수행할 때와 마찬가지로 상대주소를 주소공간상에 매핑하여 실행합니다.

라이브러리 함수를 호출할 때 non-PIE에서는 고정된 plt, got를 이용하여 함수를 호출하지만 PIE는 위치에 독립적인 실행파일, 즉 프로그램을 실행할 때마다 매핑되는 주소가 달라집니다. 그렇다면 이런 특징을 가진 PIE에서 라이브러리 함수는 어떻게 호출될까요?


위 예제를 디스어셈블 했을 때, 아래와 같이 non-PIE에서는 볼 수 없었던 코드가 보입니다.

linux_protect_4_15.png

바로 __x86.get_pc_thunk.bx라는 함수를 호출한 후 ebx 레지스터의 값을 0x194e만큼 증가시키는 코드입니다. 이 함수가 어떤 기능을 하는 지 직접 디버깅을 하면서 알아보겠습니다.

linux_protect_4_16.png

위와 같이 __x86.get_pc_thunk.bx 함수에서는 함수를 수행하고 돌아갈 주소를 ebx 레지스터에 저장합니다. 즉 이 함수를 호출하면서 어느 주소에 매핑이 되었는지를 알아온 것입니다.

linux_protect_4_17.png

그리고는 ebx 레지스터 값을 0x194e만큼 증가시켜 ebx 레지스터는 0x80002000 값을 갖게 되는데, 이 값은 .got.plt 섹션의 주소입니다.

linux_protect_4_18.png

정리하면 Position Independent Code, 즉 위치 독립 코드의 경우 코드가 어떤 주소에 매핑이 될지 모르므로 __x86.get_pc_thunk.bx 함수를 호출하여 함수 호출 후 다음으로 수행할 인스트럭션의 주소를 ebx 레지스터에 저장하고, ebx 레지스터의 값을 0x194e만큼 더해서 got영역의 주소를 알아낸 것입니다. 그런데 이 과정에서 “add ebx, 0x194e” 인스트럭션을 통해 got영역의 주소를 알게 되었는데, 0x194e라는 값은 어디서 튀어나온 값일까요?

다시 프로그램 실행 전으로 돌아가보면 아래와 같이 “add ebx, 0x194e” 인스트럭션이 위치하고 있는 오프셋은 0x6b2입니다.

linux_protect_4_19.png

그리고 got 영역의 오프셋은 0x2000입니다.

linux_protect_4_20.png

0x2000에서 0x194e를 빼면 0x6b2입니다. 즉 add 인스트럭션의 주소(오프셋)입니다. 메모리의 어느 주소에나 매핑이 되더라도 그 오프셋은 항상 동일하므로 컴파일 시 __x86.get_pc_thunk.bx 함수 호출로 정해지는 ebx 레지스터의 값에 어떤 값(X)을 더해야 got 영역의 주소가 되는 지를 결정할 수 있습니다.

X = “got영역의 주소(오프셋)” – “add 인스트럭션의 주소(오프셋)”

이렇게 got영역의 시작 주소를 알아내고 라이브러리 함수 호출(ex. puts함수 호출) 시에는 아래와 같이 got영역 시작 주소와의 오프셋을 이용하여 해당 함수의 got에 접근합니다.

linux_protect_4_21.png

puts함수가 한 번 이상 호출되지 않았으므로 아직 puts함수의 got에는 puts@plt+6의 주소가 저장되어 있습니다. 다시 plt로 돌아와서 “push 0x10” 인스트럭션 수행 후 0x80000490으로 점프합니다. 0x80000490에서는 오프셋을 통해 _dl_runtime_resolve 함수로 점프하며, 이 함수에서 _dl_fixup 함수를 통해 puts함수의 실제 주소를 알아오게 됩니다.

linux_protect_4_22.png


3. 컴파일 옵션의 의미

이번에는 지금껏 PIE 컴파일을 할 때, “-fPIE -pie”라고 해줬는데 이 각각의 의미가 무엇인지 알아보겠습니다.

먼저 정리하자면 “-fPIE“는 컴파일러를, ”-pie”는 링커를 위한 옵션입니다.

컴파일 과정을 화면에 출력해 주는 ‘-v’옵션을 이용하여 알아봅시다.

$ gcc -v -fPIE -pie -o test_pie test.c

캡처.PNG

gcc에서 cc1은 전처리된 C언어를 어셈블리어로 변환해주는 C컴파일러이고, collect2는 링커입니다. 즉 “-fPIE” 옵션은 컴파일단계에서 수행이 되고 “-pie”옵션은 링킹단계에서 수행이 됩니다. 따라서 “-pie”옵션을 주지 않고 ”-fPIE”옵션만 준다면 아래와 같이 PIE가 생성되지 않습니다.

$ gcc -fPIE -o test_no test.c

linux_protect_4_23.png
실행은 되지만 PIE는 적용되지 않은 화면

4. 실행할 때마다 주소가 바뀌는 PIE

마지막으로 챕터2에서 만들었던 address, address_pie 프로그램을 실행해보겠습니다.
먼저 일반 실행파일입니다.

linux_protect_4_24.png

몇 번을 실행하든 buf의 주소 값이 같습니다.

다음은 PIE를 실행시켜보겠습니다.

linux_protect_4_25.png

실행할 때마다 buf의 주소가 바뀝니다.

PIE는 위치 독립 실행파일이라 했습니다. 즉 실행할 때마다 매핑되는 주소가 어디든지에 상관없이 실행되는 파일로, 위와 같이 매핑되는 주소가 매번 다릅니다. 이렇게 PIE는 바이너리의 주소를 랜덤화하여 바이너리의 특정 주소의 값을 수정하는 것과 같은 공격을 방어(실행할 때마다 주소가 매번 다르므로 예측하기 어려움)합니다.

이렇게 해서 리눅스 환경에서의 메모리보호기법(ASLR, NX, ASCII Armor, Canary, RELRO, PIC, PIE)에 대해 살펴봤습니다. 모두들 수고하셨습니다~!

-written by salen

반응형
반응형

출처: https://bpsecblog.wordpress.com/2016/05/25/memory_protect_linux_3/


이번 편에서는 다음 편에서 다룰 PIE라는 개념을 위해 먼저 PIC에 대해 알아보도록 하겠습니다!
배경 지식들과 함께 알아봅시다.

* 실습 환경 Ubuntu 15.10 32bits


정적 라이브러리와 공유 라이브러리

1) 정적 라이브러리

정적 라이브러리(Static Library)는 여러 프로그램에서 사용되는 함수를 포함하는 오브젝트 파일들을 ar명령을 이용하여 하나의 아카이브 파일(.a)로 모아놓은 것입니다.

정적 라이브러리를 만드는 과정을 통해 라이브러리 내의 함수를 어떻게 다른 실행 파일에서 호출할 수 있는 지 알아보겠습니다.

$ vi my.c          // myFunc이라는 함수를 my.c 파일에 정의

1
2
3
4
5
6
7
8
#include <stdio.h>
 
void myFunc( int a, int b ){
 
    int sum = 0;
    sum = a + b;
    printf( "[myFunc] sum : %d\n", sum );
}

$ gcc -c my.c                   // gcc를 이용하여 my.c 파일의 오브젝트 코드 생성
$ ar rv libmy.a my.o     // ar 프로그램을 이용하여 라이브러리 파일(.a) 생성

메모리보호기법3-01

$ vi static_test.c          // 만든 라이브러리 함수를 호출하는 프로그램 생성

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main(){
 
    printf( "[static_test] main() start!\n" );
    myFunc( 10, 10 );
    printf( "[static_test] main() finish!\n" );
    return 0;
}

$ gcc static_test.c -o static_test libmy.a           // 정적라이브러리를 포함하여 컴파일
$ ls                                                                                  // 실행파일이 잘 생성되었는지 확인
$ ./static_test                                                             // 프로그램 실행

메모리보호기법3-02.png

위와 같이 정적 라이브러리를 링크할 경우, 링커는 다른 오브젝트 파일에서 정의되지 않은 심볼을 찾아 지정된 정적 라이브러리에서 해당 심볼을 정의하고 있는 오브젝트 파일의 사본을 추출해서 실행 파일 내에 포함시킵니다.

메모리보호기법3-03.png
다른 시스템에서도 잘 동작하는 모습

이렇게 정적 라이브러리를 이용하여 실행 파일을 생성하면 해당 실행 파일 내에 라이브러리 함수의 코드가 포함됩니다. 따라서 같은 라이브러리 함수를 여러 프로그램에서 사용하게 되면 각각의 실행 파일마다 똑같은 라이브러리 함수의 코드가 포함됩니다. 이렇게 되면 동일한 함수의 코드가 메모리상의 여러 곳에 존재하게 되므로 메모리나 하드 디스크 공간이 낭비되게 됩니다. 이러한 정적 라이브러리의 단점을 해결한 것이 다음에 설명할 공유 라이브러리입니다.

2) 공유 라이브러리

공유 라이브러리는 여러 오브젝트 파일을 하나의 오브젝트 파일로 만들어 이를 공유할 수 있도록 한 것입니다. 즉 실행 파일에 라이브러리 함수 코드가 포함되는 것이 아니라 실행 시 공유 라이브러리를 참조하는 방식(2편 참고)으로 링크됩니다.

일반적으로 공유 라이브러리는 다음과 같이 생성합니다.

$ vi my.c                        // myFunc이라는 함수를 my.c 파일에 정의

1
2
3
4
5
6
7
8
#include <stdio.h>
 
void myFunc( int a, int b ){
 
    int sum = 0;
    sum = a + b;
    printf( "myFunc] sum : %d\n", sum );
}

$ gcc -c -fPIC my.c // 독립적인 코드로 만들기 위해 gcc의 -fPIC 옵션을 이용하여 컴파일
$ gcc -shared -o libmy.so my.o          // 공유 라이브러리 즉, libmy.so 파일 생성

메모리보호기법3-04.png

$ vi /etc/ld.so.conf

메모리보호기법3-05.png

/etc/ld.so.conf.d 디렉터리 안에 *.conf 란 파일명으로 파일을 만들고 그 파일에 자신이 만든 공유 라이브러리 파일(.so)의 전체 경로를 적어둡니다.

$ vi /etc/ld.so.conf.d/mylib.conf

메모리보호기법3-06.png

추가 후 ldconfig 명령을 이용하여 캐쉬를 갱신합니다. 그러면 이제 공유 라이브러리를 다른 모든 실행파일 내에서 사용할 수 있게 됩니다.

$ vi dynamic_test.c // 만든 라이브러리 함수를 호출하는 프로그램 생성

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main(){
 
    printf( "[dynamic_test] main() start!\n" );
    myFunc( 10, 10 );
    printf( "[dynamic_test] main() finish!" );
    return 0;
}

$ gcc dynamic_test.c -o dynamic_test -lmy -L. // 공유 라이브러리 링킹

메모리보호기법3-07.png

ldd(List Dynamic Dependencies) 는 프로그램에서 요구하는 공유 라이브러리를 출력해 주는 프로그램입니다. 위와 같이 ldd를 이용하여 dynamic_test 프로그램에서 libmy.so 라는 공유 라이브러리를 참조하는 것을 확인할 수 있습니다.

공유 라이브러리를 링크한 실행 파일을 실행할 경우에는 동적 링커 로더(ld.so)가 해당 실행 파일에서 필요한 공유 라이브러리를 찾아내어 실행 시 해당 프로세스의 메모리 맵을 조작해서 공유 라이브러리와 실행 바이너리가 같은 프로세스 공간을 사용하도록 합니다. 즉 실제 라이브러리 코드는 실행 파일에 포함되어 있지 않고 공유 라이브러리에만 존재합니다. 따라서 실행 파일을 배포할 때 실행 파일과 공유 라이브러리를 함께 배포해야 합니다. 그렇지 않으면 실행 시 라이브러리를 찾을 수 없다는 에러메시지가 나타납니다.

메모리보호기법3-08.png
다른 시스템에서 라이브러리를 찾을 수 없다는 에러 발생 화면

PIC(Position Independent Code)

앞서 공유 라이브러리를 만들 때 -fPIC 옵션을 이용하여 소스를 컴파일 했습니다. 그리고 통상 GNU/리눅스의 공유 라이브러리를 만들 때는 각각의 .c 파일을 PIC가 되도록 컴파일한다고 합니다. 그렇다면 PIC가 무엇이고, PIC로 컴파일을 하면 뭐가 좋을까요?

Position-independent code(PIC)란 메모리의 어느 공간에든 위치할 수 있고 수정 없이 실행될 수 있는 “위치 독립 코드”입니다. 즉 이 코드를 사용하는 각 프로세스들은 이 코드를 서로 다른 주소에서 실행할 수 있으며, 실행 시 재배치가 필요 없습니다.

공유 라이브러리를 PIC로 만들면 뭐가 좋은 지를 예제를 통해 알아보겠습니다.

1
2
3
4
5
6
7
8
#include <stdio.h>
 
void main(){
 
    puts( "hi" );
    puts( "hi" );
    puts( "hi" );
}

다음과 같이 puts함수를 3번 호출하는 test-pic.c 파일을 생성합니다.

이 소스코드를 PIC로 컴파일 했을 때와 하지 않았을 때의 특징을 비교하기 위해 두 버전으로 컴파일 하여 공유 라이브러리를 만들어보겠습니다.

# gcc -shared -o no-pic.so test-pic.c // PIC가 아닌 공유 라이브러리 생성
# gcc -shared -fPIC -o pic.so test-pic.c // PIC 공유 라이브러리 생성

메모리보호기법3-09.png

readelf의 -d 옵션으로 생성한 공유 라이브러리들의 dynamic 섹션을 확인해 보면 PIC가 아닌 공유 라이브러리에는 TEXTREL이라는 엔트리(텍스트 내의 재배치 필요)가 있고 RELCOUNT(재배치 횟수)는 6으로 PIC 공유 라이브러리보다 큽니다(이 때 puts함수를 3회 호출하므로 PIC 공유 라이브러리에서보다 3만큼 큰 것입니다). PIC는 재배치가 필요 없음에도 불구하고 공유 라이브러리에서의 RELCOUNT가 0이 아닌 이유는 gcc가 기본적으로 사용하는 시작 파일에 포함된 코드 때문입니다. -nostartfiles 옵션을 주고 컴파일을 하면 이 값은 0이 되어 아래와 같이 RELCOUNT 엔트리가 없어집니다(이걸로 PIC는 재배치가 필요 없다는 말이 증명되었네요!).

메모리보호기법3-10.png

위 예제에서 확인한 것과 같이 PIC가 아닌 공유 라이브러리는 실행 시 6개의 주소가 재배치되어야 합니다. 지금은 puts함수를 3회만 호출하는 간단한 프로그램이므로 재배치되어야 하는 주소의 개수가 얼마 없지만 프로그램이 커져서 재배치 수가 늘어난다면 프로그램 실행 시 재배치에 걸리는 시간이 매우 커질 것입니다.

또한 PIC가 아닌 공유 라이브러리는 실행 시 재배치가 필요한 부분의 코드를 재작성하기 위해 텍스트 섹션 내의 재배치가 필요한 페이지를 로드하고 이를 재 작성하는 과정을 거치다가 copy on write가 발생하여 다른 프로세스와 텍스트 섹션을 공유할 수 없는 상황이 발생할 수도 있습니다. 공유 라이브러리의 장점이 텍스트 섹션을 다른 프로세스와 공유할 수 있어서 사용하는 것이었는데 이를 다른 프로세스와 공유할 수 없게 된다면 공유 라이브러리를 쓰는 이유가 없어지겠죠!

따라서 공유 라이브러리를 PIC로 생성하지 않으면 실행할 때 재배치에 시간이 소요된다는 단점과 다른 프로세스와 코드를 공유할 수 없게 될 수 있는 단점이 있기 때문에 통상적으로 공유 라이브러리를 작성할 때 .c 파일을 PIC로 컴파일 하는 것입니다.


non-PIC vs PIC

PIC가 아닌 공유 라이브러리와 PIC 공유 라이브러리가 각각 함수를 어떻게 호출하는 지 그 차이에 대해 알아보도록 하겠습니다.

먼저 PIC가 아닌 공유 라이브러리를 생성합니다. 여기서 my.c은 앞서 정적 라이브러리와 공유 라이브러리 설명 때 사용했던 myFunc이라는 함수가 정의되어 있는 예제입니다.

메모리보호기법3-디버깅1

printf 함수 호출 시 0x554를 call합니다. 0x554가 어느 섹션에 위치하는 지 알아보기 위해 gdb 상에서 info file 명령을 쳐보면 아래와 같이 0x554는 .text 섹션에 위치하는 코드입니다.

메모리보호기법3-디버깅2.PNG

다음으로 PIC 공유 라이브러리에서 함수를 어떻게 호출하는 지 알아보겠습니다. 아래와 같이 PIC 공유 라이브러리를 생성한 후 gdb를 이용하여 디스어셈블합니다.

메모리보호기법3-디버깅3.PNG

printf 함수 호출 시 printf@plt를 call합니다. 즉 plt를 경유하여 printf 함수를 호출합니다.


Relocatable code vs PIC

마지막으로 헷갈릴 수도 있는 Relocatable code와 PIC의 차이에 대해 정리해 드리겠습니다.

Relocatable code는 말 그대로 재배치(relocation)를 해야 하는 코드를 의미합니다. 컴파일러에 의해 생성된 코드는 실제로 메모리상의 어느 위치에 로드 될 지 알 수 없으므로 로드 시에 결정된 위치에 따라 참조하는 함수/변수의 주소를 변경해 주어야 합니다.

executable file로 링크되는 코드는 로드되는 주소가 정해져 있으므로 미리 알고 있는 주소로 link time에 재배치가 이루어지기 때문에 신경 쓸 필요가 없지만 라이브러리에 있는 함수는 로드되는 위치가 매번 달라지므로 link time에 이를 고정하여 코드에 반영시켜 둘 수가 없습니다. 따라서 별도의 섹션에 .text 영역 내에서 재배치가 필요한 위치를 저장해두고 로더가 이후에 이 영역의 값을 변경하여 재배치를 수행하게 되는 것입니다.

하지만 이는 .text 영역의 수정이 필요하다는 것을 의미하기 때문에 재배치가 이루어진 코드는 다른 위치로 로드된 동일한 코드와 더 이상 공유할 수 없게 됩니다. 이는 공유 라이브러리의 기본 개념과 어긋나는 것이므로 공유 라이브러리에는 PIC를 이용합니다. PIC는 PC-relative 주소 지정 방식을 이용하기 때문에 로드된 주소에 상관없이 link time에 계산된 오프셋만을 이용하여 원하는 함수/변수를 참조할 수 있게 됩니다.


 

이렇게 해서 다음 편에서 알아볼 PIE를 위해 이번 편에서는 PIC에 대해 다뤄봤습니다.

다음 편은 리눅스 메모리 보호기법 마지막 편으로, PIE에 대해 알아보도록 하겠습니당.

-written by salen

반응형
반응형

출처: https://bpsecblog.wordpress.com/2016/05/18/memory_protect_linux_2/


이번 편에서는 RELRO에 대해 알아보겠습니다!!
RELRO 설명에 앞서 필요한 개념들을 하나씩 알아봅시다.


Lazy Binding이란?

Dynamic Linking 방식으로 컴파일이 된 ELF 바이너리는 공유 라이브러리 내에 위치한 함수의 주소를 동적으로 알아오기 위해 GOT(Global Offset Table) 테이블을 이용합니다.

Dynamic Link 방식은 공유 라이브러리를 하나의 메모리 공간에 매핑하고 여러 프로그램에서 공유하여 사용하는 방식입니다. 실행파일 내에 라이브러리 코드를 포함하지 않으므로 PLT와 GOT를 사용하게 되는 이유이기도 합니다. PLT와 GOT를 이용하여 공유 라이브러리 내의 함수 주소를 알아오는 과정을 puts 함수 호출을 예로 들어 요약하면 다음과 같습니다.

메모리보호기법2-01.png

정리하면 puts 함수의 호출이 처음일 때는 위와 같은 과정(링커가 dl_runtime_resolve 함수를 통해 필요한 함수의 주소를 알아오고, GOT에 그 주소를 써준 후 해당 함수를 호출하는 과정)을 거치고, 처음이 아닐 때는 GOT에 puts 함수의 주소가 적혀있어 puts 함수의 주소를 알아오는 과정 없이 바로 함수를 호출합니다.

> Dynamic Linking, PLT, GOT에 대한 좀 더 자세한 설명은 이 링크를 참고해주세요.

이처럼 모든 외부 함수의 주소를 한 번에 로딩하지 않고, 함수 호출 시점에 해당 함수의 주소만 공유 라이브러리로부터 알아오는 것을 “Lazy Binding”이라고 합니다.

그렇다면 복잡해 보이는 Lazy Binding을 사용하는 이유는 뭘까요?

Static 방식으로 컴파일 하면 실행 파일 안에 라이브러리의 모든 코드가 포함되면 파일의 크기도 커집니다. 동일한 라이브러리를 사용하더라도 해당 라이브러리를 사용하는 모든 프로그램들은 라이브러리의 내용을 메모리에 매핑 시켜야 하는 단점이 있습니다.

하지만 실행 시점에 필요한 함수의 주소만 알아오면 되는 Lazy Binding은 실행 파일의 크기가 훨씬 작고 실행 시에도 상대적으로 적은 메모리를 차지하게 되므로 실행 속도도 빠르다는 이점이 있죠.


GOT Overwrite란?

이 개념은 말로 설명하기 보단 예제를 통해 알아보겠습니다.
실습 환경은 CentOS6.7 32bits이고 다음과 같은 예제를 사용하겠습니다.

1
2
3
4
5
6
#include <stdio.h>
void main(){
 
   puts("pwd");
 
}

메모리보호기법2-02.png

단순히 “pwd”라는 문자열을 출력하고 종료되는 프로그램입니다.

만약 여기서 puts 함수가 system 함수가 될 수 있다면 puts(“pwd”); 가 system(“pwd”); 로 되어 “pwd” 명령을 수행하게 될 것입니다!!

그런데 이 puts 함수를 system 함수로 어떻게 바꾸느냐???

이 때 해볼 수 있는 것이 GOT Overwrite 란 겁니다. puts 함수의 GOT를 system 함수의 주소로 덮어버린다면, puts 함수의 호출 시, GOT에 저장되어 있는 주소가 실제 puts 함수의 주소인 줄 알고 system 함수를 실행하게 됩니다.

이를 그림으로 표현하면 다음과 같습니다.

메모리보호기법2-03

위와 같은 원리로 system 함수를 호출시켜 봅시다.
쉽게 확인하기 위해 gdb를 이용하여 GOT를 overwrite 해 보겠습니다.

main 함수의 시작점에 브레이크 포인트를 걸고 예제 프로그램을 실행시킵니다.

메모리보호기법2-디버깅1.PNG

puts 함수의 호출이 처음이므로 puts 함수의 GOT에는 puts 함수의 실제 주소가 아닌 PLT+6 위치로 이동하여 puts 함수의 주소를 얻어오기 위한 과정을 거치도록 합니다.

메모리보호기법2-디버깅2.PNG

그런데 이 때, puts 함수의 GOT에 system 함수의 주소를 넣어둔다면 puts 함수 실행 시 GOT로 이동했을 때 함수의 주소가 들어있으므로 puts 함수의 주소인 줄 알고 해당 주소로 그대로 점프하여 코드를 실행할 것입니다.

메모리보호기법2-디버깅3.PNG

실제 system 함수의 주소는 0x5a1f70 이므로, puts 함수의 GOT(0x804962c)를 system 함수의 주소로 덮어줍니다.

그리고 continue를 하게 되면….
아래와 같이 “pwd” 명령이 실행된 것을 확인할 수 있습니다. 즉 puts 함수의 GOT Overwrite에 성공하였습니다.

메모리보호기법2-디버깅4.PNG

 


RELRO란?

RELRO는 Relocation Read-Only의 줄임말로, 앞서 설명한 것과 같은 공격에 대비하여 ELF 바이너리 또는 프로세스의 데이터 섹션을 보호하는 기술입니다. 즉 메모리가 변경되는 것을 보호하는 기술입니다.

바이너리 컴파일 시 Full-RELRO 옵션을 주면 .ctors, .dtors, .jcr, .dynamic, .got 섹션이 읽기전용상태(Read-Only)가 됩니다.

이러한 RELRO에는 Partial RELRO와 Full RELRO 두 가지 모드가 있습니다.

스크린샷 2016-05-18 오후 3.47.59.png

테스트 프로그램을 통해 좀 더 정확히 확인해봅시다.

1
2
3
4
5
6
7
8
#include <stdio.h>
 
int main(int argc, char *argvp[]){
    size_t = *p = (size_t *)strtol(argv[1], NULL, 16);
    p[0] = 0x41414141;
    printf("RELRO TEST : %p\n", p);
    return 0;
}

이 테스트 프로그램은 0x41414141 이라는 값을 주어진 주소에 쓰는 프로그램입니다. 즉 어느 섹션의 상태(Read-Only or Writable)를 알 수 있습니다.

이 프로그램을 이용하여 RELRO가 적용되지 않은 경우, Partial RELRO인 경우, Full RELRO인 경우를 비교해 보겠습니다.


1) RELRO가 적용되지 않은 경우

# gcc relro.c -o non_test
# ../checksec.sh –file non_test

메모리보호기법2-05.png

Full-RELRO를 적용하면 .ctors, .dtors, .jcr, .dynamic, .got 섹션이 Read-Only가 된다고 했습니다. 그렇다면 RELRO를 적용하기 전에는 위 섹션들에 데이터를 쓸 수 있을 지를 확인해 보겠습니다.

objdump를 이용하여 해당 섹션들의 위치를 구합니다.

# objdump -h non_test

메모리보호기법2-디버깅5.PNG

테스트 프로그램을 이용하여 각 섹션에 데이터를 써봅시다.

메모리보호기법2-06.png

다섯 섹션들 모두에 데이터가 써집니다.


2) Partial RELRO인 경우

# gcc relro.c -Wl,-z,relro -o partial_test
# ../checksec.sh –file partial_test

메모리보호기법2-07

이제 objdump를 이용하여 섹션들의 위치를 구합니다.

# objdump -h partial_test

메모리보호기법2-디버깅6.PNG

위치를 알았으니 테스트 프로그램을 이용하여 각 섹션에 데이터를 써보겠습니다.

메모리보호기법2-08.png

위와 같이 .ctors, .dtors, .jcr, .dynamic 섹션에 데이터 쓰기 시도 시 Segmentation fault가 발생합니다. 그 원인을 알아보기 위해 gdb로 디버깅을 하면 다음과 같습니다.

메모리보호기법2-디버깅7.PNG

디버깅을 해 보니 주어진 위치(0x08048445)에 0x41414141을 쓰려다가 Write 권한이 없어 Segmentation fault가 난 것이었습니다. 이처럼 .ctors, .dtors, .jcr, .dynamic 섹션에서도 디버깅을 해보면 위와 같이 Write 권한이 없는 것을 확인할 수 있습니다.

그리고 이제 Partial RELRO에서 GOT Overwrite 가 가능한 지를 테스트 해보겠습니다.
readelf를 이용하여 printf 함수의 GOT를 알아오고 해당 GOT에 데이터 쓰기를 시도합니다.

메모리보호기법2-디버깅8.PNG

gdb 상에서 디버깅을 해 보니 EIP가 0x41414141로 변조되어 Segmentation fault가 났던 것이었습니다. 즉 printf 함수를 실행하려고 printf 함수의 GOT에 저장된 값을 봤더니 0x41414141이었고, 이 주소가 printf 함수의 실제 주소인 줄 알고 실행시킨 것입니다.

한마디로 Partial RELRO인 경우 GOT Overwrite가 가능합니다.


3) Full RELRO인 경우

# gcc relro.c -Wl,-z,relro,-z,now -o full_test
# ../checksec.sh –file full_test

메모리보호기법2-09

이제 objdump를 이용하여 섹션들의 위치를 구합니다.

objdump -h full_test

메모리보호기법2-디버깅9.PNG

위치를 알았으니 테스트 프로그램을 이용하여 각 섹션에 데이터를 써보겠습니다.

메모리보호기법2-10.png

위와 같이 .ctors, .dtors, .jcr, .dynamic 섹션에 데이터 쓰기 시도 시 Segmentation fault가 발생합니다. 그 원인을 알아보기 위해 gdb로 디버깅을 하면 다음과 같습니다.

메모리보호기법2-디버깅10.PNG

디버깅을 해 보니 주어진 위치(0x08049ef4)에 0x41414141을 쓰려다가 Write 권한이 없어 Segmentation fault가 난 것이었습니다. .ctors, .dtors, .jcr, .dynamic 섹션에서도 디버깅을 해 보면 위와 같이 Write 권한이 없는 것을 확인할 수 있습니다.

마지막으로 Full RELRO에서 GOT Overwrite 가 가능한 지를 테스트 해보겠습니다.
readelf를 이용하여 printf 함수의 GOT를 알아오고 해당 GOT에 데이터 쓰기를 시도합니다.

메모리보호기법2-디버깅11.PNG

gdb 상에서 디버깅을 해 보니 printf 함수의 GOT를 변경하려고 했으나, write 권한이 없어서 변경되지 못하고 Segmentation fault가 난 것이었습니다.

따라서 Full RELRO인 경우 GOT Overwrite가 불가능합니다.

Full RELRO가 GOT가 읽기전용이라 뭔가 보안상 더 안전할 것 같은데 Full RELRO보다는 Partial RELRO가 더 널리 사용됩니다. 그 이유는 Full RELRO의 경우 프로세스가 시작될 때 링커에 의해 모든 메모리에 대해 재배치 작업이 일어나 실행이 느려지기 때문입니다.

자, 이렇게 해서 RELRO에 대해서도 알아보았습니다.

다음 편에서는 PIC에 대해 알아보도록 하겠습니다.

-written by salen

반응형
반응형

출처: https://bpsecblog.wordpress.com/2016/05/16/memory_protect_linux_1/


지금부터 Linux 환경에서의 메모리 보호 기법에 대해 알아봅시다!

이번 편에서는 ASLR, NX, ASCII-Armor, Stack canary에 대해 알아보겠습니다.

위키를 통해 “메모리 보호”라는 말의 정의를 알아보고 넘어갑시다.

메모리보호기법1-01

  • 실습 환경: CentOS 6.7 (32bit)

ASLR : Address Space Layout Randomization

먼저 ASLR에 대해 알아보겠습니다.

ASLR이란, 메모리상의 공격을 어렵게 하기 위해 스택이나 힙, 라이브러리 등의 주소를 랜덤으로 프로세스 주소 공간에 배치함으로써 실행할 때 마다 데이터의 주소가 바뀌게 하는 기법입니다. 

말로만 설명하면 잘 모르겠으니 눈으로 확인 해 봅시다 :)

메모리보호기법1-02.png

cat /proc/self/maps 명령으로 스택, 힙, 라이브러리 등의 주소가 랜덤하게 바뀌는 것이 확인됩니다.

여기서 cat /proc/self/maps 명령이 뭐냐하면.

/proc : process의 줄임말이며, 이 디렉터리에 프로세스의 정보들이 저장됩니다.

/proc/self : 현재 실행되고 있는 프로세스의 정보가 담겨있는 디렉토리입니다.

/proc/self/maps : 현재 실행되고 있는 프로세스의 주소 맵입니다.

즉 우리는 cat /proc/self/maps를 통해 cat에 대한 실제 주소 공간 레이아웃을 본겁니다. 그러면 이제 ASLR이 적용되지 않았을 때의 주소 공간 레이아웃을 봐봅시다.

ASLR을 해제하는 명령은 echo 0 > /proc/sys/kernel/randomize_va_space 입니다.

randomize_va_space=0 : ASLR 해제

randomize_va_space=1 : 랜덤 스택 & 랜덤 라이브러리 설정

randomize_va_space=2 : 랜덤 스택 & 랜덤 라이브러리 & 랜덤 힙 설정

 

메모리보호기법1-03.png

ASLR을 해제하니 스택, 힙, 라이브러리 등의 주소가 매번 고정적이게 됩니다. 다시 한 번 예제를 통해 ASLR이 적용됐을 때와 적용되지 않았을 때의 차이를 한눈에 보여드리겠습니다.

아래와 같이 간단하게 힙과 스택의 주소를 출력해 주는 프로그램을 짭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    char *buf = NULL;
    char *buf = NULL;
 
    buf = (char*) malloc(100);
    buf2 = "abcd";
 
    printf("[Heap] buf addr: %p\n", buf);
    printf("[Stack] buf2 addr: %p\n", buf2);
 
    return 0;
}

위 프로그램에서는 malloc을 이용해 힙 영역에 buf를 100bytes만큼 할당을 하고, buf2는 스택 영역에 저장을 합니다. 그리고는 buf와 buf2의 주소를 각각 출력해 줍니다.

gcc -o test test.cgcc 를 이용하여 컴파일을 해주고 프로그램을 실행시킵니다.

메모리보호기법1-04

왼쪽은 ASLR이 설정되어 있는 상황이며 buf와 buf2의 주소가 프로그램이 실행될 때마다 바뀝니다. 오른쪽은 ASLR이 해제되어 있는 상황이며 buf와 buf2의 주소가 변함없습니다.

이쯤되면 ASLR이 뭔지 감을 잡으셨을 것 같으니 다음으로 넘어가봅시당.


DEP : Data Execution Prevention

다음으로 DEP에 대해 알아보겠습니다.

DEP란, 데이터 영역에서 코드가 실행되는 것을 막는 기법입니다.

쉬운 예로, 공격자가 Buffer Overflow 공격을 일으켜 return address를 스택상의 한 주소(쉘코드가 위치한 주소)로 변경했다고 칩시다. DEP가 적용되지 않았을 경우에는 그대로 쉘코드가 실행이 되겠지만, DEP가 적용된 경우에는 실행권한이 없으므로 쉘코드가 실행되지 않고 프로그램에 대한 예외처리 후 종료가 됩니다.

예제 프로그램을 통해 DEP에 대해 알아봅시다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
 
int main(){
    char str[256];
    char *char = (char*)malloc(100);
 
    printf("Input: ");
    gets(str);
    printf("%p\n", str);
}

예제 프로그램은 간단한 bof 취약점이 존재하는 프로그램입니다. 이제 이 프로그램의 스택에 실행권한을 설정(-z execstack 옵션)하여 컴파일한 후 실행시키면 다음과 같습니다.

메모리보호기법1-05

그리고 이 예제 프로그램의 스택과 힙에는 실행권한이 있습니다.

메모리보호기법1-06

이제 스택에 실행권한이 있을 때와 없을 때의 차이를 위의 예제를 통해 알아보겠습니다.checksec.sh이라는 스크립트를 통해 아래와 같이 파일에 어떤 보호기법이 걸려있는 지 알 수 있습니다.

메모리보호기법1-07.png

이렇게 바이너리에 DEP가 걸려 있지 않다면(NX disabled) bof를 통해 return address를 스택 상에 쉘 코드가 위치한 곳으로 변조하면 그대로 쉘코드가 실행됩니다.

메모리보호기법1-08


ASCII-Armor

다음으로 ASCII-Armor입니다.

ASCII-Armor란, 공유라이브러리 영역의 상위 주소에 0x00을 포함시키는 방법입니다.

이 기법은 RTL(Return To Library) 공격에 대응하기 위한 방법으로, 공격자가 라이브러리를 호출하는 Buffer Overflow 공격을 해도 NULL바이트가 삽입된 주소로는 접근할 수 없습니다.

메모리보호기법1-09

잠깐 RTL 공격에 대해 이야기를 하자면, 한마디로 Return address에 libc 내의 함수를 덮어씌워 쉘코드 없이 exploit 하는 것입니다. 아무튼 RTL 공격은 libc라고 하는 공유 라이브러리 내의 함수로 리턴하게 하여 프로그램의 실행흐름을 조작하는 공격이기 때문에, 운영체제에 ASCII-Armor 보호기법이 적용되어 있다면 공격자가 라이브러리를 호출하는 공격을 한다고 하더라도 NULL바이트가 삽입되게 되므로 쉽게 exploit 하지 못하게 됩니다.


Stack Canary

마지막으로 Stack Canary에 대해 알아보겠습니다.

함수 진입 시 스택에 SFP(Saved Frame Pointer)와 return addressr 정보를 저장할 때, 이 정보들이 공격자에 의해 덮어씌워지는 것으로부터 보호하기 위해 스택상의 변수들의 공간과 SFP 사이에 특정한 값을 추가하는데 이 값을 Canary라고 합니다.

공격자가 return address를 조작하기 위해 Buffer Overflow 공격을 시도할 때, return address를 덮기 이전에 Canary 값이 먼저 덮어지기 때문에 이 Canary 값의 변조 유무로 Buffer Overflow를 탐지할 수 있습니다.

메모리보호기법1-10.png

위 화면에서 main+12는 gs:0x14에서 canary값을 얻어와 eax 레지스터에 저장하는 부분이고 main+18은 eax 레지스터에 저장된 값(canary값)을 스택에 저장하는 부분입니다. 이 때 canary 값은 다음과 같은 위치에 저장됩니다.

메모리보호기법1-11

그리고 프로그램이 기능을 다 하고 return되어 종료될 때, 프로그램 코드 실행 전 스택에 저장했던 canary 값과 gs:0x14의 원본 값을 비교하여 그 값이 다르다면 Buffer Overflow 공격을 당했다고 판단하고 그 자리에서 바로 프로그램을 종료해버립니다. (main+73~89 부분)

이제 실제로 종료가 되는지 Buffer Overflow 공격을 해서 canary값을 변조시켜 봅시다!먼저 A 300개를 a라는 파일에 저장시킨 후 프로그램 실행 시 입력시켜줬습니다.

메모리보호기법1-12.png

원래의 Canary 값과 현재 스택에 저장된 Canary 값이 바뀌어서 Buffer Overflow 공격을 탐지하고 프로그램이 종료되었습니다.

이렇게 리눅스 환경에서의 메모리 보호기법 4가지에 대해 알아보았습니다 :)

다음 편에서는 RELRO에 대해 알아보겠습니다.

-written by Salen

반응형

+ Recent posts