| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- Systemhacking #Pwnable #CTF #Write-up #WolvCTF
- dreamhack #rev-basic #reversing
- pointer
- 이너
- amd64 #x86-64 #x64 #레지스터 #컴퓨터 구조
- Webhacking #CTF #Write-up #WolvCTF
- 포인터_보고서
- Layer7 #해킹 #동아리 #시연회 #부엉이
- Today
- Total
stxxp
[Pwnable] Linux 메모리 보호 기법 정리 - PIE, Canary, ASLR, NX bit, RELRO 본문
◎ 목차 ◎
# Linux
1. PIE
> 동작, 필요성, 적용 실습, 한계
2. Canary
> 동작, 필요성, 적용 실습, 한계
3. ASLR
> 동작, 필요성, 적용 실습, 한계
4. NX bit
> 동작, 필요성, 적용 실습, 한계
5. RELRO
> 동작, 필요성, 적용 실습, 한계
# Linux
아래는 리눅스의 메모리 보호 기법이다.
1. PIE ( Position Independent Execution)
PIE 보호기법은 직관적으로 해석하면 "위치 독립 실행"을 의미한다. 이는 매번 실행할 때마다 실행 파일이 메모리의 랜덤한 주소에 로드되도록 하는 보호 기법이다. 이는 공격자가 파일의 특정 주소를 예측하기 어렵게 만든다.
▶ PIE 동작
(1) PIE가 활성화된 바이너리는 위치 독립적인 코드로 컴파일된다.
(2) 실행될 때마다 다이나믹 링커에 의해 랜덤한 메모리 주소에 매핑된다.
(3) ASLR이 함께 적용된 환경에서는 바이너리가 로드될 때마다 다른 메모리 주소를 가지게 되어, 공격자가 정확한 메모리 주소를 예측하기 어렵다.
▶ PIE 보호 기법의 필요성
(1) 메모리 주소 예측 방지: 기존에는 실행 파일이 항상 고정된 주소에 로드되었기 때문에 공격자가 쉽게 ROP(Return Oriented Programming) 공격을 수행할 수 있었다. PIE를 적용함으로써 이를 어렵게 만든다.
(2) ASLR과의 결합: ASLR만 적용되면 라이브러리 등의 주소만 랜덤화되지만, PIE를 함께 적용하면 실행 파일 자체의 베이스 주소도 랜덤화된다.
(3) Expolit 난이도 증가: 결론적으로, 주소의 랜덤화로 인해 공격자가 특정 함수나 변수의 주소 자체를 알아내기가 어려워져 공격 난이도가 상승한다.
▶ PIE 적용 실습

위와 같이 간단한 test.c 코드를 사용해 PIE를 적용 여부를 확인해 보자.


PIE 적용 여부에 따라 위와 같이 컴파일을 따로 해준다.

먼저, 미적용 파일을 여러 번 실행한 결과, 함수의 메모리 주소가 항상 동일하게 나오는 것을 알 수 있다.

다음으로, 적용된 파일을 여러 번 실행한 결과, 이번에는 매번 다른 주소가 나오는 것을 알 수 있다.
▶ PIE의 한계
(1) 정보 유출 시 무력화 가능: 만약 프로그램의 어느 부분에서든 메모리 주소가 유출되면, PIE가 무력화될 수 있다. 코드 내에 printf("%p", func_ptr);과 같은 내용이 있다면 공격자가 이를 통해 주소를 알아낼 수 있다.
(2) 퍼포먼스 오버헤드: 위치 독립적인 코드(PIC)로 컴파일되므로, 일부 성능 저하가 발생할 수 있다.
(3) 모든 공격을 막지는 못함: PIE는 주소를 랜덤화할 뿐이며, 다른 보안 기법(스택 보호, RELRO 등)과 함께 적용해야 더욱 효과적이다.
2. Canary
Canary 보호 기법은 버퍼 오버플로우 공격을 방어하기 위한 스택 보호 기법으로, 함수의 리턴 주소가 저장된 영역 앞에 특정한 값(Canary)를 배치하여 이를 검증하는 방식으로 동작한다.
만약 공격자가 버퍼 오버플로우를 시도하여 리턴 주소를 덮어 쓰려하면, Canary 값도 함께 덮어써지므로 이를 감지하여 프로그램이 종료된다.
▶ Canary 동작
(1) 함수가 실행될 때, 스택 프레임에 Canary 값을 삽입한다.
(2) 함수가 종료될 때, Canary 값이 변경되었는지 확인한다.
(3) 변경되지 않았다면 정상적으로 리턴 주소로 복귀, 변경되었다면 스택 오버플로우가 발생한 것으로 판단하여 프로그램을 강제 종료한다.
Canary 값은 다음과 같은 방식으로 설정되는 편이다.
→ 랜덤값(Random Canary): 실행할 때마다 다른 값을 사용
→ 고정값(Null Canary): "0x00 0x00 0x00 0x00"과 같은 널 바이트 4개를 연속하여 설정하는 값. 널 바이트는 문자열 복사 함수(strcpy(), gets(), sprintf() 등)의 종료 조건이기 때문에, 공격자가 canary를 덮어쓰려고 해도 포함된 널 바이트로 인해 입력이 중단되어 공격이 실패할 가능성이 높아진다.
→ 해시 기반(Hash Canary): 특정한 값(ex. 실행 바이너리 해시 등)과 스택 정보 기반 값
또한, 카나리의 크기는 보통 4bytes 또는 8bytes로 설정된다.
▶ Canary 보호 기법의 필요성
(1) BOF 공격 방어: Canary 값이 손상되었는지를 확인하여 공격을 차단할 수 있다.
(2) 스택 기반 공격 방지: 리턴 주소 덮어쓰기, ROP 등의 공격 기법을 어렵게 만든다.
(3) 비교적 낮은 성능 오버헤드: Canary 확인 과정이 간단하여 성능에 미치는 영향이 크지 않다.
▶ Canary 적용 실습

