본문으로 바로가기

안녕하세요, SATAz입니다.


MeltDown 취약점이 발생한지 1개월 가까이 되어가서 조금 늦은감이 있지만...

지난 포스팅에서 Meltdown에 대해 너무 광범위하게 소개를 드렸던 것 같아서, 이번에는 미시적인 관점으로 자세하게 파고들어가보려 합니다.


사실... 지난 포스팅의 KPTI 관련 항목에서... 잘못 소개해드린 내용도 있었습니다...

youtube를 너무 믿지 말고 좀 더 찾아볼걸... 하고 후회 많이했습니다...


이제 Meltdown에 대해, 제가 설명할 수 있는 최대한 상세하게 설명드리도록 하겠습니다.


목 차

1. MeltDown(멜트다운)이란 무엇인가?

 1-1 MeltDown(멜트다운) - 개요

 1-2 MeltDown(멜트다운) - 커널에 접근할 수 있는 것이 무슨 문제가 되는것인가?

 1-3 MeltDown(멜트다운) - 그 것을 이해하기 위해 알아야할 이론 (Background)

 1-4 MeltDown(멜트다운) - Process와 Memory, 그리고 Page-Table

 1-5 MeltDown(멜트다운) 취약점의 이해 - 잘못된 추측실행 (Abusing Speculative Execution)

 1-6 MeltDown(멜트다운) 공격 방식(코드)의 이해 - From Graz University of Technology

 1-7 MeltDown(멜트다운) 공격 시연 영상


2. MeltDown(멜트다운) 취약점을 막기위한 운영체제(OS)의 조치

 2-1 KPTI(Kernel Page-Table Isolate)패치의 적용

 2-2 KPTI(Kernel Page-Table Isolate)패치의 한계



1. MeltDown(멜트다운)이란 무엇인가?

 1-1 MeltDown(멜트다운) - 개요

Metldown은 Intel x86 아키텍처 컴퓨터 메모리 데이터 보안의 공격이라고 정의하고 있으며, 일반적으로 사용자 Application(응용프로그램)(해킹툴)을 통해서 운영체제의 시스템 메모리(System Memory)에 접근할 수 있는 취약점입니다.


즉, 공격자(Attacker)가 커널단에서 이루어지는 일련의 작업들을 모두 엿볼 수 있는 것이죠.



 1-2 MeltDown(멜트다운) - 커널에 접근할 수 있는 것이 무슨 문제가 되는것인가?

 응용프로그램이 CPU와 Memory 자원들을 사용하기 위해서는 항상 커널을 통해야 합니다. 심지어 평문을 암호화 하는 작업을 하는 것도 커널 단에서 이루어지게 되죠.


1234를 암호화 하기 위해서는 평문 "1234"를 커널로 보내고, 계산을 통해서 "abcd"라는 암호문이 도출이 되는 과정을 거친다고 하면... 커널에서는 평문 "1234"와 암호문 "abcd"를 둘 다 가지고 있는 격이 된다고 보시면 이해하기 쉬울 것입니다.


이 말고도 커널에서 진행되고 있는 모든 행위들이 노출되는 것이죠...

예를 들어, 우리가 컴퓨터에서 어떤 프로그램을 구동중에 있는지, 웹페이지에 로그인할 때 아이디와 비밀번호는 무엇을 쓰고있는지, 심지어 윈도우에 암호 입력 후 로그인할 때 입력하는  패스워드가 무엇인지...


그냥 이 취약점에 노출되는 PC 혹은 Server에서, 권한 없는 사용자가 어떤 작업들이 진행되고 있는지 몰래 훔쳐볼 수가 있다는 것이죠,


*여기에서 커널은 Windows 10 또는 Linux 등의 운영체제 자체라고 보시면 되겠습니다.


이제 어떠한 방식으로 취약점이 생기는지 알아보도록 하겠습니다.



 1-3 MeltDown(멜트다운) - 그 것을 이해하기 위해 알아야할 이론 (Background)

