이번 회차에서는 RELRO 보호기법을 공부해볼 것이다.
이전에 배웠던 background:Library에서 ELF는 GOT를 활용하여 반복되는 라이브러리 함수의 호출 비용을 줄인다 배웠다. GOT를 채우는 방식은 다양한데, 전에 배운 방법은 처음 호출될때 함수의 주소를 구하고, 이를 GOT에 적는 Lazy Binding기법을 배웠다.
Lazy binding을 하는 바이너리는 실행 중에 GOT 테이블을 업데이트할 수 있어야 하므로 GOT에 쓰기 권한이 부여된다. 그런데 이는 쓰기권한이 있기 때문에 바이너리를 취약하게 만드는 원인이 된다.
리눅스 개발자들은 이러한 문제를 해결하기 위해 프로세스의 데이터 세그먼트를 보호하는 RELocation Read-Only(RELRO)을 개발했다. RELRO는 쓰기 권한이 불 필요한 데이터 세그먼트에 쓰기권한을 제거한다.
RELRO는 적용하는 범위에 따라 두가지로 구분 된다. 하나는 부분적으로 적용하는 Partial RELRO이고, 나머지는 가장 넓은 영역에 RELRO를 적용하는 Full RELRO이다.
내 환경에서는 gcc는 Full RELRO를 기본 적용하고 PIE를 해제하면 Partial RELRO가 적용된다. checksec로 검사하면 RELRO 여부를 확인할 수 있다.
Partial RELRO는 .int_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라고 부른다.
void * __libc_malloc (size_t bytes) { mstate ar_ptr; void *victim; void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); // read hook if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0)); // call hook #if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); // ...