여러 개의 보안 설비를 하는 것은 하나의 강력한 장비에 의존하는 것보다 낫다.
이런 특성은 시스템 보안에서도 마찬가지이다. 지난 수년간 발전해온 공격 기법과 보호 기법의 발전 양상을 보면 알 수 있듯, 어떤 보호 기법이 등장히면 이를 우회하는 새로운 공격 기법이 등장하곤 한다. 어떤 공격이 새롭게 등장할지는 누구도 예상할 수 없기 때문에 시스템 개발자들은 여러 겹의 보호 기법을 적용하여 시스템이 공격당할 수 있는 표면(Attack Surface) 자체를 줄여나가려고 했다.
예를 들어, 지난 강의에서 공격 기법을 실습했던 r2s를 대상으로 셸 코드를 실행시킬 수 있었던 이유는, 첫째로 반환 주소를 임의 주소로 덮을 수 있었고, 둘째로 사용자가 데이터를 입력할 수 있는 버퍼의 주소를 알 수 있었으며, 마지막으로 그 버퍼가 실행 가능했기 때문이다. 이 중 첫 번째 조건을 만족하기 어렵게 하려고 카나리를 도입했지만, 나머지 두 조건을 방어하지 않았기 때문에 카나리만 우회하면 셸을 획득할 수 있었다.
따라서 r2s를 통한 공격자의 침입을 더 어렵게 하려면, 공격자가 메모리에서 임의 버퍼의 주소를 알기 어렵게 하고, 메모리 영역에서 불필요한 실행 권한을 제거하는 보호 기법을 추가로 도입해야한다.
이와 관련해서 시스템 개발자들은 Address Space Layout Randomization(ASLR)과 No-eXecute(NX)을 개발하고 시스템에 적용하였다.
** NX
NX
No-eXecute(NX)는 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호기법이다. 어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이 함께 있으면 시스템이 취약해지기 쉽다. 예를 들어, 코드 영역에 쓰기 권한이 있으면 공격자는 코드를 수정하여 원하는 코드가 실행되게 할 수 있고, 반대로 스택이나 데이터 영역에 실행 권한이 있으면 Return to Shellcode와 같은 공격 시도를 할 수 있다.
CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며, NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받는다. gdb의 vmmap으로 NX 적용 전후의 메모리 맵을 비교하면, 다음과 같이 NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없는 것을 확인할 수 있다. 반면 NX가 적용되지 않은 바이너리에는 스택 영역([stack])에 실행 권한이 존재하여 rwx 권한을 가지고 있음을 확인할 수 있다.
NX enable
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /home/dreamhack/nx
0x401000 0x402000 r-xp 1000 1000 /home/dreamhack/nx
0x402000 0x403000 r--p 1000 2000 /home/dreamhack/nx
0x403000 0x404000 r--p 1000 2000 /home/dreamhack/nx
0x404000 0x405000 rw-p 1000 3000 /home/dreamhack/nx
0x7ffff7d7f000 0x7ffff7d82000 rw-p 3000 0 [anon_7ffff7d7f]
0x7ffff7d82000 0x7ffff7daa000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7daa000 0x7ffff7f3f000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f3f000 0x7ffff7f97000 r--p 58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f97000 0x7ffff7f9b000 r--p 4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f9b000 0x7ffff7f9d000 rw-p 2000 218000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f9d000 0x7ffff7faa000 rw-p d000 0 [anon_7ffff7f9d]
0x7ffff7fbb000 0x7ffff7fbd000 rw-p 2000 0 [anon_7ffff7fbb]
0x7ffff7fbd000 0x7ffff7fc1000 r--p 4000 0 [vvar]
0x7ffff7fc1000 0x7ffff7fc3000 r-xp 2000 0 [vdso]
0x7ffff7fc3000 0x7ffff7fc5000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc5000 0x7ffff7fef000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fef000 0x7ffff7ffa000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
NX Disabled
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /home/dreamhack/nx_disabled
0x401000 0x402000 r-xp 1000 1000 /home/dreamhack/nx_disabled
0x402000 0x403000 r--p 1000 2000 /home/dreamhack/nx_disabled
0x403000 0x404000 r--p 1000 2000 /home/dreamhack/nx_disabled
0x404000 0x405000 rw-p 1000 3000 /home/dreamhack/nx_disabled
0x7ffff7d7f000 0x7ffff7d82000 rw-p 3000 0 [anon_7ffff7d7f]
0x7ffff7d82000 0x7ffff7daa000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7daa000 0x7ffff7f3f000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f3f000 0x7ffff7f97000 r--p 58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f97000 0x7ffff7f9b000 r--p 4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f9b000 0x7ffff7f9d000 rw-p 2000 218000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f9d000 0x7ffff7faa000 rw-p d000 0 [anon_7ffff7f9d]
0x7ffff7fbb000 0x7ffff7fbd000 rw-p 2000 0 [anon_7ffff7fbb]
0x7ffff7fbd000 0x7ffff7fc1000 r--p 4000 0 [vvar]
0x7ffff7fc1000 0x7ffff7fc3000 r-xp 2000 0 [vdso]
0x7ffff7fc3000 0x7ffff7fc5000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc5000 0x7ffff7fef000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fef000 0x7ffff7ffa000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rwxp 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
* Checksec을 이용한 NX 확인
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) 라고 칭하고 있습니다. 명칭만 다를 뿐 모두 비슷한 보호 기법입니다.
💡5.4.0 미만 버전 리눅스 커널에서의 NX
5.4.0 미만 버전은 스택 영역 뿐만 아니라 힙, 데이터 영역 등 읽기(r) 권한이 있는 모든 페이지에 실행(x) 권한을 부여합니다. 이는 5.4.0 이전 버전의 커널은, NX 미적용 시, 프로세스의 Personality에 읽기 권한이 있는 모든 페이지에 실행 권한을 부여하는 READ_IMPLIES_EXEC 플래그를 설정하기 때문입니다.
5.4.0 이상 버전의 커널은 READ_IMPLIES_EXEC를 설정하지 않고, 로더가 따로 스택 영역([stack])에만 실행 권한을 부여합니다.
* Return to Shellcode w/t NX
이전에 실습한 Return to shellcode의 예제인 r2s에 NX 보호 기법을 적용한 후, 동일한 익스플로잇을 실행했을 때의 결과를 확인해보자
r2s.c를 -zexecstack 옵션을 제거해 컴파일하고, checksec으로 확인해보면 NX가 활성화되어있다.
gcc -o r2s_nx r2s.c