1) 메모리의 서브시스템

소프트웨어가 구동될 때 CPU Core에서는 Load 명령어를 실행시켜서 Memory에 있는 내용을 요청합니다. 아래의 그림은 CPU Core의 하위 시스템을 단순화한 그림입니다.

▲CPU Core의 하위시스템을 단순화한 그림



OS를 포함한 소프트웨어는 Load 명령을 실행하기 위해서, Virtual Address를 Physical Address로 변환하는 단계를 거쳐야합니다. (이는 CPU 내부에 있는 메모리에서 일어나는 읽기 작업을 의미합니다.)


Process의 첫 번째 단계는 L1 Cache(이하 캐시) 에서 시작합니다. L1 Cache는 Data와 Instruction Cache로 분리되어있습니다.

L1 캐시는 VIPT(Virtual Indexed, Phisically Tagged cache)라고도 불리는데 VIPT는, Load 요청에 필요한 Virtual Address를 CPU가 직접 사용하여 데이터를 조회할 수 있다는 것을 의미합니다.


이는 곧 L1캐시에서 DRAM의 데이터를 일부 저장하고 있다는 것을 의미하며, 덕분에 코어의 처리 속도를 빠르게 만들어줍니다.

만약 CPU가 Load 명령을 수행하는데 필요한 데이터를, L1 캐시에서 찾을 수 없을 경우에는 DRAM에다가 데이터(내용)를 요청합니다. 

<*참고: CPU의 캐시는 속도가 무척 빠른 대신 용량이 작습니다. 따라서 우리는 PC에다가 RAM을 장착하는 것이죠. CPU 캐시를 8G까지 설계할 수 있다면, 우리는 RAM을 장착할 필요가 없을 것입니다. 만약 그런 CPU가 만들어진다면 가격은 어마어마하겠죠.>


DRAM에서 특정 메모리 주소의 데이터를 요청하는데, 위 그림의 계층 구조를 따라서 요청이 전달됩니다. 이 때 Page Table이라는 것을 사용합니다. 


Page Table은 Virtual Address를 Physical Address로 변환하는데 사용되며, 변환 과정에서는 보호비트를 활용하여 정당한 권한인지 확인을 합니다. 정당한 권한으로 Physical Address를 획득하면, L1캐시는 그 주소에 있는 데이터를 L2캐시와 L3캐시 순서로 질의(Query)할 수 있습니다.


<*주의! Page Table은 각각의 프로세스가 따로따로 갖고있습니다. 하나의 Page Table을 Kernel Process나 또다른 User Process가 함께 사용하는 것이 아닙니다.>




2) PipeLine (파이프라인) & Speculative execution (추측 실행) & 비순차적 명령어 실행 (Out-of-Order Execution, OoOE)

아래의 나무위키에서 설명하는 글을 참고 부탁드립니다.



 1-4 MeltDown(멜트다운) - Process와 Memory, 그리고 Page-Table 

하나의 메모미를 단순하게 도식화한 그림. 각각의 프로세스별로 일정 영역을 할당받아 사용하고있다.

PageTable에서 가상->물리 메모리 주소를 매핑하고있는 모습을 단순하게 도식화한 그림    





 일반 PC 혹은 Server의 메모리는, 각각의 프로세스들(Kernel 및 User Process들...)에게 일정 범위의 영역을 할당하게 됩니다. 그리고 Kernel과 User Process들은 각각의 Page Table을 갖고있습니다.  (*위의 그림에서, Kernel 프로세스와 ABC.exe, DEF.exe, GHI.exe 프로세스들은 각자 자기만의 Page Table을 갖고 있다는 의미입니다.)


위의 그림을 예를들어, 일반 User Process(ABC.exe) 할당받은 자기 주소만(0x1233 3242 ~ 0x1237 3427)을 Page Table에 Mapping하고 있다면 아무 문제가 없었겠지만, 시스템 호출(syscall)등의 요청이 필요하여 Kernel Process 영역의 메모리주소 정보를 알아야 합니다.


