dm-verity 구현

Android 4.4 이상에서는 블록 기기의 투명한 무결성 검사를 제공하는 선택적 dm-verity(device-mapper-verity) 커널 기능을 통해 자체 검사 부팅을 지원합니다. dm-verity는 루트 권한을 유지하고 기기 보안을 침해할 수 있는 영구 루트킷 방지를 지원합니다. 이 기능을 통해 Android 사용자는 기기를 부팅할 때 기기의 상태가 마지막으로 사용되었을 때와 동일한 상태인지 확인할 수 있습니다.

루트 권한을 가진 PHA(잠재적으로 위험한 애플리케이션)는 탐지 프로그램으로부터 숨을 수 있으며 다양한 방식으로 자체적으로 은폐할 수 있습니다. 루팅 소프트웨어는 흔히 탐지기보다 더 많은 권한을 갖기 때문에 탐지 프로그램에 '거짓말할' 수 있으므로 이렇게 숨거나 은폐할 수 있습니다.

dm-verity 기능을 사용하면 파일 시스템의 기본 저장소 레이어인 블록 기기를 살펴보고 예상 구성과 일치하는지 확인할 수 있습니다. 이 작업을 할 때 암호화 해시 트리를 사용합니다. 모든 블록(일반적으로 4k)에는 SHA256 해시가 있습니다.

해시 값은 페이지 트리에 저장되므로 최상위 '루트' 해시만 신뢰하면 트리의 나머지 부분을 확인할 수 있습니다. 블록을 수정하는 기능은 암호화 해시를 깨는 것과 같습니다. 이 구조를 보려면 다음 다이어그램을 참조하세요.

dm-verity-hash-table

그림 1. dm-verity 해시 테이블

공개 키는 부팅 파티션에 포함되어 있으며 기기 제조업체가 외부에서 확인해야 합니다. 이 키는 해시의 서명을 검증하고 기기의 시스템 파티션이 보호되고 변경되지 않았는지 확인하는 데 사용됩니다.

작업

dm-verity 보호는 커널에 있습니다. 따라서 루팅 소프트웨어가 커널 시작 전에 시스템 보안을 침해했다면 액세스가 유지됩니다. 이 위험을 완화하기 위해 대부분의 제조업체는 기기에 직접 삽입한 키를 사용하여 커널을 확인합니다. 기기가 출고된 후에는 이 키를 변경할 수 없습니다.

제조업체는 이 키를 사용하여 첫 번째 수준의 부트로더에서 서명을 확인하고 차례로 그다음 수준, 애플리케이션 부트로더 및 최종적으로 커널에서 서명을 확인합니다. 자체 검사 부팅을 활용하려는 각 제조업체는 커널의 무결성을 확인하는 방법을 보유하고 있어야 합니다. 커널이 확인되었다고 가정하면 커널은 블록 기기를 살펴보고 마운트될 때 기기를 확인할 수 있습니다.

블록 기기를 확인하는 한 가지 방법은 콘텐츠를 직접 해시하여 저장된 값과 비교하는 것입니다. 그러나 블록 기기 전체를 확인하려고 시도하면 시간이 오래 걸리고 기기의 전력을 많이 소모할 수 있습니다. 즉, 기기를 부팅하는 데 오랜 시간이 걸리고 사용하기 전에 기기 전력이 상당히 소모됩니다.

대신 dm-verity는 블록을 개별적으로 그리고 각 블록에 액세스할 때만 확인합니다. 블록을 메모리로 읽을 때 블록을 병렬로 해시합니다. 그런 다음, 해시를 트리에서 확인합니다. 블록을 읽는 것은 비용이 많이 드는 작업이므로 이 블록 수준 확인에 의해 발생하는 지연 시간은 상대적으로 소소하다고 볼 수 있습니다.

확인에 실패하면 기기는 블록을 읽을 수 없음을 나타내는 I/O 오류를 생성합니다. 그리고 예상대로 기기는 파일 시스템이 손상된 것처럼 보입니다.