위와 같이 BOF가 발생하는 코드를 작성해 주었다.


벌써부터 gets() 함수가 위험하다며 입력 크기를 제한하는 fgets()를 사용할 것을 경고하고 있다. 하지만 canary 적용을 확인하기 위해 경고는 무시하고 진행하겠다.

canary_off 파일의 입력으로 BOF가 터지는 40bytes를 "A"로 채워 전달했더니, segmentation fault가 발생했다. 즉, RBP와 RET이 성공적으로 덮어씌워져 리턴 주소가 무효됐음을 알 수 있다.

이번에는 같은 입력을 canary_on에 전달했다. canary_off와 달리 정상 출력되는 것처럼 보이지만, 사실상 IOT instruction(프로세스가 abort() 함수에 의해 종료될 때 발생하는 메시지 중 하나, 즉 비정상적 종료)으로 8bytes의 canary가 덮이면서 프로그램이 abort() 호출해 발생한 결과이다.

41bytes 이상 입력을 전달하면 "*** stack smashing detected ***"가 출력되어 canary 보호가 작동하는 것을 정확하게 확인할 수 있다.
사실 41bytes나 40bytes나 결국 canary 변조가 확인되는 순간 SFP까지 닿기 전에 프로그램이 종료되는 것으로 알고 있는데, 아마 zsh에서 보여지는 출력 차이가 아닌가 싶다.


GBD로 어셈블리 코드를 확인해 보면 위와 같은 차이가 있다.
<main+12>에서 fs:0x28 위치의 값이 rax에 저장되는데, 이때 로드되는 값이 canary 값이다. 보통 canary는 fs 레지스터가 가리키는 지역 저장소(Thread Local Storage, TLS)에 저장된다. 그 아랫줄에 rbp-0x8 위치로 rax(=canary값)이 삽입되어 8bytes만큼을 차지하는 것도 알 수 있다.
<main+46>에서 sub 연산으로 canary값을 검증하며, 변조된 경우에는 je로 인해 __stack_chk_fail@plt 함수를 호출하여 스택을 보호하고 있다. 변조되지 않으면 <main+62>로 넘어간다.
▶ Canary의 한계
(1) 정보 누출 위험: 고정된 canary값이 저장된 위치를 알아내면 공격이 가능해진다.
(2) Canary 우회 가능성: Double BOF, stack canary bypass, stack smashing 등의 공격으로 우회가 가능하므로 다른 보안 기법과 함께 적용해야 안전하다.
3. ASLR
ASLR(Addreass Space Layout Randomization)은 프로그램의 메모리 주소를 랜덤하게 변경하는 보호 기법이다. 이를 통해 공격자가 메모리 주소를 예측할 수 없게 만들어 실행 흐름을 변경시키는 등의 행위를 방지할 수 있다.
▶ ASLR 동작
ASLR은 정확히는 실행 파일과 관련된 공유 라이브러리, 스택, 힙이 mapping 되는 메모리 영역의 주소를 랜덤으로 배치한다. 리눅스 환경에서는 /proc/self/maps를 여러 번 읽음으로써 쉽게 확인할 수 있다.
▶ ASLR 보호 기법의 필요성
(1) BOF 공격 방어: 공격자는 보통 버퍼 오버플로우를 사용해 리턴 주소를 덮어씌워 시스템의 흐름을 제어하려고 한다. ASLR은 메모리 주소가 매번 달라지므로 공격자가 정확한 위치를 추측하는 것이 매우 어려워진다.
(2) 리턴 주소 우회 방어: 스택에서 리턴 주소를 우회하여 악성 코드를 실행하는 공격을 방지할 수 있다.
(3) 코드 재사용 공격 방어: 공격자가 악성 코드를 실행하기 위해 시스템의 라이브러리나 코드 섹션의 주소를 예측하려는 공격을 방지한다.
▶ ASLR 적용 실습

