linux 환경의 메모리 보호기법을 알아보자(5)

By salen | May 5, 2016

linux 환경의 메모리 보호기법을 알아보자 (5)





salen 선원이 linux 메모리 보호기법 시리즈에서 주구장창 등장하는 checksec.sh 를 자세히 들여다 보았습니다. 후다닥 퍼다 날라봅니다 :3




1. chechsec.sh 개요


지금껏 우리는 예제 프로그램을 만들고, 예제 프로그램에 어떤 메모리 보호기법들이 적용되어 있는지를 checksec.sh를 통해 알아봤습니다.

그런데 checksec.sh는 어떻게 ELF파일에 걸린 메모리 보호기법들을 확인할 수 있었을까요?

checksec.sh 스크립트를 vi 편집기를 이용하여 확인해보면, 아래와 같이 readelf라는 명령어를 이용하여 ELF파일의 정보를 알아냅니다.



readelf는 ELF파일(Executable and Linkable Format, 윈도우의 exe파일과 같은 실행파일)의 정보들을 볼 수 있는 명령어이며,

하나 이상의 옵션을 사용해야 합니다.

checksec.sh에서 RELRO, Stack Canary, NX, PIE의 적용 유무를 판단할 때 사용하는 옵션을 정리해보면 다음과 같습니다.



또한 readelf명령어로 ELF파일의 정보를 확인한 후 grep이라는 명령어도 사용됩니다.

grep이란, 파일에서 특정한 패턴(문자열)을 찾는 명령어입니다.


이 명령어에도 여러 옵션이 있는데, checksec.sh 스크립트에서는 -q라는 옵션과 함께 사용합니다.

-q옵션은 “출력을 억제한다”라는 의미로, 처음으로 매칭되는 것이 발견되면 더 이상 찾는 것을 중지하는 옵션입니다.


checksec.sh 스크립트 중 한 줄을 예로 들면,

아래명령은 어떤 파일에 대해 readelf -l 명령을 수행했을 때 출력되는 결과값 중

‘GNU_RELRO’라는 문자열이 있으면 다음으로 넘어가라(if문 안의 명령을 수행하라)는 뜻입니다.

없다면 else문을 실행하게 됩니다.





2. Partial/Full RELRO(RELocation-Read-Only)


이제 RELRO부터 checksec.sh 스크립트에서 사용했던 명령어와 옵션을 예제에 적용하여 확인해 보겠습니다.


$ vi relro.c                // 예제 프로그램 작성


#include <stdio.h>
int main(){
      printf( "RELRO TEST\n" );
      return 0;
}


$ gcc relro.c -Wl,-z,relro -o partial_test           // PARTIAL_RELRO 컴파일

$ gcc relro.c -Wl,-z,relro,-z,now -o full_test       // FULL_RELRO 컴파일

$ checksec.sh –file partial_test                    // 보호기법 확인

$ checksec.sh –file full_test                       // 보호기법 확인



Partial RELRO, Full RELRO 두 가지 버전으로 잘 컴파일이 되었습니다.

그리고 checksec.sh 스크립트에서 RELRO가 적용되어있는 지를 판단하는 부분은 다음과 같습니다.


$ vi checksec.sh



위 소스코드에 따르면 ELF파일의 세그먼트 헤더에 ‘GNU_RELRO’라는 문자열이 있으면

RELRO가 적용된 파일이고 문자열이 없으면 RELRO가 적용되지 않은 파일이라고 판단합니다.


세그먼트 헤더에 ’GNU_RELRO’ 문자열이 있고, dynamic 섹션에 ‘BIND_NOW’라는 문자열이 있으면 Full RELRO이고,

없으면 Partial RELRO로 판단합니다.


직접 readelf 명령을 이용하여 확인해보면 다음과 같습니다.


$ readelf -l partial_test |grep ‘GNU_RELRO’

$ readelf -l full_test |grep ‘GNU_RELRO’

// checksec.sh 스크립트에서 RELRO 적용 유무를 확인할 때 사용하는 명령어



Partial/Full RELRO 둘 다 파일의 세그먼트 헤더에 ‘GNU_RELRO’라는 문자열이 있습니다.


GNU_RELRO라는 세그먼트는 소스코드를 “-z relro” 옵션으로 컴파일하면 생성됩니다.

이는 ELF의 data 섹션(.got, .dtors, etc.)을 읽기전용으로 만들어주는 옵션입니다.


$ readelf -d partial_test |grep ‘BIND_NOW’

$ readelf -d full_test |grep ‘BIND_NOW’

// checksec.sh 스크립트에서 Full-RELRO 적용 유무를 확인할 때 사용하는 명령어



Partial-RELRO는 dynamic 섹션에 ‘BIND_NOW’ 문자열이 없고, Full-RELRO는 ’BIND_NOW’ 문자열이 있습니다.


BIND_NOW엔트리는 소스코드 컴파일 시 “-z now”을 주면 생성됩니다.

이는 글로벌 변수 및 PLT 함수 참조에 대한 모든 심볼 재배치는 load-time에 수행하겠다는 뜻입니다.




3. Stack Canary