그래서 일반 User Process (ABC.exe)의 PageTable에도 Kernel 프로세스의 Virtual & Physical Address Mapping 정보를 함께 갖고있게 되었습니다.

<*정리 : 일반 User Process의 Page Table에는 Kernel Process의 메모리주소가 들어있다>


하지만 Kernel Process의 Memory 주소는 보호비트에 의해 보호되기 때문에, 일반 User Process에서 Kernel Process 영역의 Memory 주소를 직접적으로 요청할 수 없습니다. 만약에 일반 User Process에서 커널 메모리 주소를 직접 요청할 수 있다면, 멜트다운 취약점도 필요 없이 바로 해킹이 가능했을 것입니다.


자, 이제 Metldown 취약점이 어떠한 방식으로 Kernel Process의 메모리 주소를 알아내는지 확인해보도록 합시다.




 


1-5 MeltDown(멜트다운) 취약점의 이해 - 잘못된 추측실행 (Abusing Speculative Execution)

*해커가 MeltDown 취약점을 공략하기 위해 작성한 코드를 ABC.exe라고 가정하겠습니다.

만약에 ABC.exe 코드가 아래의 명령이 실행되었을 경우를 상상해보겠습니다. <ABC.exe는 일반 User Process임을 명심해야함>


1    mov rax,[somekernelmodeaddress]


위의 명령은 실행될 것입니다. 하지만 위에서도 말했다싶이 Kernel Memory Address는 보호비트에 의해 막혀있기 때문에, 이 명령은 CPU Register에 절대로 Commit(완료)되지 않고 인터럽트가 발생할 것입니다. 


Commit 되지 않는다는 것은 우리 사람들이 읽을 수도 없고, 읽을 필요가 없다는 것을 의미합니다. (정리 : 명령을 실행하긴 하지만, 사용자에게 보여주지는 않는다.)


하지만 Intel이 Tomasulo의 알고리즘에 의존하고 있기 때문에 읽을 가능성이 있습니다. 그 가능성을 확인하기 위해 아래의 추가적인 추측성(Speculative) 코드를 추가해보도록 하겠습니다.


1    mov rax, [Somekerneladdress]

2    mov rbx, [someusermodeaddress]


#1 명령은 Kernel Memory 주소를 직접 요청하기 때문에 인터럽트가 발생합니다. 

#1 명령은 인터럽트가 발생하여 폐기가 될 예정이므로, #2 명령도 Register에 Commit하지 않습니다. 

하지만 예측 실행(Speculative Execution)으로 #2 명령이 실행은 됩니다. (정리 : #2 명령도 실행하긴 하지만, 사용자에게 보여주지는 않는다.)


비록 Commit되지는 않지만 명령 실행은 되기 때문에, #2 명령의 실행결과는 캐시계층으로 Load됩니다.

캐시는 우리의 DRAM(주메모리)보다 빠릅니다.

'someusermodeaddress'주소가 캐시에 로드 되기 때문에, 읽는 속도는 RAM에서 읽어오는 것에 비해 엄청 속도가 빨라지게 될 것입니다.


여기에서 Intel의 Meltdown 취약점의 원리가 나오게 됩니다.

1) 명령은 Commit되지 않지만, 일단 실행은 한다.

2) 인터럽트가 발생하면 그 뒤에 있는 명령 실행들까지 다 취소해야하는데, 추측실행으로 일단 실행은 하고본다.

3) 추측 실행에 의해 실행된 명령의 결과값('someusermodeaddress')은, CPU의 L1캐시에 저장한다.

4) 메모리의 주소들을 하나하나 다~ 읽어오다보면 빠르게 접근되는 값이 있을텐데, 그 값은 'someusermodeaddress'일 것이다.


이러한 이야기를 바탕으로, 아래와 같이 두 줄의 코드를 추가해보겠습니다.


1    mov rax, [Somekerneladdress]

