Cấu trúc ký trên thiết bị

Kể từ Android 12, mô-đun Android Runtime (ART) là một mô-đun Mainline. Việc cập nhật mô-đun có thể yêu cầu mô-đun này tạo lại các cấu phần phần mềm biên dịch trước khi chạy (AOT) của các tệp jar bootclasspath và máy chủ hệ thống. Vì các cấu phần phần mềm này rất nhạy cảm về bảo mật, nên Android 12 sử dụng một tính năng gọi là ký trên thiết bị để ngăn chặn việc giả mạo các cấu phần phần mềm này. Trang này trình bày về cấu trúc ký trên thiết bị và các hoạt động tương tác của cấu trúc này với các tính năng bảo mật khác của Android.

Thiết kế cao cấp

Tính năng ký trên thiết bị có 2 thành phần cốt lõi:

  • odrefresh là một phần của mô-đun ART Mainline. Nó chịu trách nhiệm tạo các cấu phần phần mềm thời gian chạy. Công cụ này kiểm tra các cấu phần phần mềm hiện có dựa trên phiên bản đã cài đặt của mô-đun ART, các tệp jar bootclasspath và các tệp jar của máy chủ hệ thống để xác định xem các cấu phần phần mềm đó đã được cập nhật hay cần được tạo lại. Nếu cần tạo lại, odrefresh sẽ tạo và lưu trữ các mã này.

  • odsign là một tệp nhị phân thuộc nền tảng Android. Nó chạy trong quá trình khởi động sớm, ngay sau khi phân vùng /data được gắn. Trách nhiệm chính của nó là gọi odrefresh để kiểm tra xem có cần tạo hoặc cập nhật bất kỳ cấu phần phần mềm nào hay không. Đối với mọi cấu phần phần mềm mới hoặc được cập nhật mà odrefresh tạo, odsign sẽ tính toán một hàm băm. Kết quả của một phép tính băm như vậy được gọi là giá trị băm của tệp. Đối với mọi cấu phần phần mềm đã tồn tại, odsign sẽ xác minh rằng các mã nhận dạng của cấu phần phần mềm hiện có khớp với các mã nhận dạng mà odsign đã tính toán trước đó. Điều này đảm bảo rằng các cấu phần phần mềm không bị giả mạo.

Trong trường hợp xảy ra lỗi, chẳng hạn như khi bản tóm tắt cho một tệp không khớp, odrefreshodsign sẽ loại bỏ tất cả các cấu phần phần mềm hiện có trên /data và cố gắng tạo lại chúng. Nếu không thành công, hệ thống sẽ quay lại chế độ JIT.

odrefreshodsign được dm-verity bảo vệ và là một phần của chuỗi Xác minh quy trình khởi động của Android.

Tính toán giá trị băm của tệp bằng fs-verity

fs-verity là một tính năng của nhân Linux, có chức năng xác minh dữ liệu tệp dựa trên cây Merkle. Việc bật fs-verity trên một tệp sẽ khiến hệ thống tệp tạo một cây Merkle trên dữ liệu của tệp bằng cách sử dụng hàm băm SHA-256, lưu trữ cây này ở một vị trí ẩn cùng với tệp và đánh dấu tệp là chỉ đọc. fs-verity tự động xác minh dữ liệu của tệp dựa trên cây Merkle theo yêu cầu khi dữ liệu được đọc. fs-verity cung cấp hàm băm gốc của cây Merkle dưới dạng một giá trị được gọi là giá trị nhận dạng tệp fs-verity và fs-verity đảm bảo rằng mọi dữ liệu được đọc từ tệp đều nhất quán với giá trị nhận dạng tệp này.

odsign sử dụng fs-verity để cải thiện hiệu suất khởi động bằng cách tối ưu hoá hoạt động xác thực mật mã của các cấu phần phần mềm đã biên dịch trên thiết bị tại thời điểm khởi động. Khi một cấu phần phần mềm được tạo, odsign sẽ bật fs-verity trên cấu phần phần mềm đó. Khi xác minh một cấu phần phần mềm, odsign sẽ xác minh chuỗi đại diện tệp fs-verity thay vì hàm băm tệp đầy đủ. Điều này giúp bạn không cần đọc và băm toàn bộ dữ liệu của cấu phần phần mềm tại thời điểm khởi động. Thay vào đó, dữ liệu tạo tác được băm theo yêu cầu của fs-verity khi được sử dụng, theo từng khối.

