nx&aslr
NX, ASLR
스택 버퍼 오버플로우로 공격하는 것을 방지하기 위해 이전에 스택 카나리라는 방법을 소개했다. 그러나 카나리 값을 실행 중에 가져오면 검증 과정을 속일 수 있기 때문에 여전히 스택 버퍼 오버플로우로 공격할 수 있다.
NX, ASLR은 카나리처럼 익스플로잇 방지 기법인데, 이는 스택 버퍼 오버플로우 공격을 할 때 RET까지 데이터를 덮어씌우는 과정에서 공격을 막는다고 할 수 있다.
공격 과정을 보면:
반환 주소를 임의 주소로 덮기 -> 카나리
스택 프레임 내 구성요소 주소를 알아내고 -> ASLR
버퍼에 쉘코드 삽입하여 실행하게 하기 -> NX
의 과정이 이루어지는데 반환 주소를 임의 주소로 덮는 것은 카나리 검증을 통해 어렵게 할 수 있었다. 하지만 가능했다 ASLR은 스택 프레임 구성요소의 주소를 알아내는 것을 막고 NX는 버퍼에 쉘코드를 삽입하고 실행하는 것을 막기 위한 방법이다.
NX
NX는 Non-eXecutable 의 약자이다. 이름처럼 실행을 방지하는 친구인데 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 역할을 한다. 실행과 쓰기 권한이 분리되지 않으면 시스템이 취약해질 수 있는데, 코드 영역에 쓰기 권한이 있으면 코드를 임의 수정해서 실행되게 할 수 있고 스택과 데이터 영역에 실행 권한이 있으면 같은 공간에 쉘코드를 집어넣고 실행하게 할 수 있다.
현대 CPU는 거의 모두가 NX를 지원하고 컴파일러도 기본적으로 NX를 적용한다. 참고로 윈도우에서는 NX를 DEP라고 한다. NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없다. 그러나 NX가 적용되지 않으면 스택 영역에 실행 권한이 존재할 수 있다. 이는 gdb의 vmmap으로 확인할 수 있다.
권한 정보
잠깐 권한의 종류를 정리하자면
r(읽기)
w(쓰기)
x(실행) 이다.
예시로 drwxr-xr-x 이면 디렉토리(d)에 대해:
소유자(첫번째: rwx): r(읽기), w(쓰기), x(실행)이 가능
소유 그룹(두번째: r-x): r(읽기), x(실행) 가능
모든 사용자(세번쨰: r-x): r(읽기), x(실행) 가능
그러니까 NX가 적용되지 않은 바이너리에는 스택 영역에 rwx 권한이 있을 수 있는 것.
vmmap, checksec
- NX Disabled
0x7ffffffde000 0x7ffffffff000 rwxp 21000 0 [stack]
- NX Enabled
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
이전의 ssh_001 문제의 실행파일은
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x8048000 0x8049000 r-xp 1000 0 /home/max/hacking/ssp_001
0x8049000 0x804a000 r--p 1000 0 /home/max/hacking/ssp_001
0x804a000 0x804b000 rw-p 1000 1000 /home/max/hacking/ssp_001
0xf7d66000 0xf7d85000 r--p 1f000 0 /usr/lib32/libc.so.6
0xf7d85000 0xf7f18000 r-xp 193000 1f000 /usr/lib32/libc.so.6
0xf7f18000 0xf7f93000 r--p 7b000 1b2000 /usr/lib32/libc.so.6
0xf7f93000 0xf7f95000 r--p 2000 22c000 /usr/lib32/libc.so.6
0xf7f95000 0xf7f96000 rw-p 1000 22e000 /usr/lib32/libc.so.6
0xf7f96000 0xf7f9b000 rw-p 5000 0 [anon_f7f96]
0xf7fc0000 0xf7fc2000 rw-p 2000 0 [anon_f7fc0]
0xf7fc2000 0xf7fc6000 r--p 4000 0 [vvar]
0xf7fc6000 0xf7fc8000 r-xp 2000 0 [vdso]
0xf7fc8000 0xf7fc9000 r--p 1000 0 /usr/lib32/ld-linux.so.2
0xf7fc9000 0xf7fed000 r-xp 24000 1000 /usr/lib32/ld-linux.so.2
0xf7fed000 0xf7ffb000 r--p e000 25000 /usr/lib32/ld-linux.so.2
0xf7ffb000 0xf7ffd000 r--p 2000 33000 /usr/lib32/ld-linux.so.2
0xf7ffd000 0xf7ffe000 rw-p 1000 35000 /usr/lib32/ld-linux.so.2
0xfffdc000 0xffffe000 rw-p 22000 0 [stack]
NX 적용 상태임을 스택 영역에서 볼 수 있다.
checksec으로도 NX 활성화 여부를 알 수 있다.
❯ checksec ssp_001
[*] '/home/max/hacking/ssp_001'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
5.4.0 미만 버전 리눅스 커널의 NX
5.4.0 이전 버전에서는 스택 영역뿐 아니라 힙, 데이터 영역 등 읽기 권한이 있는 모든 페이지에 실행 권한을 부여한다. 이는 READ_IMPLIES_EXEC플래그 때문인데 NX 비활성화시 프로세스의 personality에 읽기 권한이 있는 모든 페이지에 실행 권한을 부여한다.
5.4.0 이후에는 플래그가 아니라 로더가 따로 스택 영역에만 실행 권한을 부여한다.
NX 적용 후 익스플로잇
NX 미적용 때 익스플로잇에 성공했어도 같은 코드를 NX활성화 옵션으로 컴파일하면 스택 영역에 실행 권한이 사라지면서 쉘코드가 실행되지 못하고 Segmentation fault를 띄우고 죽어버릴 수 있다.
ASLR
ASLR은 Address Space Layout Randomization의 약자이다. 아까 스택 프레임의 구성요소 주소를 알아내는 걸 방해한다고 했는데, 이름처럼 주소 공간을 프로그램 실행 때마다 변경하는 것이다.
예를 들어 사용자 입력을 받는 문자 배열이나 gets() 함수처럼 각종 메모리에 올려야 하는 부분이 실행 때마다 달라지는 것이다. 따라서 미리 주소를 구하는 게 불가능하다는 것
ASLR은 커널에서 지원하는 보호 기법으로 다음 명령어로 확인할 수 있다.
cat /proc/sys/kernel/randomize_va_space
리눅스에서 이 값은 0, 1, 2 중에 출력값을 가지는데 숫자가 올라갈수록 강력하다:
No ASLR(0): ASLR 적용X
Conservative Randomization(1): 스택, 라이브러리, vdso
Conservative Randomization + brk(2): (1)의 영역+brk로 할당한 영역
ASLR의 동작 과정
아래 코드를 통해 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); // 코드 영역의 함수 주소
}
컴파일 후 실행해보면
~/hacking ───────────────────────────── max-env at 19:24:57
❯ ./addr
buf_stack addr: 0x7ffda7520db0
buf_heap addr: 0x364a92a0
libc_base addr: 0x7326741c1000
printf addr: 0x732674219a80
main addr: 0x401166
~/hacking ───────────────────────────── max-env at 19:25:06
❯ ./addr
buf_stack addr: 0x7ffc89981080
buf_heap addr: 0x151172a0
libc_base addr: 0x767808625000
printf addr: 0x76780867da80
main addr: 0x401166
관찰해보면:
코드 영역의
main함수를 제외한 다른 영역의 주소들이 실행 시마다 변경된다.libc_base와printf의 하위 12비트 값은 변경되지 않는다.- 리눅스는 ASLR 적용 시 파일을 페이지 단위로 임의 주소에 매핑하는데 페이지 크기가 12비트라 그 이하로는 주소가 변경되지 않는다.
libc_base와printf의 주소 차이는 항상 같다.- ASLR은 라이브러리를 임의 주소에 매핑되게 하지만 파일 그대로 매핑만 하는 것이라 매핑된 주소로부터 다른 심볼들까지의 거리는 항상 같다.

