혜랑's STORY

[System Hacking STAGE 6] Mitigation: NX & ASLR 본문

무지성 공부방/Dreamhack SystemHacking

[System Hacking STAGE 6] Mitigation: NX & ASLR

hyerang0125 2022. 2. 3. 13:53

ASLR(Address Space Layout Randomization)

- 바이너리가 실행 될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법

Exploit Tech: Return to shellcode에서 r2s는 ASLR이 적용되어 실행할 때마다 buf의 주소가 변경되었다. 실습의 편의를 위해 buf의 주소를 출력해주었으므로 buf를 공격하는 것은 어렵지 않았으나, 일반적인 바이너리의 경우 buf의 주소를 구하는 과정이 선행되어야 한다.

ASLR은 커널에서 지원하는 보호 기법이며, 다음의 명령어로 확인할 수 있다.

 $ cat /proc/sys/kernel/randomize_va_space

각 ASLR이 적용되는 메모리 영역은 다음과 같다.

  • No ASLR(0) : ASLR을 적용하지 않음
  • Conservative Randomization(1) : 스택, 힙, 라이브러리, vdso 등
  • Conservative Randomization + brk(2) : (1)의 영역과 brk로 할당한 영역

다음 코드를 예제로 사용하여 ASLR의 특징을 자세히 삺펴보자.

// Name: addr.c
// Compile: gcc addr.c -o addr -ldl -no-pie -fno-PIE

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
  char buf_stack[0x10];                   // 스택 버퍼
  char *buf_heap = (char *)malloc(0x10);  // 힙 버퍼
  
  printf("buf_stack addr: %p\n", buf_stack);
  printf("buf_heap addr: %p\n", buf_heap);
  printf("libc_base addr: %p\n",
         *(void **)dlopen("libc.so.6", RTLD_LAZY));  // 라이브러리 주소
         
  printf("printf addr: %p\n",
         dlsym(dlopen("libc.so.6", RTLD_LAZY),
               "printf"));  // 라이브러리 함수의 주소
  printf("main addr: %p\n", main);  // 코드 영역의 함수 주소
}

addr.c 코드는 메모리의 주소를 출력하는 코드이다. 컴파일하고 실행해보자.

스택 영역의 buf_stack, 힙 영역의 buf_heap, 라이브러리 함수 printf, 코드 영역의 함수 main, 라이브러리 매핑 주소 libc_base가 출력되었다. 결과를 살펴보면 다음과 같은 특징이 있다.

  • 코드 영역의 main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경된다. 즉, 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없다.
  • 바이너리를 반복해서 실행해도 printf 주소의 하위 12비트 값은 변경되지 않는다. 리눅스는 ASLR이 적용됐을 때, 파일을 페이지(page) 단위로 임의 주소에 매핑한다. 따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않는다.
  • libc_base와 printf의 주소 차이는 항상 같다. ASLR이 적용되면, 라이브러리는 임의 주소에 매핑된다. 그러나 라이브러리 파일을 그래도 매핑하는 것이므로 매핑되는 주소로부터 라이브러리의 다른 심볼들 까지의 거리(offset)는 항상 같다.

NX(No-eXecute)

- 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법

어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이 함께 있으면 시스템이 취약해지기 쉽다. 예를 들어, 코드 영역에 쓰기 권한이 있으면 공격자는 코드를 수정하여 원하는 코드가 실행되게 할 수 있고, 반대로 스택이나 데이터 영역에 실행 권한이 있으면 Return to Shellcode와 같은 공격을 시도할 수 있다.

CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며, NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받는다. gdb의 vmmap으로 NX 적용 전후의 메모리 맵을 비교하면, 다음과 같이 NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없는 것을 확인할 수 있다. 반면, NX가 적용되지 않은 바이너리에는 스택, 힙, 데이터 영역에 실행 권한이 존재하는 것을 확인할 수 있다.

Checksec을 이용한 NX 확인

이전에 실습한 Return to shellcode의 예제인 r2s에 NX 보호기법을 적용한 후, 동일한 익스플로잇을 실행했을 때의 결과를 확인해보자. -zexecstack 옵션을 제거해 컴파일하고, checksec으로 확인해보면 NX가 활성화되어 있다.

이 바이너리를 대상으로 익스플로잇 코드를 실행하면, 다음과 같이 Segmentation fault가 발생한다.

이는 NX가 적용되어 스택 영역에 실행 권한이 사라지게 되면서, 셸코드가 실행되지 못하고 종료된 것이다.

 

NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며, 이들이 할당되는 주소가 계속 변한다. 그러나 바이너리의 코드가 존재하는 영역은 여전히 실행 권한이 존재하며, 할당되는 주소도 고정되어 있다. 반환 주소를 셸 코드로 직접 덮는 대신, 이들을 활용하면 NX와 ASLR을 우회하여 공격할 수 있다. 관련된 대표적인 공격 방법으로는 RTL(Return-to-Libc)과 ROP(Return Oriented Programming)가 있다.