Tiện ích gắn thẻ bộ nhớ (MTE) Arm

Lý do nên dùng MTE

Các lỗi về độ an toàn của bộ nhớ (tức là lỗi khi xử lý bộ nhớ bằng ngôn ngữ lập trình gốc) là vấn đề thường gặp về mã. Chúng dẫn đến các lỗ hổng bảo mật cũng như các sự cố về độ ổn định.

Armv9 đã ra mắt Tiện ích gắn thẻ bộ nhớ (MTE) Arm. Đây là tiện ích phần cứng cho phép bạn phát hiện các lỗi use-after-free (tấn công bộ nhớ từ xa) và buffer-overflow (tràn bộ đệm) trong mã gốc của mình.

Kiểm tra khả năng hỗ trợ

Kể từ Android 13, một số thiết bị có hỗ trợ MTE. Để kiểm tra xem thiết bị của bạn có bật MTE khi đang chạy không, hãy thực thi lệnh sau:

adb shell grep mte /proc/cpuinfo

Nếu kết quả là Features : [...] mte, tức là thiết bị của bạn bật MTE khi đang chạy.

Một số thiết bị không bật MTE theo mặc định, nhưng cho phép nhà phát triển khởi động lại sau khi bật MTE. Đây là cấu hình thử nghiệm không được đề xuất cho việc sử dụng thông thường vì có thể làm giảm hiệu suất hoặc độ ổn định của thiết bị, nhưng có thể hữu ích cho việc phát triển ứng dụng. Để dùng chế độ này, hãy chuyển đến ứng dụng Cài đặt > Tuỳ chọn cho nhà phát triển > Tiện ích gắn thẻ bộ nhớ. Nếu không có tuỳ chọn này, tức là thiết bị của bạn không hỗ trợ bật MTE theo cách này.

Các chế độ hoạt động của MTE

MTE hỗ trợ 2 chế độ: SYNC và ASYNC. Chế độ SYNC cung cấp thông tin chẩn đoán chính xác hơn nên phù hợp hơn cho mục đích phát triển, còn chế độ ASYNC có hiệu suất cao nên được bật cho các ứng dụng đã phát hành.

Chế độ đồng bộ (SYNC)

Chế độ này được tối ưu hoá cho khả năng gỡ lỗi hơn là cho hiệu suất. Bạn có thể dùng chế độ này làm công cụ phát hiện lỗi chính xác khi mức hao tổn hiệu suất cao hơn được chấp nhận. Khi được bật, chế độ SYNC của MTE cũng hoạt động như một giải pháp giảm thiểu bảo mật.

Khi một thẻ không khớp, bộ xử lý sẽ chấm dứt quy trình hướng dẫn lưu trữ hoặc tải vi phạm bằng SIGSEGV (với si_code SEGV_MTESERR) và cung cấp đầy đủ thông tin về quyền truy cập vào bộ nhớ cũng như địa chỉ bị lỗi.

Chế độ này hữu ích trong quá trình kiểm thử như một giải pháp thay thế nhanh hơn cho HWASan mà không yêu cầu bạn phải biên dịch lại mã. Chế độ này cũng hữu ích trong phiên bản chính thức khi ứng dụng của bạn cho thấy một nền tảng dễ bị tấn công. Ngoài ra, khi chế độ ASYNC (như mô tả bên dưới) phát hiện lỗi, bạn có thể lấy được báo cáo lỗi chính xác bằng cách sử dụng API thời gian chạy để chuyển đổi quá trình thực thi sang chế độ SYNC.

Hơn nữa, khi chạy ở chế độ SYNC, trình phân bổ Android sẽ ghi lại dấu vết ngăn xếp của mọi quá trình phân bổ và giải phóng, đồng thời dùng chúng để cung cấp báo cáo lỗi chính xác hơn, bao gồm nội dung giải thích về lỗi bộ nhớ (chẳng hạn như use-after-free (tấn công bộ nhớ từ xa) hoặc buffer-overflow (tràn bộ đệm)) và dấu vết ngăn xếp của các sự kiện bộ nhớ liên quan (xem bài viết Tìm hiểu về báo cáo của MTE để biết thêm thông tin). Những báo cáo như vậy cung cấp nhiều thông tin theo ngữ cảnh hơn, đồng thời giúp việc theo dõi và khắc phục lỗi dễ dàng hơn so với ở chế độ ASYNC.

Chế độ không đồng bộ (ASYNC)

Chế độ này được tối ưu hoá cho hiệu suất hơn là cho độ chính xác của báo cáo lỗi. Bạn có thể dùng chế độ này để phát hiện lỗi về độ an toàn của bộ nhớ đó ở mức hao tổn thấp. Khi một thẻ không khớp, bộ xử lý sẽ tiếp tục quá trình thực thi cho đến mục nhân hệ điều hành gần nhất (chẳng hạn như gián đoạn lệnh gọi hệ thống hoặc bộ tính giờ), trong đó bộ xử lý chấm dứt quy trình này bằng SIGSEGV (mã SEGV_MTEAERR) mà không ghi lại địa chỉ bị lỗi hoặc quyền truy cập vào bộ nhớ.

Chế độ này giúp giảm thiểu các lỗ hổng về độ an toàn của bộ nhớ trong phiên bản chính thức trên các cơ sở mã được kiểm thử tốt, trong đó mật độ lỗi về độ an toàn của bộ nhớ được biết đến là thấp, đạt được bằng cách dùng chế độ SYNC trong quá trình kiểm thử.

Bật MTE

Cho một thiết bị

Để thử nghiệm, bạn có thể dùng các thay đổi về khả năng tương thích của ứng dụng để đặt giá trị mặc định của thuộc tính memtagMode đối với một ứng dụng không chỉ định giá trị nào trong tệp kê khai (hoặc chỉ định "default").

