Kiểm soát tính toàn vẹn của luồng

Tính đến năm 2016, khoảng 86% tất cả các lỗ hổng trên Android đều liên quan đến tính an toàn của bộ nhớ. Hầu hết các lỗ hổng đều bị kẻ tấn công khai thác bằng cách thay đổi luồng kiểm soát thông thường của một ứng dụng để thực hiện các hoạt động độc hại tuỳ ý với tất cả các đặc quyền của ứng dụng bị khai thác. Tính toàn vẹn của luồng kiểm soát (CFI) là một cơ chế bảo mật không cho phép thay đổi biểu đồ luồng kiểm soát ban đầu của một tệp nhị phân đã biên dịch, khiến việc thực hiện các cuộc tấn công như vậy trở nên khó khăn hơn đáng kể.

Trong Android 8.1, chúng tôi đã bật chế độ triển khai CFI của LLVM trong ngăn xếp đa phương tiện. Trong Android 9, chúng tôi đã bật CFI trong nhiều thành phần và cả nhân. CFI hệ thống được bật theo mặc định nhưng bạn cần bật CFI nhân.

CFI của LLVM yêu cầu biên dịch bằng Tối ưu hoá thời gian liên kết (LTO). LTO giữ lại biểu thị mã bit LLVM của các tệp đối tượng cho đến thời gian liên kết, cho phép trình biên dịch suy luận tốt hơn về những hoạt động tối ưu hoá có thể được thực hiện. Việc bật LTO sẽ giảm kích thước của tệp nhị phân cuối cùng và cải thiện hiệu suất, nhưng làm tăng thời gian biên dịch. Trong quá trình kiểm thử trên Android, sự kết hợp giữa LTO và CFI dẫn đến mức hao tổn không đáng kể về kích thước và hiệu suất của mã; trong một số trường hợp, cả hai đều được cải thiện.

Để biết thêm thông tin kỹ thuật về CFI và cách xử lý các chế độ kiểm tra kiểm soát chuyển tiếp khác, hãy xem tài liệu thiết kế LLVM.

Ví dụ và nguồn

CFI do trình biên dịch cung cấp và thêm hoạt động đo lường vào tệp nhị phân trong thời gian biên dịch. Chúng tôi hỗ trợ CFI trong chuỗi công cụ Clang và hệ thống bản dựng Android trong AOSP.

CFI được bật theo mặc định cho các thiết bị Arm64 đối với nhóm thành phần trong /platform/build/target/product/cfi-common.mk. Tính năng này cũng được bật trực tiếp trong một nhóm tệp makefile/blueprint của các thành phần đa phương tiện, chẳng hạn như /platform/frameworks/av/media/libmedia/Android.bp/platform/frameworks/av/cmds/stagefright/Android.mk.

Triển khai CFI hệ thống

CFI được bật theo mặc định nếu bạn sử dụng Clang và hệ thống xây dựng Android. Vì CFI giúp bảo vệ người dùng Android, nên bạn không được tắt CFI.

Trên thực tế, bạn nên bật CFI cho các thành phần khác. Các ứng cử viên lý tưởng là mã gốc có đặc quyền hoặc mã gốc xử lý dữ liệu đầu vào không đáng tin cậy của người dùng. Nếu đang dùng clang và hệ thống xây dựng Android, bạn có thể bật CFI trong các thành phần mới bằng cách thêm một vài dòng vào tệp makefile hoặc tệp bản thiết kế.

Hỗ trợ CFI trong tệp makefile

Để bật CFI trong tệp make, chẳng hạn như /platform/frameworks/av/cmds/stagefright/Android.mk, hãy thêm:

LOCAL_SANITIZE := cfi
# Optional features
LOCAL_SANITIZE_DIAG := cfi
LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
  • LOCAL_SANITIZE chỉ định CFI làm trình dọn dẹp trong quá trình tạo.
  • LOCAL_SANITIZE_DIAG bật chế độ chẩn đoán cho CFI. Chế độ chẩn đoán in thêm thông tin gỡ lỗi trong logcat trong quá trình gặp sự cố. Điều này rất hữu ích trong khi phát triển và kiểm thử các bản dựng của bạn. Tuy nhiên, hãy nhớ xoá chế độ chẩn đoán trên các bản dựng phát hành công khai.
  • LOCAL_SANITIZE_BLACKLIST cho phép các thành phần chọn lọc vô hiệu hoá hoạt động đo lường CFI cho từng hàm hoặc tệp nguồn. Bạn có thể sử dụng danh sách chặn như một biện pháp cuối cùng để khắc phục mọi vấn đề mà người dùng gặp phải. Để biết thêm thông tin, hãy xem phần Tắt CFI.

