메모리 보호 기법

System

라온화이트햇 핵심연구팀 이영주

Introduce

다양한 환경에서 메모리 보호기법을 조사하였습니다. 많은 메모리 보호기법이 존재하기 때문에 각 보호기법에 대한 우회 방법에 대해서는 간략하게 다뤘습니다.

INDEX

  1. Window
    • ASLR (Address Space Layout Randomization)
    • DEP (Data Execution Prevention)
    • GS (Buffer Security Check)
    • SafeSEH
    • SEHOP (Structured Exception Handling Overwrite Protection)
    • CFG (Control Flow Guard)
    • CIG (Code Integrity Guard)
    • ACG (Arbitrary Code Guard)
    • CET (Shadow stack, Indirect Branch Tracking)
  2. Linux
  3. Kernel
    • KASLR (Kernel Address Space Layout Randomization)
    • SMEP (Supervisor Mode Execution Prevention)
    • SMAP (Supervisor Mode Access Prevention)

1. ASLR(Address Space Layout Randomization)

ASLR은 실행파일과 관련된 공유 라이브러리, 스택, 힙이 매핑되는 메모리 영역의 주소를 랜덤으로 배치하는것입니다. 이를 통해 공격자가 고정된 주소를 이용하여 실행 흐름을 변경하거나 원하는 데이터를 가져오는 것을 막을 수 있습니다. ASLR은 Linux 환경에서 /proc/self/maps를 읽어서 가장 쉽게 확인할 수 있습니다.

raon@raon:~/test$ cat /proc/self/maps
55abfd61b000-55abfd61d000 r--p 00000000 103:02 2752672                   /usr/bin/cat
55abfd61d000-55abfd622000 r-xp 00002000 103:02 2752672                   /usr/bin/cat
55abfd622000-55abfd625000 r--p 00007000 103:02 2752672                   /usr/bin/cat
55abfd625000-55abfd626000 r--p 00009000 103:02 2752672                   /usr/bin/cat
55abfd626000-55abfd627000 rw-p 0000a000 103:02 2752672                   /usr/bin/cat
55abfdf39000-55abfdf5a000 rw-p 00000000 00:00 0                          [heap]
7ff2b516f000-7ff2b5191000 rw-p 00000000 00:00 0 
7ff2b5191000-7ff2b5846000 r--p 00000000 103:02 2753788                   /usr/lib/locale/locale-archive
7ff2b5846000-7ff2b586b000 r--p 00000000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7ff2b586b000-7ff2b59e3000 r-xp 00025000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7ff2b59e3000-7ff2b5a2d000 r--p 0019d000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7ff2b5a2d000-7ff2b5a30000 r--p 001e6000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7ff2b5a30000-7ff2b5a33000 rw-p 001e9000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7ff2b5a33000-7ff2b5a39000 rw-p 00000000 00:00 0 
7ff2b5a4e000-7ff2b5a4f000 r--p 00000000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7ff2b5a4f000-7ff2b5a71000 r-xp 00001000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7ff2b5a71000-7ff2b5a79000 r--p 00023000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7ff2b5a7a000-7ff2b5a7b000 r--p 0002b000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7ff2b5a7b000-7ff2b5a7c000 rw-p 0002c000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7ff2b5a7c000-7ff2b5a7d000 rw-p 00000000 00:00 0 
7ffd5fbf9000-7ffd5fc1b000 rw-p 00000000 00:00 0                          [stack]
7ffd5fd14000-7ffd5fd17000 r--p 00000000 00:00 0                          [vvar]
7ffd5fd17000-7ffd5fd18000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
raon@raon:~/test$ cat /proc/self/maps
56196357a000-56196357c000 r--p 00000000 103:02 2752672                   /usr/bin/cat
56196357c000-561963581000 r-xp 00002000 103:02 2752672                   /usr/bin/cat
561963581000-561963584000 r--p 00007000 103:02 2752672                   /usr/bin/cat
561963584000-561963585000 r--p 00009000 103:02 2752672                   /usr/bin/cat
561963585000-561963586000 rw-p 0000a000 103:02 2752672                   /usr/bin/cat
561964938000-561964959000 rw-p 00000000 00:00 0                          [heap]
7fe9c9dbe000-7fe9c9de0000 rw-p 00000000 00:00 0 
7fe9c9de0000-7fe9ca495000 r--p 00000000 103:02 2753788                   /usr/lib/locale/locale-archive
7fe9ca495000-7fe9ca4ba000 r--p 00000000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca4ba000-7fe9ca632000 r-xp 00025000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca632000-7fe9ca67c000 r--p 0019d000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca67c000-7fe9ca67f000 r--p 001e6000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca67f000-7fe9ca682000 rw-p 001e9000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca682000-7fe9ca688000 rw-p 00000000 00:00 0 
7fe9ca69d000-7fe9ca69e000 r--p 00000000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca69e000-7fe9ca6c0000 r-xp 00001000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6c0000-7fe9ca6c8000 r--p 00023000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6c9000-7fe9ca6ca000 r--p 0002b000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6ca000-7fe9ca6cb000 rw-p 0002c000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6cb000-7fe9ca6cc000 rw-p 00000000 00:00 0 
7ffe55212000-7ffe55234000 rw-p 00000000 00:00 0                          [stack]
7ffe5539a000-7ffe5539d000 r--p 00000000 00:00 0                          [vvar]
7ffe5539d000-7ffe5539e000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

위의 결과를 통해 실행파일(/usr/bin/cat), 공유 라이브러리, 스택, 힙이 적재되는 메모리 영역이 매번 바뀐다는 것을 알 수 있습니다.

이 기법을 우회할 수 있는 대표적인 방법은 아래와 같습니다.

  1. Brute Force : 주로 예측해야 하는 주소 길이가 작은 x86 실행파일에서 사용되며 동적 할당되는 heap 영역을 이용하는 경우가 많습니다. 이 방법을 통해 확률적으로 원하는 주소를 예측할 수 있습니다.
  2. Memory Leak : ASLR의 경우 메모리 영역이 매번 변경되는 것이므로 Memory Leak 취약점을 통해 특정 주소를 알 수 있다면 offset 계산을 통해 그 메모리 영역에 있는 다른 주소들도 예측할 수 있습니다.

2. DEP(Data Execution Prevention) / NX(No-Execute) bit

DEP/NX는 실행 가능한 메모리 영역이 아닌 스택, 힙과 같은 영역에서 코드가 실행되는 것을 막는 보호기법입니다. 이 기법도 ASLR 처럼 널리 사용되는 보호기법이며 공격자가 프로그램의 실행 흐름을 제어할 수 있을때 exploit을 어렵게 만듭니다. 이를 윈도우에서는 DEP라고 부르며 OS X나 리눅스에서는 NX bit라고 부릅니다. 이는 ASLR과 마찬가지로 메모리 맵을 통해 어떻게 적용되어 있는지 확인할 수 있습니다.

raon@raon:~/test$ cat /proc/self/maps
56196357a000-56196357c000 r--p 00000000 103:02 2752672                   /usr/bin/cat
56196357c000-561963581000 r-xp 00002000 103:02 2752672                   /usr/bin/cat
561963581000-561963584000 r--p 00007000 103:02 2752672                   /usr/bin/cat
561963584000-561963585000 r--p 00009000 103:02 2752672                   /usr/bin/cat
561963585000-561963586000 rw-p 0000a000 103:02 2752672                   /usr/bin/cat
561964938000-561964959000 rw-p 00000000 00:00 0                          [heap]
7fe9c9dbe000-7fe9c9de0000 rw-p 00000000 00:00 0 
7fe9c9de0000-7fe9ca495000 r--p 00000000 103:02 2753788                   /usr/lib/locale/locale-archive
7fe9ca495000-7fe9ca4ba000 r--p 00000000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca4ba000-7fe9ca632000 r-xp 00025000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca632000-7fe9ca67c000 r--p 0019d000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca67c000-7fe9ca67f000 r--p 001e6000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca67f000-7fe9ca682000 rw-p 001e9000 103:02 2756186                   /usr/lib/x86_64-linux-gnu/libc-2.30.so
7fe9ca682000-7fe9ca688000 rw-p 00000000 00:00 0 
7fe9ca69d000-7fe9ca69e000 r--p 00000000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca69e000-7fe9ca6c0000 r-xp 00001000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6c0000-7fe9ca6c8000 r--p 00023000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6c9000-7fe9ca6ca000 r--p 0002b000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6ca000-7fe9ca6cb000 rw-p 0002c000 103:02 2756182                   /usr/lib/x86_64-linux-gnu/ld-2.30.so
7fe9ca6cb000-7fe9ca6cc000 rw-p 00000000 00:00 0 
7ffe55212000-7ffe55234000 rw-p 00000000 00:00 0                          [stack]
7ffe5539a000-7ffe5539d000 r--p 00000000 00:00 0                          [vvar]
7ffe5539d000-7ffe5539e000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

위 결과를 보면 코드가 매핑된 곳 이외의 모든 영역에는 executable 권한이 없습니다. 보통 실행되어야 하는 영역에는 write 권한이 필요 없으므로 코드 영역에는 w와 x 권한이 함께 부여되지 않습니다.

NX를 우회할 수 있는 대표적인 방법은 ROP(Return Oriented Programming) 입니다. ROP는 실행가능한 코드 영역의 gadget(명령어들의 집합)을 이용하여 다른 메모리 영역에 실행권한이 없더라도 원하는 실행 흐름을 만들 수 있습니다.


3. Stack Canary / GS(Buffer Security Check)

Stack Canary는 스택 버퍼 오버플로우를 막기 위해 만들어진 기법입니다.

/assets/2020-05-01/stack_canary.png

보통 스택 버퍼 오버플로우 취약점이 발생하면 리턴 주소를 덮어 쉽게 프로그램의 실행 흐름을 제어할 수 있습니다. Stack Canary는 함수가 리턴하기 전에 미리 삽입해둔 canary 값이 변조되지 않았는지 검사해 버퍼 오버플로우를 탐지합니다. 이 때 canary가 변조되었다면 프로그램이 종료됩니다. 이를 통해 공격자가 buffer overflow 취약점 하나로는 실행흐름을 바꿀 수 없게 만듭니다.

이 기법을 우회할 수 있는 대표적인 방법은 Info Leak 입니다. canary도 결국 메모리에 저장되기 때문에 이 값을 leak할 수 있는 취약점이 있다면 canary가 변조되지 않은 것 처럼 속여 리턴 주소를 조작할 수 있습니다.


4. SafeSEH, SEHOP

SEH는 “Structured Exception Handling”의 줄임말로 윈도우에서 제공하는 예외 처리 시스템입니다.

int test(void){
__try {
 // Exception may occur here
}
__except( EXCEPTION_EXECUTE_HANDLER ) {
 // This handles the exception
}
return 0;
}

예시 코드는 위와 같습니다.

/assets/2020-05-01/sehop.png

또한 SEH는 메모리에서 위와 같이 체인으로 연결되어있습니다. 그리고 값이 스택에 존재하기 때문에 스택 버퍼 오버플로우와 같은 취약점으로 seh를 덮어서 공격할 수도 있습니다. seh를 덮게되면 스택 버퍼 오버플로우로 인해 canary가 덮히더라도 canary를 체크하기 전에 주소를 참조할때 에러가 발생하여 seh를 핸들링하는 코드로 넘어가게 됩니다. 따라서 canary를 무시하고 실행흐름을 변조할 수 있습니다. 이와 같은 seh overwrite를 막는 보호기법이 SafeSEH, SEHOP입니다.

4-1. SafeSEH

SafeSEH는 exception이 발생하고 exception 핸들러를 호출하기 전에 아래와 같은 검사를 진행합니다.

  1. exception pointer가 스택 영역인지 확인하고 스택 영역이라면 실행하지 않습니다.
  2. handler pointer가 로드된 module 주소 영역에 등록된 handler인지 확인하고 존재하지 않는다면 실행하지 않습니다.

따라서 seh를 덮어 handler를 조작하는것을 막습니다.

4-2. SEHOP

SEHOP(Structured Exception Handling Overwrite Protection)은 seh의 마지막 체인이 ntdll!_except_handler 인지 확인하여 seh overwrite를 막습니다.

위 두가지 보호기법을 우회한 예시는 이 링크에서 확인할 수 있습니다.


5. CFG(Control Flow Guard)

CFG는 컴파일할 때 보안 검사 코드를 삽입하여 함수 포인터가 존재하는 메모리를 조작하여 공격하는것을 막습니다.

/assets/2020-05-01/cfg.png

예를 들어 위와 같은 코드에서 Cb에 대한 검증이 없다면 공격자에 의해 함수 포인터가 변경되어 코드 실행 흐름이 변조될 수 있습니다. 하지만 모든 function pointer를 이용한 호출에 검사루틴을 삽입하여 검증된 pointer만 호출할 수 있게 합니다. 만약 CFG 검사가 실패하면 프로그램은 바로 종료됩니다.

CFG를 우회하는 글은 이 링크를 참조하여 확인할 수 있습니다.


6. CIG (Code Integrity Guard), ACG (Arbitrary Code Guard)

6-1. CIG

CIG는 프로세스에 의해 서명된 DLL만 로드할 수 있도록 하는 보호기법입니다. 보통 윈도우에서 exploit 할 때는 대부분 공격자가 생성한 DLL을 로드하기 때문에 CIG를 통해 서명되지 않은 DLL을 로드하는 것을 막을 수 있습니다.

6-2. ACG

ACG는 CIG와 같이 사용되며 동적으로 코드를 생성하거나 수정하지 못하게 하는 보호기법 입니다. 만약 CIG만 걸려있을 경우 DLL을 로드하지 않고 VirtualProtectVirtualAlloc을 이용해 원하는 코드를 메모리에 올릴 수 있습니다.

따라서 ACG는 VirtualProtect시 코드를 수정 가능하게 하는 PAGE_EXECUTE_READWRITE 권한을 이미지 영역에 줄 수 없게 만들고 VirtualAlloc시에도 새로운 PAGE_EXECUTE_READWRITE 권한을 가진 페이지를 만들 수 없게 막습니다.

위 두 가지 보호기법을 이용해 윈도우에서 동작하는 악성코드가 코드 인젝션을 할 수 없게 됩니다.


7. CET (Control-flow Enforcement Technology Specification)

ROP(Return-Oriented Programming), JOP(Jmp-Oriented Programming) 공격을 수행할 때, 보통 흐름을 제어할 수 있는 명령어(RET, CALL, JMP)를 사용합니다. CET는 아래와 같은 방법으로 ROP/JOP형태의 흐름제어를 막는 보호기법입니다.

7-1. Shadow stack

/assets/2020-05-01/shadow_stack.png

스택에 ROP/JOP 페이로드를 쓰는 것을 보호하는 기법으로 CALL로 다른 함수 호출 시 현재 스택과 Shadow Stack에 리턴 주소를 푸시합니다. 그리고 함수에서 탈출할 때 RET 명령어는 두 스택에 있는 리턴주소를 팝하여 비교합니다. 만약 리턴 주소가 변조되었다면 비정상적인 행동이므로 프로그램을 종료합니다. 이것을 통해 스택 버퍼 오버플로우와 같은 취약점으로 RET를 조작하여 exploit하는 것을 막을 수 있습니다.

이 보호기법을 위해 윈도우에서는 아래와 같은 새로운 명령어들을 사용합니다.

Shadow stack을 우회하는 기법은 이 링크에서 확인할 수 있습니다.

7-2. Indirect branch tracking

JMP/CALL 이후 비정상적인 행동을 검사하는 보호기법입니다. 이 보호기법에서는 ENDBR32/ENDBR64(ENDBRANCH)라는 새로운 명령어를 사용합니다. 이 명령어는 CET를 지원하지 않는 프로세서에서는 NOP으로 처리됩니다.

/assets/2020-05-01/indirect.png

CET가 활성화되면 각 함수 프롤로그에 ENDBR32/ENDBR64 명령어를 삽입하고 JMP와 CALL이후에 ENDBR32/ENDBR64 명령어가 나오지 않는다면 control protection 예외(#CP)가 발생합니다.

/assets/2020-05-01/indirect1.png

이것을 통해 ROP나 JOP에서 code gadget을 사용하지 못하게 해서 exploit을 막을 수 있습니다. 다만 모든 call이나 jmp에서 적용되는 내용은 아니고 몇 가지 예외는 존재합니다. (e.g. far direct jmp (JMP DS:[mylabel], rip)에 의존적인 jmp, jcc (jump if condition is met) 명령어)

위 두가지 보호기법을 이용해 실행흐름을 변경할 수 있는 명령어들에 대한 검사를 진행하여 exploit을 어렵게 만듭니다.


8. KASLR / SMEP / SMAP

8-1. KASLR (Kernel Address Space Layout Randomization)

KASLR이란 커널 코드 영역을 랜덤하게 할당하여 커널 exploit을 어렵게 만드는 기법입니다. 대상이 커널 코드라는 점만 제외하면 동작 과정과 우회 기법은 ASLR과 동일합니다.

8-2. SMEP (Supervisor Mode Execution Prevention)

/assets/2020-05-01/smep.png

Privilege ring(Protection ring)은 시스템을 보호하기 위해 자원의 접근에 다른 권한을 부여하는 메커니즘입니다. 현대의 OS에서는 privilege ring의 4가지 권한 중 ring 0와 ring 3만 사용하며, ring 0 권한으로 커널 코드가 실행되고 ring 3 권한에선 응용프로그램들이 실행됩니다.

SMEP는 supervisor mode (ring0)에서 특정 주소에 있는 명령어를 fetch 할때 그 주소가 만약 user mode (ring3)에서 접근가능한 주소라면 fetch하지 않게 하는 보호기법입니다. 따라서 supervisor mode일때 유저 영역의 코드를 실행할 수 없습니다.

이 보호기법은 DEP와 마찬가지로 ROP로 우회할 수 있습니다. 커널 영역에 있는 가젯들로 ROP chain을 구성하면 SMEP를 우회하여 실행 흐름을 변경할 수 있습니다.

8-3. SMAP (Supervisor Mode Access Prevention)

SMAP는 SMEP를 보완하기 위해 설계된 보호 기법입니다. SMEP의 경우 supervisor mode에서 유저 영역의 코드를 실행하는 것만 막는다면, SMAP는 user 영역의 특정 주소를 참조하는것을 막습니다.

mov rcx, qword ptr [rax]

예를 들어, 위 명령어를 supervisor mode에서 실행할 때 rax에 유저 영역의 주소가 저장되어 있다면 위 명령어는 실행되지 않습니다.

SMAP를 우회하는 대표적인 방법으로는 커널의 heap 영역에 ROP chain을 spray하고 스택 피벗을 통해 ROP하는 방법이 있습니다. 이때 heap에 원하는 값을 쓰기 위해서 주로 heap spray라는 기법을 사용합니다.


9. Reference

https://security.cs.pub.ro/summer-school/wiki/session/10

https://www.ffri.jp/assets/files/research/research_papers/SEH_Overwrite_CanSecWest2010.pdf

https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard

https://www.countercraft.eu/blog/post/arbitrary-vs-kernel/

https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf

https://eyalitkin.wordpress.com/2017/08/18/bypassing-return-flow-guard-rfg/

https://nsfocusglobal.com/what-you-should-know-about-mitigation-bypass/

https://en.wikipedia.org/wiki/Protection_ring

https://2017.zeronights.org/wp-content/uploads/materials/ZN17_Matt_Recent Exploit Trend and Mitigation, Detection Tactics-Current.pdf