Position-Independent Executable(PIE)은 ASLR이 코드 영역에도 적용되게 해주는 기술입니다.
이 기술은 보안성을 향상을 위해 도입된 것이 아니라서 엄밀하게 보호 기법은 아닙니다. 그러나 ASLR과 맞물려서 공격을 더욱 어렵게 만들었기에 여러 글에서 보호기법이라고 소개되기도 합니다.
PIC
리눅스에서 ELF는 실행 파일(Executable)과 공유 오브젝트(Shared Object, SO)로 두 가지가 존재합니다. 실행 파일은 addr처럼 일반적인 실행 파일이 해당하고, 공유 오브젝트는 libc.so와 같은 라이브러리 파일이 해당합니다.
공유 오브젝트는 기본적으로 재배치(Relocation)가 가능하도록 설계되어 있습니다. 재배치가 가능하다는 것은 메모리의 어느 주소에 적재되어도 코드의 의미가 훼손되지 않음을 의미하는데, 컴퓨터 과학에서는 이런 성질을 만족하는 코드를 Position-Independent Code(PIC)라고 부릅니다.
// Name: pic.c
// Compile: gcc -o pic pic.c
// : gcc -o no_pic pic.c -fno-pic -no-pie
#include <stdio.h>
char *data = "Hello World!";
int main() {
printf("%s", data);
return 0;
}
gcc는 PIC 컴파일을 지원합니다. PIC가 적용된 바이너리와 그렇지 않은 바이너리를 비교하기 위해 위 코드를 사용하겠습니다.
no_pic 와 pic 의 main 함수를 비교하면 main+14에서 "%s" 문자열을 printf에 전달하는 방식이 다릅니다.
no_pic 경우 0x4005a1라는 절대 주소로 문자열을 참조하고 있습니다. 반면 pic 문자열의 주소를 rip+0xa2로 참조하고 있습니다.
바이너리가 매핑되는 주소가 바뀌면 0x4005a1에 있던 데이터도 함께 이동하므로 no_pic의 코드는 제대로 실행되지 못합니다. 그러나 pic의 코드는 rip를 기준으로 데이터를 상대 참조하기 때문에 바이너리가 무작위 주소에 매핑돼도 제대로 실행될 수 있습니다.
참고로 pic 에서 저 작은 주소들은 offset 개념으로 봐도 무방하다.
주소값이 바뀌는 부분은 offset 바이트를 제외한 base 부분이며, 저기 할당된 작은 주소값은 고정적이다.
예를 들어, 첫 번째 실행했을 때 [그림 1-3] 인스트럭션 주소 값이 [0x555555555511165a] 였다면
두 번째 실행했을 때는 [0x5555555533365a]로 할당된 0x65a는 고정적이고
변하는 부분은 [0x55555555111],[0x55555555333] 이 base 부분입니다.
PIE
PIE는 무작위 주소에 매핑돼도 실행 가능한 실행 파일을 뜻합니다. ASLR이 도입되기 전에는 실행 파일을 무작위 주소에 매핑할 필요가 없었습니다. 그래서 리눅스의 실행 파일 형식은 재배치를 고려하지 않고 설계되었습니다. 이후에 ASLR이 도입되었을 때는 실행 파일도 무작위 주소에 매핑될 수 있게 하고 싶었으나, 이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가 발생할 것이 분명했습니다. 그래서 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행 파일로 사용하기로 했습니다.
pic 는 공유 오브젝트 기반 실행 파일이고(재배치 o), no_pic는 실행 파일입니다.(재배치 x)
현대 gcc는 PIE를 기본적으로 적용하므로 모든 옵션을 제거하면 PIE가 적용된 바이너리로 컴파일됩니다.
PIE는 재배치가 가능하므로, ASLR이 적용된 시스템에서는 실행 파일도 무작위 주소에 적재됩니다.
PIE 우회
1. 코드 베이스 구하기
ASLR환경에서 PIE가 적용된 바이너리는 실행될 때 마다 다른 주소에 적재됩니다. 그래서 코드 영역의 가젯을 사용하거나, 데이터 영역에 접근하려면 바이너리가 적재된 주소를 알아야 합니다. 이 주소를 PIE 베이스, 또는 코드 베이스라고 부릅니다. 코드 베이스를 구하려면 라이브러리의 베이스를 구할 때처럼 코드 영역의 임의의 주소를 알고, 그 주소에서 오프셋을 빼야 합니다.
2. Partial Overwrite
코드 베이스를 구하기 어렵다면 반환 주소의 일부 바이트만 덮는 공격을 고려해 볼 수도 있습니다. 이러한 공격 기법을 Partial Overwrite라고 부릅니다. 일반적으로 함수의 반환 주소는 호출 함수(Caller)의 내부를 가리킵니다. 특정 함수의 호출 관계는 정적 분석 또는 동적 분석으로 쉽게 확인할 수 있으므로, 공격자는 반환 주소를 예측할 수 있습니다.
ASLR의 특성상,코드 영역의 주소도 하위 12비트 값은 항상 같습니다. 따라서 사용하려는 코드 가젯의 주소가 반환 주소와 하위 한 바이트만 다르다면, 이 값만 덮어서 원하는 코드를 실행시킬 수 있습니다. 그러나 만약 두 바이트 이상이 다른 주소로 실행 흐름을 옮기고자 한다면, ASLR로 뒤섞이는 주소를 맞춰야 하므로 브루트 포싱이 필요하며, 공격이 확률에 따라 성공하게 됩니다.
마치며
이번 코스에서는 Position Independent Executable(PIE)에 대해 배웠습니다. PIE는 자체적으로는 보호 기법이 아니지만 ASLR과 맞물려서 시스템을 한 층 더 안전하게 보호하는 효과가 있습니다. 이를 우회하려면 코드 베이스를 구하거나, Partial Overwrite을 시도해 볼 수 있습니다.
PIE가 적용된 바이너리는 → 공유 오브젝트 파일 기반 실행 파일 → 재배치가 가능하게 코드가 PIC로 되어있다.→상대주소
PIE가 적용 안된 바이너리는 → 실행 파일 → 재배치가 안됨 → 절대 주소
'Dreamhack - pwnable' 카테고리의 다른 글
Hook Overwrite (2) (0) | 2023.01.19 |
---|---|
_hook Overwrite (0) | 2023.01.18 |
RELRO (RELocation Read-Only) (0) | 2023.01.16 |
basic_rop_x86 (ret2main + bss 쓰기) (0) | 2023.01.15 |
basic_rop_x86 (GOT Overwrite) (0) | 2023.01.15 |