애플리케이션은 결과 데이터 없이 진행하도록 선택할 수 있습니다(예: 결과가 애플리케이션의 기본 기능에 필요하지 않을 때). 그러나 애플리케이션이 데이터 없이 계속할 수 없으면 실패합니다.

정방향 오류 수정

Android 7.0 이상에서는 정방향 오류 수정(FEC)을 통해 dm-verity의 견고성을 향상합니다. AOSP 구현은 일반적인 Reed-Solomon 오류 수정 코드로 시작하고 인터리빙이라는 기법을 적용하여 공간 오버헤드를 줄이고 손상된 블록 중 복구 가능한 숫자를 늘립니다. FEC에 관한 자세한 내용은 오류 수정과 함께 엄격하게 시행되는 자체 검사 부팅을 참조하세요.

구현

요약

  1. ext4 시스템 이미지를 생성합니다.
  2. 생성한 이미지의 해시 트리를 생성합니다.
  3. 해시 트리의 dm-verity 테이블을 빌드합니다.
  4. dm-verity 테이블에 서명하여 테이블 서명을 생성합니다.
  5. verity 메타데이터 번들로 dm-verity 테이블 및 테이블 서명을 묶습니다.
  6. 시스템 이미지, verity 메타데이터 및 해시 트리를 연결합니다.

해시 트리 및 dm-verity 테이블에 관한 자세한 설명은 The Chromium Projects - Verified Boot(Chromium 프로젝트 - 자체 검사 부팅)를 참조하세요.

해시 트리 생성

소개에서 설명한 것처럼 해시 트리는 dm-verity의 필수 요소입니다. cryptsetup 도구는 자체적으로 해시 트리를 생성합니다. 또는 호환되는 트리를 다음과 같이 정의할 수 있습니다.

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

해시를 형성하기 위해 시스템 이미지는 레이어 0에서 4k 블록으로 분할되며, 각각 SHA256 해시가 할당됩니다. 레이어 1은 SHA256 해시만 4k 블록으로 결합하여 형성됩니다. 이에 따라 이미지가 훨씬 작아집니다. 레이어 2는 레이어 1의 SHA256 해시로 동일하게 형성됩니다.

이 작업은 이전 레이어의 SHA256 해시가 단일 블록에 적합하게 맞을 때까지 진행됩니다. 이 블록의 SHA256 해시를 얻으면 트리의 루트 해시를 갖게 됩니다.

해시 트리의 크기(및 상응하는 디스크 공간 사용량)는 확인된 파티션의 크기에 따라 다릅니다. 실제로 해시 트리의 크기는 흔히 30MB 미만으로 작은 경향이 있습니다.

이전 레이어의 해시로 자연스럽게 완전히 채워지지 않은 블록이 레이어에 있으면 블록을 0으로 패딩하여 예상되는 4k를 달성해야 합니다. 이렇게 하면 해시 트리가 삭제되지 않고 대신 빈 데이터로 완료되었음을 알 수 있습니다.

해시 트리를 생성하려면 레이어 2 해시를 레이어 1의 해시에, 레이어 3 해시를 레이어 2의 해시에 연결합니다(이 같은 방식으로 연쇄적으로). 그리고 이 모든 것을 디스크에 기록합니다. 이때 루트 해시의 레이어 0은 참조하지 않습니다.

요약하면 해시 트리를 구성하는 일반적인 알고리즘은 다음과 같습니다.

  1. 무작위 솔트(16진수 인코딩)를 선택합니다.
  2. 시스템 이미지를 4k 블록으로 언스파스(Unsparse) 변환합니다.
  3. 각 블록의 (솔트 생성된) SHA256 해시를 가져옵니다.
  4. 이러한 해시를 연쇄적으로 연결하여 수준을 형성합니다.
  5. 4k 블록 경계에 이를 때까지 수준을 0으로 패딩합니다.
  6. 수준을 해시 트리에 연결합니다.
  7. 단일 해시만 있을 때까지 이전 수준을 다음 수준의 소스로 사용하여 2-6단계를 반복합니다.

