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 này có thể yêu cầu mô-đun đó xây dựng 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 nhạy cảm về mặt bảo mật, nên Android 12 sử dụng một tính năng có tên là ký trên thiết bị để ngăn các cấu phần phần mềm này bị can thiệp. Trang này trình bày về cấu trúc ký trên thiết bị và các 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 Mainline ART. Mô-đun này chịu trách nhiệm tạo các cấu phần phần mềm thời gian chạy. Mô-đun 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 máy chủ hệ thống để xác định xem chúng đã được cập nhật hay cần được tạo lại. Nếu cần được tạo lại, odrefresh sẽ tạo và lưu trữ các cấu phần phần mềm đó.

  • odsign là một tệp nhị phân thuộc nền tảng Android. Tệp này chạy trong quá trình khởi động sớm, ngay sau khi phân vùng /data được gắn kết. Trách nhiệm chính của tệp này là gọi odrefresh để kiểm tra xem có cấu phần phần mềm nào cần được tạo hoặc cập nhật 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 quá trình tính toán hàm băm như vậy được gọi là tóm tắt 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 bản tóm tắt của cấu phần phần mềm hiện có khớp với các bản tóm tắt 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ị can thiệp.

Trong các điều kiện lỗi, chẳng hạn như khi bản tóm tắt của 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 bản tóm tắt tệp bằng fs-verity

fs-verity là một tính năng của kernel Linux thực hiện quy trình 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 xây dựng 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ó tên là bản tóm tắt tệp fs-verity, và fs-verity đảm bảo rằng mọi dữ liệu đọc từ tệp đều nhất quán với bản tóm tắt 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á quy trình xác thực mật mã của các cấu phần phần mềm được biên dịch trên thiết bị vào thời gian 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 odsign xác minh một cấu phần phần mềm, mô-đun này sẽ xác minh bản tóm tắt 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 phải đọc và băm toàn bộ dữ liệu của cấu phần phần mềm vào thời gian khởi động. Thay vào đó, dữ liệu cấu phần phần mềm được fs-verity băm theo yêu cầu khi dữ liệu được sử dụng, theo từng khối.

Trên các thiết bị có kernel không hỗ trợ fs-verity, odsign sẽ quay lại tính toán 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 bản tóm tắt sẽ giống nhau trong cả hai trường hợp. fs-verity là bắt buộc trên tất cả các thiết bị ra mắt bằng Android 11 trở lên.

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

odsign lưu trữ 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 có tên là odsign.info. Để đảm bảo odsign.info không bị can thiệp, 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 bản tóm tắt 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ị can thiệp kể từ khi được tạo. odsign thực hiện việc này bằng cách xác minh bản tóm tắt tệp. Trước tiên, mô-đun này sẽ xác minh chữ ký của odsign.info. Nếu chữ ký hợp lệ, thì odsign 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 ra mắt một tính năng mới của Kho khoá có tên là khoá giai đoạn khởi động, giúp 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 chúng?
  • Điều gì ngăn kẻ tấn công tạo khoá ký của riêng chúng và sử dụng khoá đó để ký phiên bản odsign.info của riêng chúng?

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

Các cấp giai đoạn 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 Kho khoá 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 sử 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à mô-đun này tạo được liên kết với cấp khởi động đó. Trước khi sử dụng một khoá để ký các 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 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à Kho khoá từ chối tạo khoá mới với 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, odsign sẽ từ chối khoá đó.

Kho khoá đả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 xử lý việc triển khai 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, Kho khoá sẽ tạo một 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 sử 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, bạn có thể tạo một khoá mới cho cấp khởi động đó 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 dùng được nữa.

    Khoá K0 là khoá MAX_USES_PER_BOOT=1. Điều này có nghĩa là bạn cũng 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 Kho khoá, chẳng hạn như odsign, yêu cầu tạo một khoá ở cấp khởi động i, blob của ứng dụng đó sẽ được mã hoá bằng khoá Ki. Vì Ki không dùng được sau cấp khởi động i, nên bạ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

Việc triển khai Keymaster 4.1 và KeyMint 1.0 phần lớn giống với việc 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 được ra mắt 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 sớm, khi không có mã không đáng tin cậy nào đang chạy. Điều này cung cấp thêm một lớp bảo vệ: trong quá trình triển khai Keymaster 4.0, kẻ tấn công xâm phạm 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 của riêng chúng để ký các cấu phần phần mềm. Bạn không thể thực hiện cuộc tấn công như vậy với việc triển khai Keymaster 4.1 và KeyMint 1.0, vì bạn 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, Kho khoá không truy xuất khoá công khai đó từ TEE/SE chứa khoá riêng tư tương ứng. Thay vào đó, mô-đun này sẽ truy xuất khoá công khai từ cơ sở dữ liệu trên đĩa của riêng mình. Điều này có nghĩa là kẻ tấn công xâm phạm 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 cặp khoá công khai/riêng tư dưới sự kiểm soát của chúng.

Để 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ẽ 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ý, mô-đun này sẽ 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, thì khoá công khai là đáng tin cậy, vì bạn chỉ có thể sử dụng khoá HMAC ở các cấp khởi động sớm và do đó, kẻ tấn công không thể tạo khoá này.