Apple iOS의 새로운 메모리 방어선, MIE 해부
Apple iOS의 새로운 메모리 방어선, MIE 해부
MIE는 UAF 익스플로잇을 어떻게 어렵게 만드는가?
Mobile Device를 노리는 정교한 공격에서 반복해서 등장하는 출발점은 메모리 커럽션 버그이다. Native Code의 buffer overflow나 use-after-free와 같은 버그로 코드 실행 권한을 확보한 뒤, 이를 샌드박스 탈출(sandbox escape)이나 권한 상승(privilege escalation)으로 이어가는 흐름이다. 이에 대응하기 위해 Vendor에서는 여러 가지 ASLR, PAC, CFI 같은 Mitigation을 도입해 공격의 exploit cost를 높였지만, 여전히 전통적인 Mitigation에는 한계가 존재했다.
Arm은 이 빈틈을 줄이기 위해 MTE(Memory Tagging Extension)를 내놓았다. Android는 MTE를 개발 단계의 버그 탐지와 실제 기기의 exploit mitigation에 활용한다. Apple은 기본 MTE만으로는 실시간 mitigation에 부족하다고 판단했고, 동기식 EMTE와 타입 기반 allocator, 태그 기밀성 보호를 묶어 MIE(Memory Integrity Enforcement)를 설계했다. 2025년 A19 기반 iPhone에 처음 적용된 MIE가 단순한 “Apple판 MTE”라고 보기 어려운 이유다. AOSP MTE 문서, Apple MIE 설계 글
둘은 같은 아이디어에서 출발하지만 이름이 가리키는 범위가 다르다. MTE는 Arm이 정의한 하드웨어 기능이고, MIE는 Apple이 하드웨어와 운영체제를 함께 설계한 방어 체계다. 이 글에서는 태그 검사의 원리부터 Android의 배포 방식, Apple이 MIE에서 추가한 보호, 그리고 M5 Pro에서 직접 확인한 use-after-free 차단 결과까지 차례로 살펴본다.
Same Address, Different Object
메모리 오류는 크게 두 방향으로 생긴다. 할당의 끝을 넘어가거나, 할당의 수명이 끝난 뒤에도 접근하는 것이다.
| 분류 |
잘못된 접근 |
예시 |
| 공간적 오류(spatial) |
객체의 허용 범위를 벗어남 |
버퍼 끝을 넘어 옆 할당에 쓰기 |
| 시간적 오류(temporal) |
객체의 유효 수명이 끝난 뒤 접근 |
free() 이후 이전 포인터로 읽기 |
가상 메모리의 페이지 권한은 페이지 전체를 보호한다. 같은 읽기·쓰기 가능 페이지에 여러 작은 객체가 있으면, 객체 A에서 객체 B로 넘어가는 접근을 페이지 권한만으로 구분하기 어렵다. 태깅은 이보다 작은 단위에서 접근의 적합성을 검사한다.
이때 검사 질문은 “이 주소가 어느 객체의 몇 번째 바이트인가?”가 아니라 “이 포인터의 태그와 접근 대상 메모리의 태그가 일치하는가?”다. 두 질문은 다르며, 이 차이가 태깅의 장점과 한계를 함께 만든다.
How MTE Tags Memory
일반적인 AArch64 MTE는 16바이트 granule마다 4비트 allocation tag를 관리한다. 접근에 사용되는 포인터의 비트 59–56에는 logical tag가 들어간다. 태그 검사 대상으로 설정된 메모리에서 CPU는 두 값을 비교한다. TBI(Top Byte Ignore)는 주소 변환에서 상위 바이트를 무시하고, MTE는 그 공간의 태그로 메모리 접근을 검사한다. Linux arm64 MTE 문서
Pointer A
logical tag = 3
|
+-----------+-----------+
| |
access A cross into B
| |
v v
+----------------------+ +----------------------+
| Allocation A | | Allocation B |
| allocation tag = 3 | | allocation tag = 7 |
+----------+-----------+ +----------+-----------+
| 3 == 3 | 3 == 7
v v
Tag check passes Tag check fails
이 도식은 포인터가 옆 할당으로 넘어갔을 때 일어나는 태그 비교를 단순화한 예다.
Android의 Scudo allocator는 할당과 해제 시 메모리 태그를 갱신한다. 힙 태깅은 allocator가 담당하고, 스택 태깅은 LLVM의 함수 계측과 객체 수명 처리를 필요로 한다. bionic, 동적 로더, Zygote, debuggerd도 활성화와 오류 보고에 참여한다. bionic MTE 구현 문서
use-after-free에도 같은 원리가 적용된다.
Pointer (tag 3) Memory
| |
| -------- Valid access ---------> | allocation tag = 3
| <----------- Allowed ----------- | (3 == 3)
| |
| free and retag to 7
| |
| ---- Access through stale ----X | (3 != 7)
| Tag check fails |
같은 가상 주소를 새 객체가 재사용하면 allocator는 메모리에 새 태그를 부여한다. 이전 포인터에는 기존 태그가 남아 있으므로 접근할 때 태그 불일치가 발생한다.
What Four Bits Mean
4비트는 최대 16개 값을 표현한다. 두 태그가 16개 값에서 독립적이고 균일하게 선택된다는 단순 모델에서는 일치 확률이 1/16, 불일치 확률이 15/16 = 93.75%다. 실제 탐지 확률은 allocator의 태그 선택과 재사용 방식에 따라 달라진다. AOSP도 여러 태그 배정 전략을 설명한다. AOSP MTE 문서
4 bits / 16 bytes로 계산한 3.125%는 태그를 촘촘히 저장했을 때의 메타데이터 비율이다. 실제 메모리 사용량과 CPU 비용은 정렬, allocator 메타데이터, 진단 정보, 페이지 관리까지 포함해 측정한다.
When Android MTE Stops an Invalid Access
태그가 다르면 오류라는 데까지는 간단하다. 그다음 질문은 “지금 이 접근을 막는가, 아니면 오류를 기록해두고 나중에 종료하는가?”다. Android MTE를 읽을 때는 이 부분을 놓치기 쉽다.
| 모드 |
읽기 오류 |
쓰기 오류 |
해석 |
| SYNC |
해당 접근에서 동기적으로 fault |
해당 접근에서 동기적으로 fault |
정확한 오류 위치를 제공하고 잘못된 접근을 차단 |
| ASYNC |
오류를 기록하고 실행을 계속할 수 있음 |
오류를 기록하고 실행을 계속할 수 있음 |
나중에 종료되므로 접근 시점의 차단과는 다름 |
| ASYMM |
SYNC 방식 |
ASYNC 방식 |
읽기와 쓰기에 서로 다른 정책 적용 |
모드별 검사 방식은 Linux arm64 MTE 문서에 정리되어 있다.
Android SYNC는 SIGSEGV / SEGV_MTESERR, ASYNC는 SIGSEGV / SEGV_MTEAERR로 보고한다. 앱 manifest에서는 sync, async, off, default를 요청할 수 있으며, ASYMM은 지원 하드웨어에서 OS가 CPU 검사 모드로 선택한다. AOSP MTE 문서
SYNC는 잘못된 접근 시점에 fault를 발생시키고, ASYNC는 오류를 기록한 뒤 나중에 보고한다. 따라서 보안 동작을 비교할 때는 크래시 유무와 함께 검사 시점을 확인해야 한다.
Android의 요청 모드와 실제 CPU 검사 모드도 다를 수 있다. OEM은 ASYNC를 요청한 프로세스를 특정 CPU에서 SYNC 또는 ASYMM으로 실행하도록 구성할 수 있다. AOSP의 배포 설정 문서는 명시적인 sync가 할당·해제 스택 기록 비용을 추가한다는 점과, async 요청을 더 엄격한 CPU 모드로 올리는 구성을 설명한다. AOSP MTE configuration
Android의 태깅 정책은 배포판마다 다르다. 예를 들어 GrapheneOS는 자체 allocator와 커널 allocator의 메모리 태깅을 함께 제공한다. GrapheneOS features
Enabling Heap Tagging for an Android App
개발 중인 앱에서는 AndroidManifest.xml의 android:memtagMode로 힙 태깅을 요청할 수 있다. 아래는 SYNC 설정이다. Android 부분은 공식 문서의 설정을 설명하며, 실기기 실행 결과는 뒤의 Mac 예제에만 포함했다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:memtagMode="sync" />
</manifest>
현재 NDK 문서는 스택 태깅을 Android 14 QPR3부터 지원한다고 설명한다. 스택 검사는 MTE 지원 기기에서 별도로 계측한 디버그 빌드를 사용한다. 커스텀 allocator는 할당과 해제 과정에 태그 처리를 구현해야 한다. Android NDK MTE 가이드
Android SYNC 보고서는 오류 주소와 backtrace, 상황에 따라 할당·해제 이력을 제공한다. ASYNC 보고서는 나중에 발생한 SEGV_MTEAERR 시점을 기록한다. AOSP MTE reports
MIE Protects More Than the Tag Check
Apple MIE는 secure allocator + 동기식 EMTE + Tag Confidentiality Enforcement를 결합한다. 현재 플랫폼 보안 문서는 A19 및 M5 이후 프로세서를 지원 범위로 설명한다. “iOS MIE”라고 알려져 있지만 Mac에서도 관련 기능을 사용할 수 있다. Apple Platform Security
[Allocation request + type]
|
v
[Type-aware allocator] <........ [Protect tags and allocator state]
| :
v :
[Placement policy + memory tagging] :
| :
v :
[Synchronous CPU EMTE checking] <................
|
v
[OS fault handling on mismatch]
MIE는 CPU의 태그 비교에 타입 기반 배치와 allocator 상태 보호를 결합한다. 실제 할당은 메모리 종류에 맞는 allocator 경로를 거친다.
Type-Aware Allocation
Apple은 커널의 kalloc_type, 사용자 공간의 xzone malloc, WebKit의 libpas를 MIE의 구성 요소로 설명한다. 타입별 배치와 작은 할당의 태깅은 서로 보완한다. 2025년 iPhone 출시 당시에는 커널과 70개 이상의 사용자 공간 프로세스에 이 보호를 적용했다고 발표했다. Apple MIE 설계 글
타입 기반 allocator는 할당의 타입 descriptor를 이용해 메모리 영역을 선택한다. 컴파일러는 malloc, calloc, realloc, C++ new 등의 호출을 타입 인식 함수로 바꿀 수 있다. 사용자 정의 wrapper가 타입 정보를 잃으면 추가 처리가 필요하다. Apple typed allocation 가이드
타입 기반 allocator는 같은 메모리 영역에 배치할 객체의 종류를 제한한다. 객체의 수명과 경계는 기존 코드의 검사와 함께 관리한다. kalloc_type의 배경과 위협 모델은 Apple의 별도 분석에서 확인할 수 있다. kalloc_type 설계 분석
EMTE and Tag Confidentiality
Apple은 EMTE가 비태깅 메모리로 향하는 접근에도 추가 제약을 둔다고 설명한다. Apple Platform Security MIE에서는 SPTM으로 커널 allocator의 backing store와 태그 저장소를 보호하며, 태그 기밀성 정책은 타이밍·추측 실행에 의한 누출도 다룬다. Apple MIE 설계 글
태그의 선택과 보관 방식은 공격자가 올바른 태그를 추측하는 난이도에 직접 영향을 준다. TikTag 연구는 특정 MTE 구현에서 추측 실행을 통해 태그가 노출되는 경로를 분석했다. MIE의 Tag Confidentiality Enforcement는 이처럼 태그 값이 새어나가는 경로까지 방어 범위에 포함한다. TikTag 연구 논문
Deployment and Coverage: Android MTE vs. Apple MIE
| 비교 기준 |
Android의 MTE 활용 |
Apple MIE |
| 비교 단위 |
Arm 기능의 OS·allocator·앱 배포 방식 |
Apple의 통합 mitigation 설계 |
| 오류 처리 |
요청 모드, CPU 지원, OEM 정책에 따라 다름 |
플랫폼 설계는 동기식 EMTE 중심; 앱 soft mode는 별도 확인 |
| 보호 활성화 |
기기·OS·프로세스 설정 확인 필요 |
하드웨어 지원과 서드파티 앱의 opt-in을 구분 |
| 객체 배치 |
사용하는 allocator와 배포판 확인 필요 |
타입 기반 배치를 MIE 구성에 포함 |
| 실제 증거 |
해당 프로세스의 설정과 tombstone |
앱 서명 설정과 태그 fault 보고서 |
| 성능 비교 |
같은 workload에서 측정해야 함 |
같은 workload에서 측정해야 함 |
MIE는 태그 검사와 함께 allocator와 태그 기밀성까지 보호 범위에 포함한다. Android도 SYNC를 지원하므로 두 플랫폼은 실제 배포 모드와 보호 범위를 기준으로 비교해야 한다.
성능 비교에는 같은 애플리케이션과 부하를 사용한 별도 측정이 필요하다. 이 글의 예제에서는 태그 검사의 동작을 확인한다.
Reproducing the Check on an M5 Pro
실험에서는 64바이트를 할당한 뒤 해제 전과 후에 각각 읽어 봤다. 일반 바이너리와 힙 태깅 entitlement를 적용한 바이너리를 실행해 use-after-free가 어떻게 처리되는지 비교했다.
Test Environment
| 항목 |
관측값 |
| 칩 |
Apple M5 Pro |
| OS |
macOS 27.0.1, build 26A434 |
| 아키텍처 |
native arm64 |
| Xcode |
27.0, build 27A266a |
| 컴파일러 |
Apple clang 21.0.0 |
hw.optional.arm.FEAT_MTE |
1 |
| 실행일 |
2026-10-05, Asia/Seoul |
표의 값은 실험한 M5 Pro에서 직접 확인했다.
같은 예제를 실행하기 전에 다음 명령으로 환경을 확인할 수 있다.
sw_vers
uname -m
sysctl -n machdep.cpu.brand_string
sysctl -n hw.optional.arm.FEAT_MTE
xcodebuild -version
FEAT_MTE=1이면 하드웨어가 MTE를 지원한다.
Test Program and Entitlements
아래 코드를 mie_demo.c로 저장한다. 프로그램은 valid와 uaf 중 하나를 인자로 받고, 해제 전에 포인터의 logical tag를 출력한다. uaf 경로는 메모리를 해제한 뒤 이전 포인터로 다시 읽으며, volatile을 사용해 실제 읽기가 일어나도록 했다.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 2 || (strcmp(argv[1], "valid") && strcmp(argv[1], "uaf"))) {
fprintf(stderr, "usage: %s valid|uaf\n", argv[0]);
return 2;
}
setvbuf(stdout, NULL, _IONBF, 0);
unsigned char *p = malloc(64);
if (!p) return 1;
memset(p, 0x41, 64);
printf("allocation=%p logical_tag=0x%lx\n", (void *)p,
(unsigned long)(((uintptr_t)p >> 56) & 0xf));
if (!strcmp(argv[1], "valid")) {
printf("valid read: 0x%02x\n", (unsigned)*p);
free(p);
return 0;
}
volatile unsigned char *stale = p;
free(p);
puts("about to read freed memory (intentional undefined behavior)");
unsigned char value = *stale;
printf("read completed: 0x%02x\n", (unsigned)value);
return 0;
}
다음 내용은 checked.entitlements로 저장한다. 이 문서만으로 예제를 옮길 수 있도록 plist 전체를 적용했다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hardened-process</key>
<true/>
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
<string>1</string>
<key>com.apple.security.hardened-process.checked-allocations</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations.enable-pure-data</key>
<true/>
</dict>
</plist>
checked-allocations로 메모리 태깅을 활성화하고, enhanced-security-version-string에는 문자열 1을 사용했다. checked-allocations 문서, 보호 버전 문서
64바이트 버퍼도 태깅 대상에 포함되도록 enable-pure-data를 추가했다. pure-data entitlement 문서
태그 오류가 발생했을 때 프로세스가 종료되도록 soft-mode는 넣지 않았다. soft-mode 문서
Build and Run
두 파일이 있는 디렉터리에서 다음 명령을 실행한다. mie-baseline은 일반 빌드이고, mie-checked에는 메모리 태깅 entitlement를 넣어 ad-hoc 서명한다.
xcrun clang -arch arm64 -O0 -g -Wall -Wextra \
mie_demo.c -o mie-baseline
cp mie-baseline mie-checked
codesign --force --sign - \
--entitlements checked.entitlements mie-checked
codesign --verify --strict mie-checked
codesign -d --entitlements - mie-checked
./mie-baseline valid
./mie-baseline uaf
./mie-checked valid
./mie-checked uaf
마지막 ./mie-checked uaf는 태그 오류로 종료된다. ~/Library/Logs/DiagnosticReports/에 생성된 mie-checked-*.ips 파일에서 EXC_ARM_MTE_TAGCHECK_FAIL과 MTE_FAIL을 확인했다.
Observed Result
| 바이너리 |
경로 |
관측한 logical tag |
결과 |
| baseline |
valid |
0x0 |
정상 읽기 |
| baseline |
uaf |
0x0 |
잘못된 읽기가 완료됨 |
| checked |
valid |
0x5 |
정상 읽기 |
| checked |
uaf |
0xa |
SIGKILL과 태그 fault로 종료 |
checked UAF의 처리 흐름을 정리하면 다음과 같다.
mie-checked Tagged allocator EMTE check macOS
| | | |
| ---- malloc(64) --> | | |
| <-- pointer 0xa --- | | |
| ---- free(p) -----> | | |
| |--+ | |
| | | retag memory | |
| |<-+ | |
| -------- read through stale pointer ---> X |
| | | -- tag fault --> |
X <------------------- SIGKILL / MTE_FAIL ------------------- |
같은 프로세스의 macOS .ips 보고서에서 필요한 필드만 추출했다.
{
"exception": {
"type": "EXC_BAD_ACCESS",
"signal": "SIGKILL",
"subtype": "EXC_ARM_MTE_TAGCHECK_FAIL at 0x0a00000105355660"
},
"termination": {
"namespace": "MTE_FAIL",
"code": 262
}
}
macOS .ips 보고서에는 signal이 SIGKILL, termination namespace가 MTE_FAIL로 기록됐다. shell에서는 종료 상태가 137로 표시됐다.
포인터 출력에서 0xa는 비트 59–56의 값이다. 또한 주소와 태그는 실행마다 달라질 수 있다.
baseline에서는 해제된 메모리 읽기가 완료됐고, checked 바이너리에서는 같은 읽기가 태그 오류로 중단됐다. 두 실행 결과에서 checked-allocation 적용 여부에 따른 차이를 확인할 수 있었다.
Enabling MIE in an App
Xcode target의 Signing & Capabilities → Enhanced Security → Enable Hardware Memory Tagging에서 활성화한다. 태그 오류로 프로세스가 종료되는 동작을 확인하려면 Enable Soft Mode for Memory Tagging을 해제한다. 공식 문서는 M5 이후 Mac과 A19 이후 지원 기기를 명시한다. Apple Enhanced Security 가이드
Coverage and Limits of Memory Tagging
앞의 예제에서는 UAF를 잡았다. 메모리 태깅은 granule과 allocation에 부여된 태그를 검사한다. 바이트 단위의 경계와 프로그램의 의미는 기존 코드에서 함께 검사해야 한다.
| 상황 |
태그 검사에서 보이는 정보 |
| 같은 할당 안의 필드 간 경계 침범 |
두 필드에 같은 태그를 사용할 수 있음 |
| 같은 granule 안의 작은 경계 오류 |
같은 granule 태그를 공유함 |
| 서로 다른 객체의 태그가 우연히 같음 |
작은 태그 공간에서 충돌 가능 |
| 유효한 포인터로 잘못된 필드를 수정 |
주소와 태그의 일치 여부만 검사함 |
| 별도 태깅이 없는 메모리 관리 경로 |
태깅이 활성화된 경로에 검사가 적용됨 |
예를 들어 하나의 64바이트 할당 내부에서 두 논리적 필드가 같은 태그를 공유하면, 필드 사이를 넘어가도 태그 값은 그대로다. 이 경우에는 코드의 경계 검사와 수명 관리가 접근을 제어한다.
MTE는 CPU가 태그를 검사한다. HWASan은 포인터 태깅과 shadow memory를 사용하고, 컴파일러가 load/store 검사 코드를 삽입한다. LLVM HWASan 설계
이번 M5 Pro 예제는 ASan과 HWASan 없이 빌드했고, macOS .ips 보고서에서 태그 fault를 확인했다.
MTE를 이해할 때 출발점은 포인터와 메모리의 태그 비교다. MIE를 이해하려면 그 비교를 둘러싼 allocator, 보호 범위, 태그 기밀성까지 한 걸음 더 살펴봐야 한다. M5 Pro에서 본 작은 UAF 크래시는 그중 한 부분이 실제로 작동하는 모습을 보여준다. 기능 이름만 보는 것보다, 어떤 설정으로 어떤 접근이 멈췄는지 확인하는 편이 두 mitigation의 차이를 더 잘 드러낸다.