Bạn có thể tìm thấy các thay đổi này trong trình đơn cài đặt chung > Hệ thống > Nâng cao > Tuỳ chọn cho nhà phát triển > Các thay đổi về khả năng tương thích của ứng dụng. Thao tác đặt NATIVE_MEMTAG_ASYNC hoặc NATIVE_MEMTAG_SYNC sẽ bật MTE cho một ứng dụng cụ thể.

Bạn cũng có thể đặt chúng bằng lệnh am như sau:

  • Đối với chế độ SYNC (đồng bộ): $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Đối với chế độ ASYNC (không đồng bộ): $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

Trong Gradle

Bạn có thể bật MTE cho mọi bản gỡ lỗi của dự án Gradle bằng cách đưa

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

vào app/src/debug/AndroidManifest.xml. Thao tác này sẽ ghi đè memtagMode của tệp kê khai bằng tính năng đồng bộ hoá cho các bản gỡ lỗi.

Ngoài ra, bạn có thể bật MTE cho mọi bản dựng có một buildType tuỳ chỉnh. Để thực hiện việc này, hãy tạo buildType riêng và đưa XML vào app/src/<name of buildType>/AndroidManifest.xml.

Đối với một APK trên thiết bị bất kỳ được hỗ trợ

Theo mặc định, MTE bị tắt. Nếu muốn dùng MTE, ứng dụng có thể đặt android:memtagMode bên dưới thẻ <application> hoặc <process> trong AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

Khi được đặt trên thẻ <application>, thuộc tính này ảnh hưởng đến tất cả các quy trình mà ứng dụng dùng, đồng thời có thể bị ghi đè cho từng quy trình riêng bằng cách đặt thẻ <process>.

Tạo bản dựng có khả năng đo lường

Việc bật MTE như giải thích trước đó sẽ giúp phát hiện các lỗi hỏng bộ nhớ trên vùng nhớ khối xếp gốc. Để phát hiện tình trạng hỏng bộ nhớ trên ngăn xếp, ngoài việc bật MTE cho ứng dụng, bạn cần xây dựng lại mã bằng khả năng đo lường. Ứng dụng thu được sẽ chỉ chạy trên các thiết bị có hỗ trợ MTE.

Để tạo mã gốc (JNI) của ứng dụng bằng MTE, hãy làm như sau:

ndk-build

Trong tệp Application.mk:

APP_CFLAGS := -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag
APP_LDFLAGS := -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag

CMake

Đối với mỗi mục tiêu trong CMakeLists.txt:

target_compile_options(${TARGET} PUBLIC -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag)
target_link_options(${TARGET} PUBLIC -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag)

Chạy ứng dụng

Sau khi bật MTE, bạn có thể dùng và kiểm thử ứng dụng như bình thường. Nếu vấn đề về độ an toàn của bộ nhớ được phát hiện, thì ứng dụng sẽ gặp sự cố với tombstone tương tự như sau (lưu ý SIGSEGV với SEGV_MTESERR cho chế độ SYNC hoặc SEGV_MTEAERR cho chế độ ASYNC):

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

Hãy xem bài viết Tìm hiểu về báo cáo MTE trong tài liệu về AOSP (Dự án nguồn mở Android) để biết thêm chi tiết. Bạn cũng có thể gỡ lỗi ứng dụng bằng Android Studio và trình gỡ lỗi sẽ ngừng ở dòng gây ra hành vi truy cập bộ nhớ không hợp lệ.

Người dùng nâng cao: Sử dụng MTE trong trình phân bổ của riêng bạn

Để dùng MTE cho bộ nhớ không được phân bổ thông qua các trình phân bổ hệ thống thông thường, bạn cần sửa đổi trình phân bổ để gắn thẻ bộ nhớ và con trỏ.

Bạn cần phân bổ các trang cho trình phân bổ bằng cách sử dụng PROT_MTE trong cờ prot của mmap (hoặc mprotect).

Tất cả các lượt phân bổ được gắn thẻ cần phải có kích thước đồng đều 16 byte, vì thẻ chỉ có thể được gán cho các phân đoạn 16 byte (còn được gọi là hạt).

Sau đó, trước khi trả về một con trỏ, bạn cần dùng lệnh IRG để tạo một thẻ ngẫu nhiên và lưu trữ thẻ đó trong con trỏ.

Hãy dùng các lệnh sau đây để gắn thẻ cho bộ nhớ cơ bản:

  • STG: gắn thẻ 1 hạt 16 byte
  • ST2G: gắn thẻ 2 hạt 16 byte
  • DC GVA: gắn thẻ khối bộ nhớ đệm bằng cùng một thẻ

Ngoài ra, các lệnh sau đây cũng khởi tạo giá trị bằng 0 cho bộ nhớ:

  • STZG: gắn thẻ và khởi tạo giá trị bằng 0 cho 1 hạt 16 byte
  • STZ2G: gắn thẻ và khởi tạo giá trị bằng 0 cho 2 hạt 16 byte
  • DC GZVA: gắn thẻ và khởi tạo giá trị bằng 0 cho khối bộ nhớ đệm thông qua cùng một thẻ

Lưu ý rằng các CPU cũ không hỗ trợ những lệnh này nên bạn cần chạy chúng khi thoả mãn điều kiện là MTE được bật. Bạn có thể kiểm tra xem MTE có được bật cho quy trình của mình hay không:

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

Bạn có thể tham khảo cách triển khai Scudo.

Tìm hiểu thêm

Bạn có thể tìm hiểu thêm trong MTE User Guide for Android OS (Hướng dẫn sử dụng MTE dành cho hệ điều hành Android) do Arm viết.