ASLR이 적용되면 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등이 무작위 주소에 매핑되므로, 공격자가 이 영역들을 공격에 활용하기 어려워진다. 그런데 지난 강의의 실습 addr을 떠올려보면, 다른 영역의 주소는 계속 바뀌었지만 main 함수의 주소는 매번 같았다. 이런 특징을 이용하여 공격자는 고정된 주소의 코드 가젯을 활용한 ROP를 수행할 수 있었다.
이번 강의에서 배울 Position-Independent Executable(PIE)은 ASLR이 코드 영역에도 적용되게 해주는 기술이다. 이 기술은 보안성 향상을 위해 도입이 된 것이 아니라서 엄밀히 말하자면 보호기법은 아니다. 그러나 실제로는 ASLR과 맞물려서 공격을 더욱 어렵게 만들었기에 여러 글이나 발표에서 보호기법이라고 소개되기도 한다. 드림핵에서는 이를 보호기법이 아니라 하나의 기술로 소개한다.
아래 결과는 Mitigation: NX&ASLR 강읭서 사용한 예제 코드의 실행 결과이다.
$ gcc addr.c -o addr -ldl -no-pie -fno-PIE
$ ./addr
buf_stack addr: 0x7ffffd430560
buf_heap addr: 0x7d0010
printf addr: 0x7f6a241ee810
main addr: 0x400736
$ ./addr
buf_stack addr: 0x7fff22f2c2a0
buf_heap addr: 0x2126010
printf addr: 0x7f1efab79810
main addr: 0x400736

** PIC와 PIE
PIC
리눅스에서 ELF는 실행파일(Executable)과 공유 오브젝트(Shared Object, SO)로 두 가지가 존재한다. 실행파일은 addr 처럼 일반적인 실행파일이 해당하고, 공유 오브젝트는 libc.so와 같은 라이브러리 파일이 해당한다.
공유 오브젝트는 기본적으로 재배치(relocation)가 가능하도록 설계되어있다. 재배치가 가능하다는 것은 메모리의 어느 주소에 적재되어도 코드의 의미가 훼손되지 않음을 의미하는데, 컴퓨터 과학에서는 이런 성질을 만족하는 코드를 Position-Independent Code(PIC)라고 한다.
$ file addr
addr: ELF 64-bit LSB executable
$ file /lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object
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+33에서 "%s" 문자열을 printf에 전달하는 방식이 조금 다르다. no_pic에서는 0x4005a1라는 절대 주소로 문자열을 참조하고 있다. 반면 pic는 문자열의 주소를 rip+0xa2로 참조하고 있다.
$ gdb ./no_pic
pwndbg> x/s 0x4005a1
0x4005a1: "%s"
근데 내 실습 창에서는 0x402011에서 참조하고 있다.


$ gdb ./pic
pwndbg> x/s 0x711
0x711: "%s"


얘도 내 실습창에선 rip+0xeaf로 참조하고 있음
바이너리가 매핑되는 주소가 매뀌면 0x4005a1(내 pc: 0x402011)에 있던 데이터도 함께 이동하므로 no_pic의 코드는 제대로 실행되지 못한다. 그러나 pic의 코드는 rip를 기준으로 데이터를 상대참조(Relative Addressing)하기 때문에 바이너리가무작위 주소에 매핑돼도 제대로 실행할 수 있다.
push rbp
mov rbp,rsp
-mov rax,QWORD PTR [rip+0x200b3e] # 0x601030 <data>
+mov rax,QWORD PTR [rip+0x2009ab] # 0x201010 <data>
mov rsi,rax
-mov edi,0x4005a1
+lea rdi,[rip+0xa2] # 0x711
mov eax,0x0
-call 0x4003f0 <printf@plt>
+call 0x530 <printf@plt>
mov eax,0x0
pop rbp
ret
PIE
Position-Independent Executable(PIE)은 무작위 주소에 매핑돼도 실행 가능한 실행파일을 뜻한다. ASLR이 도입되기 전에는 실행 파일을 무작위 주소에 매핑할 필요가 없었다. 그래서 리눅스의 실행파일 형식은 재배치를 고려하지 않고 설계되었다. 이후에 ASLR이 도입되었을 때는 실행 파일도 무작위 주소에 매핑될 수 있게하고싶었으나, 이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가 발생할 것이 분명했다. 그래서 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행파일로 사용하기로 했다.
실제로 리눅스의 기본 실행 파일 중 하나인 /bin/ls의 파일 헤더를 살펴보면, Type이 공유 오브젝트(Shared Object)를 나타내는 DYN(ET_DYN)임을 알 수 있다.
$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x6ab0
Start of program headers: 64 (bytes into file)
Start of section headers: 136224 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30