이 바이너리를 대상으로 익스플로잇 코드를 실행하면, 다음과 같이 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)
** ASLR
ASLR
Address Space Layout Randomization(ASLR)은 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호기법이다.
Exploit Tech: Return to shellcode에서 r2s는 ASLR이 적용되어 실행할 때마다 buf의 주소가 변경되었다. 해당 바이너리는 실습의 편의를 위해 buf의 주소를 출력해주었으므로 buf를 공격에 활용하는 것이 어렵지 않았다. 그러나 일반적인 바이너리였다면 buf의 주소를 구하는 과정이 선행되어야 할 것이다.
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로 할당한 영역
아래의 코드를 예제로 사용하여 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의 특징
addr.c 코드는 메모리의 주소를 출력하는 코드이다. gcc로 컴파일하고 실행해보면 아래와 같은 결과를 확인할 수 있다.
$ 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

스택 영역의 buf_stack, 힙 영역의 buf_heap, 라이브러리 함수 printf, 코드 영역의 함수 main, 그리고 라이브러리 매핑 주소 libc_base가 출력되었다. 결과를 보면 다음과 같은 특징이 있다.
- 코드 영역의 main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경된다. 실행할 때마다 주소가 변경되기 때문에 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없다.
- 바이너리를 반복해서 실행해도 libc_base 주소 하위 12비트 값과 printf 주소 하위 12비트 값은 변경되지 않는다. 리눅스는 ASLR이 적용됐을 때, 파일을 페이지(page) 단위로 임의 주소에 매핑한다. 따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않는다.
- 페이지와 페이징에 대해서는 나중에 개설될 컴퓨터 과학 커리큘럼에서 자세히 소개하겠음.
- libc_base와 printf의 주소 차이는 항상 같다. ASLR이 적용되면, 라이브러리는 임의 주소에 매핑된다. 그러나 라이브러리 파일을 그대로 매핑하는 것이므로 매핑된 주소로부터 라이브러리의 다른 심볼들까지의 거리(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)
마치며
마치며
NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며, 이들이 할당되는 주소가 계속 변합니다. 그러나 바이너리의 코드가 존재하는 영역은 여전히 실행 권한이 존재하며, 할당되는 주소도 고정되어 있습니다.
코드 영역에는 유용한 코드 가젯들과 함수가 포함되어 있습니다. 반환 주소를 셸 코드로 직접 덮는 대신, 이들을 활용하면 NX와 ASLR을 우회하여 공격할 수 있습니다. 관련된 대표적인 공격 방법으로는 Return-to-Libc (RTL)과 Return Oriented Programming (ROP)가 있습니다. 다음 강의부터는 이 공격 기법들에 대한 배경 지식을 살펴보고, 이들을 이용하여 ASLR과 NX 그리고 카나리를 우회하는 실습을 해보도록 하겠습니다.
다음 강의에서는 라이브러리와 동적 링크, 그리고 정적 링크에 대한 내용을 다루겠습니다. 🚩
키워드
|
'공부중 > 시스템 해킹' 카테고리의 다른 글
| [Dreamhack] Exploit Tech: Return to Library (0) | 2024.09.19 |
|---|---|
| [Dreamhack] Library-Static Link vs. Dynamic Link & Quiz (0) | 2024.09.18 |
| [Dreamhack] Exploit Tech: Return to Shellcode (0) | 2024.09.17 |
| [Dreamhack] Exploit Tech: Return Address Overwrite (0) | 2024.09.16 |
| [Dreamhack] Stack Buffer Overflow (0) | 2024.09.15 |
댓글