위의 dreamhack의 실습 코드를 사용하여 직접 주소를 출력해 보겠다.

다음과 같이 컴파일해주고

실행을 시킨 결과는 위와 같다. buf_stack, buf_heap, libc_base, printf의 주소는 실행될 때마다 변경된다. 코드 영역의 main만 매번 동일한 결과가 출력되고 있다. 코드 영역이 변하지 않는 이유는 성능 최적화와 실행의 안정성을 위해 OS에서 코드 영역 위치를 고정시켰기 때문이라고 한다.
▶ ASLR의 한계
(1) Brute Force 우회: 실행 파일이 작은 경우, 프로그램 메모리 배치가 예측할 수 있는 범위 내에 있을 수 있다. 이때, 주소를 무작위로 대입하면 랜덤화된 주소를 추측할 가능성이 있다. malloc()이나 calloc()을 사용해 동적 할당된 메모리가 대상이 될 수 있다.
(2) Memory Leak 우회: 프로그램에 할당됐던 메모리 중, 해제되지 않거나 관리가 미흡한 메모리 영역에 대해 추적이 가능할 수 있다. 이렇게 발생한 메모리 누수로 주요 메모리 주소를 얻을 수 있는 위험이 있다.
4. NX bit
NX bit(No eXecute Bit)는 프로그램에서 스택, 힙 등의 실행이 불가능한 영역에서 코드가 실행되는 것을 막는 보호 기법이다.
▶ NX bit 동작
NX-bit는 각 메모리 페이지에 실행 가능한 플래그를 설정한다(메모리 보호 설정). 해당 플래그가 설정된 페이지는 CPU가 실행할 수 없으며, 데이터만 읽고 쓸 수만 있다. 프로세서 명령어(ex. MOV, ADD, SUB 등)가 실행되지 않도록 한다.
스택에 데이터를 쌓아두더라도 실행 권한이 없으므로 해당 영역에서는 코드를 실행할 수 없다.
▶ NX bit 보호 기법의 필요성
(1) BOF 공격 방어: 공격자가 코드 영역에 악성 코드를 주입하고 이를 실행하는 것을 방지하는 데에 목적이 있다. 기본적으로 다른 보호 기법과 함께 사용해야 유의미하다.
▶ NX bit 적용 실습

BOF가 발생하는 위의 코드를 사용하였다.

먼저, 리눅스 환경에서 기본적으로 켜져 있는 ASLR를 비활성화시켜준다.(실습이 끝나면 다시 2로 활성화시켜줘야 이후에 문제가 없으니 주의!)


지금 어떻게 컴파일해도 NX-disabled에서 자꾸 NX-unknown이라고 뜨는데 원인을 잘 모르겠다. nn번째 시도;;
gcc -fno-stack-protector -fno-pie -z execstack -o NX-disabled test.c //NX:unkown, PIE:enabled
gcc -fno-stack-protector -fno-pie -z execstack -o NX-disabled test.c //스택 실행 가능하게 >> 에러(relocation R_X86_64_32 against `.rodata' can not be used when making a PIE object; recompile with -fPIE
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status)
gcc -o NX-disabled test.c -fno-stack-protector -z execstack -m64 //NX:unknown
머리가 터지기 전에 일단 보류....
▶ NX bit의 한계
(1) ROP 우회: NX-bit가 활성화된 상태에서도 ROP를 이용해 실행 가능한 코드를 호출할 가능성이 있다. ROP는 실행 가능한 코드 영역의 가젯을 이용해 실행 권한이 없는 메모리 영역에 대해서도 원하는 실행 흐름으로 변경할 수 있다.
5. RELRO
RELRO(RELocation Read-Only)는 링커가 메모리에서 데이터를 수정하지 못하도록 특정 섹션을 읽기 전용으로 설정한다. 이를 통해 공격자는 메모리에 배치되어 있는 데이터를 변조할 수 없게 된다.
▶ RELRO 동작
(1) No RELRO: read-only 속성이 전혀 부여되지 않은 상태로, 모든 메모리의 데이터가 변경 가능하다.
(2) Partial RELRO: 특정한 메모리의 부분에 대해서만 read-only 속성이 부여된 상태이다.
(3) Full RELRO: 모든 메모리에 대해 read-only 속성이 부여된 상태이다.
▶ RELRO 보호 기법의 필요성
(1) RELRO가 비활성화된 경우, 공격자는 GOT(Global Offset Table)나 PLT(Procedure Linkage Table)를 수정해 함수의 주소를 조작할 수 있다.
▶ RELRO 적용 실습

