NX (No-eXecute, 실행 방지)
개념
메모리 영역을 실행과 쓰기로 분리하여 보안성을 높이는 보호 기법이다. 코드 영역에 쓰기 권한이 있으면 공격자는 코드를 수정하여 원하는 코드가 실행되도록 할 수 있고, 스택이나 데이터 영역에 실행권한이 있으면 셸코드를 해당 영역에 입력한 후 리턴 어드레스를 조작해 셸코드를 실행하도록 공격할 수도 있다.
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x50];
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
printf("[1] Leak the canary\n");
printf("Input: ");
fflush(stdout);
read(0, buf, 0x100);
printf("Your input is '%s'\n", buf);
puts("[2] Overwrite the return address");
printf("Input: ");
fflush(stdout);
gets(buf);
return 0;
}
-zexecstack: 스택에 실행 권한 부여하여 NX 우회
$ gcc -o r2s r2s.c -zexecstack
checksec 결과:
NX disabled, Has RWX segments
ASLR (Address Space Layout Randomization, 주소 랜덤화)
개념
바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소로 할당하는 보호 기법이다. 이를 통해 리턴 주소 예측 어렵게 하여 버퍼 오버플로우 같은 공격을 무력화 한다. 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); // 코드 영역의 함수 주소
}
NX + ASLR
NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며, 할당되는 주소가 계속 변한다. 하지만 바이너리 코드가 존재하는 영역은 여전히 실행권한이 존재하며 할당되는 주소도 고정되어 있다.
대표적인 공격 기법 : Return-to-Libc (RTL), Return Oriented Programming (ROP)
라이브러리
컴퓨터 시스템에서 프로그램들이 함수나 변수를 공유해 사용 가능하게 하는 구성 요소
- 대표 함수: printf, scanf, strlen, memcpy, malloc 등
- 종류: 동적 라이브러리 / 정적 라이브러리
링크
컴파일의 마지막 단계
- 컴파일 과정
- 소스코드 → Compiler → 어셈블리어
- 어셈블리어 → Assembler → 목적 파일(Object File)
- 목적 파일 + 라이브러리 → Linker → 실행 파일(./a.out)
동적 링크 (Dynamic Link)
- 실행 시 동적 라이브러리가 프로세스 메모리에 매핑됨
- 함수 호출 시, 매핑된 라이브러리에서 해당 함수 주소를 찾아 실행
- 중간 테이블: PLT(Procedure Linkage Table)
- 실제 함수 주소 저장: GOT(Global Offset Table)
- ex) puts 함수가 있을 경우 → PLT 주소(0x401040)를 먼저 호출
- 대부분의 환경에서 동적 링크 방식 사용
정적 링크 (Static Link)
- 필요한 함수들이 실행 파일에 직접 포함됨
- 함수 호출 시 외부 라이브러리 참조 없이 자신의 함수처럼 호출
- 장점: 탐색 비용 없음
- 단점: 여러 바이너리에 중복 포함되면 용량 낭비 발생
- ex) puts 함수가 0x40c140에 위치하면 해당 주소를 직접 호출함
PLT & GOT
라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블
바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑되고 이 상태에서 라이브러리 함수 호출 시, 함수의 이름을 바탕으로 라이브러리에서 심볼을 탐색함. 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 됨
이런 과정을 runtime resolve -> 동적 링크된 바이너리에서만 필요, runtime resolve를 하기 위한 코드는 plt에 존재
이때, 반복적으로 호출되는 함수의 정의를 매번 탐색하는 건 비효율적이라 ELF는 GOT라는 테이블을 두고 resolve된 함수의 주소를 해당 테이블에 저장하여 호출 시 사용