2    and rax, 1

3    mov rbx, [rax+someusermodeaddress]


이 전의 코드와 같이 #1 mov 명령으로 걸린 인터럽트 때문에 #1, #3 라인의 명령은 commit 되지는 않습니다. 하지만 예측실행 기능의 영향으로 #1, #3 라인 명령은 실행됩니다.


그리고 그 결과는 캐시에 저장됩니다. 이 후 rax+someusermodeaddress을 알아내기 위한 작업을 거치는데, 메모리 주소들을 하나하나 읽어들이다보면 빠르게 읽혀지는 주소가 있을 것입니다. 그 빠르게 읽혀지는 주소에 'someusermodeaddress' 값을 빼면 rax가 무슨 값을 갖고있었는지 알 수 있게되죠.


사실, 위의 내용만으로는 이해하기 어려우실 것입니다. 저도 그랬으니까요...

이제 Graz 기술대학에서 발표한 논문에 나와있는 유명한 코드로 설명을 드려보도록 하겠습니다.



 1-6 MeltDown(멜트다운) 공격 방식(코드)의 이해 - From Graz University of Technology


1     raise_exception();

2     // the line below is never reached

3     access(probe_array[data * 4096]);

항목 1 : 추측성 실행(speculative execution)에 의한 비순차적 실행 (out-of-order execution)으로 설계된 부 채널(side-effects) 예제


1     ; rcx = kernel address

2     ; rbx = probe array

3     retry:

4     mov al, byte [rcx]

5     shl rax, 0xc

6     jz retry

7     mov rbx, qword [rbx + rax]

항목 2 : Meltdown의 핵심 명령 시퀀스 (코드)


*상황 1 : 해커는 커널 메모리주소 rcx에 들어있는 '비밀 값 X'를 알아내고 싶어합니다.

*상황 2 : probe array(일반 user process가 자유롭게 접근할 수 있는 영역)의 크기는 256 * 4096 bytes입니다. 

<256=page 길이 / 4096 : page 1개당 크기 (bytes)>

<즉 user process가 접근 가능한 메모리 공간은 1,048,576 byte이며, 이는 곧 256개의 페이지를 사용할 수 있다는 것을 의미합니다.>



1) 첫 째로, L1 캐시에 있는 값을 모두 날려야 합니다. Flush+Reload 작업을 통해서 L1 캐시에 남아있는 페이지드를 깡그리 날려버립니다.


    mov al, byte [rcx]


2) 그리고 #4 명령이 실행되면서, 커널 메모리 주소 rcx에 있는 '비밀 값 X'(이하 'X)를 rax 레지스터에 저장합니다. 하지만 rcx는 커널 메모리 주소이기 때문에, 잘못된 권한(일반 user process)의 접근이기 때문에, 명령 실행 이후에 exception(예외) 인터럽트가 발생됩니다.

3) 인터럽트에 의해서 #5~#7 명령이 모두 취소되어야 하지만, 추측 실행(speculative execution)에 의해서 #5~#7명령도 결국 실행됩니다. (Commit되지는 않기 때문에 해커에게 직접적으로 보여주지는 않습니다.)


    shl rax, 0xc


4) #5 명령이 실행되면서, 'X'에다가 4096을 곱합니다.

<'X' = 레지스터 rax에 들어있는 비밀 값 / 4096 = page 1개당 크기 (bytes)>

<16진수 0xc = 10진수 12 >

<'비밀 값 X'를 2진수로 변환하고, 뒤에다가 0을 12개 붙이라는 소리입니다.>

<2진수 0을 12개 붙이면 4096을 곱한 값과 같습니다. 2진수는 정말 대단합니다...>


7     mov rbx, qword [rbx + rax]


5) #7 명령이 실행되면서, probe array의 시작주소 + ('X' * 4096) 계산을 해서 rbx에 저장을 합니다. 

왜 이런 짓을 하냐구요? 말을 아래와 같이 바꿔보도록 하겠습니다.