Trên các thiết bị có nhân không hỗ trợ fs-verity, odsign sẽ quay lại tính toán các bản tóm tắt tệp trong không gian người dùng. odsign sử dụng cùng một thuật toán băm dựa trên cây Merkle như fs-verity, vì vậy, các giá trị nhận dạng là như nhau trong cả hai trường hợp. fs-verity là yêu cầu bắt buộc trên tất cả các thiết bị khởi chạy bằng Android 11 trở lên.

Lưu trữ các bản tóm tắt tệp

odsign lưu trữ các bản tóm tắt tệp của các cấu phần phần mềm trong một tệp riêng biệt có tên là odsign.info. Để đảm bảo odsign.info không bị giả mạo, odsign.info được ký bằng một khoá ký có các thuộc tính bảo mật quan trọng. Cụ thể, khoá này chỉ có thể được tạo và sử dụng trong quá trình khởi động sớm, tại thời điểm đó, chỉ có mã đáng tin cậy đang chạy; hãy xem phần Khoá ký đáng tin cậy để biết thông tin chi tiết.

Xác minh giá trị băm của tệp

Trong mỗi lần khởi động, nếu odrefresh xác định rằng các cấu phần phần mềm hiện có đã được cập nhật, thì odsign sẽ đảm bảo rằng các tệp không bị giả mạo kể từ khi được tạo. odsign thực hiện việc này bằng cách xác minh các bản tóm tắt tệp. Trước tiên, nó sẽ xác minh chữ ký của odsign.info. Nếu chữ ký hợp lệ, thì odsign sẽ xác minh rằng bản tóm tắt của mỗi tệp khớp với bản tóm tắt tương ứng trong odsign.info.

Khoá ký đáng tin cậy

Android 12 giới thiệu một tính năng mới của Kho khoá có tên là khoá giai đoạn khởi động để giải quyết các mối lo ngại về bảo mật sau đây:

  • Điều gì ngăn kẻ tấn công sử dụng khoá ký của chúng tôi để ký phiên bản odsign.info của riêng họ?
  • Điều gì ngăn chặn kẻ tấn công tạo khoá ký riêng và dùng khoá đó để ký phiên bản odsign.info của riêng họ?

Các khoá giai đoạn khởi động chia chu trình khởi động của Android thành nhiều cấp độ và liên kết một cách mật mã việc tạo và sử dụng khoá với một cấp độ cụ thể. odsign tạo khoá ký ở cấp độ ban đầu, khi chỉ có mã đáng tin cậy đang chạy, được bảo vệ thông qua dm-verity.

Các cấp độ khởi động được đánh số từ 0 đến số kỳ diệu 1000000000. Trong quá trình khởi động của Android, bạn có thể tăng cấp độ khởi động bằng cách đặt một thuộc tính hệ thống từ init.rc. Ví dụ: mã sau đây đặt cấp độ khởi động thành 10:

setprop keystore.boot_level 10

Các ứng dụng của Keystore có thể tạo các khoá được liên kết với một cấp độ khởi động nhất định. Ví dụ: nếu bạn tạo một khoá cho cấp độ khởi động 10, thì khoá đó chỉ có thể được dùng khi thiết bị ở cấp độ khởi động 10.

odsign sử dụng cấp độ khởi động 30 và khoá ký mà nó tạo ra sẽ được liên kết với cấp độ khởi động đó. Trước khi sử dụng khoá để ký cấu phần phần mềm, odsign sẽ xác minh rằng khoá được liên kết với cấp độ khởi động 30.

Điều này giúp ngăn chặn 2 cuộc tấn công được mô tả trước đó trong phần này:

  • Kẻ tấn công không thể sử dụng khoá đã tạo, vì vào thời điểm kẻ tấn công có cơ hội chạy mã độc hại, cấp độ khởi động đã tăng lên trên 30 và Kho khoá từ chối các thao tác sử dụng khoá.
  • Kẻ tấn công không thể tạo khoá mới, vì vào thời điểm kẻ tấn công có cơ hội chạy mã độc hại, cấp độ khởi động đã tăng lên trên 30 và Keystore từ chối tạo khoá mới có cấp độ khởi động đó. Nếu kẻ tấn công tạo một khoá mới không được liên kết với cấp độ khởi động 30, thì odsign sẽ từ chối khoá đó.

Keystore đảm bảo rằng cấp độ khởi động được thực thi đúng cách. Các phần sau đây trình bày chi tiết hơn về cách thực hiện việc này cho các phiên bản KeyMint (trước đây là Keymaster) khác nhau.

Triển khai Keymaster 4.0

