Home KAISER-hiding the kernel from userspace
Post
Cancel

KAISER-hiding the kernel from userspace

유저스페이스, 커널스페이스에 대해 더 알아보다가 lwn.net의 글을 발견해 번역해 두었다. 아직 Meltdown이 세상에 알려지기 전에 쓰여진 글인거 같은데, (2017년 11월 15일에 원문이 작성되었다.) 글에 나온 논문과 다른 자료를 더 조사해야 하겠지만, 일단 좋은 글인거 같아서 번역해 두었다. 원문은 https://lwn.net/Articles/738975/에서 확인할 수 있다.

intro

Linux는 초창기부터 커널의 메모리를 모든 실행중인 프로세스의 주소 공간에 매핑시켰다. 이러한 것은 상당한 수준의 성능 향상을 가져왔고, 프로세서의 MMU(Memory Management Unit)이 Userspace에서 이러한 커널 메모리에 접근하는 것을 막았다. 이러한 매핑에 연관된 미묘한 보안 이슈가 조명되었고, x86 아키텍처에서 이러한 매핑을 중단하고자 하는 패치가 조속히 발표되고 있다.

주소 공간의 역사

32비트 시스템에서는 실행 중인 프로세스의 주소 공간 레이아웃은 하단부 (Bottom) 3GB (0x00000000에서 0xbfffffff까지)를 사용자 공간 용도 (Userspace)로, 상단부 (Top) 1GB(0xc0000000에서 0xffffffff까지)를 커널 용도(Kernelspace)로 할당했다. 각 프로세스는 하단 3GB에서 자신만의 메모리를 가졌고, 커널 공간 매핑은 모든 프로세스에서 동일했다. x86_64 시스템에서는 Userspace 가상 주소 공간이 0에서 0x7fffffffffff(하위 47비트)까지이며, 커널 공간 매핑은 0xffff880000000000 이상의 범위에 흩어져 있다. 사용자 공간이 커널을 위해 예약된 주소 공간을 어느 정도 확인할 수는 있지만, 실제로 해당 메모리에 접근할 수는 없다.

이 매핑 방식은 과거에 문제를 일으켰다. 예를 들어 32비트 시스템에서는 각 프로세스의 주소 공간 총 크기를 3GB로 제한한다. 커널의 관점에서 문제는 더 심각할 수 있는데, 커널은 직접적으로 1GB 이하의 물리적 메모리만 접근할 수 있기 때문이다. 커널이 이보다 더 많은 메모리를 사용하려면 복잡한 “고메모리” (High- memory) 메커니즘을 구현해야 했다. 32비트 시스템은 이러한 대량의 메모리(20세기의 관점에서 “대량”)를 사용하는 데 적합하지 않았지만, 커널을 사용자 공간에 매핑한 것은 문제를 더 악화시켰다.

그럼에도 불구하고, 이 메커니즘은 단순한 이유로 지속되었다: 이를 제거하면 시스템이 상당히 느려질 것이다. 커널을 영구적으로 매핑하면 사용자 공간과 커널 공간 사이를 전환할 때 프로세서의 TLB (Translation lookaside buffer)를 플러시할 필요가 없으며, 커널 공간에 대한 TLB 항목을 플러시하지 않아도 된다. TLB를 플러시하는 것은 여러 가지 이유로 비용이 많이 드는 작업이다: TLB를 다시 채우기 위해 페이지 테이블에 접근해야 하는 것은 물론, 플러시 자체가 느리기 때문에 비용의 큰 부분을 차지할 수 있다.

2003년으로 돌아가면, 인고 몰나르(Ingo Molnar)는 사용자 공간과 커널 공간이 각각 전체 4GB 주소 공간을 갖고, 문맥 전환마다 프로세서가 이를 전환하는 다른 메커니즘을 구현했다. lwn.net 이러한 “4G/4G” 메커니즘은 일부 사용자에게 문제를 해결해 주었고, 일부 배포판에서 이를 도입했으나, 관련 성능 비용으로 인해 메인라인 커널에는 포함되지 않았다. 이후로 두 주소 공간을 분리하자는 제안은 심각하게 고려되지 않았다.

Rethinking the shared address space

