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

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ữ nguyên biểu diễn 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ẽ làm 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ại làm tăng thời gian biên dịch. Trong quá trình kiểm thử trên Android, việc kết hợp LTO và CFI sẽ dẫn đến chi phí không đáng kể cho 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 lượt 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 được cung cấp bởi trình biên dịch và thêm tính nă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 xây dựng Android trong AOSP.

CFI được bật theo mặc định cho các thiết bị Arm64 đối với tập hợp thành phần trong /platform/build/target/product/cfi-common.mk. CFI cũng được bật trực tiếp trong một tập hợp tệp makefile/tệp bản thiết kế 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 đảm bảo an toàn cho 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 bổ sung. Các ứng 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 sử 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 makefile, 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 xây dựng.
  • 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ố, rất hữu ích khi phát triển và kiểm thử bản dựng. Tuy nhiên, hãy nhớ xoá chế độ chẩn đoán trên các bản dựng sản xuất.
  • LOCAL_SANITIZE_BLACKLIST cho phép các thành phần chọn lọc tắt tính nă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 đen như 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 có thể tồn tại. Để biết thêm thông tin chi tiết, hãy xem Tắt CFI.

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

Để bật CFI trong 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 loại hàmlỗi không khớp loại mã hợp ngữ.

Lỗi không khớp loại 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 loại động với loại tĩnh được dùng trong lệnh gọi. CFI chỉ cho phép các lệnh gọi hàm thành viên ảo và không ảo chuyển đến các đối tượng là lớp dẫn xuất của loại tĩnh của đối tượng được 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 các giả định này, tính nă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 kiểm soát 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 đề có thể xảy ra khác là cố gắng bật CFI trong mã 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 loại.

Để 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 cùng một chữ ký hàm 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 (không thể được trỏ 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à không thể khắc phục tất cả, 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 đen. Bạn không nên làm như vậy vì việc này sẽ tắt các lượt kiểm tra CFI trên các hàm này, từ đó mở ra bề mặt tấn công.

Tắt CFI

Chúng tôi không quan sát thấy bất kỳ chi phí hiệu suất nào, vì vậy bạn không cần tắt CFI. Tuy nhiên, nếu có tác động đến người dùng, bạn có thể chọn lọc 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 đen trình dọn dẹp tại thời gian biên dịch. Danh sách đen 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 đen cho từng 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 khả nă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 đen, hãy xem tài liệu Clang ở thượng nguồ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 bài kiểm thử CTS 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ị.