Các phiên bản Keymaster khác nhau sẽ xử lý việc triển khai các khoá giai đoạn khởi động theo cách khác nhau. Trên các thiết bị có TEE/StrongBox Keymaster 4.0, Keymaster xử lý việc triển khai như sau:

  1. Trong lần khởi động đầu tiên, Keystore sẽ tạo khoá đối xứng K0 với thẻ MAX_USES_PER_BOOT được đặt thành 1. Điều này có nghĩa là khoá chỉ có thể được dùng một lần cho mỗi lần khởi động.
  2. Trong quá trình khởi động, nếu cấp độ khởi động tăng lên, một khoá mới cho cấp độ khởi động đó có thể được tạo từ K0 bằng cách sử dụng hàm HKDF: Ki+i=HKDF(Ki, "some_fixed_string"). Ví dụ: nếu bạn chuyển từ cấp độ khởi động 0 sang cấp độ khởi động 10, thì HKDF sẽ được gọi 10 lần để lấy K10 từ K0.
  3. Khi cấp độ khởi động thay đổi, khoá cho cấp độ khởi động trước đó sẽ bị xoá khỏi bộ nhớ và các khoá được liên kết với các cấp độ khởi động trước đó sẽ không còn nữa.

    Khoá K0 là khoá MAX_USES_PER_BOOT=1. Điều này cũng có nghĩa là không thể sử dụng khoá đó sau này trong quá trình khởi động, vì ít nhất một quá trình chuyển đổi cấp độ khởi động (sang cấp độ khởi động cuối cùng) luôn xảy ra.

Khi một ứng dụng Keystore (chẳng hạn như odsign) yêu cầu tạo khoá ở cấp độ khởi động i, blob của khoá đó sẽ được mã hoá bằng khoá Ki. Vì Ki không có sẵn sau cấp độ khởi động i, nên không thể tạo hoặc giải mã khoá này trong các giai đoạn khởi động sau này.

Triển khai Keymaster 4.1 và KeyMint 1.0

Quy trình triển khai Keymaster 4.1 và KeyMint 1.0 phần lớn giống với quy trình triển khai Keymaster 4.0. Điểm khác biệt chính là K0 không phải là khoá MAX_USES_PER_BOOT mà là khoá EARLY_BOOT_ONLY. Khoá này được giới thiệu trong Keymaster 4.1. Bạn chỉ có thể sử dụng khoá EARLY_BOOT_ONLY trong các giai đoạn khởi động ban đầu, khi không có mã không đáng tin cậy nào đang chạy. Điều này mang lại một mức độ bảo vệ bổ sung: trong quá trình triển khai Keymaster 4.0, kẻ tấn công xâm nhập hệ thống tệp và SELinux có thể sửa đổi cơ sở dữ liệu Kho khoá để tạo khoá MAX_USES_PER_BOOT=1 riêng nhằm ký các cấu phần phần mềm. Không thể xảy ra cuộc tấn công như vậy với các hoạt động triển khai Keymaster 4.1 và KeyMint 1.0, vì chỉ có thể tạo khoá EARLY_BOOT_ONLY trong quá trình khởi động sớm.

Thành phần công khai của khoá ký đáng tin cậy

odsign truy xuất thành phần khoá công khai của khoá ký từ Kho khoá. Tuy nhiên, Keystore không truy xuất khoá công khai đó từ TEE/SE chứa khoá riêng tư tương ứng. Thay vào đó, nó sẽ truy xuất khoá công khai từ cơ sở dữ liệu trên đĩa của chính nó. Điều này có nghĩa là kẻ tấn công xâm nhập hệ thống tệp có thể sửa đổi cơ sở dữ liệu Kho khoá để chứa một khoá công khai thuộc một cặp khoá công khai/riêng tư mà chúng kiểm soát.

Để ngăn chặn cuộc tấn công này, odsign sẽ tạo một khoá HMAC bổ sung có cùng cấp độ khởi động với khoá ký. Sau đó, khi tạo khoá ký, odsign sẽ dùng khoá HMAC này để tạo chữ ký của khoá công khai và lưu trữ chữ ký đó trên đĩa. Trong các lần khởi động tiếp theo, khi truy xuất khoá công khai của khoá ký, khoá này sẽ dùng khoá HMAC để xác minh rằng chữ ký trên đĩa khớp với chữ ký của khoá công khai đã truy xuất. Nếu chúng khớp nhau, khoá công khai sẽ đáng tin cậy vì khoá HMAC chỉ có thể được dùng ở các cấp khởi động ban đầu và do đó, không thể do kẻ tấn công tạo ra.