현대의 64비트 시스템에서는 주소 공간이 더 이상 가상 메모리의 양을 크게 제한하지 않지만, 보안과 관련된 또 다른 문제가 있다. 시스템을 강화하는 중요한 기술은 커널 주소 공간 레이아웃 랜덤화(Kernel Address Space Layout Randomnization)(KASLR)로, 부팅 시 가상 주소 공간에서 커널의 위치를 무작위로 배치한다. 공격자가 메모리에서 커널의 위치를 알지 못하도록 함으로써 KASLR은 많은 유형의 공격을 상당히 어렵게 만든다. 커널의 실제 위치가 사용자 공간으로 유출되지 않는 한, 공격자는 어둠 속에서 더듬거리게 된다.

문제는 이 정보가 여러 방법으로 유출된다는 것이다. 이러한 누수 (Leak) 중 많은 부분은 커널 주소가 민감한 정보가 아니었던 더 단순한 시절로 거슬러 올라간다; 실제로 2003년에 편집자가 그러한 누수를 하나 소개하기도 했다. kernel git log 당시에는 해당 정보를 노출하는 것에 대해 아무도 걱정하지 않았다. 최근에는 커널로부터의 직접적인 누수를 차단하기 위한 일관된 노력이 이루어졌지만, 하드웨어 자체가 커널의 위치를 공개한다면 그러한 노력은 거의 도움이 되지 않을 것이다. 그리고 실제로 그런 일이 일어나고 있는 것으로 보인다.

다니엘 그루스(Daniel Gruss et al.) 등은 [PDF]에서 KASLR에 대한 하드웨어 기반의 여러 공격을 인용하고 있다. 이들은 오류 처리에서의 시간 차이를 악용하거나, 프리페치 명령의 동작을 관찰하거나, 인텔 TSX(트랜잭셔널 메모리) 명령을 사용하여 오류를 강제하는 등의 기술을 사용한다. 아직 공개되지 않은 다른 채널도 존재한다는 소문도 있다. 이 모든 경우에 프로세서는 실행 중인 프로세스가 해당 위치에 실제로 접근할 수 있는지 여부와 상관없이 페이지 테이블에 대상 주소가 매핑되어 있는지 여부에 따라 메모리 접근 시도에 다르게 반응한다. 이러한 차이를 사용하여 커널이 공격을 인지하지 못하는 상태에서 커널의 위치를 찾을 수 있다.

하드웨어에서 정보 누수를 수정하는 것은 어려우며, 어떤 경우에는 배포된 시스템이 취약한 상태로 남아있을 가능성이 높다. 그러나 이러한 정보 누수에 대한 실질적인 방어책이 있다: 커널의 페이지 테이블을 사용자 공간에서 완전히 접근할 수 없게 만드는 것이다. 다시 말해, 시스템을 강화하기 위해 사용자 공간에 커널을 매핑하는 관행을 종료할 필요가 있다.

KAISER

위에서 언급한 논문은 x86-64 커널의 분리된 주소 공간 구현을 제공하며, 이를 “KAISER”라고 부른다. KAISER는 “효율적으로 부채널을 제거하기 위한 커널 주소 격리” (Kernel Address Isolation to have Side-channels Efficiently Removed : KAISER)를 의미한다. 이 구현은 메인라인에 포함되기에는 적합하지 않았지만, 데이브 한센(Dave Hansen)이 이를 받아들여 크게 수정했다.patches결과적으로 패치 세트(여전히 “KAISER”라 불림)는 세 번째 수정에 이르렀으며, 비교적 짧은 시간 안에 업스트림에 포함될 가능성이 높아 보인다.

현재 시스템에서는 각 프로세스에 대한 하나의 페이지 테이블만을 가지지만, KAISER는 두 개를 페이지 테이블을 사용하게 된다. 하나는 사실상 변경되지 않은 채로 유지되며, 커널 공간과 사용자 공간 주소를 모두 포함하지만 시스템이 커널 모드에서 실행될 때만 사용된다. 두 번째 “섀도” (shadow) 페이지 테이블은 모든 사용자 공간 매핑의 복사본을 포함하지만 커널 측을 생략한다. 대신 시스템 호출과 인터럽트를 처리하는 데 필요한 최소한의 커널 공간 매핑만이 있게 된다. 페이지 테이블을 복사하는 것이 비효율적으로 보일 수 있지만, 복사는 페이지 테이블 계층 구조의 최상위 수준에서만 이루어지므로 데이터의 대부분은 두 복사본 간에 공유된다.

