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 lỗi bộ nhớ trong 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. Chiến lược phát hành đĩa đơn ứ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 byteST2G
: gắn thẻ 2 hạt 16 byteDC 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 byteSTZ2G
: gắn thẻ và khởi tạo giá trị bằng 0 cho 2 hạt 16 byteDC 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.