• Wonhyuk Yang

[Arm64] KSMA attack과 init_pg_dir 도입 분석

최종 수정일: 2021년 7월 22일


arm64 아키텍처에서는 다른 아키텍처와 다르게 부팅때 사용되는 페이지 테이블이 다른데요. init_mm의 구조체가 아래와 같이 구성되어 있어, 초기 페이지 테이블이 swapper_pg_dir인지 init_pg_dir였는지 혼동하기 쉬운 것 같아요.

init_mm 구조체는 모든 아키텍처에서 사용되는 녀석인데요, 보시다시피 pgd의 초기 값을 swapper_pg_dir으로 세팅해요.


그런데, 밑에 INIT_MM_CONTEXT 매크로가 있는데요. 이 녀석이 arm64 아키텍처일 경우 pgd를 init_pg_dir로 교체해줘요. 정말 독특하지요? 매크로의 구성은 아래와 같아요~

그러면 자연스럽게 "왜 arm64 아키텍처만 init_pg_dir이라는 초기 페이지 테이블을 사용하지?" 라는 질문이 떠오르는데요, 결론부터 얘기하면 Arm64의 KSMA(Kernel Space Mirroring Attack) 공격을 어렵게 하기 위해 도입된 것이에요.


자, 그러면 이제부터 KSMA 공격은 무엇이고, 그것을 어떻게 어렵게 만드는지에 대해 알아보도록 할게요!


KSMA

해당 공격 기법은 2018 Black hat에서 소개되었어요. KSMA는 ARM MMU feature을 이용하여, EL0(user)에서 시스템 콜 없이 커널 이미지에 Read/Write 할 수 있게 하는 방법이에요.


Arm에서 MMU가 활성화 되어있다면, 가상 주소는 아래 그림처럼 여러 단계의 변환을 통해 물리 주소로 변환되어요.

각 단계의 페이지 테이블들은 엔트리를 가지고 있고, 엔트리들은 3가지 타입으로 분류되요.

  1. Table descriptor: 다음 단계의 테이블의 주소를 저장해요.

  2. Block descriptor: 매핑된 물리 주소를 저장해요(Block 단위로)

  3. Page descriptor: 매핑된 물리 주소를 저장해요(Page 단위로)


Block와 Page descriptor 이 두 엔트리들은 매핑된 메모리에 관한 속성들을 아래와 같이 저장해요.



그 중에서 AP 필드는 Read/Write 권한을 저장해요. 즉, 매핑된 물리 주소에 Read/Write 할 수 있는지를 알려주는 속성이에요. AP가 가질 수 있는 값들은 아래의 테이블과 같아요.

이때 AP가 01일 때, Exception Level 0일 때도 Read/Write 할 수 있어요. 따라서, pgd로 사용되는 swapper_pg_dir에 AP가 01로 설정된 block descriptor를 삽입한다면 유저 영역에서 커널 영역을 마음대로 접근할 수 있는 것이지요.


아래 그림은 swapper_pg_dir에 1. 커널 이미지의 물리 주소를 매핑하고, 2. 유저 영역이 접근 가능한 새로운 block descriptor을 넣은 모습이에요.

페이지 테이블이 위와 같이 구성되면 물리 메모리 0x3002000에 접근하기 위해서, 커널 이미지의 가상 주소나 KSAM을 통한 가상 주소를 통해 접근할 수 있어요.

  • Kernel image virtual address

VA: 0xFFFFFFC03002000 -> PA: 0x3002000

EL1(Kernel)에서만 접근가능하다.

  • KSMA

VA: 0xFFFFFFC23002000 -> PA: 0x3002000

EL0(User)에서도 Read/Write 가능하다.


즉, 커널의 페이지 테이블에 엔트리를 삽입할 수 있다면, KSMA으로 커널 영역을 User 영역에서 접근할 수 있도록 만들 수 있는 것이지요.

 

Patch concept


그러면 해당 공격을 막기 위해서는 어떻게 해야할까요? 패치에서는 "공격의 대상이 되는 swapper_pg_dir을 rodata로 옮기는 것" 이 핵심 컨셉이에요.


이것을 구현하기 위해 아래와 같은 패치 시리즈가 도입되었어요.


Patch series

  1. arm64/mm: Separate boot-time page tables from swapper_pg_dir

  2. arm64/mm: use fixmap to modify swapper_pg_dir

  3. arm64/mm: move runtime pgds to rodata

Patch summary

  • 부팅 초기 단계에 수행했던 swapper_pg_dir의 역할은 init_pg_dir로 대체된다.

  • swapper_pg_dir은 한 페이지로 축소되며, rodata 섹션에 위치하게 된다.

  • init_pg_dir이 추가되어 page_init에서 새로운 pgd를 할당받지 않아도 된다.

  • swapper_pg_dir에 값을 쓰기 위해서는 fixmap에 매핑해야 한다.