프로세스가 사용자 모드에서 실행될 때마다 섀도 페이지 테이블이 활성화된다. 따라서 커널의 주소 공간 대부분은 프로세스에서 완전히 숨겨지며, 알려진 하드웨어 기반 공격을 무력화할 수 있다. 시스템이 시스템 호출, 예외 또는 인터럽트에 응답하여 커널 모드로 전환해야 할 때마다 다른 페이지 테이블로 전환된다. 사용자 공간으로 돌아가는 코드는 다시 섀도 페이지 테이블을 활성화해야 한다.

KAISER가 제공하는 방어는 완전하지 않으며, 커널 모드로 돌아가는 데 필요한 소량의 커널 정보는 여전히 존재해야 한다. 패치 설명에서 한센은 다음과 같이 썼다:

최소한의 커널 페이지 테이블은 커널로의 진입/출구 함수, 인터럽트 디스크립터(IDT), 커널 트램펄린 스택(kernel trampoline stack) 등 커널 진입/출구에 필요한 정보만 매핑하려고 한다. 이 최소한의 커널 데이터는 여전히 커널의 ASLR 베이스 주소를 노출할 수 있다. 그러나 이 최소한의 커널 데이터는 모두 신뢰할 수 있는 정보로, 사용자 제어 데이터를 포함하는 커널 매핑 (Direct kernel mapping)의 데이터보다 악용하기 더 어렵다.

패치에는 언급되어 있지 않지만, 남은 최소한의 커널 정보의 존재가 이러한 시도를 망칠 가능성이 있다면, 이는 아마도 나머지 커널과는 별도의 무작위화된 주소에 위치할 수 있을 것이다.

단일 페이지 테이블 세트를 사용하는 데 따르는 성능 문제는 여전히 존재한다. 그러나 더 최근의 프로세서는 프로세스 컨텍스트 식별자(PCID) 형태로 일부 도움을 제공한다. 이 식별자는 TLB 항목을 태그하며, TLB 조회는 실행 중인 스레드의 PCID가 일치할 때만 성공한다. PCID를 사용하면 문맥 전환 시 TLB를 플러시할 필요가 없으므로 시스템 콜 동안에 페이지 테이블 전환 비용이 상당히 줄어든다. 다행히도 커널은 4.14 개발 주기 동안 PCID에 대한 지원을 받았다. 그럼에도 불구하고 KAISER를 사용할 때는 성능 저하가 발생할 것이다:

KAISER는 시스템 콜이나 인터럽트를 수행하는 모든 작업에 성능에 영향을 미친다. 새로운 명령어 (instruction : CR3 조작)만으로도 시스템 콜이나 인터럽트에 몇 백 사이클이 추가된다. 우리가 실행한 대부분의 작업의 부하는 한 자릿수 성능 저하를 보여준다. 일반적으로 5%가 적절한 수치이다. 우리가 본 최악의 경우는 많은 시스템 콜과 컨텍스트 스위칭 (Context switching)을 수행하는 루프백(loopback) 네트워크 테스트에서 약 30%의 성능 저하를 보여주었다.

불과 얼마 전까지만 해도, 이러한 성능 저하를 가진 보안 관련 패치는 메인라인 포함이 고려되지도 않았을 것이다. 그러나 시대는 변했고, 대부분의 개발자는 강화된 커널이 더 이상 선택 사항이 아님을 깨달았다. 그럼에도 불구하고, 성능 저하를 원하지 않는 사용자를 위해 KAISER를 활성화하거나 비활성화할 수 있는 옵션, 아마도 실행 시간에 이를 제공할 수 있을 것이다.

종합해보면, KAISER는 빠르게 진행되는 패치 세트처럼 보인다. 거의 완성된 형태로 등장했고, 즉시 많은 주요 커널 개발자들의 주목을 받았다. 리누스 토르발스는 이 아이디어를 명백히 지지하고 있으며, 개선될 수 있는 사항을 지적하기는 했지만, 큰 틀에서는 지지하는 입장을 보였다. 이 코드를 병합할 시간표에 대해 공개적으로 언급된 바는 없지만, 버전 4.15에 등장하는 것이 완전히 불가능한 것은 아닐 것이다.

This post is written by david61song