Arm Memory Tagging Extension(MTE)

MTE를 사용해야 하는 이유

메모리 안전 버그는 네이티브 프로그래밍 언어로 메모리를 처리할 때 발생하는 오류로, 흔한 코드 문제입니다. 이로 인해 안정성 문제와 보안 취약점도 발생하게 됩니다.

Armv9에 네이티브 코드에서 use-after-free 및 buffer-overflow 버그를 포착할 수 있는 하드웨어 확장 프로그램인 Arm Memory Tagging Extension(MTE)이 도입되었습니다.

지원 여부 확인

Android 13부터 선정된 일부 기기에서 MTE를 지원합니다. MTE가 사용 설정된 상태에서 기기가 실행 중인지 확인하려면 다음 명령어를 실행합니다.

adb shell grep mte /proc/cpuinfo

결과가 Features : [...] mte이면 MTE가 사용 설정된 상태로 기기가 실행되고 있는 것입니다.

일부 기기에서는 MTE가 기본적으로 사용 설정되지는 않지만 개발자가 MTE를 사용 설정한 상태로 재부팅할 수는 있습니다. 이는 실험용 구성으로, 기기 성능이나 안정성을 저하시킬 수 있으므로 일반적인 용도로는 사용하지 않는 것이 좋습니다. 하지만 앱 개발에는 유용할 수 있습니다. 이 모드에 액세스하려면 설정 앱에서 개발자 옵션 > Memory Tagging Extension으로 이동합니다. 이 옵션이 없으면 기기에서 MTE 사용 설정을 지원하지 않는 것입니다.

MTE 작동 모드

MTE는 SYNC와 ASYNC의 두 가지 모드를 지원합니다. SYNC 모드는 더 나은 진단 정보를 제공하므로 개발 목적에 더 적합하고, ASYNC 모드는 출시된 앱에 사용 설정할 수 있으며 고성능을 제공합니다.

동기 모드(SYNC)

이 모드는 성능보다 디버그 가능성에 최적화되어 있으며, 높은 성능 오버헤드가 허용될 경우 정밀한 버그 감지 도구로 사용할 수 있습니다. 이 모드를 사용 설정하면 MTE SYNC가 보안 완화 역할도 합니다.

태그 불일치 시 프로세서는 문제가 되는 로드의 프로세스를 종료하거나, SIGSEGV(si_code SEGV_MTESERR 포함) 및 메모리 액세스와 장애가 발생한 주소에 관한 전체 정보가 포함된 지침을 저장합니다.

이 모드는 HWASan의 대안으로 코드를 다시 컴파일할 필요가 없어 속도가 빨라 유용하거나 프로덕션 환경에서 앱이 취약한 공격 노출 영역을 나타낼 때 유용합니다. 또한 ASYNC 모드(아래에 설명됨)에서 버그를 발견하면 런타임 API를 사용하여 실행을 SYNC 모드로 전환하는 방식으로 정확한 버그 신고를 얻을 수 있습니다.

그 외에도 SYNC 모드에서 실행할 경우 Android 할당자는 모든 할당 및 할당 해제에 관한 스택 트레이스를 기록하고 이를 사용하여 메모리 오류 설명(예: use-after-free 또는 buffer-overflow)과 관련 메모리 이벤트의 스택 트레이스가 포함된 더 나은 오류 보고서를 제공합니다(자세한 내용은 MTE 보고서 이해하기 참고). 이러한 보고서를 통해 더 많은 문맥 정보를 얻을 수 있고 ASYNC 모드에서 더 쉽게 버그를 추적하고 수정할 수 있습니다.

비동기 모드(ASYNC)

이 모드는 버그 신고의 정확성보다 성능에 최적화되어 있으며 메모리 안전 버그를 감지하는 도구(오버헤드 낮음)로 사용할 수 있습니다. 태그 불일치 시 프로세서는 가장 가까운 커널 항목(예: syscall 또는 타이머 중단)까지 실행을 계속하고 여기서 오류 주소나 메모리 액세스를 기록하지 않고 (코드 SEGV_MTEAER)를 사용하여 프로세스를 종료합니다.

메모리 안전 버그의 밀도가 낮은 것으로 알려진(테스트 중에 SYNC 모드를 사용하면 됨) 잘 테스트된 코드베이스의 프로덕션 환경에서 메모리 안전 취약점을 완화할 때 이 모드를 사용하면 유용합니다.