PIE on ASLR
PIE는 재배치가 가능하므로, ASLR이 적용된 시스템에서는 실행 파일도 무작위 주소에 적재된다. 반대로, ASLR이 적용되지 않은 시스템에서는 PIE가 적용된 바이너리라도 무작위 주소에 적재되지 않는다. Mitigation: ASLR&NX에서 사용한 예제를 이번에는 PIE를 적용하여 컴파일하고 실행 결과를 확인해본다. 현대의 gcc는 PIE를 기본적으로 적용하므로 모든 옵션을 제거하면 PIE가 적용된 바이너리로 컴파일된다.
PIE가 적용되자 main 함수의 주소가 매 실행마다 바뀌고 있다.
$ gcc -o pie addr.c -ldl
$ ./pie
buf_stack addr: 0x7ffc85ef37e0
buf_heap addr: 0x55617ffcb260
libc_base addr: 0x7f0989d06000
printf addr: 0x7f0989d6af00
main addr: 0x55617f1297ba
$ ./pie
buf_stack addr: 0x7ffe9088b1c0
buf_heap addr: 0x55e0a6116260
libc_base addr: 0x7f9172a7e000
printf addr: 0x7f9172ae2f00
main addr: 0x55e0a564a7ba
$ ./pie
buf_stack addr: 0x7ffec6da1fa0
buf_heap addr: 0x5590e4175260
libc_base addr: 0x7fdea61f2000
printf addr: 0x7fdea6256f00
main addr: 0x5590e1faf7ba
** PIE 우회
코드 베이스 구하기
ASLR 환경에서 PIE가 적용된 바이너리는 실행될 때마다 다른 주소에 적재된다. 그래서 코드 영역의 가젯을 사용하거나, 데이터 영역에 접근하려면 바이너리가 적재된 주소를 알아야한다. 이 주소를 PIE 베이스, 또는 코드 베이스라고 한다. 코드 베이스를 구하려면 라이브러리의 베이스 주소를 구할때 처럼 코드 영역의 임의 주소를 읽고, 그 주소에서 오프셋을 빼야한다. 이 과정은 Exploit Tech: Return Oriented Programming에서 라이브러리의 베이스 주소를 구하는 과정과 크게 다르지 않으므로 자세히 설명하진 않을 것이다.
Partial Overwrite
코드 베이스를 구하기 어렵다면 반환 주소의 일부 바이트만 덮는 공격을 고려해볼수도 있다. 이러한 공격 기법을 Partial Overwrite라고 부른다. 일반적으로 함수의 반환 주소는 호출 함수(Caller)의 내부를 가리킨다. 특정 함수의 호출 관계는 정적 분석 또는 동적 분석으로 쉽게 확인할 수 있으므로, 공격자는 반환 주소를 예측할 수 있다.
ASLR의 특성 상, 코드 영역의 주소도 하위 12비트 값은 항상 같다. 따라서 사용하려는 코드 가젯의 주소가 반환 주소와 하위 한 바이트만 다르다면, 이 값만 덮어서 원하는 코드를 실행할 수 있다. 그러나 만약 두 바이트 이상이 다른 주소로 실행 흐름을 옮기고자 한다면, ASLR로 뒤섞이는 주소를 맞춰야하므로 브루트 포싱이 필요하며, 공격이 확률에 따라 성공하게 된다.
마치며
마치며
이번 강의에서는 Position Independent Executable (PIE)에 대해 배웠습니다. PIE는 자체적으로는 보호 기법이 아니지만 ASLR과 맞물려서 시스템을 한 층 더 안전하게 보호하는 효과가 있습니다. 이를 우회하려면 코드 베이스를 구하거나, Partial Overwrite을 시도해볼 수 있습니다.
다음 강의에서는 Partial Overwrite을 일으킬 수 있는 Off by One 취약점에 대해 배워보겠습니다. 🚩
키워드
|
Q1. 다음은 file 명령어로 바이너리를 확인한 모습이다. PIE가 적용된 바이너리는 무엇인가?
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2.0, stripped
이유: PIE (Position Independent Executable) 바이너리는 파일 속성에서 "shared object"로 표시된다.
Q2. ASLR을 꺼도 PIE가 적용된 프로세스는 무작위 주소에 매핑된다.
X
이유: 반대로, ASLR이 적용되지 않은 시스템에서는 PIE가 적용된 바이너리라도 무작위 주소에 적재되지 않는다.
Q3. Shared Object는 실행할 수 있다.
O
이유: 이후에 ASLR이 도입되었을 때는 실행 파일도 무작위 주소에 매핑될 수 있게하고싶었으나, 이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가 발생할 것이 분명했다. 그래서 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행파일로 사용하기로 했다.
PIE가 적용된 경우, 실행파일처럼 동작한다.
'공부중 > 시스템 해킹' 카테고리의 다른 글
| [Dreamhack] Exploit Tech: Hook Overwrite (0) | 2024.09.27 |
|---|---|
| [Dreamhack] Background: RELRO / bypass: Hook Overwrite (0) | 2024.09.25 |
| [Dreamhack] Exploit Tech: ROP x64 (0) | 2024.09.22 |
| [Dreamhack] Exploit Tech: Return Oriented Programming (ROP) (0) | 2024.09.22 |
| [Dreamhack] Exploit Tech: Return to Library (0) | 2024.09.19 |
댓글