지난 글(Return to shellcode)를 보면 쉘을 딸 수 있었던 이유는 다음과 같다.
- return addr을 임의의 주소로 덮기 가능 → canary 도입 ⇒ canary leak 으로 우회
- 버퍼의 주소 알아내기 가능
- 버퍼안의 쉘코드 실행 가능
공격을 막기 위해 시스템 개발자들은 여려 겹의 보호 기법을 적용해 공격당할 수 있는 표면(Attack Surface) 를 줄여나가려 한다. Attack surface를 줄이기 위한 메모리 보호기법에 대해 드림핵을 기반으로 작성하였다.
요약
- return addr을 임의의 주소로 덮기 가능 → canary 도입 ⇒ canary leak 으로 우회 가능
- 버퍼의 주소 알아내기 가능 → ASLR 도입
- 버퍼안의 쉘코드 실행 가능 → NX-bit 도입
ASLR ( Address Space Layout Randomization)
바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호기법이다. ASLR이 꺼져있으면 다음과 같이 매 실행마다 buf의 주소가 같다.
ASLR on/off 확인
$ cat /proc/sys/kernel/randomize_va_space
2
cat /proc/sys/kernel/randomize_va_space 명령어를 입력했을 때 0, 1, 2의 값이 출력될 수 있다. 값에 따라 적용되는 영역은 다음과 같다.
- 0 : No ASLR = ASLR을 적용하지 않음
- 1 : Conservative Randomization = 스택, 힙, 라이브러리, vdso 등에 ASLR 적용
- 2 : Conservative Randomization + brk = 스택, 힙, 라이브러리, vdso 등의 영역 + brk로 할당한 영역
아래의 해당 값을 바꿔 ASLR 단계 및 on/off를 조정한다.
$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
$ cat /proc/sys/kernel/randomize_va_space
0
ASLR 단계에 따른 주소 확인
단계에 따라 스택, 힙, 라이브러리, 라이브러리 매핑 주소, 코드 영역의 주소가 랜덤한지 아닌지 확인해보자. 예제코드는 아래와 같다.
// 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); // 코드 영역의 함수 주소
}
0 - ASLR off
매 실행마다 모든 주소가 동일한 것을 확인할 수 있다.
1 - ASLR 스택, 힙, 라이브러리, vdso에 적용
매 실행마다 buf의 주소, 라이브러리 주소, 라이브러리 매핑 주소가 달라진다. 이때 힙영역의 주소는 동일한데 이는 malloc이 호출될 때 상황에 따라 아래와 같이 분기하는데 brk로 syscall을 호출해 메모리를 확보했기 때문(brk영역은 ASLR값이 2일때 적용됨)으로 보인다. 자세한 내용은 다음에.. 정리하겠다..
2 - ASLR 1 + brk
스택 영역의 buf_stack
, 힙 영역의 buf_heap
, 라이브러리 함수의 printf
, 코드 영역의 함수 main
, 라이브러리 매핑 주소의 libc_base
를 보면 특징이 있다.
- 코드 영역(main)을 제외한 다른 영역의 주소는 매번 바뀐다.
- 변수나 함수, 라이브러리의 주소는 매번 바뀌기 때문에 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없다.
- libc_base, printf의 하위 12비트(비트임 바이트 아님)값은 변경되지 않는다
- 리눅스는 파일을 페이지(page) 단위로 임의 주소로 mapping
- 페이지 크기인 12비트이하의 주소는 바뀌지 않는다.
- 페이지는 일반적으로 4KB로 4KB = 2^12이므로 하위 12비트는 바뀌지 않는 것이다.
- libc_base와 printf의 주소 차이는 항상 동일
- 라이브러리의 시작주소부터 다른 심볼들까지의 거리(offset)은 항상 동일
- libc_base + printf offset(0x64f00) = printf addr
NX - No eXecute
코드 실행 가능한 메모리 영역과 쓰기 가능한 메모리 영역을 분리하는 보호기법이다. 코드 영역에 쓰기 권한이 있으면 코드를 수정해 원하는 코드를 실행할 수 있고, 스택이나 데이터 영역에 실행 권한이 있으면 입력으로 쉘코드를 주입해 쉘을 딸 수 있다.
NX bit는 컴파일러 옵션(-zexecstack : 스택에 실행권한 부여)을 통해 바이너리에 적용할 수 있고, NX bit가 적용된 바이너리는 각 메모리 영역에 필요한 권한만 부여받을 수 있다. gdb의 vmmap으로 NX bit 적용 전 후 메모리 맵을 비교하면, NX bit 적용된 바이너리는 코드영역 외에 실행권한이 없는 것을 알 수 있다. NX bit 적용되지 않은 바이너리는 스택, 힙, 데이터 영역에도 실행 권한이 존재한다.
'CS > system' 카테고리의 다른 글
[System] PLT & GOT (0) | 2022.11.28 |
---|---|
[System] library - Static Link vs Dynamic Link (1) | 2022.11.26 |
[System][Dreamhack] Exploit tech : Return to Shellcode (0) | 2022.09.06 |
[System][Dreamhack] wargame - ssp_001 스택 순서 (0) | 2022.08.24 |
[System][Dreamhack] bypass canary (0) | 2022.08.10 |