[ Bypass PIE & RELRO ]

PIE

: ASLR이 코드 영역에도 적용되게 해주는 기술

 

  이 기술은 보안성 향상을 위해 도입된 것이 아니라서 엄밀하게는 보호 기법이 아니다. 그러나 실제로는 ASLR과 맞물려서 공격을 더욱 어렵게 만들었기에 여러 글이나 발표에서 보호 기법이라고 소개되기도 한다.

 

▶ PIC

  리눅스에서 ELF는 실행 파일(Executable)과 공유 오브젝트(Shared Object, SO)로 두 가지가 존재한다. 실행 파일은 addr처럼 일반적인 실행 파일이 해당하고, 공유 오브젝트는 libc.so와 같은 라이브러리 파일이 해당한다.

  공유 오브젝트는 기본적으로 재배치(Relocation)가 가능하도록 설계되어 있다. 재배치가 가능하다는 것은 메모리의 어느 주소에 적재되어도 코드의 의미가 훼손되지 않음을 의미하는데, 컴퓨터 과학에서는 이런 성질을 만족하는 코드를 Position-Independent Code(PIC)라고 부른다.

 

  gcc는 PIC 컴파일을 지원한다. 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;
}

 

▶ PIC 코드 분석

  no_pic와 pic의 main 함수를 비교해보면, main+14에서 “%s” 문자열을 printf에 전달하는 방식이 조금 다르다. no_pic에서는 0x4005a1라는 절대 주소로 문자열을 참조하고 있다. 반면 pic는 문자열의 주소를 rip+0xa2로 참조하고 있다.

  바이너리가 매핑되는 주소가 바뀌면 0x4005a1에 있던 데이터도 함께 이동하므로 no_pic의 코드는 제대로 실행되지 못한다. 그러나 pic의 코드는 rip를 기준으로 데이터를 상대 참조(Relative Addressing)하기 때문에 바이너리가 무작위 주소에 매핑돼도 제대로 실행될 수 있다.

 

 

▶ PIE(Position-Independent Executable)

: 무작위 주소에 매핑돼도 실행 가능한 실행 파일

 

  ASLR이 도입되기 전에는 실행 파일을 무작위 주소에 매핑할 필요가 없었다. 그래서 리눅스의 실행 파일 형식은 재배치를 고려하지 않고 설계되었다. 이후에 ASLR이 도입되었을 때는 실행 파일도 무작위 주소에 매핑될 수 있게 하고 싶었으나, 이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가 발생할 것이 분명했다. 그래서 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행 파일로 사용하기로 했다.

  실제로 리눅스의 기본 실행 파일 중 하나인 /bin/ls는 공유 오브젝트 형식을 띄고 있다.

 

▶ PIE on ASLR

  PIE는 재배치가 가능하므로, ASLR이 적용된 시스템에서는 실행 파일도 무작위 주소에 적재된다. Mitigation: ASLR&NX에서 사용한 예제를 이번에는 PIE를 적용하여 컴파일하고 실행 결과를 확인해보자. 현대의 gcc는 PIE를 기본적으로 적용하므로 모든 옵션을 제거하면 PIE가 적용된 바이너리로 컴파일된다.

 

PIE가 적용되자 main함수의 주소가 매 실행마다 바뀌고 있음을 알 수 있다.

 

▶ PIE 우회

- 코드 베이스 구하기

  ASLR환경에서 PIE가 적용된 바이너리는 실행될 때 마다 다른 주소에 적재된다. 그래서 코드 영역의 가젯을 사용하거나, 데이터 영역에 접근하려면 바이너리가 적재된 주소를 알아야 한다. 이 주소를 PIE 베이스, 또는 코드 베이스라고 부른다. 코드 베이스를 구하려면 라이브러리의 베이스 주소를 구할 때 처럼 코드 영역의 임의 주소를 읽고, 그 주소에서 오프셋을 빼야합니다. 

 

- Partial Overwrite

  코드 베이스를 구하기 어렵다면 반환 주소의 일부 바이트만 덮는 공격을 고려해볼 수도 있다. 이러한 공격 기법을 Partial Overwrite라고 부른다. 일반적으로 함수의 반환 주소는 호출 함수(Caller)의 내부를 가리킨다. 특정 함수의 호출 관계는 정적 분석 또는 동적 분석으로 쉽게 확인할 수 있으므로, 공격자는 반환 주소를 예측할 수 있다.

  ASLR의 특성 상, 코드 영역의 주소도 하위 12비트 값은 항상 같다. 따라서 사용하려는 코드 가젯의 주소가 반환 주소와 하위 한 바이트만 다르다면, 이 값만 덮어서 원하는 코드를 실행시킬 수 있다. 그러나 만약 두 바이트 이상이 다른 주소로 실행 흐름을 옮기고자 한다면, ASLR로 뒤섞이는 주소를 맞춰야 하므로 브루트 포싱이 필요하며, 공격이 확률에 따라 성공하게 된다.