위의 코드를 사용해 partial과 full의 권한 차이를 비교해 보겠다.


gcc는 기본적으로 Full RELRO를 적용해 주고, PIE를 해제하면 Partial RELRO를 적용한다.

parRelro를 실행한 결과, 0x404000 ~ 0x405000까지 쓰기 권한이 할당되어 있었다. 힙 영역의 쓰기 권한은 RELRO와 직접적인 연관이 없다.


objdump -h으로 ELF 실행 파일의 섹션 헤더 정보를 확인할 수 있는데, 각 섹션의 이름, 주소, 크기, 권한(ex. rwx), 파일 내 위치 등의 정보를 볼 수 있다. parRelro의 쓰기 권한이 허용된 섹션에는 동적 링크와 연관된 got, got.plt, data, bss 정보가 할당되어 있었다.
VMA(Virtual Memory Address) vs LMA( Load Memory Address)
| VMA | 프로그램이 실행될 때, 해당 섹션이 메모리에서 차지할 주소. = 실행 중 실제 메모리에 로드되는 위치 |
| LMA | 실행 파일에서 해당 섹션이 실제 저장된 파일 내의 주소. = 실행 중 메모리로 로드되기 전, 디스크에 있는 파일 내 위치 |

fullRelro를 실행한 결과, 0x560a6b9d6000 ~ 0x560a6b9d7000까지 쓰기 권한이 할당되어 있었다.


fullRelro가 매핑된 위치인 0x560a6b9d2000에 .data 섹션의 VMA 0x4000을 더하면 0x560a6b9d6000이다. 이는 쓰기 권한이 할당된 영역에 해당하며, 아래의 .bss 섹션도 마찬가지이다.
즉, fullRelro에서는 GOT / PLT에 쓰기 권한이 할당되지 않아 GOT를 수정해 함수 호출을 우회하는 GOT Overwrite가 불가능하다.
▶ RELRO의 한계
(1) Partial RELRO의 GOT Overwrite: Partial RELRO에서는 .got.plt 섹션에 쓰기 권한이 부여되어 있기에 GOT Overwrite 공격 가능하다.
(2) RTL 공격: full RELRO가 적용된 상태에서도 system()과 같은 기존 라이브러리 호출을 이용한 RTL 공격이 가능하다.
(3) Heap Spraying: 힙 영역에 악의적인 데이터를 삽입하고, 이를 사용해 우회함으로써 full RELRO에서 메모리 할당 시 발생 가능한 취약점을 이용할 수 있다.
[] 정리 []
| 보호 기법 | 설명 | 목표 |
| PIE | 실행 파일이 로드될 때, 매번 주소가 바뀜 | 실행 파일 주소를 랜덤화하여 파일 위치를 알 수 없게 함 |
| ASLR | 프로그램 실행 시 메모리 주소를 무작위로 배치함 | 메모리 주소 예측을 방지하여 BOF 공격을 어렵게 함 |
| NX-bit | 메모리 영역에서 코드 실행을 방지하는 비트 | 코드 실행이 가능한 영역을 제한해 악성 코드 실행을 방지함 |
| RELRO | 실행 중 재배치된 주소를 읽기 전용으로 설정함 | GOT, PLT 수정 방지 및 코드 안정성 강화 |
헷갈릴 수 있는 4가지 보호 기법의 차이점을 정리해 봤다. 결론적으로 모두 같이 쓰이는 게 베스트.
※ 출처 ※
*메모리 보호 기법: https://core-research-team.github.io/2020-05-01/memory
*NX bit 실습: