[Research] Linux Kernel Exploit - KPTI(1)

[Research] Linux Kernel Exploit - KPTI(1)

서론

해당 포스트에서는 Linux Kernel Basic 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법(mitigation)을 하나씩 우회하여 공격하는 과정을 포스트하려고 한다. 이전 포스트와 다르게 해당 포스트에서는 KPTI 보호기법을 주제로 하며 연구를 진행해본 결과 KPTI에 대한 우회는 단순한 Kernel ROP에서 벗어나지 않아서 보다 본질적으로 KPTI에 대해 이해하며 해당 보호기법은 어떠한 이유로 생성되었는 지를 연구해본 결과를 작성하겠다.



KPTI


meltdown


KPTI(Kernel Page Table Isolation)은 커널 공간과 유저 공간의 전환이 일어날 경우, 개별적인 페이지 테이블을 사용하여 유저 공간 페이지 테이블의 경우 최소한의 커널 주소만을 포함하도록 하는 보호기법이다.


KPTI가 적용되지 않은 기존의 User Process가 사용하는 Page Table의 경우에는 Kernel Address가 매핑되어 있다. 이러한 점을 고려한다면 User Application이 Kernel Mode로 전환되는 경우(System Call, Interrupt…)에서 직접적으로 Kernel에서의 데이터를 사용할 수 있기에 Overhead를 줄일 수 있다는 장점을 가지고 있지만 다르게 해석한다면 User Application에서 Kernel Memory의 정보가 유출될 수 있다는 취약점을 지니고 있다.


앞서 이야기 한 취약점은 2018년 당시 매우 선풍적인 인기(?)를 끌었던 meltdown에 관한 취약점이다. 그렇다면 실제 meltdown 취약점이 어떠한 방식으로 발생하는 지 분석해보며 보호기법 KPTI에 대한 심도 깊은 이해를 진행해보겠다.



MELTDOWN - 배경지식

위험성


상단의 그림은 운영체제의 가장 첫 시간의 배우는 기본적인 형태의 레이아웃이다. 일반적으로 사용자 어플리케이션이 직접적으로 하드웨어인 CPU나 RAM등의 직접적으로 접근할 수 없는 원론적인 형태의 이야기를 나타낼 때 나오는 그림이다. 메모리를 직접적으로 제어하는 것은 커널이며 사용자 어플리케이션을 통해 주요한 정보를 입력하여 메모리의 상주되는 경우라도 커널이 관여한다는 의미이다.

또한 이전까지의 포스팅을 통해서 커널 메모리에 어떠한 중요한 형태의 정보들이 상주되어있는 지 본능적으로 알 고 있다. meltdown은 코드를 실행하거나 하는 형태의 취약점은 아니지만 공격자의 입장에서는 코드를 실행하여 결과적으로 얻고 싶은 것이 정보라고 한다면 유출(leak) 취약점은 매우 파급력 높으며 위험한 형태의 취약점으로 볼 수 있다.

Meltdown 취약점을 이해하기 위해서는 배경지식이 필요하다. CPU의 성능의 효율을 올리기 위한 캐시 시스템과 페이지 테이블, 그리고 파이프 라인 및 예측 실행과 같은 형태의 개념이다. 모교의 빛인 존경하는 두 교수님을 통해서 해당 개념들을 심도있게 이해할 수 있었으며 재미를 위해 이를 다시 간략히 기술하며 리마인드 해보겠다.


캐시 시스템


CPU는 해당 CPU가 지원하는 bit에 맞는 전기적 신호인 데이터를 해석하여 그에 따라서 진행할 뿐이다. 그러한 데이터가 유의미한 전기적 신호가 되기 위해서는 하나의 뭉탱이(?)로 되어 있어야 하며 이는 프로그램이라고 불리우는 전기적 조합의 불과하다. 그렇다면 CPU가 유의미한 전기적 조합에 대한 해석을 하기 위해서는 이러한 전기적 조합이 저장된 곳이 있어야 하며 이는 메인 메모리 또는 하드 디스크에 일반적으로 저장이 된다.

