By salen | May 4, 2016
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
위 소스를 다음과 같이 PIC 공유 라이브러리로 컴파일합니다.
$ gcc -c -fPIC mysum.c
// 독립적인 코드로 만들기 위해 -fPIC 옵션을 이용하여 컴파일
$ gcc -shared -o libmysum.so mysum.o
// 공유 라이브러리 즉, libmysum.so 파일 생성
$ vi test.c // 만든 라이브러리 함수를 호출하는 프로그램 생성
위 소스를 공유 라이브러리와 함께 컴파일하여 PIE를 만들어보겠습니다.
$ gcc -fPIE -pie test.c -o test_pie -lmysum -L.
$ ./test_pie
test_pie를 실행하면 위와 같이 잘 동작하며,
test_pie 프로그램은 앞에서 만들어줬던 libmysum.so 공유 라이브러리를 아래와 같이 참조하고 있습니다.
$ ldd test_pie
그리고 checksec.sh을 이용하여 정말로 PIE가 걸렸는지 확인해 보면 다음과 같습니다.
$ checksec.sh –file test_pie
이렇게 checksec.sh 스크립트를 이용하여 실행파일이 PIE인지 아닌지를 알 수 있습니다.
2. non-PIE vs PIE
그렇다면 이제 본격적으로 PIE가 아닌 일반 실행파일과 PIE의 차이점을 알아보겠습니다.
예제는 간단히 buf에 저장된 값과 buf의 주소를 출력해주는 소스입니다.
이 소스를 non-PIE버전과 PIE버전으로 컴파일을 합니다.
$ gcc -o address address.c // 일반 실행파일 생성
$ gcc -fPIE -pie -o address_pie address.c // PIE 생성
그리고 아래와 같이 checksec.sh 스크립트를 실행해보면
일반 실행파일은 “No PIE”, PIE는 ”PIE enabled”라고 표시됩니다.
$ checksec.sh –file address
$ checksec.sh –file address_pie
파일의 타입을 확인할 수 있는 file 명령어를 일반 실행파일과 PIE에 대해 실행해 보면
아래와 같이 일반 실행파일은 “executable”이고, PIE는 “shared object”입니다.
$ file address
$ file address_pie
다음으로 다른 예제를 통해 함수 호출시 non-PIE와 PIE의 차이에 대해 알아보겠습니다.
위 예제에서는 사용자 정의 함수, puts, printf, system함수를 호출합니다.
이제 위 예제를 non-PIE, PIE 두 버전으로 컴파일하여 gdb로 디스어셈블 해보겠습니다.
$ gcc -o got got.c // 일반 실행파일 생성
$ gcc -fPIE -pie -o got_pie got.c // PIE 생성
# gdb -q got
일반 실행파일인 경우 정해진 주소에 코드들이 위치합니다.
또한 puts, printf, system함수의 plt도 절대주소로 고정되어 있으며
사용자정의함수인 func함수의 경우도 절대주소인 0x804847b에서부터 위치합니다.
# gdb -q got_pie
PIE인 경우 일반 실행파일과는 다르게 매우 작은 주소에 코드들이 위치합니다.
이는 PIC 공유 라이브러리처럼 주소공간의 어느 위치에 매핑되어도 작동하도록 상대주소로 되어있는 것입니다.
동적 링커는 PIE를 실행할 때 PIC 공유 라이브러리에 대해 수행할 때와 마찬가지로 상대주소를 주소공간상에 매핑하여 실행합니다.
라이브러리 함수를 호출할 때 non-PIE에서는 고정된 plt, got를 이용하여 함수를 호출하지만
PIE는 위치에 독립적인 실행파일, 즉 프로그램을 실행할 때마다 매핑되는 주소가 달라집니다.
그렇다면 이런 특징을 가진 PIE에서 라이브러리 함수는 어떻게 호출될까요?
위 예제를 디스어셈블 했을 때, 아래와 같이 non-PIE에서는 볼 수 없었던 코드가 보입니다.
바로 __x86.get_pc_thunk.bx라는 함수를 호출한 후 ebx 레지스터의 값을 0x194e만큼 증가시키는 코드입니다.
이 함수가 어떤 기능을 하는 지 직접 디버깅을 하면서 알아보겠습니다.
위와 같이 __x86.get_pc_thunk.bx 함수에서는 함수를 수행하고 돌아갈 주소를 ebx 레지스터에 저장합니다.
즉 이 함수를 호출하면서 어느 주소에 매핑이 되었는지를 알아온 것입니다.
그리고는 ebx 레지스터 값을 0x194e만큼 증가시켜 ebx 레지스터는 0x80002000 값을 갖게 되는데,
이 값은 .got.plt 섹션의 주소입니다.
정리하면 Position Independent Code, 즉 위치 독립 코드의 경우 코드가 어떤 주소에 매핑이 될지 모르므로
__x86.get_pc_thunk.bx 함수를 호출하여 함수 호출 후 다음으로 수행할 인스트럭션의 주소를 ebx 레지스터에 저장하고,
ebx 레지스터의 값을 0x194e만큼 더해서 got영역의 주소를 알아낸 것입니다.
그런데 이 과정에서 “add ebx, 0x194e” 인스트럭션을 통해 got영역의 주소를 알게 되었는데, 0x194e라는 값은 어디서 튀어나온 값일까요?
다시 프로그램 실행 전으로 돌아가보면 아래와 같이 “add ebx, 0x194e” 인스트럭션이 위치하고 있는 오프셋은 0x6b2입니다.
그리고 got 영역의 오프셋은 0x2000입니다.
0x2000에서 0x194e를 빼면 0x6b2입니다.
즉 add 인스트럭션의 주소(오프셋)입니다.
메모리의 어느 주소에나 매핑이 되더라도 그 오프셋은 항상 동일하므로 컴파일 시 __x86.get_pc_thunk.bx 함수 호출로 정해지는 ebx 레지스터의 값에 어떤 값(X)을 더해야 got 영역의 주소가 되는 지를 결정할 수 있습니다.
X = “got영역의 주소(오프셋)” - “add 인스트럭션의 주소(오프셋)”
이렇게 got영역의 시작 주소를 알아내고 라이브러리 함수 호출(ex. puts함수 호출) 시에는
아래와 같이 got영역 시작 주소와의 오프셋을 이용하여 해당 함수의 got에 접근합니다.
puts함수가 한 번 이상 호출되지 않았으므로 아직 puts함수의 got에는 puts@plt+6의 주소가 저장되어 있습니다.
다시 plt로 돌아와서 “push 0x10” 인스트럭션 수행 후 0x80000490으로 점프합니다.
0x80000490에서는 오프셋을 통해 _dl_runtime_resolve 함수로 점프하며,
이 함수에서 _dl_fixup 함수를 통해 puts함수의 실제 주소를 알아오게 됩니다.
3. 컴파일 옵션의 의미
이번에는 지금껏 PIE 컴파일을 할 때, “-fPIE -pie”라고 해줬는데 이 각각의 의미가 무엇인지 알아보겠습니다.
먼저 정리하자면 “-fPIE“는 컴파일러를, ”-pie”는 링커를 위한 옵션입니다.
컴파일 과정을 화면에 출력해 주는 ‘-v’옵션을 이용하여 알아봅시다.
$ gcc -v -fPIE -pie -o test_pie test.c
gcc에서 cc1은 전처리된 C언어를 어셈블리어로 변환해주는 C컴파일러이고, collect2는 링커입니다.
즉 “-fPIE” 옵션은 컴파일단계에서 수행이 되고 “-pie”옵션은 링킹단계에서 수행이 됩니다.
따라서 “-pie”옵션을 주지 않고 ”-fPIE”옵션만 준다면 아래와 같이 PIE가 생성되지 않습니다.
$ gcc -fPIE -o test_no test.c
# 실행은 되지만 PIE는 적용되지 않은 화면
4. 실행할 때마다 주소가 바뀌는 PIE
마지막으로 챕터2에서 만들었던 address, address_pie 프로그램을 실행해보겠습니다.
먼저 일반 실행파일입니다.
몇 번을 실행하든 buf의 주소 값이 같습니다.
다음은 PIE를 실행시켜보겠습니다.
실행할 때마다 buf의 주소가 바뀝니다.
PIE는 위치 독립 실행파일이라 했습니다.
즉 실행할 때마다 매핑되는 주소가 어디든지에 상관없이 실행되는 파일로, 위와 같이 매핑되는 주소가 매번 다릅니다.
이렇게 PIE는 바이너리의 주소를 랜덤화하여 바이너리의 특정 주소의 값을
수정하는 것과 같은 공격을 방어(실행할 때마다 주소가 매번 다르므로 예측하기 어려움)합니다.
이렇게 해서 리눅스 환경에서의 메모리보호기법(ASLR, NX, ASCII Armor, Canary, RELRO, PIC, PIE)에 대해 살펴봤습니다.
모두들 수고하셨습니다~!