이러한 작업의 결과는 루트 해시인 단일 해시입니다. 이 해시 및 솔트는 dm-verity 매핑 테이블을 구성하는 동안 사용됩니다.

dm-verity 매핑 테이블 빌드

커널의 블록 기기(또는 타겟) 및 해시 트리의 위치(동일한 값)를 식별하는 dm-verity 매핑 테이블을 빌드합니다. 이 매핑은 fstab 생성 및 부팅에 사용됩니다. 또한 이 테이블은 블록의 크기 및 해시 트리의 시작 위치인 hash_start(특히 이미지 시작 부분의 블록 번호)도 식별합니다.

verity 타겟 매핑 테이블 필드에 관한 자세한 설명은 cryptsetup을 참조하세요.

dm-verity 테이블에 서명

dm-verity 테이블에 서명하여 테이블 서명을 생성합니다. 파티션 확인 시 먼저 테이블 서명의 유효성이 검사됩니다. 이 검사는 부팅 이미지의 고정된 위치에 있는 키를 대상으로 실행됩니다. 키는 일반적으로 기기의 고정된 위치에 자동으로 포함되도록 제조업체의 빌드 시스템에 포함됩니다.

이 서명과 키 조합을 사용하여 파티션을 확인하려면 다음 단계를 따르세요.

  1. /verity_key에서 /boot 파티션에 libmincrypt와 호환되는 형식의 RSA-2048 키를 추가합니다. 해시 트리를 확인하는 데 사용되는 키의 위치를 파악합니다.
  2. 관련 항목의 fstab에서 fs_mgr 플래그에 verify를 추가합니다.

메타데이터 번들로 테이블 서명 묶기

verity 메타데이터 번들로 dm-verity 테이블 및 테이블 서명을 묶습니다. 메타데이터 블록 전체가 버전이 지정되므로 확장될 수 있습니다(예: 두 번째 유형의 서명 추가 또는 일부 순서 변경).

상태 검사로서 매직 번호는 테이블을 식별하는 데 도움이 되는 각 테이블 메타데이터 세트와 연결됩니다. 길이가 ext4 시스템 이미지 헤더에 포함되므로 데이터 자체의 내용을 몰라도 메타데이터를 검색할 수 있습니다.

이렇게 하면 확인되지 않은 파티션을 확인하도록 선택하지 않게 됩니다. 그러면 이 매직 번호가 없을 때 확인 프로세스가 중단됩니다. 이 번호는 다음과 유사합니다.
0xb001b001

16진수의 바이트 값은 다음과 같습니다.

  • 첫 번째 바이트 = b0
  • 두 번째 바이트 = 01
  • 세 번째 바이트 = b0
  • 네 번째 바이트 = 01

다음 다이어그램은 verity 메타데이터의 분석 항목을 보여줍니다.

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

그리고 다음 표에서는 이러한 메타데이터 필드를 설명합니다.

표 1. Verity 메타데이터 필드

필드 용도 크기
매직 번호 fs_mgr에서 상태 검사로 사용 4바이트 0xb001b001
버전 메타데이터 블록의 버전을 지정하는 데 사용 4바이트 현재는 0
서명 PKCS1.5 패딩 형식의 테이블 서명 256바이트
테이블 길이 dm-verity 테이블의 길이(바이트) 4바이트
테이블 앞서 설명한 dm-verity 테이블 테이블 길이 바이트
패딩 이 구조는 길이가 32k까지 0으로 패딩됩니다. 0

dm-verity 최적화

dm-verity 성능을 최대한 활용하려면 다음과 같이 해야 합니다.

  • 커널에서 ARMv7용 NEON SHA-2 및 ARMv8용 SHA-2 확장을 사용 설정합니다.
  • 다른 미리 읽기 및 prefetch_cluster 설정을 실험하여 기기에 가장 적합한 구성을 찾습니다.