Patch review

이제 각각의 패치를 살펴보도록 할게요~


1. Separate boot-time page tables from swapper_pg_dir

해당 패치에서는 기존에 swapper_pg_dir이 하던 역할을 새로 도입한 init_pg_dir이 하도록 변경해요. 그리고 swapper_pg_dir은 페이지 1개로 사이즈가 줄어들어요.

링커 스크립터에서 init_pg_dir이 swapper_pg_dir의 역할을 대체하는 것이 보이시나요? 따라서 기존에 swapper_pg_dir을 사용하던 부분은 아래처럼 전부 init_pg_dir로 교체하였어요.

그리고 당연히 정규 매핑하는 부분도 변경이 되었어요.

  • Line 9~10에서 이전의 버전에서는 정규 매핑을 하기 위해서는 임시 pgd를 할당 받고, fixmap과 매핑하여 사용할 수 있도록 했는데요, 우리는 커널 이미지에 포함되어 있는 swapper_pg_dir을 사용하면 되므로 이 부분은 삭제되었어요.

  • Line 11~15에서 우리는 swapper_pg_dir을 pgd로 사용하므로 map의 인자가 swapper_pg_dir로 교체되었어요.

  • Line 25~26에서 이전의 버전에서는 swapper_pg_dir에 새로 할당 받아 매핑 테이블을 구성한 pgd를 복사했는데, 이 패치부터는 swapper_pg_dir에 바로 매핑을 하였으므로 복사가 불필요해요.

  • Line 28: init_pg_dir로 세팅된 init_mm.pgd 값을 swapper_pg_dir로 교체하도록 해요.

  • Line 30~31: Line 9~10과 짝을 이루는 부분으로 불필요하므로 삭제해요.

  • Line 37~41: 이전과 비슷하게 페이지 테이블로 사용되지 않는 부분을 할당 해제하도록 해요.


2. use fixmap to modify swapper_pg_dir

해당 패치에서는 RDONLY 섹션에 속해질 swapper_pg_dir을 커널에서 write할 수 있도록 임시 매핑을 해주는 루틴을 추가해줘요.

set_pgd 함수는 이름 그대로 pgdp에 엔트리를 write하는 함수에요. 만약 swapper_pg_dir이 RDONLY라면 해당 write는 Page fuault를 일으키겠지요? 따라서, 해당 물리 주소를 fixmap과 매핑하여(write 속성도 포함하여) fixmap address를 접근해요. 그러면 Page fault 없이 write할 수 있겠지요? 이 내용을 구현한 것이 set_swapper_pgd 함수에요.

  • Line 4~7에서는 접근하는 pgd가 swapper_pg_dir인지 확인하고, 맞다면 fixmap과 매핑하여 write하는 set_swapper_pgd 함수를 호출해요.

  • Line 21에서 pgdp의 물리 주소를 fixmap과 매핑해요.

  • Line 22에서는 매핑한 fixmap address에 pgd를 write해요.

  • Line 28에서는 매핑한 fixmap을 unmap 해줘요.

마찬가지로, swapper_pg_dir를 접근하는 page_inint 함수도 곧바로 swapper_pg_dir을 사용하는 것이 아니라 fixmap과 매핑한 가상 주소를 사용하도록 변경했어요.

3. move runtime pgds to rodata

이제 swapper_pg_dir을 RDONLY 섹션으로 옮길 준비가 다 되었으니 링커 스크립터를 변경해서 RDONLY 섹션에 속하도록 변경해요.

변경 사항은 많아 보이지만, 사실 심볼들이 다른 섹션으로 이동했을 뿐이에요. arm64의 RDONLY 영역은 Line 4의 주석에 나온 것처럼, RO_DATA 매크로에서부터 Line 26 init_begin까지에요. 해당 패치는 idmap 페이지 테이블과 tramp 페이지 테이블, swapper 페이지 테이블들 그리고 이 외의 것들을 RDONLY 섹션으로 이동시킨 것이지요.


정리

앞서 KSMA 기법을 살펴보았고 이를 대비하기 위한 패치 시리지를 살펴봤어요. 결국 패치를 통해 swapper_pg_dir에 쉽사리 새로운 엔트리를 write할 수 없게 만든 것이지요. "왜 arm64 아키텍처만 init_pg_dir을 사용하지?" 라는 질문이 여기까지 도달하게 되었지만 역사적인 배경을 살펴보니 꽤나 재밌는 이유를 찾을 수 있었네요 ;)

조회수 110회댓글 1개

관련 게시물

전체 보기