Hỗ trợ CFI trong các tệp bản thiết kế

Để bật CFI trong một tệp bản thiết kế, chẳng hạn như /platform/frameworks/av/media/libmedia/Android.bp, hãy thêm:

   sanitize: {
        cfi: true,
        diag: {
            cfi: true,
        },
        blacklist: "cfi_blacklist.txt",
    },

Khắc phục sự cố

Nếu đang bật CFI trong các thành phần mới, bạn có thể gặp phải một số vấn đề với lỗi không khớp kiểu hàmlỗi không khớp kiểu mã hợp ngữ.

Lỗi không khớp kiểu hàm xảy ra vì CFI chỉ cho phép các lệnh gọi gián tiếp chuyển đến các hàm có cùng kiểu động như kiểu tĩnh được dùng trong lệnh gọi. CFI hạn chế các lệnh gọi hàm thành viên ảo và không ảo chỉ nhảy đến các đối tượng là lớp dẫn xuất của kiểu tĩnh của đối tượng dùng để thực hiện lệnh gọi. Điều này có nghĩa là khi bạn có mã vi phạm một trong hai giả định này, hoạt động đo lường mà CFI thêm vào sẽ bị huỷ. Ví dụ: dấu vết ngăn xếp cho thấy SIGABRT và logcat chứa một dòng về tính toàn vẹn của luồng điều khiển tìm thấy sự không khớp.

Để khắc phục vấn đề này, hãy đảm bảo rằng hàm được gọi có cùng loại đã được khai báo tĩnh. Sau đây là 2 ví dụ về CL:

Một vấn đề khác có thể xảy ra là bạn đang cố gắng bật CFI trong mã có chứa các lệnh gọi gián tiếp đến hợp ngữ. Vì mã hợp ngữ không được nhập, nên điều này dẫn đến sự không khớp về kiểu.

Để khắc phục vấn đề này, hãy tạo trình bao bọc mã gốc cho mỗi lệnh gọi hợp ngữ và cung cấp cho trình bao bọc chữ ký hàm giống như con trỏ gọi. Sau đó, trình bao bọc có thể trực tiếp gọi mã hợp ngữ. Vì các nhánh trực tiếp không được CFI đo lường (chúng không thể được định hướng lại trong thời gian chạy và do đó không gây ra rủi ro bảo mật), nên điều này sẽ khắc phục vấn đề.

Nếu có quá nhiều hàm hợp ngữ và bạn không thể sửa tất cả, thì bạn cũng có thể đưa tất cả các hàm chứa lệnh gọi gián tiếp đến hợp ngữ vào danh sách chặn. Bạn không nên dùng cách này vì nó sẽ tắt các quy trình kiểm tra CFI trên những hàm này, từ đó mở ra bề mặt tấn công.

Tắt CFI

Chúng tôi không nhận thấy bất kỳ chi phí hiệu suất nào, vì vậy bạn không cần phải tắt CFI. Tuy nhiên, nếu có tác động đến người dùng, bạn có thể chọn tắt CFI cho từng hàm hoặc tệp nguồn bằng cách cung cấp tệp danh sách chặn trình dọn dẹp tại thời điểm biên dịch. Danh sách chặn hướng dẫn trình biên dịch tắt tính năng đo lường CFI ở các vị trí được chỉ định.

Hệ thống xây dựng Android hỗ trợ danh sách chặn theo thành phần (cho phép bạn chọn các tệp nguồn hoặc hàm riêng lẻ sẽ không nhận được hoạt động đo lường CFI) cho cả Make và Soong. Để biết thêm thông tin chi tiết về định dạng của tệp danh sách chặn, hãy xem tài liệu Clang ở nguồn trên.

Xác nhận kết quả

Hiện tại, không có bài kiểm thử CTS nào dành riêng cho CFI. Thay vào đó, hãy đảm bảo rằng các kiểm thử CTS đều vượt qua khi bật hoặc tắt CFI để xác minh rằng CFI không ảnh hưởng đến thiết bị.