매우 직관적이고 원시적인 방법으로 생각해보면 필요할 때 마다 RAM에 데이터를 요청하는 방법이 있으나 이는 속도적으로 보았을 때 매우 비효율적인 방법이다. 이러한 문제들을 효율적으로 해결하고자 CPU 설계자들은 여러가지 방법을 도입했고 CPU가 빠르게 접근할 수 있도록 CPU 내부에 적은 형태의 데이터를 저장할 수 있는 곳을 설계했다. 이는 특정 메모리의 위치한 인접한 부분에 다시 접근할 확률이 높다 및 접근하였던 곳에 짧은 시간내 다시 접근할 확률이 높다는 locality라는 특성을 이용하면 매우 괜찮고 효율적인 방법이다.


페이지 테이블

CPU가 특정 메모리를 조회하기 위해서는 물리적인 메모리 위치에 대한 조회를 수행해야 한다. 즉 Virtual Address를 Physical Address로 변환해주는 과정이 필요하다는 의미이다. Virtual Address가 어떠한 Physical Address와 매칭되는 지에 대한 정보를 또한 저장해야 하며 이는 Page Table이라는 형태로 RAM에 저장된다. 이를 이용하면 실제 물리 메모리의 모든 메모리를 로드하지 않아도 다른 프로세스와 공유하는 메모리의 경우는 동일한 페이지 넘버를 제공하는 형태로의 이점도 존재한다. 하지만 페이지 테이블의 경우는 프로세스마다 개별적으로 하나씩 존재한다.


파이프 라인

CPU가 명령어를 수행할 경우 일련의 처리되는 과정이 있다. 메모리에서 명령어를 가져오며(fetch), 이를 해석하며 레지스터를 읽고(decode), 해석한 결과를 직접 수행하며(execute), 메모리에 있는 피연산자에 접근하며(memory), 연산의 결과를 레지스터에 작성(write back)하는 과정을 거친다. 이는 병렬적으로 처리가 가능하며 무수히 많은 instruction을 수행시킬 때에 있어 이를 병렬적으로 처리하면 보다 효율적일 것이다.

예를 들어 컵라면을 7개를 만든다고 하였을 경우 스프와 후레이크를 넣고 뜨거운 물을 끓이고 이를 익기까지 기다린 후 다음 컵라면을 만드는 것이 아니라 뜨거운 물을 넣어두고 다음 컵라면을 제조하는 형태로 진행할 것이다. 완전히 좋은 예시는 아니지만 충분한 인사이트를 얻기에는 적절한 예시라고 생각된다.



MELTDOWN - 원리

기본 원리

해당 시점에서는 앞서 제시하였던 기술들이 MELTDOWN과 어떠한 상관관계를 미치고 있는 지 고민해볼 필요가 있다. 페이지 테이블의 항목에서 언급하였던 것과 같이 모든 프로세스는 각자의 페이지 테이블을 보유하고 있다. 또한 커널에 요청을 하기 위해서 본인 프로세스의 주소만을 매칭한 것이 아니라 커널의 주소도 페이지 테이블에 맵핑(Mapping)되어 있다.

이를 다시 정리하면 일반적인 User Process의 페이지 테이블의 경우는 Kernel의 Virtual Address를 Physical Address로 맵핑하기 위한 정보를 가지고 있다는 의미가 된다. 하지만 운영체제의 특성으로 인해 User Process에서 Kernel Process 영역의 Memory 주소를 직접적으로 요청할 수는 없다.


Abusing Speculative Execution

멜트다운(Meltdown) 취약점의 원리를 이해하기 위해서 User Process에서 다음과 같은 명령어가 수행하려 한다는 것을 가정하겠다.


mov rax, [kernel_memory_address]

해당 명령어를 수행하며 결과적으로 인터럽트가 발생할 것이다. 왜냐하면 유저모드에서 직접적으로 커널 메모리의 데이터를 읽는 것은 불가능하기 때문이다. 하지만 해당 명령어는 단계적으로 보았을 경우 수행될 것이다. 언급하지 않았던 내용이 서로간의 영향을 끼치지 않는 독립적인 형태의 명령어는 미리 수행하여 결과를 준비해놓는 기술인 예측 실행(speculative)이 있다. 그러한 이유로 사용자에게 해당 데이터의 정보를 레지스터에 세팅하진 않지만 CPU의 캐시 정보에는 미리 로드될 것이다.


mov rax, [kernel_memory_address]
and rax, 1
mov rbx, [rax+user_memory_address]