Stack Canary 적용 유무를 checksec.sh 스크립트에서 어떻게 알아냈는지 알아보겠습니다.


$ vi canary.c           // 예제 프로그램 작성


#include <stdio.h>
int main(){
      char str[10];
      gets(str);
      printf( "%s\n", &str);
      return 0;
}


$ gcc -o canary canary.c            // 예제 컴파일

$ checksec.sh –file canary         // 보호기법 확인



예제를 컴파일 후 checksec.sh 스크립트를 이용하여 보호기법을 확인해 보면

Canary found로 Canary가 추가되어 있는 것을 확인할 수 있습니다.

(요즘 gcc 컴파일러는 gets함수와 같이 buffer overflow에 취약한 함수들이 소스에 포함되면

별도의 컴파일 옵션을 주지 않아도 canary를 추가해 줍니다!)


다음으로 checksec.sh 스크립트에서 Canary가 적용되어있는 지를 판단하는 부분은 다음과 같습니다.


$vi checksec.sh



위 소스코드에 따르면 ELF파일의 심볼에 “__stack_chk_fail”이라는 문자열이 있으면

(해당 파일의 소스코드에서 __stack_chk_fail함수가 있으면)

Canary가 적용된 파일이고 문자열이 없으면 Canary가 적용되지 않은 파일이라고 판단합니다.


직접 readelf 명령을 이용하여 확인해보면 다음과 같습니다.


$ readelf -s canary |grep ‘__stack_chk_fail’

// checksec.sh 스크립트에서 Canary 적용 유무를 확인할 때 사용하는 명령어



위처럼 Canary가 적용된 ELF파일에는 __stack_chk_fail이라는 심볼이 있습니다.

그리고 비교를 위해 앞서 사용했던 Canary가 적용되지 않은 partial_test라는 프로그램에 대해서

readelf명령을 실행해보면 다음과 같이 __stack_chk_fail이라는 심볼이 없는 것을 확인할 수 있습니다.


$ readelf -s partial_test |grep ‘__stack_chk_fail’

// checksec.sh 스크립트에서 Canary 적용 유무를 확인할 때 사용하는 명령어



__stack_chk_fail 함수는 Canary 값이 변조되었을 때 호출되는 함수입니다.

__stack_chk_fail 함수는 glibc내에 다음과 같이 정의되어 있으며,

“stack smashing detected”라는 문자열과 함께 __fortify_fail 함수를 호출합니다.

__fortify_fail 함수에서는 backtrace 정보와 memory map을 출력하고 프로그램을 종료시킵니다.



따라서 __stack_chk_fail이라는 함수가 해당 파일 내에 있으면 Canary가 적용되어있다고 판단합니다.




4. NX(No eXecute, DEP)


NX 적용 유무를 checksec.sh 스크립트에서 어떻게 알아냈는지를 알아보겠습니다.


$ vi nx.c        // 예제 프로그램 작성


#include <stdio.h>
int main() {
     printf("NX TEST\n");
     return 0;
}


$ gcc -o nx nx.c                       // 예제 컴파일(NX enabled)

$ gcc -z execstack nx.c -o non_nx      // 예제 컴파일(NX disabled)

$ checksec.sh –file nx                // 보호기법 확인

$ checksec.sh –file non_nx            // 보호기법 확인



예제를 NX가 적용된 버전과 적용되지 않은 버전으로 컴파일 후

checksec.sh 스크립트를 이용하여 적용된 보호기법을 확인해 본 화면입니다.

위 화면과 같이 스택에 실행권한을 주고 컴파일 한 non_nx는 NX가 적용되어있지 않습니다.


다음으로 checksec.sh 스크립트에서 NX가 적용되어있는 지를 판단하는 부분은 다음과 같습니다.


$ vi checksec.sh



위 소스코드에 따르면 ELF파일의 세그먼트 헤더 중 ‘GNU_STACK’에 해당하는 라인에서,

‘RWE’라는 문자열이 있으면 NX가 적용되지 않은 파일로, 없으면 NX가 적용된 파일이라고 판단합니다.

직접 readelf 명령을 이용하여 확인해보면 다음과 같습니다.


$ readelf -W -l nx |grep ‘GNU_STACK’

$ readelf -W -l non_nx |grep ‘GNU_STACK’

// checksec.sh 스크립트에서 NX 적용 유무를 확인할 때 사용하는 명령어



NX가 적용된 ELF파일의 GNU_STACK에는 RW라는 문자열이 있고,  NX가 적용되지 않은 ELF파일의 GNU_STACK에는 RWE라는 문자열이 있습니다.


여기서 RW는 Read, Write(읽기, 쓰기) 권한을 의미하며,

RWE는 Read, Write, Execute(읽기, 쓰기, 실행하기) 권한을 의미합니다.

정리하면 NX가 적용되면 STACK에 실행권한이 빠진 Read, Write 권한만 존재(STACK 실행 불가능)하며,

NX가 적용되지 않으면 STACK에 실행권한이 추가(STACK 실행 가능)됩니다.




5. PIE(Position Independent Executable)


