[Pwn] Pwnable · 작성: 2026-07-19 19:31:03 · 수정: 2026-07-21 23:11:54 · 조회 23
ASLR(Address Space Layout Randomization)을 "주소를 랜덤화하는 보호 기법"이라고만 알면, 그 랜덤화가 정확히 커널의 어느 단계에서 이뤄지는지, 그리고 왜 정보 유출 취약점 단 하나가 이 보호 전체를 무의미하게 만드는지를 놓치게 된다.
프로세스가 execve()로 새로 실행될 때, 리눅스 커널의 로더(binfmt_elf.c의 ELF 로딩 경로)는 스택·힙(정확히는 brk/mmap 영역)·공유 라이브러리(mmap으로 매핑되는 libc 등)·(PIE인 경우) 실행 파일 자신의 베이스 주소를 결정한다. ASLR이 켜져 있으면(/proc/sys/kernel/randomize_va_space), 커널은 이 각 영역의 시작 주소에 get_random_bytes()로 얻은 커널 난수를 이용해 계산한 오프셋을 더해서 매 실행마다 다른 위치에 배치한다. 중요한 건 **"완전히 무작위"가 아니라 "정렬 제약과 엔트로피 비트 수 안에서의 무작위"**라는 점이다 — x86-64 리눅스에서 힙/스택/mmap 영역의 랜덤화 엔트로피는 각각 수십 비트 수준으로, 이론상 무차별 대입은 비현실적이지만 **"주소 전체를 다시 계산할 필요 없이, 베이스 주소 하나만 알면 나머지 오프셋은 고정된 값을 더하기만 하면 된다"**는 성질이 그대로 남는다 — 바이너리 내부 함수 간 상대 거리, libc 내부 함수 간 상대 거리는 ASLR과 무관하게 항상 고정되어 있기 때문이다.
libc가 로드되는 시작 주소(베이스)가 랜덤화되어도, system() 함수가 그 베이스로부터 몇 바이트 떨어져 있는지, "/bin/sh" 문자열이 몇 바이트 떨어져 있는지는 같은 libc 바이너리라면 항상 동일하다. 그래서 공격자가 **libc 안의 아무 함수 주소 하나(예: printf의 GOT 엔트리에 이미 채워진 실제 런타임 주소)**만 유출시키면, 거기서 고정 오프셋을 빼서 libc 베이스 주소를 역산하고, 그 베이스에 system의 고정 오프셋을 더해 정확한 system() 주소를 계산해낼 수 있다. ASLR은 "베이스 주소"라는 단 하나의 값만 감추고 있을 뿐이고, 그 값 하나가 새어나가는 순간 그 프로세스의 나머지 모든 주소 계산이 결정론적으로 풀린다는 게 이 보호 기법의 근본적인 한계다.
system/execve, 그리고 "/bin/sh" 문자열의 오프셋을 각각 더해 정확한 절대 주소 확보pop rdi; ret gadget과 조합해 리턴 주소를 이 계산된 주소로 덮어씀 → ASLR이 켜져 있어도 정확히 원하는 함수를 호출PIE는 "바이너리 자기 자신"에도 ASLR을 적용하는 컴파일 옵션이다. PIE가 없으면 실행 파일 자신의 코드/데이터 주소는 고정이라, 바이너리 안의 gadget이나 win() 같은 함수는 ASLR과 무관하게 항상 같은 주소에 있어서 익스플로잇이 훨씬 쉬워진다. PIE가 걸려있으면 이 주소들도 매번 바뀌므로, 바이너리 자신의 주소 하나도 별도로 유출시켜야 한다 — checksec 결과에서 PIE 여부를 확인하는 이유가 여기 있다.
ASLR을 "주소를 숨기는 기법"이 아니라 **"베이스 주소라는 단 하나의 비밀에 나머지 모든 계산이 의존하도록 만든 구조"**로 이해하면, 왜 실전 익스플로잇에서 "메모리 손상 취약점 하나"보다 오히려 "정보 유출 취약점 하나"의 가치가 더 높게 평가되는 경우가 많은지가 같은 논리로 설명된다.