MTE 사용 설정

단일 기기의 경우

실험의 경우 호환성 변경사항을 사용하여 매니페스트에서 어떤 값도 지정하지 않거나 "default"를 지정하는 애플리케이션의 memtagMode 속성 기본값을 설정할 수 있습니다.

이 옵션은 전역 설정 메뉴의 시스템 > 고급 > 개발자 옵션 > 앱 호환성 변경사항에서 확인할 수 있습니다. NATIVE_MEMTAG_ASYNC 또는 NATIVE_MEMTAG_SYNC를 설정하면 특정 애플리케이션에서 MTE가 사용 설정됩니다.

다음과 같이 am 명령어를 사용하여 이를 설정할 수 있습니다.

  • SYNC 모드: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • ASYNC 모드: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

Gradle의 경우

Gradle 프로젝트의 모든 디버그 빌드에 MTE를 사용 설정하려면

<?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>

app/src/debug/AndroidManifest.xml로 변환 이렇게 하면 디버그 빌드의 동기화로 매니페스트의 memtagMode가 재정의됩니다.

또는 맞춤 buildType의 모든 빌드에 MTE를 사용 설정할 수 있습니다. 이렇게 하려면 자체 buildType을 만들고 XML을 app/src/<name of buildType>/AndroidManifest.xml에 넣습니다.

지원되는 기기의 APK의 경우

MTE가 기본적으로 사용되지 않습니다. MTE를 사용하려는 앱은 AndroidManifest.xml<application> 또는 <process> 태그 아래에서 android:memtagMode를 설정하면 사용할 수 있습니다.

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

<application> 태그에 설정된 경우 이 속성은 애플리케이션에서 사용하는 모든 프로세스에 영향을 미치며 <process> 태그를 설정하여 개별 프로세스에 관해 재정의할 수 있습니다.

앱 실행

MTE를 사용 설정한 후 평소와 같이 앱을 사용하고 테스트합니다. 메모리 안전 문제가 감지되면 앱은 다음과 같이 Tombstone과 함께 앱이 비정상 종료됩니다(SYNC의 경우 SIGSEGVSEGV_MTESERR, ASYNC의 경우 SEGV_MTEAERR).

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

자세한 내용은 AOSP 문서의 MTE 보고서 이해하기를 참고하세요. 또한 Android 스튜디오로 앱을 디버그하면 잘못된 메모리 액세스를 야기하는 라인에서 디버거가 중지됩니다.

고급 사용자: 자체 할당자에서 MTE 사용

일반 시스템 할당자를 통해 할당되지 않은 메모리에 MTE를 사용하려면 할당자를 수정하여 메모리와 포인터에 태그를 지정해야 합니다.

할당자 페이지는 mmap(또는 mprotect)의 prot 플래그에 PROT_MTE를 사용하여 할당해야 합니다.

태그는 16바이트 청크(그래뉼이라고도 함)에만 할당될 수 있으므로 태그가 지정된 모든 할당은 16바이트로 정렬되어야 합니다.

그런 다음 포인터를 반환하기 전에 IRG 명령을 사용하여 무작위 태그를 생성하고 이를 포인터에 저장해야 합니다.

다음 안내에 따라 기본 메모리에 태그를 지정하세요.

  • STG: 단일 16바이트 그래뉼에 태그 지정
  • ST2G: 두 개의 16바이트 그래뉼에 태그 지정
  • DC GVA: 동일한 태그가 있는 캐시라인에 태그 지정

또는 다음 안내에 따라 메모리를 0으로 초기화하는 방법도 있습니다.

  • STZG: 단일 16바이트 그래뉼에 태그를 지정하고 0으로 초기화
  • STZ2G: 두 개의 16바이트 그래뉼에 태그를 지정하고 0으로 초기화
  • DC GZVA: 동일한 태그로 캐시라인에 태그를 지정하고 0으로 초기화

이러한 안내는 이전 CPU에서는 지원되지 않으므로 MTE가 사용되는 경우에 조건부로 실행해야 합니다. 프로세스에 MTE가 사용 설정되어 있는지 확인할 수 있습니다.

#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;
}

Scudo 구현이 유용한 참고 자료가 될 수 있습니다.

자세히 알아보기

자세한 내용은 Arm에서 작성한 Android OS용 MTE 사용자 가이드를 참고하세요.