probe array의 시작주소 = 내 프로세스의 0번째 페이지

'X' * 4096 = 'X'번째 페이지


위와 같이 바뀐 말로 다시 #7 명령을 설명드리자면 결국, 'X'번째 페이지의 내용을 rbx에 저장하라는 의이기도 합니다.

<<'X' 번째 페이지의 메모리 주소는 곧 'X' * 4096'이라는 것을 의미합니다.>>


6) rbx 레지스터에 저장하라는 #7명령에 의해서, 'X'번째 페이지<메모리주소 = { rbx+('X' * 4096) }>의 내용이 L1 캐시에 올라가게 되었습니다.


    access(probe_array[data * 4096]);


7) 해커는 자기 process의 0~255번째 페이지를 하나하나 Access해봅니다. ( 0~266 = 156개 = probe array의 page 길이) 그런데 유난히도 빠르게 접근되는 녀석이 1개가 있습니다. 바로 L1 캐시에 load되어있는 'X'번째 페이지에 있는 내용만은 빠르게 접근이 가능했습니다.


아래의 메모리 주소로 접근 요청을 하지 않았을까 싶습니다.

[rbx + 0 * 4096], [rbx + 1 * 4096], [rbx + 2 * 4096][rbx + 3 * 4096][rbx + 4 * 4096], ... [rbx + 255 * 4096]

▲0~255 번째 페이지를 모두 Load했을 때의 Access 속도를 나타낸 그림

8) 위의 그래프에서 제일 빠른 접근이 가능한 페이지는 84번째 페이지였습니다. 이렇게 '비밀 값 X'는 84였다는 것을, Access 속도를 통해 확인할 수 있습니다. 


'X'번째, 즉 84번째 페이지<메모리주소 = { rbx+('84' * 4096) }>의 내용이 무엇이 들어있는지는 중요하지가 않습니다. 그냥 빠르게 접근 가능한 메모리 주소가, 우리가 찾고있는 값이었던 것이죠.


이해하기 어려우실 것이라 생각합니다. 그에 맞추어 비유를 잘 설명해놓은 글을 인용하도록 하겠습니다.




 1-7 MeltDown(멜트다운) 공격 시연 영상







<영상 출처 :: https://meltdownattack.com/ >




2. MeltDown(멜트다운) 취약점을 막기위한 운영체제(OS)의 조치

 2-1 KPTI(Kernel Page-Table Isolate)패치의 적용

 MeltDown 취약점을 막기위한 조치로, 각 운영체제 벤더사들은 각종 패치를 내놓았습니다. 그 중 리눅스에서는 KPTI (Kernel Page Table Isolate)라는 패치를 적용하였는데요, 사실 이 패치도 임시방편일 뿐입니다. 애초에 Meltdown은 Intel CPU의 추측실행과 비순차명령실행 등과 같은 설계적인 오류에서 발생한 문제이기 때문이죠. (Windows도 비슷한 방식의 패치가 적용되었습니다.)


1) KPTI(Kernel Page-Table Isolate)의 개요

KPTI(Kernel Page-Table Isolate)는 Meltdown 취약점을 보완하기 위해서 리눅스 커널에 적용된 기술입니다. 이름 그대로 '커널의 페이지 테이블을 분리'하는 기술이죠. 


KPTI는 2016년에 고안된 KAISER(Kernel Address Isolation to have Side-channel Efficiently Removed)를 기반으로 개발된 기술입니다. 그리고 2017년(Meltdown이 알려지기 전)에 공개되었습니다.


2) KAISER(Kernel Address Isolation to have Side-channel Efficiently Removed)의 개요

 KAISER를 설명드리기 위해서는 KASLR(Kernel Address Space Layout Randomization)기술을 먼저 설명드려야할 것 같습니다.


2014년 KASLR 기술이 만들어졌고 이 기술은 KPTI와 비슷하게, 일반 user process들에게 kernel address mapping을 감추기 위해서 만들어졌습니다. 그리고 실제 리눅스 커널에 적용되었죠. 

 

