System Hacking/인터루드 스터디

260528 PIE, RELRO 개념정리

daaaay 2026. 5. 28. 18:15

1. PIE (Position-Independent Executable)

PIE는 바이너리(실행 파일)가 메모리의 무작위 주소에 매핑되어도 정상적으로 실행될 수 있도록 만들어주는 보호 기법이다.

ASLR이 메모리 영역을 섞을 때, 코드 영역(바이너리 자체)까지 랜덤화할 수 있도록 지원하는 핵심 기술이다.

 

ELF 파일 포맷과 재배치(Relocation)

리눅스의 실행 파일 형식인 ELF는 크게 두 가지 형태로 존재한다.

  1. Executable (실행 파일): 주소가 고정되어 있어 재배치가 불가능하다.
  2. Shared Object (SO, 공유 오브젝트/라이브러리): 메모리의 어느 주소에 적재되어도 코드의 의미가 보존되도록 설계되어 재배치가 가능하다.

PIE가 적용된 바이너리는 내부적으로 Executable이 아닌 SO(Shared Object) 형태로 컴파일된다. 즉, 실행 파일 자체를 라이브러리처럼 재배치 가능한 상태로 만드는 것이다.

PIC (Position-Independent Code)와 상대 주소

PIE가 임의의 주소에서 실행될 수 있는 이유는 내부 코드가 PIC(위치 독립 코드)로 작성되었기 때문이다.

  • 절대 주소 방식 (PIE 미적용): "메모리 0x400667번지로 이동해라." (주소가 고정되어야만 작동함)
  • 상대 주소 방식 (PIE 적용): "현재 위치(RIP)로부터 +0x200만큼 떨어진 곳으로 이동해라." (어디에 매핑되든 상대적인 거리만 알면 작동함)

ASLR + PIE 적용 시의 메모리 변화

PIE 자체는 '재배치가 가능한 상태'를 만들 뿐이며, 실제로 매번 주소를 무작위로 섞는 것은 ASLR의 역할이다. 따라서 PIE는 반드시 ASLR과 함께 적용되어야 의미가 있다.

  • ASLR만 적용 시: 스택, 힙, 라이브러리 주소는 바뀌지만 main 함수(코드 영역)의 주소는 항상 0x400667 등으로 고정된다.
  • ASLR + PIE 적용 시: 코드 영역까지 무작위 주소에 매핑되므로, main 함수의 주소를 포함한 모든 메모리 주소가 실행할 때마다 완전히 바뀐다.

PIE 우회 기법

  1. 베이스 주소 유출 (Base Leak):
    바이너리 매핑 주소(Code Base) + 고정된 Offset = 실제 함수의 메모리 주소
    따라서 포맷 스트링 버그(FSB) 등을 통해 메모리에 적재된 코드 영역의 특정 주소 하나를 유출(Leak)하면, 오프셋 연산을 통해 베이스 주소를 구하고 원하는 모든 함수의 주소를 알아낼 수 있다.
    PIE 환경에서도 함수들 간의 오프셋(거리)은 변하지 않는다.
  2. 부분 덮어쓰기 (Partial Overwrite):
    주소가 랜덤화되더라도 페이지 매핑의 특성상 하위 12bit(3자리)는 변하지 않는다. 이를 이용해 리턴 주소의 하위 1~2바이트만 원하는 함수의 주소로 덮어써서 실행 흐름을 확률적으로 조작하는 기법이다.

2. RELRO (RELocation Read-Only)

RELRO는 프로세스의 데이터 세그먼트(특히 함수 주소가 저장되는 GOT 영역 등)를 보호하기 위해 메모리의 쓰기 권한을 엄격하게 통제하는 기법이다. 바인딩(Binding) 시점에 따라 Partial RELRO와 Full RELRO로 나뉜다.

바인딩(Binding)의 두 가지 방식

이해를 위해 함수 주소를 탐색하고 GOT에 저장하는 바인딩 방식을 먼저 구분해야 한다.

  • Lazy Binding (지연 바인딩): 프로그램 실행 중에 외부 함수가 처음 호출될 때 주소를 탐색하고 GOT에 기록한다.
  • Now Binding (즉시 바인딩): 프로그램이 시작될 때 필요한 모든 외부 함수의 주소를 한 번에 다 탐색해서 GOT에 기록해 둔다.

적용 범위에 따른 분류

1. RELRO 미적용 (No RELRO)

  • Lazy Binding을 사용하므로 프로세스 실행 중에 GOT를 지속적으로 업데이트해야 한다. (GOT 영역에 쓰기 권한 존재)
  • 프로세스 시작과 종료 시 실행되는 함수 주소가 담긴 .init_array 및 .fini_array 영역에도 쓰기 권한이 있다.
  • 공격: GOT Overwrite 및 .init_array Overwrite가 모두 가능하다.

2. Partial RELRO (부분 적용)

  • .init_array와 .fini_array 섹션을 읽기 전용(Read-Only)으로 변경하여 보호한다.
  • 그러나 여전히 Lazy Binding을 사용하기 때문에 GOT 영역은 두 개로 분리된다.
    • .got: Now Binding되는 일부 변수들이 저장됨. (쓰기 권한 X)
    • .got.plt: Lazy Binding되는 핵심 라이브러리 함수들이 저장됨. 계속 업데이트해야 하므로 (쓰기 권한 O)
  • 공격 (우회): .got.plt 영역에는 여전히 쓰기 권한이 있으므로, 이전에 실습했던 GOT Overwrite 기법이 그대로 통한다.

3. Full RELRO (완전 적용)

  • 프로세스 시작 시 강제로 모든 함수를 Now Binding 처리한다.
  • 실행 전에 모든 주소를 기록해 버렸으므로, 프로그램 실행 중에는 더 이상 GOT를 업데이트할 필요가 없다.
  • 따라서 .got.plt 섹션 자체가 아예 생성되지 않으며, 오직 .got 섹션만 존재하고 전체 GOT 영역에 쓰기 권한이 제거(Read-Only)된다.
  • 오직 순수 데이터 영역인 .data와 .bss에만 쓰기가 가능하다.
  • 공격 (우회): GOT 영역 자체를 수정할 수 없으므로 GOT Overwrite가 원천 차단된다. 대신 라이브러리(libc) 내부에 존재하는 __malloc_hook, __free_hook 같은 쓰기 가능한 함수 포인터를 변조하는 Hook Overwrite 기법을 사용해야 한다.

3. 메모리 보호 기법 요약 

시스템 해킹 방어를 위해 현대 바이너리에 적용되는 4대 핵심 보호 기법이다. 

  • NX (No-eXecute): 데이터 영역(스택, 힙)에서 기계어를 실행하지 못하게 막는다. (스택 실행 권한 X)
  • ASLR (Address Space Layout Randomization): 스택, 힙, 라이브러리의 주소를 실행할 때마다 섞는다. (주소 무작위화)
  • PIE (Position-Independent Executable): ASLR과 결합하여 코드 영역(바이너리 자체)의 주소까지 섞는다. (코드 베이스 무작위화 및 상대 주소 사용)
  • RELRO (RELocation Read-Only): * Partial: .init_array 등을 보호하지만 GOT 덮어쓰기는 가능하다.
    • Full: 전체 GOT 영역을 읽기 전용으로 만들어 GOT 덮어쓰기를 원천 차단한다.