RELRO

  • 함수가 처음 호출될 때 함수의 주소를 구하고, 이를 GOT에 적는 것을 Lazy Binding 이라고 한다.
  • Lazy Binding을 하는 바이너리는 실행 중에 GOT 테이블을 업데이트 해야 하므로 쓰기 권한이 부여되지만, 이 과정에서 바이너리를 취약하게 만드는 원인이 된다.
  • ELF의 데이터 세그먼트에 프로세스 초기화 및 종료와 관련된 .init_array, .fini_array가 있는데 이 영역들은 프로세스의 시작과 종료에 실행할 함수들의 주소를 저장하고 있다.
    • 이 부분에 공격자가 임의로 쓸 수 있는 값이 있다면 프로세스의 실행 흐름이 조작될 수 있다.
  • 리눅스 개발자들은 이러한 문제를 해결하고자 프로세스의 데이터 세그먼트를 보호하는 RELocation Read-Only(RELRO)를 개발했고, RELRO는 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한을 제거한다.
  • RELRO는 RELRO를 적용하는 범위에 따라 두 가지로 구분된다. 하나는 RELRO를 부분적으로 적용하는 Partial RELRO이고, 나머지는 가장 넓은 영역에 RELRO를 적용하는 Full RELRO이다.


RELRO 실습 코드

// Name: relro.c
// Compile: gcc -o prelro relro.c -no-pie -fno-PIE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
  FILE *fp;
  char ch;
  fp = fopen("/proc/self/maps", "r");
  while (1) {
    ch = fgetc(fp);
    if (ch == EOF) break;
    putchar(ch);
  }
  return 0;
}


RELRO 검사

  • checksec 도구로 RELRO가 Partial인지 Full인지 검사할 수 있다.
  • 보통 gcc는 Full RELRO를 기본으로 적용하며, PIE를 해제하면 Partial RELRO를 적용한다.

image-20240408191452986


Partial RELRO 권한

  • 아래는 prelro 바이너리를 실행한 결과고 0x40400부터 0x405000까지의 주소에 쓰기 권한을 확인할 수 있다.
  • objdump -h ./prerlo 명령으로 섹션 헤더를 참조해보면 .got.plt, .data, .bss 가 할당되어 있다.
  • 이 섹션들의 주소가 위 주소 사이에 존재하므로 쓰기가 가능하지만 init_arry와 fini_array는 위 주소 범위에 해당하지 않으므로 쓰기가 불가능한 것을 알 수 있다.

image-20240408191912567


.got와 .got.plt

Partial RELRO가 적용된 바이너리는 got와 관련된 섹션이 .got와 .got.plt 두 가지가 존재한다. 전역 변수 중에서 실행되는 시점에 바인딩되는 변수는 .got에 위치한다. 바이너리가 실행될 때는 이미 바인딩이 완료되었으므로 .got에는 쓰기 권한을 부여하지 않는다. 반면 .got.plt 는 실행 중에 값이 써져야 하므로 쓰기 권한이 부여된다. Partial RELRO가 적용된 바이너리에서 대부분 함수들의 GOT 엔트리는 .got.plt에 저장된다.


Full RELRO

  • relro.c 의 실습 코드에서 별도의 컴파일 옵션 없이 컴파일하면 Full RELRO가 적용된 바이너리가 생성된다.
  • 바이너리를 실행하여 메모리 맵을 확인해보면, 전과 다르게 got에는 쓰기 권한이 제거되어 있으며 data와 bss에만 쓰기 권한이 있다.
  • 바이너리를 실행하면 쓰기 권한이 있는 주소를 찾고, objdump -h ./frelro 명령을 통해 data의 섹션 주소와 매핑된 첫 주소를 더하면 쓰기 권한이 있는지 확인할 수 있다.
  • Full RELRO가 적용되면 라이브러리 함수들의 주소가 바이너리의 로딩 시점에 모두 바인딩된다. 따라서 GOT에는 쓰기 권한이 부여되지 않는다.

image-20240408211428873


RELRO 우회

  • Partial RELRO의 경우, .init_array .fini_array 에 대한 쓰기 권한이 제거되어 두 영역을 덮어쓰는 공격을 수행하기 어려워진다. 하지만 .got.plt 영역에 대한 쓰기 권한이 존재하므로 GOT Overwrite 공격을 활용할 수 있다.
  • Full RELRO의 경우 .init_array .fini_array뿐만 아니라 .got영역에도 쓰기 권한이 제거되었다. 그래서 공격자들은 덮어쓸 수 있는 다른 함수 포인터를 찾다가 라이브러리에 위치한 hook을 찾아냈다. 라이브러리 함수의 대표적인 hook이 malloc hook과 free hook이다. 원래 이 함수 포인터는 동적 메모리의 할당과 해제 과정에서 버그를 디버깅하기 쉽게 하려고 만들어 졌다.
  • malloc 함수의 코드를 살펴보면, 함수의 시작 부분에서 __malloc_hook이 존재하는지 검사하고, 존재하면 이를 호출한다. __malloc_hook은 libc.so에서 쓰기 가능한 영역에 위치한다. 따라서 공격자는 libc가 매핑된 주소를 알 때, 이 변수를 조작하고 malloc을 호출하여 실행 흐름을 조작할 수 있다. 이와 같은 공격 기법을 통틀어 Hook Overwrite라고 부른다.


요약

  • RELRO 보호 기법은 프로세스의 데이터 영역을 보호하기 위해 도입된 보호 기법
  • Partial RELRO 바이너리는 got에 쓰기 권한이 남아 있어 GOT Overwrite 공격으로 우회 가능
  • Full RELRO 바이너리는 데이터 영역의 모든 불필요한 쓰기 권한이 삭제되어 Hook Overwrite등의 새로운 공격 기법 사용해야 함

댓글남기기