1. 서론
공격자가 메모리에서 임의 버퍼 주소를 알기 어렵게 하고,
메모리 영역에서 불필요한 실행 권한을 제거하는
보호 기법을 추가로 도입해야 한다.
이와 관련하여 시스템 개발자들은
Address Space Layout Randomization (ASLR)과
No-eXecute(NX)을 개발하고 시스템에 적용했다.
2. ASLR
Address Space Layout Randomization (ASLR)은
바이너리가 실행될 때마다, 스택, 힙, 공유 라이브러리 등을
임의의 주소에 할당하는 보호 기법이다.
ASLR은 커널에서 지원하는 보호 기법이며,
다음의 명령어로 확인할 수 있다.
$ cat /proc/sys/kernel/randomize_va_space
2
리눅스에서 이 값은 0, 1, 또는 2의 값을 가질 수 있다.
각 ASLR에 적용되는 메모리 영역은 다음과 같다.
- No ASLR(0): ASLR을 적용하지 않음
- Conservative Randomization(1): 스택, 힙, 라이브러리, vdso 등
- Conservative Randomization + brk(2): (1)의 영역과 brk로 할당한 영역
Figure 1. 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); // 코드 영역의 함수 주소
}
ASLR 특징
$ gcc addr.c -o addr -ldl -no-pie -fno-PIE
$ ./addr
buf_stack addr: 0x7ffcd3fcffc0
buf_heap addr: 0xb97260
libc_base addr: 0x7fd7504cd000
printf addr: 0x7fd750531f00
main addr: 0x400667
$ ./addr
buf_stack addr: 0x7ffe4c661f90
buf_heap addr: 0x176d260
libc_base addr: 0x7ffad9e1b000
printf addr: 0x7ffad9e7ff00
main addr: 0x400667
$ ./addr
buf_stack addr: 0x7ffcf2386d80
buf_heap addr: 0x840260
libc_base addr: 0x7fed2664b000
printf addr: 0x7fed266aff00
main addr: 0x400667
- 코드 영역의 main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경된다.
- 실행할 때마다 주소가 변경되기 때문에 실행하기 전에 주소를 예측할 수 없다.
- 바이너리를 반복해서 실행해도 libc_base 주소 하위 12비트 값과 printf 주소 하위 12비트 값은 변경되지 않는다. 리눅스는 ASLR이 적용됐을 때, 페이지(Page) 단위로 임의 주소에 매핑한다. 따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않는다.
- libc_base와 printf의 주소 차이는 항상 같다. ALSR이 적용되면, 라이브러리는 임의 주소에 매핑된다. 그러나 라이브러리의 파일을 그대로 매핑하는 것이므로 매핑된 주소로부터 라이브러리의 다른 심볼들 까지의 거리(Offset)는 항상 같다.
>>> hex(0x7fd7504cd000 - 0x7fd750531f00) # libc_base addr - printf addr
'-0x64f00'
>>> hex(0x7ffad9e1b000 - 0x7ffad9e7ff00)
'-0x64f00'
$ objdump -D /lib/x86_64-linux-gnu/libc.so.6 | grep 064f00 -A3
0000000000064f00 <_IO_printf@@GLIBC_2.2.5>:
64f00: 48 81 ec d8 00 00 00 sub $0xd8,%rsp
64f07: 84 c0 test %al,%al
64f09: 48 89 74 24 28 mov %rsi,0x28(%rsp)
3. NX
No eXecute(NX)는 실행에 사용되는 메모리 영역과
쓰기에 사용되는 메모리 영역을 분리하는 보호 기법이다.
어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이
함께 있으면 시스템이 취약해지기 쉽다.
예를 들어, 코드 영역에 쓰기 권한이 있으면
공격자는 코드를 수정하여 원하는 코드가 실행되게 할 수 있고,
반대로 스택이나 데이터 영역에 실행 권한이 있으면
Return to Shellcode와 같은 공격을 시도할 수 있다.
CPU가 NX를 지원하면 컴퍼일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며,
NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받는다.
gdb의 vmap으로
NX 적용 전후의 메모리 맵 비교
NX Enable
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00401000 r-xp /home/dreamhack/nx
0x00600000 0x00601000 r--p /home/dreamhack/nx
0x00601000 0x00602000 rw-p /home/dreamhack/nx
0x00007ffff79e4000 0x00007ffff7bcb000 r-xp /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7bcb000 0x00007ffff7dcb000 ---p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcb000 0x00007ffff7dcf000 r--p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcf000 0x00007ffff7dd1000 rw-p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dd1000 0x00007ffff7dd5000 rw-p mapped
0x00007ffff7dd5000 0x00007ffff7dfc000 r-xp /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7fd7000 0x00007ffff7fd9000 rw-p mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
NX Disabled
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00401000 r-xp /home/dreamhack/nx_disabled
0x00600000 0x00601000 r-xp /home/dreamhack/nx_disabled
0x00601000 0x00602000 rwxp /home/dreamhack/nx_disabled
0x00007ffff79e4000 0x00007ffff7bcb000 r-xp /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7bcb000 0x00007ffff7dcb000 ---p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcb000 0x00007ffff7dcf000 r-xp /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcf000 0x00007ffff7dd1000 rwxp /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dd1000 0x00007ffff7dd5000 rwxp mapped
0x00007ffff7dd5000 0x00007ffff7dfc000 r-xp /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7fd7000 0x00007ffff7fd9000 rwxp mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r-xp /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rwxp /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffe000 0x00007ffff7fff000 rwxp mapped
0x00007ffffffde000 0x00007ffffffff000 rwxp [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
Checksec을 이용한 NX 확인
NX Enabled
$ checksec ./nx
[*] '/home/dreamhack/nx'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
NX Disabled
$ checksec ./nx_disabled
[*] '/home/dreamhack/nx_disabled'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
NX의 다양한 명칭
NX를 인텔은 XD(eXecute Disable), AMD는 NX, 윈도우는 DEP(Data Execution Prevention),
ARM에서는 XN(eXecute Never)라고 정하고 있다. 명칭만 다를 뿐 모두 비슷한 보호 기법이다.
Return to Shellcode w/t NX
예전에 실습한 Return to shellcode의 예제인 r2s에 NX 보호기법을 적용한 후,
동일한 익스플로잇 실행했을 때의 결과를 확인
r2s.c를 -zexecstack 옵션을 제거해 컴파일하고,
checksec으로 확인해보면 NX가 활성화되어 있다.
$ checksec ./r2s_nx
[*] '/home/dreamhack/r2s_nx'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
익스플로잇 코드를 실행하면,
다음과 같이 Segmentation fault가 발생한다.
이는 NX가 적용되어 스택 영역에 실행 권한이 사라지게 되면서,
쉘코드가 실행되지 못하고 종료된 것이다.
$ python exploit.py
[+] Starting local process './r2s_nx': pid 48085
[+] Address of buf: 0x7ffd0757ed20
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0xd466b1c002721100
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process './r2s_nx' stopped with exit code -11 (SIGSEGV) (pid 48085)
4. 정리
NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며,
이들이 할당되는 주소가 계속 변한다.
그러나 바이너리의 코드가 존재하는 영역은
여전히 실행 권한이 존재하며, 할당되는 주소도 고정되어 있습니다.
코드 영역에는 유용한 코드 가젯들과 함수가 포함되어 있다.
반환 주소를 쉘코드로 직접 덮는 대신, 이들을 활용하면 NX와 ASLR을 우회하여 공격할 수 있다.
관련된 대표적인 공격 방법으로는
RTL (Return-to-Libc)과
ROP(Return Oriented Programming)가 있다.
키워드
- Address Space Layout Randomization(ASLR): 메모리를 무작위 주소에 할당하는 보호 기법, 최신 커널들은 대부분 적용되어 있음. 리눅스에서는 페이지 단위로 할당이 이루어지므로 하위 12비트는 변하지 않는다는 특징이 있음.
- NX(No-eXecute bit): 프로세스의 각 세그먼트에 필요한 권한만 부여하는 보호 기법, 일반적으로 코드 영역에는 읽기와 실행을, 나머지 영역에는 읽기와 쓰기 권한이 부여됨.
'드림핵 - 시스템 해킹' 카테고리의 다른 글
Exploit Tech: Return Oriented Programming (0) | 2023.05.23 |
---|---|
Exploit Tech: Return to Library (0) | 2023.05.15 |
Static Link vs. Dynamic Link (0) | 2023.05.08 |
Stack Canary (0) | 2023.05.02 |
Exploit Tech: Return Address Overwrite (0) | 2023.03.29 |