시스템 메커니즘 02 - 하드웨어 사이드 채널 취약점
시스템 메커니즘 02 - 하드웨어 사이드 채널 취약점
하드웨어 사이드 채널 취약점
최신 CPU 내부 레지스터 연산, 데이터 이동 매우 빠름(피코초 단위)
- 레지스터는 희소 자원임
- 운영체제와 애플리케이션 코드는 항상 CPU에게 데이터를 레지스터에서 주 메모리로 이동하게 지시
- 반대도 마찬가지
캐시(Cache)
- CPU 내부에 위치해 직접적으로 액세스 가능한 메모리
- 비용이 비싸지만 빠른 속도
램(RAM)
- CPU에서 외부 버스를 통해 액세스 가능한 메모리
- 느리지만 비용이 저렴하고 용량이 큼
메모리 계층
- 메모리 위치에 따라 정의됨
- CPU에 가까울수록 더 빠르고 용량이 작음
최신 CPU는 코어에 직접 엑세스 가능한 3 계층의 캐시 메모리 운용
- L1, L2 캐시
- 코어에 가장 가깝고 각 코어가 개별적 소유
- L3 가장 멀리 있고 모든 CPU 코어 간 공유
- 임베디드의 경우 L3 캐시가 존재하지 않음
캐시의 주요 특징
- CPU의 레지스터와 비슷한 액세스 시간
- 여전히 레지스터보단 느리지만
- 메인 메모리의 액세스 시간은 100배 가량 더 느림
- 메인 메모리의 액세스는 상당한 성능 저하
메인 메모리의 액세스 저하를 해결하기 위해 다양한 정책을 수행
- 이런 정책에 따라 사이드 채널 공격(side-channel attacks)가 만들어짐
- 스펙컬레이티브(Speculative) 공격이라고도 함
- 엔드 유저의 보안성을 무력화하는 데 있어 효과적인 공격
비순차적 실행
최신 마이크로프로세서는 파이프라인(pipeline)을 활용하여 기계어를 실행
- 파이프라인의 단계
- 명령을 가져오고 해독
- 레지스터 할당과 이름 변경
- 명령 재배치
- 실행과 종료 등
- 메모리 성능 저하 문제 해결을 위한 대표적인 정책
- 실행 엔진이 자원이 있을 때마다 순서와 상관없이 명령을 수행하게 함
- CPU 코어를 최대한 활용하고자 커밋되거나 필요한 명령이 확실해질 때까지 수백개의 명령을 예상해 실행 가능
비순차적 실행(out-of-order execution)의 문제
- 분기 명령(branch instruction)과 관련
- 기계어 코드에서 2가지의 실행 흐름을 만듬
- 올바른 실행 흐름은 이전에 실행한 명령에 따라 결정됨
- 조건 계산의 경우 느린 램 메모리에 접근하는 이전 명령에 따라 성능 저하 발생 가능
- 다음 올바른 명령의 비순차적 실행을 계속하기 전 조건을 정의하는 명령이 끝날 때까지 대기해야 함
- 즉, 메모리 버스가 메모리 접근을 완료할 때까지 대기
- 비슷한 문제로 간접 분기(indirect branch)에도 있음
- 기계어 코드에서 2가지의 실행 흐름을 만듬
- CPU 실행 엔진은 분기의 대상을 알 수 없음
- 보통 jump와 call 명령
- 해당 주소를 메인 메모리에서 가져와야 함
- 추측 수행(speculative execution)이라고도 함
- CPU의 파이프라인이 다양한 명령을 병렬적으로 순서 없이 해석하고 수행하는 것을 의미
- 그러나 결과가 레지스터에 저장되지 않고 메모리에 데이터를 쓰는 동작은 분기 명령 수행 전까지 보류됨
CPU 분기 예측기
어떤 분기 조건이 판단되기 전 CPU가 분기될 실행 흐름을 알 수 있는 방법
- CPU 패키지에 포함된 분기 예측기와 분기 대상 예측기
분기 예측기
- 분기가 완전히 결정되기 전에 미리 예측할 수 있게 함
구체적인 구현(CPU마다 다를 수 있음)
- 분기 대상 버퍼(BTB, Branch Target Buffer)라는 내부 캐시
- 인덱싱 함수를 통해 생성된 주소 태그를 사용하여 분기 목적지의 주소를 기록
- 과거 조건 분기에 대한 기록임
- 처음 분기 명령이 실행될 때 대상 주소가 여기에 저장됨
- 보통 첫 실행에서는 실행 파이프라인이 멈추고, CPU는 조건 또는 대상 주소를 메인 메모리에서 가져오고자 대기됨
- 같은 분기가 두 번째 실행될 때 BTB에 저장된 대상 주소가 사용되여 예측된 대상을 파이프라인으로 가져옴
- 인덱싱 함수를 통해 생성된 주소 태그를 사용하여 분기 목적지의 주소를 기록
분기 예측 실패(branch misprediction)
- 예측이 틀렸을 경우 추측적 실행 결과가 폐기됨
- 다른 경로가 CPU 파이프라인에 들어가고 올바른 분기의 실행이 재개됨
- 낭비되는 CPU 사이클은 분기 조건을 기다리는 것보다 나쁘지 않음
- CPU 캐시에 흔적이 남는다는 취약점이 있음
CPU 캐시
캐시 라인(cache lines)
- 메모리와 캐시 사이의 데이터 이동에 정해진 블록 단위 크기
캐시 엔트리
- 캐시 라인이 메모리에서 캐시에 복사될 때 만들어지는 것
- 복사된 데이터와 요청된 메모리를 구분하는 태그 정보를 가지고 있음
- 분기 대상 예측기와는 다르게 물리 메모리 주소로 찾음
- 이렇게 하지 않으면 주소 공간의 변화를 처리하기가 복잡함
- 분기 대상 예측기와는 다르게 물리 메모리 주소로 찾음
캐시는 주어진 물리 주소를 쪼개어 구분함
- 상위 비트는 태그
- 이는 메모리 주소가 속한 캐시 블록을 유일하게 구분하는 구분자
- 하위 비트는 캐시 라인과 라인 내의 오프셋
CPU가 메모리를 읽는 과정
- 메모리를 읽거나 쓸 때 일치하는 캐시 엔트리를 체크함
- 대상 메모리의 데이터를 가지는 캐시를 찾았다면 캐시 히트가 발생
- 즉시 캐시 라인에서 데이터를 읽거나 쓸 수 있음
- 그렇지 않다면 캐시 미스가 발생
- 새로운 엔트리를 캐시에 만들고 캐시에서 데이터를 얻을 수 있도록 메인 메모리의 데이터를 복사함
CPU가 새로운 내용을 메모리 주소에 쓰기 명령을 받음
- 메모리와 대응되는 캐시 라인을 갱신함
- 그 후 일정 시점에 메모리 페이지에 적용된 캐시 정책에 따라 다시 RAM에도 쓰게 됨
- 캐시 정책(Write-back, Write-through)
- 캐시 일관성 프로토콜
- 주 CPU가 캐시 블록을 갱신한 이후 최신이 아닌 데이터를 사용하지 않도록 보장
캐시 미스(Cache Miss)가 발생한 경우
- 새로운 엔트리의 공간을 위해 CPU는 존재하는 캐시 블록을 밀어냄
- 이런 동작은 배치 정책(placement policy)에 따라 수행됨
배치 정책에 따른 캐시 종류
- 직접 매핑(Direct-mapped) 캐시
- 배치 정책이 특정 가상 주소에 대해 하나의 블록만 대체할 수 있음
- 전체 연관(Full-associative) 캐시
- 어떤 캐시 항목이든 새로운 데이터를 갖고 있을 수 있게 하는 방식
- 캐시 항목이 같은 블록 번호를 갖고 있어야 함
- 어떤 캐시 항목이든 새로운 데이터를 갖고 있을 수 있게 하는 방식
- N 방향 집합 연관(N-way Set Associative) 캐시
- 위 2가지 방법의 중간
- 대부분의 캐시가 이 방식을 채택함
- 방향은 캐시를 나눈 것을 의미함
- 각각은 같은 용량을 가지고 같은 방식으로 찾음
- 위 2가지 방법의 중간
4-Way Set Associative Cache
- 4개의 물리 주소가 가리키는 데이터를 4개의 같은 캐시 라인에 각각 저장 가능함
- 물론 캐시 라인의 태그는 다름
사이드 채널 공격
CPU 캐시의 구조적인 취약점을 이용함
- CPU의 실행 엔진은 명령이 실제 처리될 때까지 연산 결과를 기록하지 않음
- 여러 명령이 비순차적으로 실행되어도 레지스터와 메모리에 실제 영향을 미치지 않아도 캐시에는 구조적인 취약점이 있었음
- 비순차 실행 엔진과 분기 예측기에 대한 공격 방식
- 가장 파괴적이고 효과적인 하드웨어 사이드 채널 공격 2가지
- 멜트다운(Meltdown)
- 스펙터(Spectre)
멜트다운
악의적 데이터 캐시 로드(RDCL, Rogue Data Cache Load)라고도 함
- 악성 유저 모드 프로세스가 모든 메모리를 읽는 것이 가능하게 함
- 액세스 불가능한 커널 메모리에 액세스도 가능케 함
- 비순차 실행 엔진이 공격 대상
- 메모리 액세스와 메모리 액세스하는 명령의 권한 검사 사이의 경쟁 조건을 이용
멜트 다운의 공격 방식
- 악성 유저 모드 프로세스가 캐시 전체를 플러시함으로 시작
- 이 명령은 유저 모드에서도 호출 가능함
- 금지된 커널 메모리 액세스 명령 후에 의도된 캐시를 채우는 명령을 붙여 실행
- 프로브 배열(probe array)를 사용
- 원래 프로세스는 커널 메모리에 액세스할 수 없으므로 프로세서에 의해 예외 발생
- 예외는 애플리케이션에 의해 인지되어 처리되지 못한다면 프로세스는 바로 종료됨
- 그러나 비순차 실행 방식에 때문에 CPU는 이미 액세스 금지된 커널 메모리에 액세스하는 명령을 수행했고 캐시에 흔적이 남아 있음
- 하지만 실제 반영되지 않은 상태
- 따라서 RAM과 CPU 레지스터를 통해서는 알 수 없음
- 그러나 비순차 실행 방식에 때문에 CPU는 이미 액세스 금지된 커널 메모리에 액세스하는 명령을 수행했고 캐시에 흔적이 남아 있음
- 악성 애플리케이션은 전체 캐시를 탐색함
- 이때 캐시를 채우는 데 걸리는 시간을 측정
- 이 시간이 특정 기준보다 짧다면 캐시에 데이터가 존재하는 것
- 공격자는 이것을 활용하여 커널 메모리의 정확한 데이터를 읽어낼 수 있게 됨
공식 멜트다운 연구 문서에서 인용한 내용
- 1MB 배열 탐색에 대한 액세스 시간을 보여줌
- 1개의 페이지를 제외하고 각 페이지 접근 시간이 유사함
- 비밀 데이터를 한 번에 한 바이트씩 읽을 수 있고 한 바이트에 256개의 값만 있을 수 있다 가정
- 캐시 히트가 일어난 정확한 페이지를 알아낼 수 있음
- 공격자가 커널 메모리 어느 곳에 비밀 데이터가 있는지 알 수 있게 됨
스펙터
멜트다운과 유사함
- 역시 비순차 실행 방식의 약점을 이용
- 스펙터의 주 공격 대상은 분기 예측기와 분기 대상 예측기
스펙터 공격 방식
- 공격 개시 단계에서 공격자는 권한이 낮은 프로세스(공격자의 프로세스)에서 CPU 분기 예측기가 잘못된 학습을 하도록 반복적 연산 수행
- 두 번째 단게에서 공격자는 높은 권한을 가진 애플리케이션이 잘못된 예측 분기로 명령 에측을 수행하게 만듬
- 이때 수행하는 명령은 공격 대상이 되는 프로세스의 중요한 정보를 마이크로아키텍처 채널로 옮김
- 보통 CPU 캐시
- 이때 수행하는 명령은 공격 대상이 되는 프로세스의 중요한 정보를 마이크로아키텍처 채널로 옮김
- 마지막 단계에서 공격자는 권한이 낮은 프로세스로부터 CPU 캐시에 담긴 중요한 정보를 얻어냄
- 이때 전체 캐시를 탐색하는 작업을 수행
- 멜트다운 공격의 방식을 그대로 사용
- 결과적으로 피해자의 비밀 데이터를 얻게 됨
- 이때 전체 캐시를 탐색하는 작업을 수행
스펙터 공격의 첫 번째 변종
- 위의 방식으로 공격 대상 프로세스 메모리 상의 비밀 데이터를 얻어냄
- CPU 분기 예측기가 잘못된 분기를 하도록 유발함으로 가능
- 메모리상 버퍼의 범위 검사 함수에서는 보통 분기 동작을 포함
- 버퍼가 비밀 데이터와 근접해있고 공격자가 분기 오프셋을 제어 가능하다고 했을 때
- 공격자는 반복적으로 분기 예측기를 학습시켜 CPU가 해당 분기로 수행하게 함
- 그 후 공격자는 CPU 캐시를 잘 정의된 방식으로 준비
- 범위 검사 시 사용되는 메모리상의 버퍼 크기 정보와 같은 것은 캐시에 있으면 안됨
- 범위 검사를 하는 분기문에서 에러가 발생할 오프셋을 제공했다 가정
- CPU 분기 예측기는 예측된 실행 흐름을 따라가게됨
- 그러나 흐름이 틀림
- 명령은 범위를 넘어서는 데이터에 액세스하게 되고 그곳에는 비밀 정보가 담겨 있음
- 결과적으로 공격자는 전체 캐시를 탐색하므로 비밀 데이터를 얻어낼 수 있게 됨
- 멜트다운 공격과 비슷
스펙터 공격의 두 번째 변종
- CPU 분기 대상 예측기를 악용
- 간접 분기문을 공격함
- 공격자에 의해 잘못 학습된 간접 분기문은 공격 대상 프로세스의 임의 메모리 데이터를 읽어낼 수 있음
- 잘못된 분기 대상을 학습시키고 BTB를 이용해 추측을 실행할 때 잘못된 위치로 분기하게 만듬
- 공격 대상 메모리상에서 분기 대상 메모리는 가젯(Gadget)을 가리켜야 함
- 가젯 - 비밀 데이터에 액세스할 수 있고 캐시에 저장할 수 있게 하는 명령
- 공격자는 간접적으로 공격 대상 프로세스가 있는 한 개 이상의 메모리나 레지스터의 내용을 조정할 필요가 있음
- 보통 API가 신뢰할 수 없는 입력 데이터를 받아들였을 때 가능해짐
- 공격자는 분기 예측기를 학습시킨 뒤 CPU 캐시를 플러시하고 권한 높은 서비스를 수행
- 서비스를 구현한 프로세스는 공격자에게 제어되는 프로세스와 유사한 간접 분기문이 구현되어 있어야 함
- CPU 분기 예측기는 잘못된 주소에 있는 가젯을 실행하게 됨
- 첫 번째 스펙터 방식과 멜트다운에서처럼 CPU 캐시에 문제를 일으킴
- 그 결과로 낮은 권한에서도 데이터를 읽는게 가능해짐
기타 사이드 채널 공격
추측성 저장소 우회(SSB, Speculative Store Bypass)
- CPU 명령 최적화로 인해 발생
- 이전의 저장 작업에 의존하지 않고 평가한 로드 명령의 결과가 처리될 때까지 대기하지 않고 예상해서 실행할 수 있게함
- 예측이 틀렸다면 로드 명령은 연산이 오래된 데이터를 읽게 되어 중요한 정보를 저장할 가능성이 있음
- 이러한 추측 실행 중 실행된 다른 작업으로 전달될 수 있음
- 이후에 메모리에 액세스할 수 있고 마이크로아키텍처(CPU 캐시)의 잘못된 상태를 유발
- 공격자는 이 잘못된 상태 정보로 비밀 데이터를 취득함
포섀도우(Foreshadow) L1TF라고도 함
- 하드웨어 인클레이브(SGX)에서 비밀을 훔치고 일반 유저 모드에서 일반적인 공격으로 시도됨
현대 추측 실행 엔진의 2가지 하드웨어 결함을 악용
- 액세스할 수 없는 가상 메모리에 대한 예측
- PTE의 유효 비트가 없는 데이터에 액세스할 때 예외 발생
- 그러나 해당 PTE가 해석되면 CPU는 읽었던 데이터를 기반으로 추측을 수행
- 다른 사이드 채널 공격처럼 실제 명령이 반영되지는 않지만 취약점을 이용 가능
- 더 나아가 특정 상황에서는 게스트 물리 주소를 해석할 때 유효하지 않은 2단계 주소 변환 테이블 엔트리를 만나면 동일한 취약점 발생
- CPU 코어 논리 프로세서(하이퍼스레드)의 추론
- 하이퍼스레딩 환경에서는 2개의 논리 스레드가 단일 캐시를 공유함
- 하나의 논리 프로세서가 높은 권한으로 코드를 수행하는 중 다른 논리프로세서가 다른 논리 프로세서의 취약점을 이용할 수 있음
인텔 CPU는 캐시되거나 되지 않은 메모리에 액세스하기 위해 중간 버퍼를 가짐
- 그리고 마이크로 명령을 재배치함
- 마이크로아키텍처 데이터 샘플링(MDS, Microarchitectural Data Sampling) 류의 공격은 이런 중간 버퍼의 취약점을 이용함
중간 버퍼들
- 저장 버퍼(Store buffers)
- 저장 연산을 하며 내부의 임시 마이크로아키텍처 버퍼에 데이터를 씀
- CPU로 하여금 캐시나 주 메모리에 데이터를 실제로 쓰기 전 명령을 계속 수행할 수 있게 함
- 로드 연산이 같은 메모리에 있는 데이터를 대상으로 하면 프로세서는 저장 버퍼에서 데이터를 바로 가져옴
- 채우기 버퍼(Fill buffers)
- 첫 번째 단계의 캐시 미스 데이터를 모으기 위한 요소
- CPU 캐시와 CPU 비순차 실행 엔진 사이에 위치
- 이전 메모리 요청에 대한 데이터를 유지하고 해당 데이터는 추측 수행되는 로드 동작에 넘어갈 수 있음
- 로드 포트(Load ports)
- 메모리나 I/O 포트에서 로드 동작을 수행하고자 사용하는 CPU 내부 구성 요소
이런 버퍼들은 보통 하나의 CPU 코어에 속함
- SMT 스레드 사이에 공유됨
- 비밀 데이터가 이 사이에 공유될 수 있음을 의미