RELRO

  ELF는 GOT를 활용하여 반복되는 라이브러리 함수의 호출 비용을 줄인다. GOT에 값을 채우는 방식은 다양하다. Lazy Binding은 함수가 처음 호출될 때 함수의 주소를 구하고, 이를 GOT에 적는 방법이다.

  Lazy binding을 하는 바이너리는 실행 중에 GOT 테이블을 업데이트할 수 있어야 하므로 GOT에 쓰기 권한이 부여된다. 그런데 이는 앞서 소개한 공격 기법들에서 알 수 있듯, 바이너리를 취약하게 만드는 원인이 된다.

  또한, ELF의 데이터 세그먼트에는 프로세스의 초기화 및 종료와 관련된 .init_array, .fini_array가 있다. 이 영역들은 프로세스의 시작과 종료에 실행할 함수들의 주소를 저장하고 있는데, 여기에 공격자가 임의로 값을 쓸 수 있다면, 프로세스의 실행 흐름이 조작될 수 있다.

  리눅스 개발자들은 이러한 문제를 해결하고자 프로세스의 데이터 세그먼트를 보호하는 RELocation Read-Only(RELRO)을 개발했다. RELRO는 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한을 제거한다.

  RELRO는 RELRO를 적용하는 범위에 따라 두 가지로 구분된다. 하나는 RELRO를 부분적으로 적용하는 Partial RELRO이고, 나머지는 가장 넓은 영역에 RELRO를 적용하는 Full RELRO이다.

▶ Partial RELRO

// Name: relro.c
// Compile: gcc -o prelro relro.c -no-pie -fno-PIE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
  FILE *fp;
  char ch;
  fp = fopen("/proc/self/maps", "r");
  while (1) {
    ch = fgetc(fp);
    if (ch == EOF) break;
    putchar(ch);
  }
  return 0;
}

 

- RELRO 검사

  실습 환경의 gcc는 Full RELRO를 기본 적용하며, PIE를 해제하면 Partial RELRO를 적용한다. 바이너리의 RELRO 여부도 checksec으로 검사할 수 있다.

 

▶ Partial RELRO 권한

  prelro를 실행해보면 0x601000부터 0x602000까지의 주소에는 쓰기 권한이 있는 것을 확인할 수 있다. 섹션 헤더를 참조해보면 해당 영역에는 .got.plt, .data, .bss가 할당되어 있다. 따라서 이 섹션들에는 쓰기가 가능하다.

  반면, .init_array와 .fini_array는 각각 0x600e10과 0x600e18에 할당되어 쓰기가 불가능하다.

 

※ .got와 .got.plt

  Partial RELRO가 적용된 바이너리는 got와 관련된 섹션이 .got와 .got.plt로 두 개가 존재한다. 전역 변수 중에서 실행되는 시점에 바인딩(now binding)되는 변수는 .got에 위치한다. 바이너리가 실행될 때는 이미 바인딩이 완료되어있으므로 이 영역에 쓰기 권한을 부여하지 않다.

  반면 실행 중에 바인딩(lazy binding)되는 변수는 .got.plt에 위치한다. 이 영역은 실행 중에 값이 써져야 하므로 쓰기 권한이 부여된다. Partial RELRO가 적용된 바이너리에서 대부분 함수들의 GOT 엔트리는 .got.plt에 저장된다.

 

▶ Full RELRO

  frelro를 실행하여 메모리 맵을 확인하고, 이를 섹션 헤더 정보와 종합해보면 got에는 쓰기 권한이 제거되어 있으며 data와 bss에만 쓰기 권한이 있다. Full RELRO가 적용되면 라이브러리 함수들의 주소가 바이너리의 로딩 시점에 모두 바인딩된다. 따라서 GOT에는 쓰기 권한이 부여되지 않는다.

 

▶ RELRO 기법 우회

  Partial RELRO의 경우, .init_array와 .fini_array에 대한 쓰기 권한이 제거되어 두 영역을 덮어쓰는 공격을 수행하기 어려워진다. 하지만, .got.plt 영역에 대한 쓰기 권한이 존재하므로 GOT overwrite 공격을 활용할 수 있다.

  Full RELRO의 경우, .init_array, .fini_array 뿐만 아니라 .got 영역에도 쓰기 권한이 제거된다. 그래서 공격자들은 덮어쓸 수 있는 다른 함수 포인터를 찾다가 라이브러리에 위치한 hook을 찾아냈다. 라이브러리 함수의 대표적인 hook이 malloc hook과 free hook이다. 원래 이 함수 포인터는 동적 메모리의 할당과 해제 과정에서 발생하는 버그를 디버깅하기 쉽게 하려고 만들어졌다.

  malloc 함수의 코드를 살펴보면, 함수의 시작 부분에서 __malloc_hook이 존재하는지 검사하고, 존재하면 이를 호출한다. __malloc_hook은 libc.so에서 쓰기 가능한 영역에 위치한다. 따라서 공격자는 libc가 매핑된 주소를 알 때, 이 변수를 조작하고 malloc을 호출하여 실행 흐름을 조작할 수 있다. 이와 같은 공격 기법을 통틀어 Hook Overwrite라고 부른다.

 

<요약>

RELocation Read-Only(RELRO): 불필요한 데이터 영역에 쓰기 권한을 제거함.

Partial RELRO: init array, fini array 등 여러 섹션에 쓰기 권한을 제거함. Lazy binding을 사용하므로 라이브러리 함수들의 GOT 엔트리는 쓰기가 가능함. GOT Overwrite등의 공격으로 우회가 가능함.

Full RELRO: init array, fini array 뿐만 아니라 GOT에도 쓰기 권한을 제거함. Lazy binding을 사용하지 않으며 라이브러리 함수들의 주소는 바이너리가 로드되는 시점에 바인딩됨. libc의 malloc hook, free hook과 같은 함수 포인터를 조작하는 공격으로 우회할 수 있음.

+ Recent posts