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

By salen | May 2, 2016

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





이번 편에서는 RELRO에 대해 알아보겠습니다!!

RELRO 설명에 앞서 필요한 개념들을 하나씩 알아봅시다.




Lazy Binding이란?


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


Dynamic Link 방식은 공유 라이브러리를 하나의 메모리 공간에 매핑하고 여러 프로그램에서 공유하여 사용하는 방식입니다.

실행파일 내에 라이브러리 코드를 포함하지 않으므로 PLT와 GOT를 사용하게 되는 이유이기도 합니다.

PLT와 GOT를 이용하여 공유 라이브러리 내의 함수 주소를 알아오는 과정을 puts 함수 호출을 예로 들어 요약하면 다음과 같습니다.



정리하면 puts 함수의 호출이 처음일 때는 위와 같은 과정

(링커가 dl_runtime_resolve 함수를 통해 필요한 함수의 주소를 알아오고,

GOT에 그 주소를 써준 후 해당 함수를 호출하는 과정)을 거치고,

처음이 아닐 때는 GOT에 puts 함수의 주소가 적혀있어 puts 함수의 주소를 알아오는 과정 없이 바로 함수를 호출합니다.


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


이처럼 모든 외부 함수의 주소를 한 번에 로딩하지 않고,

함수 호출 시점에 해당 함수의 주소만 공유 라이브러리로부터 알아오는 것을 “Lazy Binding”이라고 합니다.

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

Static 방식으로 컴파일 하면 실행 파일 안에 라이브러리의 모든 코드가 포함되면 파일의 크기도 커집니다.

동일한 라이브러리를 사용하더라도 해당 라이브러리를 사용하는 모든 프로그램들은

라이브러리의 내용을 메모리에 매핑 시켜야 하는 단점이 있습니다.

하지만 실행 시점에 필요한 함수의 주소만 알아오면 되는 Lazy Binding은 실행 파일의 크기가 훨씬 작고

실행 시에도 상대적으로 적은 메모리를 차지하게 되므로 실행 속도도 빠르다는 이점이 있죠.




GOT Overwrite란?


이 개념은 말로 설명하기 보단 예제를 통해 알아보겠습니다.

실습 환경은 CentOS6.7 32bits이고 다음과 같은 예제를 사용하겠습니다.


#include <stdio.h>
void main(){

   puts("pwd");

}



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


만약 여기서 puts 함수가 system 함수가 될 수 있다면

puts(“pwd”); 가 system(“pwd”); 로 되어 “pwd” 명령을 수행하게 될 것입니다!!


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

이 때 해볼 수 있는 것이 GOT Overwrite 란 겁니다.

puts 함수의 GOT를 system 함수의 주소로 덮어버린다면,

puts 함수의 호출 시, GOT에 저장되어 있는 주소가 실제 puts 함수의 주소인 줄 알고 system 함수를 실행하게 됩니다.

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



위와 같은 원리로 system 함수를 호출시켜 봅시다.

쉽게 확인하기 위해 gdb를 이용하여 GOT를 overwrite 해 보겠습니다.

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


puts 함수의 호출이 처음이므로 puts 함수의 GOT에는 puts 함수의 실제 주소가 아닌

PLT+6 위치로 이동하여 puts 함수의 주소를 얻어오기 위한 과정을 거치도록 합니다.



그런데 이 때, puts 함수의 GOT에 system 함수의 주소를 넣어둔다면

puts 함수 실행 시 GOT로 이동했을 때 함수의 주소가 들어있으므로

puts 함수의 주소인 줄 알고 해당 주소로 그대로 점프하여 코드를 실행할 것입니다.



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

그리고 continue를 하게 되면….

아래와 같이 “pwd” 명령이 실행된 것을 확인할 수 있습니다. 즉 puts 함수의 GOT Overwrite에 성공하였습니다.





RELRO란?


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


바이너리 컴파일 시 Full-RELRO 옵션을 주면

.ctors, .dtors, .jcr, .dynamic, .got 섹션이 읽기전용상태(Read-Only)가 됩니다.

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



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


#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



Full-RELRO를 적용하면 .ctors, .dtors, .jcr, .dynamic, .got 섹션이 Read-Only가 된다고 했습니다.

그렇다면 RELRO를 적용하기 전에는 위 섹션들에 데이터를 쓸 수 있을 지를 확인해 보겠습니다.

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


# objdump -h non_test



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



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




Partial RELRO인 경우


# gcc relro.c -Wl,-z,relro -o partial_test

# ../checksec.sh –file partial_test



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


# objdump -h partial_test



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



위와 같이 .ctors, .dtors, .jcr, .dynamic 섹션에 데이터 쓰기 시도 시 Segmentation fault가 발생합니다.

그 원인을 알아보기 위해 gdb로 디버깅을 하면 다음과 같습니다.



디버깅을 해 보니 주어진 위치(0x08048445)에 0x41414141을 쓰려다가 Write 권한이 없어 Segmentation fault가 난 것이었습니다.

이처럼 .ctors, .dtors, .jcr, .dynamic 섹션에서도 디버깅을 해보면 위와 같이 Write 권한이 없는 것을 확인할 수 있습니다.

그리고 이제 Partial RELRO에서 GOT Overwrite 가 가능한 지를 테스트 해보겠습니다.

readelf를 이용하여 printf 함수의 GOT를 알아오고 해당 GOT에 데이터 쓰기를 시도합니다.



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



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


objdump -h full_test



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



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



디버깅을 해 보니 주어진 위치(0x08049ef4)에 0x41414141을 쓰려다가 Write 권한이 없어 Segmentation fault가 난 것이었습니다.

.ctors, .dtors, .jcr, .dynamic 섹션에서도 디버깅을 해 보면 위와 같이 Write 권한이 없는 것을 확인할 수 있습니다.

마지막으로 Full RELRO에서 GOT Overwrite 가 가능한 지를 테스트 해보겠습니다.

readelf를 이용하여 printf 함수의 GOT를 알아오고 해당 GOT에 데이터 쓰기를 시도합니다.



gdb 상에서 디버깅을 해 보니 printf 함수의 GOT를 변경하려고 했으나,

write 권한이 없어서 변경되지 못하고 Segmentation fault가 난 것이었습니다.

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

Full RELRO가 GOT가 읽기전용이라 뭔가 보안상 더 안전할 것 같은데 Full RELRO보다는 Partial RELRO가 더 널리 사용됩니다.

그 이유는 Full RELRO의 경우 프로세스가 시작될 때 링커에 의해 모든 메모리에 대해 재배치 작업이 일어나 실행이 느려지기 때문입니다.

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

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

comments powered by Disqus