ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Linux Memory Protection] - RELRO
    System hacking training/Knowledge 2019. 5. 20. 20:22



    오늘은 RELRO에 대해 알아봅시다.

    RELRO란?
    -> Relocation Read-Only의 약자로, ELF바이너리 또는 프로세스의 데이터 섹션을 보호하는 기술입니다.
    즉 메모리가 변경되는 것을 보호하는 기술입니다.

    이를 이해하기 위해서는 다음과 같은 개념을 알고 있어야합니다.

    - Lazy Binding
    - GOT Overwrite



    먼저 Lazy Binding이란?
    -> Dynamic Linking 방식으로 컴파일 된 ELF 바이너리는 공유 라이브러리 내에 위치한 함수의 주소를 동적으로 알아오기 위해
    GOT(Global Offset Table) 테이블을 이용하는데, 이렇게 모든 외부 함수의 주소를 한 번에 로딩하지 않고, 함수 호출 시점에 해당 함수의 주소만 공유 라이브러리로부터 알아오는 것은 Lazy Binding이라고 합니다.

    Lazy Binding을 통해 라이브러리의 내용을 모두 메모리에 매핑하지 않고 실행시점에서 필요한 함수의 주소만 알아오면서
    파일의 크기가 작아지고, 상대적으로 적은 메모리를 사용하여 실행 속도 또한 빠르게 됩니다.

    이러한 Lazy Binding과정에서 PLT와 GOT를 이용하게 되는데요.

    PLT는 Procedure Linkage Table의 약자로, 외부 프로시저를 연결해주는 테이블을 의미합니다.
    PLT를 통해서 다른 라이브러리에 있는 프로시저를 호출해 사용할 수 있습니다.

    GOT는 Global Offset Table의 약자로, PLT가 참조하는 테이블이며 프로시저들의 주소가 들어있습니다.

    함수를 호출(PLT를 호출)하면 GOT로 점프하는데 GOT에는 함수의 실제 주소가 쓰여져있습니다.

    PLT와 GOT에 대해서는  https://hackstoryadmin.tistory.com/61 에 설명되어있으니 참고하시면 될 것같습니다.

    아무튼 결론적으로
    Dynamic Linking 방식으로 컴파일 된 ELF 바이너리는 함수 호출과정에서 Lazy Binding이라는 과정을 거치고,
    이 과정에서 GOT를 이용하게 됩니다.

    다음으로 GOT Overwrite에 대해 간략하게 개념을 잡아봅시다.

    GOT Overwrite란?
    -> GOT는 PLT가 참조하는 테이블이며, 프로시저들의 실제 주소가 들어있다고 했습니다.
    예를 들어 printf 함수의 주소가 저장되어있는 GOT를 Overwrite하여 system함수로 변조한다면

    printf함수가 호출될 때 system함수가 호출되게 될 것입니다.
    바로 이를 GOT Overwrite라고 합니다.



    이제 본격적으로 RELRO가 하는 일에 대해 알아봅시다.

    바이너리를 컴파일 할 때 Full, Partial옵션을 줄 수 있는데요.
    옵션에 따른 GOT 상태는 다음과 같습니다.

    [Parial RELRO]
    GOT Writeable / 함수 호출 시 해당 함수의 주소를 알아옴

    [Full RELRO]
    GOT Read-Only / ELF 바이너리 실행시 GOT에 모든 라이브러리 주소 바인딩

    [+] .ctors, .dtors, .jcr, .dynamic, .got영역 Read-Only

    이 때 Full-RELRO의 경우
    got뿐만 아니라 .ctors, .dtors, .jcr, .dynamic 영역을 읽기 전용 섹션으로 만듭니다.

    예제를 통해 정확히 알아봅시다.

    /* RELRO.c */
    #include <stdio.h>

    int main(int argc, char *argv[]){
        size_t *p = (size_t *)strtol(argv[1], NULL, 16);
        p[0] = 0x41414141;
        printf("RELRO TEST : %p\n", p);
        return 0;
    }

    해당 예제는 0x41414141이라는 값을 주어진 주소에 쓰는 프로그램입니다.

    # RELRO가 적용되지 않은 경우

    [컴파일 옵션]
    gcc -o RELRO RELRO.c -m32 -no-pie -Wl,-z,norelro



    checksec 결과 RELRO가 없는 것을 확인할 수 있고,
    이제 objdump 를 이용하여 각 섹션의 주소를 구한 뒤 예제 바이너리에 인자값으로 입력해봅시다.



    objdump -h RELRO의 결과 .dynamic , .got의 영역의 주소를 구할 수 있습니다.



    또한 두 섹션 모두 데이터가 써지는 것을 확인 할 수 있습니다.

    # Partial RELRO인 경우

    [컴파일 옵션]
    gcc -o RELRO RELRO.c -m32 -no-pie -Wl,-z,relro



    역시 checksec 결과 Partial RELRO인 것을 확인 할 수 있고



    각 섹션의 주소를 구한 뒤



    데이터 쓰기를 시도 했을 때 그림과 같이 segmentation fault가 뜨는 것을 확인 할 수 있습니다.

    gdb로 디버깅을 해봅시다.



    디버깅 결과 인자값으로 .got의 주소 0x8049ffc가 가리키는 주소에 0x41414141을 쓰려다 Write권한이 없어 segmentation fault가 난 것이었습니다.

    이제 printf함수의 got를 변경해보겠습니다.



    이 역시 segmentation fault가 났는데요.



    core파일을 열어보면 eip가 0x41414141로 변조된 것을 확인할 수 있습니다.
    즉, printf함수 실행시 got가 0x41414141로 변경되어 segmentation fault가 났던 것이였습니다.

    Partial RELRO의 경우 Full-RELRO와 다르게 .got섹션이 쓰기가 가능합니다.

    따라서 Partial RELRO의 경우 got overwrite가 가능합니다.

    다음으로 Full-RELRO의 경우 어떻게 동작하는지 알아봅시다.

    # Full-RELRO의 경우

    [컴파일 옵션]
    gcc -o RELRO RELRO.c -m32 -no-pie -Wl,-z,relro,-z,now



    checksec 확인 결과 Full RELRO가 걸려있는 것을 확인할 수 있고, 역시 objdump를 이용하여 각 섹션의 주소를 구해줍시다.



    이후 예제 프로그램에 인자로 넣어주면



    당연하게도 segmentation fault가 발생합니다.
    Full-RELRO가 적용된 바이너리는 앞서 설명한 바와 같이 .ctors, .dtors, .jcr, .dynamic, .got 섹션을 Read-Only권한을 부여하기 때문에
    Write 권한 없는 섹션에 0x41414141을 쓰려다 발생한 것입니다.

    이제 Full-RELRO의 경우 GOT Overwrite가 가능한지 확인해봅시다.



    printf의 got를 overwrite하려고 하였지만 partial일 때와는 다르게 eip가 변조되지 않은것을 확인 할 수 있습니다.



    got의 write권한이 없기 때문에 segmentation fault가 발생합니다.
    따라서 Full-RELRO의 경우 GOT Overwrite가 불가능합니다.

    보안상으로 Full-RELRO가 좋지만, Partial이 더 자주 사용된다고 합니다.
    이유는 Full일 경우 프로세스가 시작될 때 링커에 의해 모든 메모리에 대해 재배치 작업이 일어나 실행이 느려지기 때문이라고 하네요!

    이상으로 RELRO 포스팅을 마칩니다. 

    반응형
Designed by Tistory.