하지만 시간이 흘러....

KASLR으로 돌아가는 시스템에서 Kernel Address mapping을 숨겼지만, 특정 메모리 주소의 내용을 노출시키는 부 채널 공격(side-channel attack)이 가능하다는 것이 밝혀졌습니다. 그래서 메모리 주소의 내용을 보호하기 위해 만들어진 기술이 KAISER입니다.


KASLR은 그냥 Memory Address Mapping이 누출되는 것을 막는 것에 반해, KAISER는 데이터 유출을 막을 수 있다는 특징을 갖고 있습니다. 이는 곧 Meltdown 취약점을 보완할 수 있다는 것도 함께 의미합니다.


3) KPTI(Kernel Page-Table Isolate)기술에 대한 설명

앞선 1-4 항목에서도 설명드렸다싶이, 각각의 process들은 각자의 Page Table을 갖고 있습니다.

KPTI를 사용하지 않던 기존에는 User Process가 사용하는 Page Table에도 모든 Kernel Address가 매핑되어있었습니다.


Kernel Address를 모두 매핑시켜놓았다는 부분에서, User Application이 System을 호출하거나 인터럽트가 수신되면 바로바로 Kernel에서 데이터를 꺼내서 쓸 수 있기 때문에 부하(overhead)(TLB Flush, Page Table Swapping, etc...)를 줄일 수 있다는 장점이 있었는데...


속도가 빠른(=성능이 좋은) 대신에 Kernel Memory의 정보가 유출된다는 결과(MeltDown)를 초래하게 되었던 것이죠.


▲KPTI 적용 전 -> KPTI적용 후를 비교한 그림


KPTI는 System Process냐, User Process냐에 따라서, Page Table에서 매핑시켜주는 Kernel Address를 다르게 제공합니다.


특징 1) System Process들에게는 기존과 동일하게, Page Table에서 모든 Kernel Address Mapping 정보를 제공해준다.

특징 2) 하지만 일반 User Process들에게 syscall, interrupt, exception 등을 입력하는데 필요한 최소한의 Kernel Address Mapping을 제공합니다.


User Process의 Page Table은 최소한의 Kernel Address Mapping 정보를 받기 때문에, Kernel 메모리의 정보 유출을 막을 수 있게 되었습니다.


 2-2 KPTI(Kernel Page-Table Isolate)패치의 한계

1) CPU 설계의 문제인데 왜 OS에서 패치를 적용하나?

 애초에 Meltdown 취약점은 CPU의 비순차 명령 실행(Out-of-Order Execution)에 의한 설계 오류로 인해 발생하였습니다. 마이크로 op에서 생긴 문제를 아무리 OS에서 보완한다고 해도, 언젠가는 그와 비슷한 취약점이 또 다시 발견될 것입니다.


2) KPTI가 적용되면 속도가 느려진다고?

 게다가 KPTI 기술을 적용하면 User Process는 최소한의 Kernel Address Mapping 정보만 얻을 수 있기 때문에, 잦은 Kernel 메모리 정보의 요청이 필요한 User Process들은 성능 저하가 발생할 수 밖에는 없습니다.


KAISER를 고안해낸 사람들은 0.28%정도 속도가 저하된다고 측정하였지만, 리눅스 개발자들은 5% ~ 30%까지 속도 저하가 발생한 케이스가 있다고 합니다. 




*참고자료

meltdown and spectre - https://meltdownattack.com/

Cyberus Technology BLOG - http://blog.cyberus-technology.de/posts/2018-01-03-meltdown.html

Google Project Zero BLOG - https://googleprojectzero.blogspot.kr/2018/01/reading-privileged-memory-with-side.html

Wikipedia - https://en.wikipedia.org/wiki/Kernel_page-table_isolation

나무위키 - https://namu.wiki/w/CPU%EA%B2%8C%EC%9D%B4%ED%8A%B8


반응형