다음과 같은 형태는 어떠한 방식으로 수행될 지 확장을 해보겠다. 당연히 첫 번째 명령어를 수행하면 인터럽트가 발생할 것이다. 하지만 예측 실행(speculative execution)으로 인해서 명령어가 rax+user_memory_address에 해당하는 정보가 캐시에 저장될 것이다. 이러한 특성을 이용할 경우 무차별 대입의 형태로 user_memory_address+i에서 i의 값을 증가시키며 메모리를 읽으려고 수행할 경우 캐시에 로드되어 있는 정보는 상대적으로 빠르게 읽힐 것이다. 이러한 시간 정보(side channel)를 활용하여 빠르게 읽힌 경우의 i값은 결과적으로 rax에 값이 되며 이는 결과적으로 알아내고자 하였던 kernel_memory_address 내부에 있던 값이다.



CVE-2017-5754

raise_exception()
access(probe_array[data*4096])

// rcx = kernel_address
// rbx = probe_array
retry
mov al, byte[rcx]
shl rax, 0xC
jz retry
mov rbx, qword[rbx + rax]

먼저 Graz University of Technology에서 제공한 코드임을 밝힌다. 해당 과정을 순차적으로 해석해보면 멜트다운 취약점에 대한 보다 깊은 이해를 얻을 수 있을 것이라는 판단이 들었다. 가장 먼저 수행되어야 하는 동작은 cache에 데이터를 모두 날려야 한다. 이후 rcx에 해당하는 커널 주소에 접근하여 al 레지스터에 담게된다. 이를 12bit의 shift left를 수행하면 결과적으로는 4096이 곱해진다. 해당 의미는 al 레지스터에 담긴 n번째 페이지라고 볼 수 있다. 이후 검증하고자 하는 페이지의 정보를 rbx 레지스터에 로드한다.

명령어에 대한 해석은 다음과 같으나 실질적으로 명령어는 정상적으로 실행되지 않는다. mov al, byte[rcx]를 수행하게 되면 커널 메모리에 대한 접근으로 인터럽트가 발생하기 때문이다. 하지만 레지스터에 commit되지 않을 뿐 실질적으로 캐시에 해당 데이터가 로드되는 것을 알 수 있다. 이후 앞서 제시하였던 결과와 같이 반복적으로 접근하였을 때 하나의 특정 페이지가 빠르게 접근될 경우 빠르게 접근된 페이지의 번호가 kernel_address의 담긴 값으로 판단할 수 있다.



PoC 영상

https://www.youtube.com/watch?v=RbHbFkh6eeE



다시 보는 KPTI

KPTI라는 보호기법을 발생할수 있도록 하였던 멜트다운(Meltdown) 취약점에 대한 이해를 선행하였으며 해당 시점에서 KPTI에 대한 정리가 필요할 것으로 판단되어 다음의 항목을 작성한다. 상단의 그림을 확인하면 좌측의 경우 KPTI가 적용되어 있지 않았을 경우 기재된 모드가 접근할 수 있는 메모리의 영역을 도식화 시킨 형태이다. User Mode 및 Kernel Mode의 무관하게 많은 영역에 접근할 수 있는 것으로 확인된다. 하지만 우측의 경우는 User Mode가 접근할 수 있는 영역에서 Kernel Space가 줄어드는 것으로 확인된다.

멜트다운(Meltdown)은 사용자 모드의 경우에도 페이지 테이블을 통해 커널의 영역을 접근할 수 있음에서 문제가 발생한다는 것이다. 이를 통해 캐시에는 데이터가 로드가 되기 때문에 다음과 같은 취약점이 발생하는 것이다. 그러한 문제를 발생시키지 않기 위해서 접근 가능한 자체의 영역을 최소화 함으로 하는 보호기법이다.

하지만 이는 어디까지나 이론상의 기재된 내용으로 사실상 공격관점에서 어떠한 부분이 달라지는 지 크게 와닿지가 않는다. 그러므로 하나의 작성에 해당 내용을 끝내려고 하였지만 실제 해당 보호기법이 적용되었을 때의 익스플로잇을 진행해보아서 이에 대한 인사이트를 늘릴 필요가 있어 보인다.



참고

b30w0lf님의 운영체제(Operating System)
DREAMHACK - Linux Kernel Exploit
INFLEARN - 리눅스 커널 해킹. A부터 Z까지
https://en.wikipedia.org/wiki/Kernel_page-table_isolation
https://www.lazenca.net/pages/viewpage.action?pageId=25624859
https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame
https://sata.kr/entry/보안-Issue-Meltdown멜트다운-취약점을-파헤쳐보자-1
https://sf-jam.tistory.com/102



한줄평

세상은 넓고 고수는 많다.