마지막으로 PIE 적용 유무를 checksec.sh 스크립트에서 어떻게 알아냈는지 알아보겠습니다.


$ vi pie.c             // 예제 프로그램 작성


#include <stdio.h>
int main() {
      printf("PIE TEST\n");
      return 0;
}


$ gcc -fPIE -pie pie.c -o pie           // 예제 컴파일(PIE enabled)

$ gcc -o non_pie pie.c                  // 예제 컴파일(PIE disabled)

$ checksec.sh –file pie                // 보호기법 확인

$ checksec.sh –file non_pie            // 보호기법 확인



예제를 PIE를 적용한 버전과 적용하지 않은 버전으로 컴파일 후

checksec.sh 스크립트를 이용하여 적용된 보호기법을 확인해보면 위와 같습니다.

그렇다면 checksec.sh 스크립트에서 어떻게 PIE가 적용되어있는 지를 판단했는지를 알아보겠습니다.


$ vi checksec.sh



위 소스코드를 해석하면 다음과 같습니다.


  1. ELF파일의 Type이 EXEC이면 No PIE
  2. ELF파일의 Type이 DYN이고, dynamic 섹션에 DEBUG 엔트리가 있다면 PIE enabled
  3. ELF파일의 Type이 DYN이고, dynamic 섹션에 DEBUG 엔트리가 없다면 DSO(Dynamic Shared Object)
  4. ELF파일의 Type이 EXEC가 아니면 Not ELF file


$ readelf -h pie |grep ‘Type’

$ readelf -h non_pie |grep ‘Type’

// checksec.sh 스크립트에서 PIE 적용 유무를 확인할 때 사용하는 명령어(1)



$ readelf -d pie |grep ‘(DEBUG)’

// checksec.sh 스크립트에서 PIE 적용 유무를 확인할 때 사용하는 명령어(2)


PIE가 적용된 ELF파일은 DYN(Shared object file) 타입이고,

PIE가 적용되지 않은 ELF파일은 EXEC(Executable file) 타입입니다.

또한 PIE가 적용된 ELF파일의 dynamic 섹션에는 DEBUG 엔트리가 존재합니다.


ELF파일의 타입에는 크게 3가지가 존재합니다.



위 표에 따르면 일반 실행파일은 EXEC 타입이고

PIE는 실행과 링크가 가능한 공유 오브젝트 파일이므로 DYN 타입입니다.


따라서 파일이 DYN 타입이 아닌 EXEC 타입인 경우, PIE가 아닌 일반 실행파일로 판단했던 것입니다.

다음으로 오브젝트 파일이 동적 링킹 방식인 경우,

파일의 program header table(시스템 로더에 제공할 정보를 담고 있는 테이블)에 PT_DYNAMIC이라는 타입의 세그먼트가 생깁니다.

이 세그먼트는 동적 링킹 정보가 있는 .dynamic 섹션을 포함합니다.


아래 표는 executable(실행 가능 파일, EXEC)과 shared object(공유 오브젝트 파일, DYN)의 dynamic 섹션의 엔트리들을 나열한 것입니다.



표와 같이 DEBUG엔트리는 일반 실행파일의 경우 Optional(선택적), 공유 오브젝트 파일의 경우에는 Ignored(사용안함)입니다.


여기서 DEBUG엔트리란?

  • 동적 링커가 제공하는 debug 구조체의 포인터 (run time 시)

  • 실행 가능한(executable) 파일에만 이 엔트리가 존재

  • 디버깅 가능


PIE는 shared object이지만 실행이 가능한(executable) 파일이므로 dynamic 섹션에 DEBUG 엔트리가 존재합니다.

아래 화면은 공유라이브러리와 PIE 바이너리의 DEBUG 섹션 존재유무를 출력해 본 결과입니다.



그래서 ELF파일이 DYN 타입이고, dynamic 섹션에 DEBUG엔트리가 존재한다면 PIE가 적용된 바이너리라고 판단했던 것입니다.




6. 마무리


이번 편에서는 checksec.sh에서 어떻게 ELF파일에 적용된 메모리보호기법들을 알아냈는지 알아봤습니다.

굳이 이 스크립트를 쓰지 않아도 readelf 명령어를 이용해서

한땀한땀 섹션의 정보, 심볼 정보 등등을 확인해본다면 ELF파일에 적용된 메모리보호기법들을 알아낼 수 있겠네요!


하지만 그럼에도 불구하고 checksec.sh 스크립트를 이용하는 이유는 당연히 “편해서”입니다.

일일이 ELF헤더보고.. 심볼 확인하고… 하면 번거롭기도 하고 귀찮잖아요~ㅎㅎ

특히 대회 때는 시간이 없기 때문에 checksec.sh과 같은 스크립트를 이용해서 명령어 단 한 줄로 파일에 대한 정보를 뽑아내면 좋겠지요! :)


지금까지 리눅스 환경에서의 메모리 보호기법에 대해 알아보느라 수고 많이 하셨습니다.

번외편인 이번 편까지 해서 이 글은 이만 마무리 짓도록 하겠습니당!! 헿ㅎ


comments powered by Disqus