iOS Exploiting
Reading time: 48 minutes
tip
AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
iOS Exploit Mitigations
1. Code Signing / Runtime Signature Verification
Introduced early (iPhone OS → iOS) 이것은 기본적인 보호 중 하나입니다: 모든 실행 가능한 코드(apps, dynamic libraries, JIT-ed code, extensions, frameworks, caches)는 Apple의 신뢰 루트로 이어지는 인증서 체인으로 암호화되어 서명되어야 합니다. 런타임에서 바이너리를 메모리에 로드하기 전에(또는 특정 경계를 넘어 점프하기 전에) 시스템은 서명을 확인합니다. 코드가 변경되었거나(비트 플립, 패치) 서명이 없으면 로드가 실패합니다.
- 무력화: 익스플로잇 체인에서의 “고전적인 payload drop + execute” 단계; arbitrary code injection; 기존 바이너리를 수정하여 악의적 로직을 삽입하는 것.
- 동작 원리:
- Mach-O loader(및 dynamic linker)는 코드 페이지, 세그먼트, entitlements, team IDs, 그리고 서명이 파일 내용 전체를 포함하는지 등을 확인합니다.
- JIT caches나 동적으로 생성된 코드 같은 메모리 영역의 경우, Apple은 해당 페이지들이 서명되었거나 특별한 API(e.g.
mprotect
with code-sign checks)를 통해 검증되도록 강제합니다. - 서명에는 entitlements와 식별자들이 포함되며, OS는 특정 API나 권한 있는 기능이 특정 entitlements을 요구함을 강제하고 이는 위조할 수 없습니다.
Example
익스플로잇이 프로세스에서 코드 실행을 얻고 heap에 shellcode를 쓰고 그곳으로 점프하려고 한다고 가정합시다. iOS에서는 해당 페이지가 executable로 표시되는 동시에 code-signature 제약을 만족해야 합니다. 그 shellcode는 Apple의 인증서로 서명되지 않았기 때문에 점프가 실패하거나 시스템이 해당 메모리 영역을 executable로 만드는 것을 거부합니다.2. CoreTrust
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust는 바이너리(시스템 및 사용자 바이너리 포함)의 런타임 서명 검증을 Apple의 루트 인증서에 대해 수행하는 서브시스템으로, 사용자 공간의 캐시된 신뢰 저장소를 신뢰하는 대신 동작합니다.
- 무력화: 설치 후 바이너리 변조, 시스템 라이브러리나 사용자 앱을 교체/패치하려는 jailbreaking 기법; 신뢰된 바이너리를 악성 버전으로 바꿔 시스템을 속이려는 시도.
- 동작 원리:
- 로컬 트러스트 데이터베이스나 인증서 캐시를 신뢰하는 대신, CoreTrust는 Apple의 루트를 직접 참조하거나 보안 체인에서 중간 인증서를 확인합니다.
- 기존 바이너리에 대한 파일시스템 수준의 변경(예: 수정)이 감지되어 거부되도록 보장합니다.
- entitlements, team IDs, code signing 플래그 등 메타데이터를 로드 시 바이너리에 결합시킵니다.
Example
jailbreak이 `SpringBoard`나 `libsystem`을 패치된 버전으로 교체하여 persistence를 얻으려 할 수 있습니다. 하지만 OS의 loader나 CoreTrust가 검사를 수행하면 서명 불일치(또는 변경된 entitlements)를 감지하고 실행을 거부합니다.3. Data Execution Prevention (DEP / NX / W^X)
Introduced in many OSes earlier; iOS had NX-bit / w^x for a long time DEP는 writable로 표시된 페이지(데이터)는 실행 불가, executable로 표시된 페이지는 쓰기 불가로 강제합니다. 단순히 heap이나 stack 영역에 shellcode를 쓰고 실행할 수 없습니다.
- 무력화: 직접적인 shellcode 실행; 고전적인 buffer-overflow → injected shellcode로 점프.
- 동작 원리:
- MMU / 메모리 보호 플래그(페이지 테이블)를 통해 분리를 강제합니다.
- writable한 페이지를 executable로 표시하려는 시도는 시스템 검사를 유발하며(금지되거나 code-sign 승인이 필요함) 거부됩니다.
- 많은 경우 페이지를 executable로 만드는 것은 추가 제약이나 검사를 수행하는 OS API를 통해서만 가능합니다.
Example
오버플로가 heap에 shellcode를 쓴다. 공격자는 `mprotect(heap_addr, size, PROT_EXEC)`을 시도해 실행 가능하게 만들려 하지만 시스템은 이를 거부하거나 새 페이지가 code-sign 제약을 통과해야 한다고 검증합니다(그 shellcode는 통과 불가).4. Address Space Layout Randomization (ASLR)
Introduced in iOS ~4–5 era (roughly iOS 4–5 timeframe) ASLR은 주요 메모리 영역들의 베이스 주소를 프로세스 실행마다 무작위화합니다: libraries, heap, stack 등. gadget들의 주소는 실행마다 달라집니다.
- 무력화: ROP/JOP를 위해 gadget 주소를 하드코딩하는 것; 정적 익스플로잇 체인; 알려진 오프셋으로의 블라인드 점프.
- 동작 원리:
- 각 로드된 라이브러리/동적 모듈은 무작위화된 오프셋으로 rebased 됩니다.
- stack과 heap의 베이스 포인터도(일정한 엔트로피 한도 내에서) 무작위화됩니다.
- 때때로 다른 영역(e.g. mmap allocations)도 무작위화됩니다.
- information-leak mitigations와 결합되어, 공격자는 런타임에 base 주소를 알아내기 위해 먼저 주소를 leak해야 합니다.
Example
ROP 체인은 `0x….lib + offset`에 gadget이 있다고 기대합니다. 하지만 `lib`는 실행마다 다르게 재배치되므로 하드코딩된 체인은 실패합니다. 익스플로잇은 gadget 주소를 계산하기 전에 모듈의 base 주소를 먼저 leak해야 합니다.5. Kernel Address Space Layout Randomization (KASLR)
Introduced in iOS ~ (iOS 5 / iOS 6 timeframe) 사용자 ASLR과 유사하게, KASLR은 부팅 시 kernel text 및 기타 커널 구조의 베이스를 무작위화합니다.
- 무력화: 커널 코드나 데이터의 고정 위치에 의존하는 커널 레벨 익스플로잇; 정적 커널 익스플로잇.
- 동작 원리:
- 매 부팅마다 커널의 base 주소가 무작위화됩니다(범위 내).
task_structs
,vm_map
같은 커널 데이터 구조도 재배치되거나 오프셋될 수 있습니다.- 공격자는 커널 포인터를 먼저 leak하거나 information disclosure 취약점을 이용해 오프셋을 계산해야 합니다.
Example
로컬 취약점이 `KERN_BASE + offset`에 있는 커널 함수 포인터를 손상시키려 합니다. 그러나 `KERN_BASE`가 알려져 있지 않기 때문에 공격자는 먼저 주소를 leak(e.g. read primitive를 통해)하여 올바른 주소를 계산해야 합니다.6. Kernel Patch Protection (KPP / AMCC)
Introduced in newer iOS / A-series hardware (post around iOS 15–16 era or newer chips) KPP(또는 AMCC)는 커널 텍스트 페이지의 무결성을 지속적으로 모니터링합니다(해시나 체크섬 방식). 허용된 창(window) 외에서 패치나 inline hook, 코드 수정이 감지되면 커널 패닉이나 재부팅을 트리거합니다.
- 무력화: 영구적인 커널 패칭(커널 명령어 수정), inline hooks, 정적 함수 덮어쓰기.
- 동작 원리:
- 하드웨어나 펌웨어 모듈이 커널 텍스트 영역을 모니터링합니다.
- 주기적 또는 필요 시 페이지를 다시 해시하고 예상 값과 비교합니다.
- 허용된 업데이트 창 외에서 불일치가 발생하면 디바이스를 패닉시켜 크래시를 유발합니다(영구 악성 패치 방지).
- 공격자는 탐지 창을 피하거나 정 Legitimate 패치 경로를 사용해야 합니다.
Example
익스플로잇이 `memcmp`와 같은 커널 함수 prologue를 패치하려고 합니다. 그러나 KPP는 코드 페이지의 해시가 예상 값과 일치하지 않음을 감지하고 커널 패닉을 유발하여 패치가 안정화되기 전에 디바이스를 종료합니다.7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR은 하드웨어로 강제되는 메커니즘입니다: 부팅 초기에 kernel text가 잠기면 EL1(커널)에서 해당 코드 페이지를 더 이상 쓸 수 없게 됩니다.
- 무력화: 부팅 이후 EL1 권한으로 커널 코드를 수정하려는 모든 시도(예: 패치, 인플레이스 코드 주입).
- 동작 원리:
- 부팅(secure/bootloader 단계) 동안 메모리 컨트롤러(또는 보안 하드웨어 유닛)가 커널 텍스트를 포함하는 물리 페이지를 읽기 전용으로 표시합니다.
- 익스플로잇으로 완전한 커널 권한을 얻더라도 해당 페이지들을 써서 명령을 패치할 수 없습니다.
- 수정하려면 부트 체인을 먼저 타협하거나 KTRR 자체를 무력화해야 합니다.
Example
권한 상승 익스플로잇이 EL1로 점프해 kernel 함수(예: syscall handler)에 trampoline을 쓰려고 합니다. 그러나 KTRR이 해당 페이지를 읽기 전용으로 잠그므로 쓰기가 실패하거나 폴트가 발생하여 패치가 적용되지 않습니다.8. Pointer Authentication Codes (PAC)
Introduced with ARMv8.3 (hardware), Apple beginning with A12 / iOS ~12+
- PAC는 포인터 값(리턴 주소, 함수 포인터, 특정 데이터 포인터)의 변조를 감지하기 위해 포인터의 사용되지 않는 상위 비트에 작은 암호 서명(“MAC”)을 삽입하는 하드웨어 기능으로, ARMv8.3-A에서 도입되었습니다.
- 서명(“PAC”)은 포인터 값과 함께 modifier(컨텍스트 값, 예: stack pointer 또는 구분 값)를 기반으로 계산됩니다. 따라서 같은 포인터 값도 다른 컨텍스트에서는 다른 PAC를 가집니다.
- 사용 시점에 포인터를 참조하거나 분기하기 전에 authenticate 명령이 PAC를 검사합니다. 유효하면 PAC를 제거하여 순수 포인터를 얻고, 유효하지 않으면 포인터가 “poisoned”되거나 폴트가 발생합니다.
- PAC를 생성/검증하는 데 사용되는 키는 특권 레지스터(EL1, kernel)에 보관되며 user mode에서 직접 읽을 수 없습니다.
- 많은 시스템에서 포인터의 모든 64비트를 사용하지 않기 때문에(e.g. 48-bit address space) 상위 비트는 PAC를 저장해도 유효 주소에 영향을 주지 않습니다.
Architectural Basis & Key Types
-
ARMv8.3는 다섯 개의 128-bit 키(각각 두 개의 64-bit 시스템 레지스터로 구현)를 도입합니다.
-
APIAKey — instruction pointers 용 (domain “I”, key A)
-
APIBKey — 두 번째 instruction pointer 키 (domain “I”, key B)
-
APDAKey — data pointers 용 (domain “D”, key A)
-
APDBKey — data pointers 용 (domain “D”, key B)
-
APGAKey — generic 키, 포인터가 아닌 데이터나 기타 일반 용도 서명
-
이 키들은 특권 시스템 레지스터에 저장되며(EL1/EL2 등에서만 접근 가능), user mode에서는 접근 불가입니다.
-
PAC는 암호 함수(ARM은 QARMA를 제안)를 통해 계산됩니다. 입력은:
- 포인터 값(정규화된 부분)
- modifier(스택 포인터 같은 컨텍스트 값)
- 비밀 키
- 내부 튜닝 로직 결과 PAC가 포인터 상위 비트에 저장된 값과 일치하면 인증이 성공합니다.
Instruction Families
명명 규약은: PAC / AUT / XPAC, 그 다음 도메인 문자가 옵니다.
PACxx
명령은 포인터에 서명하고 PAC를 삽입합니다AUTxx
명령은 authenticate + strip (검증 후 PAC 제거)XPACxx
명령은 검증 없이 PAC를 제거합니다
Domains / suffixes:
Mnemonic | Meaning / Domain | Key / Domain | Example Usage in Assembly |
---|---|---|---|
PACIA | Sign instruction pointer with APIAKey | “I, A” | PACIA X0, X1 — sign pointer in X0 using APIAKey with modifier X1 |
PACIB | Sign instruction pointer with APIBKey | “I, B” | PACIB X2, X3 |
PACDA | Sign data pointer with APDAKey | “D, A” | PACDA X4, X5 |
PACDB | Sign data pointer with APDBKey | “D, B” | PACDB X6, X7 |
PACG / PACGA | Generic (non-pointer) signing with APGAKey | “G” | PACGA X8, X9, X10 (sign X9 with modifier X10 into X8) |
AUTIA | Authenticate APIA-signed instruction pointer & strip PAC | “I, A” | AUTIA X0, X1 — check PAC on X0 using modifier X1, then strip |
AUTIB | Authenticate APIB domain | “I, B” | AUTIB X2, X3 |
AUTDA | Authenticate APDA-signed data pointer | “D, A” | AUTDA X4, X5 |
AUTDB | Authenticate APDB-signed data pointer | “D, B” | AUTDB X6, X7 |
AUTGA | Authenticate generic / blob (APGA) | “G” | AUTGA X8, X9, X10 (validate generic) |
XPACI | Strip PAC (instruction pointer, no validation) | “I” | XPACI X0 — remove PAC from X0 (instruction domain) |
XPACD | Strip PAC (data pointer, no validation) | “D” | XPACD X4 — remove PAC from data pointer in X4 |
특수/별칭 형태:
PACIASP
는PACIA X30, SP
의 약식(링크 레지스터를 SP를 modifier로 서명)AUTIASP
는AUTIA X30, SP
(링크 레지스터를 SP로 인증)RETAA
,RETAB
(인증 후 반환) 또는BLRAA
(인증 후 분기) 같은 결합형이 ARM 확장/컴파일러 지원에서 존재합니다.- 또한 modifier가 암묵적으로 0인
PACIZA
/PACIZB
같은 제로-수정자 변형도 있습니다.
Modifiers
modifier의 주요 목적은 PAC를 특정 컨텍스트에 바인딩하여 같은 주소가 다른 컨텍스트에서 재사용되는 것을 방지하는 것입니다. 해시에 salt를 추가하는 것과 유사합니다.
따라서:
- modifier는 PAC 계산에 혼합되는 컨텍스트 값(다른 레지스터)입니다. 일반적 선택지는 stack pointer(
SP
), frame pointer, 또는 객체 ID 등입니다. - SP를 modifier로 사용하는 것은 return address signing에 흔히 쓰입니다: PAC는 특정 스택 프레임에 묶입니다. 다른 프레임에서 LR을 재사용하려 하면 modifier가 바뀌어 PAC 검증이 실패합니다.
- 동일한 포인터 값이라도 다른 modifier로 서명되면 서로 다른 PAC를 가집니다.
- modifier는 비밀일 필요는 없지만 이상적으로는 공격자가 제어하지 못해야 합니다.
- 의미 있는 modifier가 없는 경우 일부 형태는 zero나 암묵 상수를 사용합니다.
Apple / iOS / XNU Customizations & Observations
- Apple의 PAC 구현은 부팅 시마다 변하는 per-boot diversifiers를 포함하여 부팅 간 재사용을 방지합니다.
- 또한 PAC가 user mode에서 서명된 것이 kernel 모드에서 간단히 재사용되지 않도록 하는 cross-domain mitigations를 포함합니다.
- Apple Silicon(M1) 역공학 결과, nine modifier types와 키 제어를 위한 Apple 특수 시스템 레지스터가 있음이 밝혀졌습니다.
- Apple은 return address signing, kernel 데이터의 pointer 무결성, signed thread contexts 등 많은 커널 서브시스템에서 PAC를 사용합니다.
- Google Project Zero는 강력한 메모리 read/write primitive가 주어지면(특히 A12-era 장치에서) kernel PAC(A keys)를 위조할 수 있음을 보여주었고, Apple은 이 경로들을 많이 패치했습니다.
- Apple 시스템에서는 일부 키가 커널 전역으로 사용되는 반면 사용자 프로세스는 프로세스별 키 랜덤성을 받을 수 있습니다.
PAC Bypasses
- Kernel-mode PAC: theoretical vs real bypasses
- 커널 PAC 키와 로직은 특권 레지스터, diversifier, 도메인 분리 등으로 엄격히 통제되므로 임의의 서명된 커널 포인터를 위조하기는 매우 어렵습니다.
- Azad의 2020년 "iOS Kernel PAC, One Year Later"는 iOS 12-13에서 일부 부분적 우회(서명 가젯, 서명된 상태 재사용, 보호되지 않은 간접 분기)를 찾았지만, 범용 우회는 없었다고 보고합니다. bazad.github.io
- Apple의 "Dark Magic" 커스터마이즈는 exploitable surface를 더욱 축소합니다(도메인 전환, 키별 활성화 비트 등). i.blackhat.com
- Apple silicon(M1/M2)에서 알려진 kernel PAC bypass CVE-2023-32424가 Zecao Cai 등에게 보고되었습니다. i.blackhat.com
- 그러나 이러한 우회는 대개 매우 특정한 가젯이나 구현 버그에 의존하며, 범용적인 우회는 아닙니다.
따라서 커널 PAC는 매우 강력한 보호로 간주되지만 완벽하지는 않습니다.
- User-mode / runtime PAC bypass techniques
이들은 더 흔하며, PAC가 동적으로 적용되는 방식이나 런타임 프레임워크의 불완전성을 악용합니다. 아래는 분류와 예시입니다.
2.1 Shared Cache / A key issues
- dyld shared cache는 시스템 프레임워크와 라이브러리의 큰 pre-linked blob입니다. 매우 널리 공유되기 때문에 shared cache 내부의 함수 포인터들은 이미 서명된 상태로 많은 프로세스에서 사용됩니다. 공격자는 이러한 이미 서명된 포인터를 "PAC oracle"로 표적화합니다.
- 일부 우회 기법은 shared cache에 존재하는 A-key 서명 포인터를 추출하거나 재사용하여 우회합니다.
- "No Clicks Required" 발표는 shared cache 위에서 오라클을 구성해 상대적 주소를 추론하고 서명된 포인터와 결합해 PAC를 우회하는 방법을 설명합니다. saelo.github.io
- 사용자 공간의 공유 라이브러리로부터의 함수 포인터 import가 PAC로 충분히 보호되지 않아 공격자가 함수 포인터를 변경하지 않고도 얻을 수 있었던 사례들이 있습니다. (Project Zero 버그 엔트리) bugs.chromium.org
2.2 dlsym(3) / dynamic symbol resolution
- 알려진 우회 중 하나는
dlsym()
을 호출해 이미 서명된 함수 포인터(A-key로 서명, diversifier zero)를 얻는 것입니다.dlsym
이 합법적으로 서명된 포인터를 반환하므로 이를 사용하면 PAC를 위조할 필요가 없습니다. - Epsilon의 블로그는 일부 우회가 이를 어떻게 악용하는지 설명합니다:
dlsym("someSym")
은 서명된 포인터를 반환하고 간접 호출에 사용될 수 있습니다. blog.epsilon-sec.com - Synacktiv의 "iOS 18.4 --- dlsym considered harmful"는 iOS 18.4에서
dlsym
을 통해 반환된 심볼 포인터들이 잘못 서명되었거나 diversifier 버그를 가진 사례를 설명하여 의도치 않은 PAC 우회를 가능하게 했습니다. Synacktiv - dyld의 논리에서는
result->isCode
인 경우 반환된 포인터를__builtin_ptrauth_sign_unauthenticated(..., key_asia, 0)
으로 서명하는 동작이 있습니다(즉, 컨텍스트 0). blog.epsilon-sec.com
따라서 dlsym
은 user-mode PAC 우회에서 빈번한 벡터입니다.
2.3 Other DYLD / runtime relocations
- DYLD loader와 동적 재배치 로직은 복잡하며 때때로 재배치를 수행하기 위해 페이지를 임시로 read/write로 매핑한 뒤 다시 read-only로 바꿉니다. 공격자는 이러한 창을 악용합니다. Synacktiv의 발표는 relocation을 이용한 타이밍 기반 PAC 우회인 "Operation Triangulation"을 설명합니다. Synacktiv
- DYLD 페이지는 이제 SPRR / VM_FLAGS_TPRO 같은 보호로 보호되지만 이전 버전은 더 약한 가드만 있었습니다. Synacktiv
- WebKit exploit 체인에서는 DYLD loader가 종종 PAC 우회의 표적이 됩니다(재배치, interposer hooks를 통해). Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- 사용자 공간 익스플로잇 체인에서는 Objective-C 런타임 메소드들(예:
NSPredicate
,NSExpression
,NSInvocation
)이 제어 호출을 은닉해 전달하는 데 사용됩니다. - PAC 도입 전의 iOS에서는 fake NSInvocation 객체를 사용해 제어를 전달하는 익스플로잇이 있었습니다. PAC가 도입되면서 수정이 필요했지만 SLOP(SeLector Oriented Programming) 기법은 PAC 환경에서도 확장되어 사용됩니다. Project Zero
- 원래 SLOP 기법은 fake invocations를 만들어 ObjC 호출을 체인하는 방식이었고, ISA나 selector 포인터가 항상 PAC로 완전히 보호되지 않는다는 사실을 악용했습니다. Project Zero
- PAC가 부분적으로만 적용되는 환경에서는 method/selector/target 포인터가 항상 PAC로 보호되지 않아 우회 여지가 생깁니다.
Example Flow
Example Signing & Authenticating
``` ; Example: function prologue / return address protection my_func: stp x29, x30, [sp, #-0x20]! ; push frame pointer + LR mov x29, sp PACIASP ; sign LR (x30) using SP as modifier ; … body … mov sp, x29 ldp x29, x30, [sp], #0x20 ; restore AUTIASP ; authenticate & strip PAC ret; Example: indirect function pointer stored in a struct ; suppose X1 contains a function pointer PACDA X1, X2 ; sign data pointer X1 with context X2 STR X1, [X0] ; store signed pointer
; later retrieval: LDR X1, [X0] AUTDA X1, X2 ; authenticate & strip BLR X1 ; branch to valid target
; Example: stripping for comparison (unsafe) LDR X1, [X0] XPACI X1 ; strip PAC (instruction domain) CMP X1, #some_label_address BEQ matched_label
</details>
<details>
<summary>예시</summary>
A buffer overflow는 스택의 return address를 덮어쓴다. 공격자는 target gadget address를 쓰지만 올바른 PAC를 계산할 수 없다. 함수가 반환할 때 CPU의 `AUTIA` 명령이 PAC 불일치로 인해 fault가 발생한다. 체인은 실패한다.
Project Zero의 A12 (iPhone XS) 분석은 Apple의 PAC가 어떻게 사용되는지와 공격자가 memory read/write primitive를 가졌을 때 PAC를 위조하는 방법들을 보여주었다.
</details>
### 9. **Branch Target Identification (BTI)**
**Introduced with ARMv8.5 (later hardware)**
BTI는 **간접 분기 대상**을 검사하는 하드웨어 기능이다: `blr` 또는 간접 호출/점프를 실행할 때, 대상은 **BTI landing pad**(`BTI j` 또는 `BTI c`)로 시작해야 한다. landing pad가 없는 gadget 주소로 점프하면 예외가 발생한다.
LLVM의 구현 문서는 BTI 명령의 세 가지 변형과 그것들이 분기 유형에 어떻게 매핑되는지를 설명한다.
| BTI Variant | What it permits (which branch types) | Typical placement / use case |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | Targets of *call*-style indirect branches (e.g. `BLR`, or `BR` using X16/X17) | Put at entry of functions that may be called indirectly |
| **BTI J** | Targets of *jump*-style branches (e.g. `BR` used for tail calls) | Placed at the beginning of blocks reachable by jump tables or tail-calls |
| **BTI JC** | Acts as both C and J | Can be targeted by either call or jump branches |
- branch target enforcement로 컴파일된 코드에서는, 컴파일러가 각 유효한 간접-분기 대상(함수 시작이나 점프로 도달 가능한 블록)에 BTI 명령(C, J, 또는 JC)을 삽입하여 간접 분기가 오직 그 장소들로만 성공하도록 한다.
- **Direct branches / calls** (즉 고정 주소의 `B`, `BL`)는 BTI로 **제한되지 않는다**. 가정은 코드 페이지는 신뢰할 수 있고 공격자가 이를 변경할 수 없다는 것이므로(따라서 direct branches는 안전하다).
- 또한, **RET / return** 명령은 일반적으로 PAC 또는 return signing 메커니즘으로 return 주소가 보호되기 때문에 BTI로 제한되지 않는다.
#### Mechanism and enforcement
- CPU가 “guarded / BTI-enabled”로 표시된 페이지에서 **간접 분기 (BLR / BR)** 를 디코드할 때, 대상 주소의 첫 번째 명령이 허용된 BTI (C, J, 또는 JC)인지 검사한다. 그렇지 않으면 **Branch Target Exception**이 발생한다.
- BTI 명령 인코딩은 이전 ARM 버전에서 NOP로 예약된 opcode를 재사용하도록 설계되었다. 따라서 BTI-enabled 바이너리는 BTI를 지원하지 않는 하드웨어에서도 이전 호환성을 유지한다: 그 명령들은 NOP로 동작한다.
- BTI를 추가하는 컴파일러 패스는 필요한 곳에만 삽입한다: 간접 호출될 수 있는 함수들이나 점프로 도달 가능한 기본 블록들.
- 일부 패치와 LLVM 코드는 BTI가 *모든* 기본 블록에 삽입되는 것이 아니라 — switch / jump tables 등에서 가능한 분기 대상인 블록들에만 삽입된다는 것을 보여준다.
#### BTI + PAC synergy
PAC는 포인터 값(원본)을 보호한다 — 간접 호출/리턴의 체인이 변조되지 않았음을 보장한다.
BTI는 유효한 포인터조차도 적절히 표시된 entry point만을 가리켜야 함을 보장한다.
결합하면, 공격자는 올바른 PAC를 가진 유효한 포인터뿐만 아니라 그 대상이 BTI를 포함하고 있어야 한다. 이는 exploit gadgets를 구성하는 난이도를 증가시킨다.
#### Example
<details>
<summary>예시</summary>
공격이 `0xABCDEF`에 있는 gadget으로 피벗하려고 하는데 그곳이 `BTI c`로 시작하지 않는다. CPU는 `blr x0`을 실행할 때 대상을 검사하고 유효한 landing pad가 아니므로 fault를 발생시킨다. 따라서 많은 gadgets는 BTI 접두어를 포함하지 않으면 사용 불가능해진다.
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**Introduced in more recent ARMv8 extensions / iOS support (for hardened kernel)**
#### PAN (Privileged Access Never)
- **PAN**은 **ARMv8.1-A**에서 도입된 기능으로, **privileged code** (EL1 또는 EL2)가 **user-accessible (EL0)** 로 표시된 메모리를 **읽거나 쓰지 못하게** 한다. 단, PAN을 명시적으로 비활성화하면 예외가 허용된다.
- 아이디어는: 커널이 속임수나 침해를 당하더라도, kernel이 user-space 포인터를 임의로 역참조하려면 먼저 PAN을 *해제*해야 하므로 **ret2usr** 스타일 exploit이나 사용자가 제어하는 버퍼의 오용 위험을 줄인다.
- PAN이 활성화되어 있을 때 (PSTATE.PAN = 1), privileged load/store 명령이 “EL0에서 접근 가능”한 가상 주소에 접근하면 **permission fault**가 발생한다.
- 커널이 정당하게 user-space 메모리에 접근해야 할 때(예: user buffer로 데이터 복사), 커널은 **일시적으로 PAN을 비활성화**하거나 “unprivileged load/store” 명령을 사용해야 그 접근을 허용할 수 있다.
- ARM64의 Linux에서는 PAN 지원이 2015년경에 도입되었다: 커널 패치가 기능 감지를 추가하고 `get_user` / `put_user` 등을 PAN을 해제하는 변형으로 바꿨다.
**핵심 뉘앙스 / 제한 / 버그**
- Siguza 등은 ARM 설계의 명세 버그(또는 모호한 동작) 때문에 **execute-only user mappings**(`--x`)이 PAN을 **유발하지 않을 수 있다**고 지적했다. 즉, user 페이지가 읽기 권한 없이 실행 가능으로 표시되면, 커널의 읽기 시도가 아키텍처에서 “EL0에서 접근 가능”으로 간주되지 않아 PAN을 우회할 수 있다. 이는 특정 구성에서 PAN 우회로 이어진다.
- 따라서 iOS / XNU가 execute-only user 페이지를 허용하는 경우(예: 일부 JIT 또는 code-cache 설정), 커널은 PAN이 활성화된 상태에서도 해당 페이지에서 우연히 읽기를 수행할 수 있다. 이는 일부 ARMv8+ 시스템에서 알려진 미세한 exploitable 영역이다.
#### PXN (Privileged eXecute Never)
- **PXN**은 페이지 테이블 플래그이다(페이지 테이블 항목의 리프 또는 블록 엔트리). 해당 페이지는 **privileged 모드에서 실행 불가**(즉 EL1이 실행할 때)임을 나타낸다.
- PXN은 커널(또는 모든 privileged 코드)이 user-space 페이지에서 명령을 점프하거나 실행하는 것을 방지한다. 결과적으로, privileged 수준에서의 제어 흐름 재지정이 user 메모리로 향하는 것을 차단한다.
- PAN과 결합되면 다음을 보장한다:
1. Kernel은 기본적으로 user-space 데이터를 읽거나 쓸 수 없다 (PAN)
2. Kernel은 user-space 코드를 실행할 수 없다 (PXN)
- ARMv8 페이지 테이블 포맷에서 리프 엔트리에는 `PXN` 비트(그리고 unprivileged execute-never를 위한 `UXN`)가 속성 비트로 있다.
따라서 커널에 손상된 함수 포인터가 user 메모리를 가리키더라도, 그곳으로 분기하려 하면 PXN 비트가 fault를 발생시킨다.
#### Memory-permission model & how PAN and PXN map to page table bits
PAN / PXN의 작동 방식을 이해하려면 ARM의 변환 및 권한 모델을 살펴봐야 한다(단순화):
- 각 페이지 또는 블록 엔트리는 읽기/쓰기, privileged 대 unprivileged에 대한 **AP[2:1]**와 실행 불가 제한을 위한 **UXN / PXN** 비트 등 속성 필드를 가진다.
- PSTATE.PAN이 1일 때(활성화), 하드웨어는 수정된 의미를 강제한다: EL0에서 접근 가능하다고 표시된 페이지에 대한 privileged 접근은 차단되어 fault를 발생시킨다.
- 앞서 언급한 버그 때문에, 읽기 권한 없이 실행 전용으로 표시된 페이지는 특정 구현에서 “EL0에서 접근 가능”으로 간주되지 않을 수 있어 PAN을 우회한다.
- 페이지의 PXN 비트가 설정되면, 높은 권한 수준에서의 instruction fetch라도 실행이 금지된다.
#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)
강화된 커널 설계(예: Apple이 사용하는 방식)에서는:
- 커널은 기본적으로 PAN을 활성화한다(따라서 privileged 코드는 제약을 받는다).
- 정당하게 user-space 메모리를 읽거나 써야 하는 경로(예: syscall 버퍼 복사, I/O, read/write user pointer)에서는 커널이 일시적으로 **PAN을 비활성화**하거나 해당 접근을 허용하는 특수 명령을 사용한다.
- 작업을 마친 후에는 PAN을 다시 활성화해야 한다.
- PXN은 페이지 테이블을 통해 강제된다: user 페이지는 PXN = 1로 설정되어(따라서 커널이 해당 페이지를 실행할 수 없음) 커널 페이지는 그렇지 않다.
- 커널은 어떤 코드 경로도 user 메모리 영역으로의 실행 흐름을 초래하지 않도록 보장해야 한다(그렇지 않으면 PXN을 우회할 수 있음) — 따라서 “user-controlled shellcode로 점프”하는 익스플로잇 체인은 차단된다.
앞서 언급한 execute-only 페이지를 통한 PAN 우회 때문에, 실제 시스템에서 Apple은 execute-only user 페이지를 비활성화하거나 명세 취약점을 우회하는 패치를 적용할 수 있다.
#### Attack surfaces, bypasses, and mitigations
- **PAN bypass via execute-only pages**: 앞서 논의한 바와 같이, 명세의 간극으로 인해 읽기 권한 없는 execute-only 사용자 페이지는 일부 구현에서 PAN의 적용을 받지 않을 수 있다. 이는 공격자가 execute-only 섹션을 통해 데이터를 전달하는 비정상적인 경로를 제공할 수 있다.
- **Temporal window exploit**: 커널이 PAN을 필요한 것보다 더 긴 기간 동안 비활성화하면, 레이스나 악의적 경로가 그 창을 이용해 의도치 않은 user 메모리 접근을 수행할 수 있다.
- **Forgotten re-enable**: 코드 경로가 PAN을 다시 활성화하지 못하면, 이후의 커널 연산이 잘못하여 user 메모리에 접근할 수 있다.
- **Misconfiguration of PXN**: 페이지 테이블이 user 페이지에 PXN을 설정하지 않거나 user code 페이지를 잘못 매핑하면, 커널이 user-space 코드를 실행하도록 속을 수 있다.
- **Speculation / side-channels**: 투기적 우회와 유사하게, PAN / PXN 검사에 대한 일시적 위반을 초래하는 마이크로아키텍처적 부작용이 있을 수 있다(하지만 이러한 공격은 CPU 설계에 크게 의존한다).
- **Complex interactions**: JIT, shared memory, code regions 같은 고급 기능에서는 커널이 user-mapped 영역에서 특정 메모리 접근이나 실행을 허용해야 할 필요가 있다; PAN/PXN 제약 하에서 이를 안전하게 설계하는 것은 비단순하다.
#### Example
<details>
<summary>Code Example</summary>
Here are illustrative pseudo-assembly sequences showing enabling/disabling PAN around user memory access, and how a fault might occur.
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs"> </span></div>
// Suppose kernel entry point, PAN is enabled (privileged code cannot access user memory by default)
; Kernel receives a syscall with user pointer in X0 ; wants to read an integer from user space mov X1, X0 ; X1 = user pointer
; disable PAN to allow privileged access to user memory MSR PSTATE.PAN, #0 ; clear PAN bit, disabling the restriction
ldr W2, [X1] ; now allowed load from user address
; re-enable PAN before doing other kernel logic MSR PSTATE.PAN, #1 ; set PAN
; ... further kernel work ...
; Later, suppose an exploit corrupts a pointer to a user-space code page and jumps there BR X3 ; branch to X3 (which points into user memory)
; Because the target page is marked PXN = 1 for privileged execution, ; the CPU throws an exception (fault) and rejects execution
만약 커널이 그 사용자 페이지에 대해 **PXN**을 설정하지 않았다면, 분기(branch)가 성공할 수 있고 — 이는 보안상 취약합니다.
커널이 사용자 메모리 접근 후 PAN을 다시 활성화하는 것을 잊으면, 추가적인 커널 로직이 우발적으로 임의의 사용자 메모리를 읽거나 쓸 수 있는 창이 열립니다.
사용자 포인터가 실행 전용 페이지(읽기/쓰기가 없는 실행 권한만 있는 사용자 페이지)를 가리키는 경우, PAN 사양 버그 하에서는 `ldr W2, [X1]`가 PAN이 활성화된 상태에서도 예외를 일으키지 않을 수 있으며, 구현에 따라 우회 익스플로잇을 가능하게 할 수 있습니다.
</details>
<details>
<summary>Example</summary>
커널 취약점이 사용자 제공 함수 포인터를 받아 커널 컨텍스트에서 호출하려고 시도한다고 합시다(예: `call user_buffer`). PAN/PXN 하에서는 그 동작이 금지되거나 fault를 발생시킵니다.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI는 64비트 포인터의 최상위 바이트를 주소 변환에서 무시한다는 의미입니다. 이는 OS나 하드웨어가 실제 주소에 영향을 주지 않고 포인터의 최상위 바이트에 **태그 비트**를 삽입할 수 있게 해줍니다.
- TBI는 **Top Byte Ignore**의 약자이며(때때로 *Address Tagging*이라 불림), 많은 ARMv8+ 구현에서 사용 가능한 하드웨어 기능으로, 64비트 포인터의 최상위 8비트(비트 63:56)를 주소 변환 / load/store / instruction fetch 시에 **무시**합니다.
- 결과적으로 CPU는 포인터 `0xTTxxxx_xxxx_xxxx`(여기서 `TT` = top byte)를 주소 변환 시 `0x00xxxx_xxxx_xxxx`로 취급하여 최상위 바이트를 마스킹합니다. 최상위 바이트는 소프트웨어가 **메타데이터 / 태그 비트**를 저장하는 데 사용할 수 있습니다.
- 이는 소프트웨어가 각 포인터에 바이트 단위의 태그를 인밴드(in-band)로 삽입할 여유 공간을 제공하며, 메모리 참조 대상 주소를 변경하지 않습니다.
- 아키텍처는 load, store, instruction fetch가 실제 메모리 접근을 수행하기 전에 포인터의 최상위 바이트를 마스킹(즉 태그를 제거)하여 처리하도록 보장합니다.
따라서 TBI는 **논리적 포인터**(포인터 + 태그)와 메모리 연산에 사용되는 **실제 주소**를 분리합니다.
#### Why TBI: Use cases and motivation
- **Pointer tagging / metadata**: 최상위 바이트에 추가 메타데이터(예: 객체 타입, 버전, 바운드, 무결성 태그)를 저장할 수 있습니다. 나중에 포인터를 사용할 때 하드웨어 수준에서 태그가 무시되므로 메모리 접근을 위해 수동으로 태그를 제거할 필요가 없습니다.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI는 MTE가 기반으로 삼는 하드웨어 메커니즘입니다. ARMv8.5에서 **Memory Tagging Extension**은 포인터의 비트 59:56을 **논리적 태그**로 사용하고 이를 메모리에 저장된 **allocation tag**와 대조합니다.
- **Enhanced security & integrity**: TBI를 pointer authentication(PAC)이나 런타임 체크와 결합하면 포인터 값뿐 아니라 태그까지 올바른지 강제할 수 있습니다. 공격자가 태그를 맞추지 않고 포인터를 덮어쓰면 태그 불일치가 발생합니다.
- **Compatibility**: TBI는 선택적 기능이고 하드웨어가 태그 비트를 무시하기 때문에 기존의 태그 없는 코드는 정상적으로 계속 작동합니다. 태그 비트는 레거시 코드에 대해 사실상 “신경 쓰지 않아도 되는” 비트가 됩니다.
#### Example
<details>
<summary>Example</summary>
함수 포인터의 최상위 바이트에 태그(예: `0xAA`)가 포함되어 있었습니다. 익스플로잇이 포인터의 하위 비트만 덮어썼지만 태그는 건드리지 않았고, 커널이 검증하거나 정제할 때 태그가 맞지 않아 포인터가 거부되거나 실패합니다.
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (일부 리포트는 macOS / Apple silicon에서 PPL을 보고했으며, Apple은 유사 보호를 iOS에도 도입하고 있습니다)
- PPL은 **커널 내부의 보호 경계(intra-kernel protection boundary)**로 설계되었습니다: 커널(EL1)이 손상되어 읽기/쓰기 권한을 갖게 되더라도, 특정 **민감한 페이지들**(특히 페이지 테이블, 코드 서명 메타데이터, 커널 코드 페이지, entitlements, trust caches 등)을 **자유롭게 수정할 수 없어야** 합니다.
- 이는 사실상 **“커널 내부의 작은 신뢰된 구성 요소(커널 내의 커널)”**를 만듭니다 — PPL은 오직 자신만이 보호된 페이지를 수정할 수 있는 **상승된 권한**을 가진 작은 신뢰 도메인입니다. 다른 커널 코드는 변경을 수행하기 위해 PPL 루틴을 호출해야 합니다.
- 이는 커널 익스플로잇의 공격 표면을 줄여줍니다: 커널 모드에서 임의의 R/W/execute 권한을 얻었다 하더라도, 공격 코드는 중요 구조를 변경하려면 PPL 도메인에 진입하거나 PPL을 우회해야 합니다.
- 최신 Apple silicon(A15+ / M2+)에서는 Apple이 많은 경우 페이지 테이블 보호를 위해 PPL을 대체하는 **SPTM (Secure Page Table Monitor)**으로 전환하고 있습니다.
다음은 공개 분석을 바탕으로 PPL이 어떻게 동작하는지에 대한 설명입니다.
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Apple 하드웨어는 **APRR (Access Permission ReRouting)**이라는 메커니즘을 사용합니다. 이는 페이지 테이블 엔트리(PTE)가 전체 권한 비트 대신 작은 인덱스를 포함할 수 있게 하고, 그 인덱스들은 APRR 레지스터를 통해 실제 권한으로 매핑됩니다. 이를 통해 도메인별로 권한을 동적으로 재매핑할 수 있습니다.
- PPL은 APRR을 활용하여 커널 컨텍스트 내에서 권한을 분리합니다: 오직 PPL 도메인만 인덱스와 실제 권한 간의 매핑을 업데이트할 수 있습니다. 즉, non-PPL 커널 코드가 PTE를 쓰거나 권한 비트를 변경하려 할 때, APRR 로직이 이를 허용하지 않거나 읽기 전용 매핑을 강제합니다.
- PPL 코드 자체는 제한된 영역(예: `__PPLTEXT`)에서 실행되며, 일반적으로 진입 게이트가 임시로 허용될 때까지 실행 가능 또는 쓰기 가능하지 않습니다. 커널은 보호 작업을 수행하기 위해 PPL 진입 지점(“PPL 루틴”)을 호출합니다.
#### Gate / Entry & Exit
- 커널이 보호된 페이지(예: 커널 코드 페이지의 권한 변경 또는 페이지 테이블 수정)를 바꿔야 할 때, 검증을 수행한 뒤 PPL 도메인으로 전환하는 **PPL 래퍼** 루틴을 호출합니다. PPL 도메인 밖에서는 보호된 페이지들이 사실상 읽기 전용이거나 수정 불가능합니다.
- PPL 진입 동안, APRR 매핑은 PPL 영역의 메모리 페이지들이 PPL 내에서 **실행 가능 & 쓰기 가능**하도록 조정됩니다. 종료 시에는 다시 읽기 전용 / 비쓰기 가능으로 되돌립니다. 이를 통해 검토된 PPL 루틴만이 보호된 페이지를 쓸 수 있게 합니다.
- PPL 외부에서 커널 코드가 보호된 페이지를 쓰려 하면, 그 도메인의 APRR 매핑이 쓰기를 허용하지 않기 때문에 fault(권한 거부)가 발생합니다.
#### Protected page categories
PPL이 일반적으로 보호하는 페이지들에는 다음이 포함됩니다:
- 페이지 테이블 구조(translation table entries, 매핑 메타데이터)
- 특히 중요한 로직을 담은 커널 코드 페이지
- 코드 서명 메타데이터(신뢰 캐시, 서명 블롭 등)
- entitlements 테이블, 서명 강제 테이블
- 서명 검사 우회를 가능하게 하거나 자격 증명 조작을 허용하는 다른 고가치 커널 구조
아이디어는 커널 메모리가 완전히 제어된 상태라도, 공격자가 PPL 루틴을 손상시키거나 PPL을 우회하지 않는 한 이러한 페이지를 단순히 패치하거나 재작성할 수 없도록 하는 것입니다.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- Project Zero의 공개 보고서는 **stale TLB entries**를 이용한 우회를 설명합니다.
- 아이디어:
1. 물리 페이지 A와 B를 할당하고 이를 PPL 페이지로 표시합니다(따라서 보호됨).
2. 두 개의 가상 주소 P와 Q를 매핑하는데, 이들의 L3 변환 테이블 페이지가 각각 A와 B에서 옵니다.
3. 별도 스레드를 띄워 Q에 지속적으로 접근하여 Q의 TLB 엔트리를 살아있게 유지합니다.
4. `pmap_remove_options()`를 호출하여 P부터 시작하는 매핑을 제거하는데, 버그로 인해 코드가 P와 Q의 TTE들을 모두 제거하지만 TLB 무효화는 P에 대해서만 수행하여 Q의 stale 엔트리는 남아 있게 됩니다.
5. B(즉 Q의 테이블)를 재사용하여 임의의 메모리를 매핑합니다(예: PPL로 보호된 페이지). stale TLB 엔트리가 여전히 Q의 이전 매핑을 가리키므로 그 매핑은 해당 컨텍스트에서 유효하게 남습니다.
6. 이를 통해 공격자는 PPL 인터페이스를 거치지 않고도 PPL로 보호된 페이지를 쓰기 가능한 매핑으로 대체할 수 있습니다.
- 이 익스플로잇은 물리 매핑과 TLB 동작을 세밀하게 제어할 필요가 있었습니다. 이는 TLB 무효화 및 매핑 일관성에 의존하는 보안 경계가 얼마나 신중해야 하는지를 보여줍니다.
- Project Zero는 이러한 우회가 미묘하고 드물지만 복잡한 시스템에서는 가능하다고 평가했습니다. 그럼에도 불구하고 PPL을 유의미한 완화책으로 보고 있습니다.
2. **Other potential hazards & constraints**
- 커널 익스플로잇이 직접 PPL 루틴으로 진입할 수 있다면(PPL 래퍼 호출을 통해) 제한을 우회할 수 있습니다. 따라서 인수 검증이 매우 중요합니다.
- PPL 코드 자체의 버그(예: 산술 오버플로, 경계 검사 결함)는 PPL 내부에서 범위를 벗어난 수정을 허용할 수 있습니다. Project Zero는 `pmap_remove_options_internal()`의 이런 버그가 우회에 이용되었음을 관찰했습니다.
- PPL 경계는 하드웨어 강제(APRR, 메모리 컨트롤러)에 불가분하게 연결되어 있으므로, 하드웨어 구현만큼 강력합니다.
#### Example
<details>
<summary>Code Example</summary>
Here’s a simplified pseudocode / logic showing how a kernel might call into PPL to modify protected pages:
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```c
// In kernel (outside PPL domain)
function kernel_modify_pptable(pt_addr, new_entry) {
// validate arguments, etc.
return ppl_call_modify(pt_addr, new_entry) // call PPL wrapper
}
// In PPL (trusted domain)
function ppl_call_modify(pt_addr, new_entry) {
// temporarily enable write access to protected pages (via APRR adjustments)
aprr_set_index_for_write(PPL_INDEX)
// perform the modification
*pt_addr = new_entry
// restore permissions (make pages read-only again)
aprr_restore_default()
return success
}
// If kernel code outside PPL does:
*pt_addr = new_entry // a direct write
// It will fault because APRR mapping for non-PPL domain disallows write to that page
The kernel can do many normal operations, but only through ppl_call_*
routines can it change protected mappings or patch code.
Example
A kernel exploit tries to overwrite the entitlement table, or disable code-sign enforcement by modifying a kernel signature blob. Because that page is PPL-protected, the write is blocked unless going through the PPL interface. So even with kernel code execution, you cannot bypass code-sign constraints or modify credential data arbitrarily. On iOS 17+ certain devices use SPTM to further isolate PPL-managed pages.PPL → SPTM / Replacements / Future
- On Apple’s modern SoCs (A15 or later, M2 or later), Apple supports SPTM (Secure Page Table Monitor), which replaces PPL for page table protections.
- Apple calls out in documentation: “Page Protection Layer (PPL) and Secure Page Table Monitor (SPTM) enforce execution of signed and trusted code … PPL manages the page table permission overrides … Secure Page Table Monitor replaces PPL on supported platforms.”
- The SPTM architecture likely shifts more policy enforcement into a higher-privileged monitor outside kernel control, further reducing the trust boundary.
MTE | EMTE | MIE
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
- Tag assignment
- When memory is allocated (e.g. in kernel or user space via secure allocators), a secret tag is assigned to that block.
- The pointer returned to the user or kernel includes that tag in its high bits (using TBI / top byte ignore mechanisms).
- Tag checking on access
- Whenever a load or store is executed using a pointer, the hardware checks that the pointer’s tag matches the memory block’s tag (allocation tag). If mismatch, it faults immediately (since synchronous).
- Because it's synchronous, there is no “delayed detection” window.
- Retagging on free / reuse
- When memory is freed, the allocator changes the block’s tag (so older pointers with old tags no longer match).
- A use-after-free pointer would therefore have a stale tag and mismatch when accessed.
- Neighbor-tag differentiation to catch overflows
- Adjacent allocations are given distinct tags. If a buffer overflow spills into neighbor’s memory, tag mismatch causes a fault.
- This is especially powerful in catching small overflows that cross boundary.
- Tag confidentiality enforcement
- Apple must prevent tag values being leak (because if attacker learns the tag, they could craft pointers with correct tags).
- They include protections (microarchitectural / speculative controls) to avoid side-channel leakage of tag bits.
- Kernel and user-space integration
- Apple uses EMTE not just in user-space but also in kernel / OS-critical components (to guard kernel against memory corruption).
- The hardware/OS ensures tag rules apply even when kernel is executing on behalf of user space.
Example
``` Allocate A = 0x1000, assign tag T1 Allocate B = 0x2000, assign tag T2// pointer P points into A with tag T1 P = (T1 << 56) | 0x1000
// Valid store *(P + offset) = value // tag T1 matches allocation → allowed
// Overflow attempt: P’ = P + size_of_A (into B region) *(P' + delta) = value → pointer includes tag T1 but memory block has tag T2 → mismatch → fault
// Free A, allocator retags it to T3 free(A)
// Use-after-free: *(P) = value → pointer still has old tag T1, memory region is now T3 → mismatch → fault
</details>
#### Limitations & challenges
- **Intrablock overflows**: 오버플로가 같은 할당 내에 머물러 경계를 넘지 않고 태그가 동일하게 유지되면, tag mismatch가 이를 잡아내지 못합니다.
- **Tag width limitation**: 태그에 사용할 수 있는 비트 수가 매우 적습니다(예: 4비트 등) — 제한된 네임스페이스.
- **Side-channel leaks**: 만약 태그 비트가 cache / speculative execution 등을 통해 leak될 수 있다면, 공격자가 유효한 태그를 알아내 우회할 수 있습니다. Apple의 tag confidentiality enforcement는 이를 완화하려는 목적입니다.
- **Performance overhead**: 각 load/store에서 태그 검사가 추가 비용을 유발하므로, Apple은 하드웨어를 최적화해 오버헤드를 낮춰야 합니다.
- **Compatibility & fallback**: 오래된 하드웨어나 EMTE를 지원하지 않는 부품에서는 fallback이 필요합니다. Apple은 MIE가 지원되는 장치에서만 활성화된다고 주장합니다.
- **Complex allocator logic**: allocator는 태그 관리, retagging, 경계 정렬, mis-tag 충돌 회피 등을 처리해야 합니다. allocator 로직의 버그는 취약점을 유발할 수 있습니다.
- **Mixed memory / hybrid areas**: 일부 메모리는 untagged(레거시)로 남아 있을 수 있어 상호 운용성이 더 까다로워집니다.
- **Speculative / transient attacks**: 많은 마이크로아키텍처 방어와 마찬가지로, speculative execution이나 micro-op fusion이 검사를 일시적으로 우회하거나 태그 비트를 leak할 수 있습니다.
- **Limited to supported regions**: Apple은 EMTE를 커널이나 보안에 민감한 서브시스템 등 선택된 고위험 영역에서만 적용할 수 있으며, 전역 적용하지 않을 수 있습니다.
---
## Key enhancements / differences compared to standard MTE
Here are the improvements and changes Apple emphasizes:
| Feature | Original MTE | EMTE (Apple’s enhanced) / MIE |
|---|---|---|
| **Check mode** | Supports synchronous and asynchronous modes. In async, tag mismatches are reported later (delayed)| Apple insists on **synchronous mode** by default—tag mismatches are caught immediately, no delay/race windows allowed.|
| **Coverage of non-tagged memory** | Accesses to non-tagged memory (e.g. globals) may bypass checks in some implementations | EMTE requires that accesses from a tagged region to non-tagged memory also validate tag knowledge, making it harder to bypass by mixing allocations.|
| **Tag confidentiality / secrecy** | Tags might be observable or leaked via side channels | Apple adds **Tag Confidentiality Enforcement**, which attempts to prevent leakage of tag values (via speculative side-channels etc.).|
| **Allocator integration & retagging** | MTE leaves much of allocator logic to software | Apple’s secure typed allocators (kalloc_type, xzone malloc, etc.) integrate with EMTE: when memory is allocated or freed, tags are managed at fine granularity.|
| **Always-on by default** | In many platforms, MTE is optional or off by default | Apple enables EMTE / MIE by default on supported hardware (e.g. iPhone 17 / A19) for kernel and many user processes.|
Apple이 하드웨어와 소프트웨어 스택을 모두 제어하기 때문에 EMTE를 강하게 적용하고 성능 문제를 완화하며 side-channel 구멍을 닫을 수 있습니다.
---
## How EMTE works in practice (Apple / MIE)
Here’s a higher-level description of how EMTE operates under Apple’s MIE setup:
1. **Tag assignment**
- 메모리가 할당될 때(예: 커널이나 secure allocator를 통한 user space), 해당 블록에 비밀 태그(secret tag)가 할당됩니다.
- 사용자나 커널에 반환되는 포인터는 해당 태그를 상위 비트에 포함합니다(예: TBI / top byte ignore 메커니즘 사용).
2. **Tag checking on access**
- 포인터로 load/store가 수행될 때마다 하드웨어는 포인터의 태그와 메모리 블록(할당된)의 태그가 일치하는지 검사합니다. 불일치하면 즉시 fault가 발생합니다(동기식).
- 동기식이므로 “지연된 탐지” 창이 존재하지 않습니다.
3. **Retagging on free / reuse**
- 메모리가 해제될 때 allocator는 블록의 태그를 변경합니다(예전 태그를 가진 포인터는 더 이상 일치하지 않음).
- 따라서 use-after-free 포인터는 오래된 태그를 가지고 있어 접근 시 불일치가 발생합니다.
4. **Neighbor-tag differentiation to catch overflows**
- 인접한 할당들에는 서로 다른 태그를 부여합니다. 버퍼 오버플로우가 이웃 메모리로 흘러들어가면 tag mismatch로 인해 fault가 발생합니다.
- 경계를 넘는 작은 오버플로우를 포착하는 데 특히 효과적입니다.
5. **Tag confidentiality enforcement**
- 공격자가 태그를 알게 되면 올바른 태그로 포인터를 조작할 수 있으므로, Apple은 태그 값이 leak되지 않도록 방지해야 합니다.
- 이를 위해 speculative side-channel 등을 통한 유출을 막는 보호장치를 포함합니다.
6. **Kernel and user-space integration**
- Apple은 EMTE를 user-space뿐 아니라 kernel / OS-중요 구성요소에도 적용해 커널의 메모리 손상을 방지합니다.
- 하드웨어/OS는 커널이 user space를 대신해 실행할 때에도 태그 규칙이 적용되도록 보장합니다.
EMTE가 MIE에 통합되어 있기 때문에 Apple은 주요 공격 표면 전반에서 EMTE를 동기식 모드로 사용하며, 단순한 옵션이나 디버깅 모드로 남겨두지 않습니다.
---
## Exception handling in XNU
When an **exception** occurs (e.g., `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC`, etc.), the **Mach layer** of the XNU kernel is responsible for intercepting it before it becomes a UNIX-style **signal** (like `SIGSEGV`, `SIGBUS`, `SIGILL`, ...).
이 과정은 사용자 공간으로 전달되거나 BSD signal로 변환되기 전까지 여러 계층의 예외 전파 및 처리 단계를 거칩니다.
### Exception Flow (High-Level)
1. **CPU triggers a synchronous exception** (e.g., invalid pointer dereference, PAC failure, illegal instruction, etc.).
2. **Low-level trap handler** runs (`trap.c`, `exception.c` in XNU source).
3. The trap handler calls **`exception_triage()`**, the core of the Mach exception handling.
4. `exception_triage()` decides how to route the exception:
- First to the **thread's exception port**.
- Then to the **task's exception port**.
- Then to the **host's exception port** (often `launchd` or `ReportCrash`).
If none of these ports handle the exception, the kernel may:
- **Convert it into a BSD signal** (for user-space processes).
- **Panic** (for kernel-space exceptions).
### Core Function: `exception_triage()`
The function `exception_triage()` routes Mach exceptions up the chain of possible handlers until one handles it or until it's finally fatal. It's defined in `osfmk/kern/exception.c`.
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">c</span></div>
```c
void exception_triage(exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt);
일반적인 호출 흐름:
exception_triage() └── exception_deliver() ├── exception_deliver_thread() ├── exception_deliver_task() └── exception_deliver_host()
모두 실패하면 → bsd_exception()
에 의해 처리되고 → SIGSEGV
같은 시그널로 변환됩니다.
예외 포트
각 Mach 객체(thread, task, host)는 예외 메시지가 전송되는 exception ports를 등록할 수 있습니다.
다음 API에 의해 정의됩니다:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
각 예외 포트는 다음을 갖습니다:
- A mask (어떤 예외를 수신할지)
- A port name (메시지를 수신할 Mach 포트)
- A behavior (커널이 메시지를 보내는 방식)
- A flavor (어떤 thread state를 포함할지)
Debuggers and Exception Handling
A debugger (예: LLDB)는 대상 task 또는 thread에 exception port를 설정하며, 보통 task_set_exception_ports()
를 사용합니다.
예외가 발생하면:
- Mach 메시지가 debugger 프로세스로 전송됩니다.
- debugger는 예외를 처리(재개, 레지스터 수정, 명령 건너뛰기)할지 처리하지 않을지 결정할 수 있습니다.
- debugger가 처리하지 않으면 예외는 다음 레벨로 전파됩니다 (thread → task → host).
Flow of EXC_BAD_ACCESS
-
Thread가 유효하지 않은 포인터를 역참조 → CPU가 Data Abort를 발생시킵니다.
-
커널 트랩 핸들러가
exception_triage(EXC_BAD_ACCESS, ...)
를 호출합니다. -
메시지가 전송됩니다:
-
Thread port → (debugger가 breakpoint를 가로챌 수 있음).
-
debugger가 무시하면 → Task port → (프로세스 수준의 핸들러).
-
무시되면 → Host port (대개 ReportCrash).
- 아무도 처리하지 않으면 →
bsd_exception()
이 이를SIGSEGV
로 변환합니다.
PAC Exceptions
When Pointer Authentication (PAC) fails (signature mismatch), a special Mach exception is raised:
EXC_ARM_PAC
(type)- Codes may include details (예: key type, pointer type).
바이너리에 TFRO_PAC_EXC_FATAL
플래그가 있으면, 커널은 PAC 실패를 fatal로 취급하여 debugger 가로채기를 우회합니다. 이는 공격자가 debugger를 사용해 PAC 검사를 우회하는 것을 막기 위한 것으로, platform binaries에 대해 활성화됩니다.
Software Breakpoints
A software breakpoint (int3
on x86, brk
on ARM64) is implemented by causing a deliberate fault.
The debugger catches this via the exception port:
- instruction pointer 또는 메모리를 수정합니다.
- 원래 명령을 복원합니다.
- 실행을 재개합니다.
같은 메커니즘으로 PAC 예외를 "catch"할 수 있습니다 — 단, TFRO_PAC_EXC_FATAL
이 설정된 경우에는 절대 debugger에 도달하지 않습니다.
Conversion to BSD Signals
만약 어떤 핸들러도 예외를 수용하지 않으면:
-
커널은
task_exception_notify() → bsd_exception()
을 호출합니다. -
이는 Mach 예외를 신호로 매핑합니다:
Mach Exception | Signal |
---|---|
EXC_BAD_ACCESS | SIGSEGV or SIGBUS |
EXC_BAD_INSTRUCTION | SIGILL |
EXC_ARITHMETIC | SIGFPE |
EXC_SOFTWARE | SIGTRAP |
EXC_BREAKPOINT | SIGTRAP |
EXC_CRASH | SIGKILL |
EXC_ARM_PAC | SIGILL (on non-fatal) |
### Key Files in XNU Source
-
osfmk/kern/exception.c
→exception_triage()
,exception_deliver_*()
의 핵심입니다. -
bsd/kern/kern_sig.c
→ 신호 전달 로직. -
osfmk/arm64/trap.c
→ 저수준 트랩 핸들러. -
osfmk/mach/exc.h
→ 예외 코드와 구조체들. -
osfmk/kern/task.c
→ Task exception port 설정.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
커널은 zone allocator (kalloc
)을 사용했으며, 고정 크기 "zones"로 나뉘어 있었습니다.
각 zone은 단일 크기 클래스의 할당만 저장했습니다.
스크린샷에서:
Zone Name | Element Size | Example Use |
---|---|---|
default.kalloc.16 | 16 bytes | 매우 작은 커널 구조체, 포인터. |
default.kalloc.32 | 32 bytes | 작은 구조체, 객체 헤더. |
default.kalloc.64 | 64 bytes | IPC 메시지, 아주 작은 커널 버퍼. |
default.kalloc.128 | 128 bytes | OSObject 의 일부와 같은 중간 크기 객체. |
… | … | … |
default.kalloc.1280 | 1280 bytes | 큰 구조체, IOSurface/그래픽 메타데이터. |
작동 방식:
- 각 할당 요청은 가장 가까운 zone 크기로 올림됩니다.
(예: 50바이트 요청은
kalloc.64
zone에 속합니다). - 각 zone의 메모리는 freelist에 보관되었습니다 — 커널이 해제한 청크는 해당 zone으로 되돌아갔습니다.
- 64바이트 버퍼를 오버플로우하면 동일한 zone의 다음 객체를 덮어쓰게 됩니다.
이 때문에 heap spraying / feng shui가 매우 효과적이었습니다: 동일한 크기 클래스로 할당을 뿌려 이웃 객체를 예측할 수 있었기 때문입니다.
The freelist
각 kalloc zone 내부에서, 해제된 객체들은 즉시 시스템으로 반환되지 않고 freelist로 갔습니다. freelist는 사용 가능한 청크들의 연결 리스트였습니다.
-
청크가 해제될 때, 커널은 그 청크의 시작 부분에 포인터를 씁니다 → 같은 zone의 다음 자유 청크의 주소.
-
zone은 첫 번째 자유 청크를 가리키는 HEAD 포인터를 유지했습니다.
-
할당은 항상 현재 HEAD를 사용했습니다:
-
HEAD를 팝(그 메모리를 호출자에게 반환).
-
HEAD = HEAD->next로 업데이트(해제된 청크의 헤더에 저장된 값).
-
해제는 청크를 다시 푸시했습니다:
-
freed_chunk->next = HEAD
-
HEAD = freed_chunk
따라서 freelist는 해제된 메모리 자체 내부에 구성된 단순한 연결 리스트였습니다.
Normal state:
Zone page (64-byte chunks for example):
[ A ] [ F ] [ F ] [ A ] [ F ] [ A ] [ F ]
Freelist view:
HEAD ──► [ F ] ──► [ F ] ──► [ F ] ──► [ F ] ──► NULL
(next ptrs stored at start of freed chunks)
freelist 악용
free chunk의 첫 8바이트가 freelist pointer이므로, 공격자는 이를 변조할 수 있다:
-
Heap overflow로 인접한 freed chunk에 침투 → 그 “next” pointer를 덮어쓰기.
-
Use-after-free로 freed object에 쓰기 → 그 “next” pointer를 덮어쓰기.
그 다음 동일 크기 할당 시:
- allocator는 손상된 chunk를 pop한다.
- 공격자가 제공한 “next” pointer를 따른다.
- 임의의 메모리를 가리키는 포인터를 반환하여 fake object primitives 또는 targeted overwrite를 가능하게 한다.
Visual example of freelist poisoning:
Before corruption:
HEAD ──► [ F1 ] ──► [ F2 ] ──► [ F3 ] ──► NULL
After attacker overwrite of F1->next:
HEAD ──► [ F1 ]
(next) ──► 0xDEAD_BEEF_CAFE_BABE (attacker-chosen)
Next alloc of this zone → kernel hands out memory at attacker-controlled address.
This freelist 설계는 하드닝 이전에 exploit을 매우 효과적으로 만들었다: heap sprays로 인한 예측 가능한 이웃, raw pointer freelist 링크, 그리고 타입 분리가 없어 공격자가 UAF/overflow 버그를 임의의 kernel 메모리 제어로 승격시킬 수 있었다.
Heap Grooming / Feng Shui
heap grooming의 목표는 공격자가 overflow나 use-after-free를 트리거했을 때, 타겟(피해자) 객체가 공격자가 제어하는 객체 바로 옆에 오도록 힙 레이아웃을 조성하는 것이다.
이렇게 하면 메모리 손상 발생 시 공격자는 통제된 데이터로 피해자 객체를 신뢰성 있게 덮어쓸 수 있다.
절차:
- Spray allocations (fill the holes)
- 시간이 지나면서 kernel heap은 단편화된다: 일부 zone에는 이전에 free된 객체들로 인해 구멍이 생긴다.
- 공격자는 먼저 많은 더미 할당을 만들어 이러한 간격을 채운다. 이렇게 해서 힙은 “꽉 찬(packed)” 상태가 되어 예측 가능해진다.
- Force new pages
- 구멍이 채워지면 다음 할당은 zone에 새로 추가된 페이지에서 와야 한다.
- 새로운 페이지는 객체들이 흩어져 있지 않고 클러스터링되게 만든다.
- 이는 공격자가 이웃 제어를 훨씬 더 잘 할 수 있게 해준다.
- Place attacker objects
- 공격자는 이제 다시 스프레이를 하여 그 새 페이지들에 공격자가 제어하는 객체들을 많이 만든다.
- 이 객체들은 모두 같은 zone에 속하므로 크기와 배치가 예측 가능하다.
- Free a controlled object (make a gap)
- 공격자는 의도적으로 자신이 만든 객체 중 하나를 free한다.
- 이렇게 하면 힙에 “구멍”이 생기고 allocator는 이후 같은 크기의 다음 할당을 위해 그 슬롯을 재사용하게 된다.
- Victim object lands in the hole
- 공격자는 kernel에게 피해자 객체(손상시키려는 것)를 할당하도록 유도한다.
- 그 구멍이 freelist에서 첫 번째 사용 가능한 슬롯이므로 피해자는 공격자가 free한 위치에 정확히 배치된다.
- Overflow / UAF into victim
- 이제 공격자 객체가 피해자 주변에 제어된 상태로 존재한다.
- 자신의 객체에서 overflow하거나 free된 객체를 재사용함으로써 공격자는 피해자의 메모리 필드를 선택한 값으로 신뢰성 있게 덮어쓸 수 있다.
왜 작동하는가:
- Zone allocator의 예측성: 같은 크기의 할당은 항상 같은 zone에서 나온다.
- Freelist 동작: 새 할당은 가장 최근에 free된 청크를 먼저 재사용한다.
- Heap sprays: 공격자는 예측 가능한 내용으로 메모리를 채우고 레이아웃을 제어한다.
- 결과: 공격자는 피해자 객체가 어디에 놓일지와 그 옆에 어떤 데이터가 놓일지를 제어한다.
Modern Kernel Heap (iOS 15+/A12+ SoCs)
Apple은 allocator를 강화하여 heap grooming을 훨씬 어렵게 만들었다:
1. From Classic kalloc to kalloc_type
- Before: 각 사이즈 클래스(16, 32, 64, … 1280 등)마다 단일
kalloc.<size>
zone이 존재했다. 그 크기의 어떤 객체든 그곳에 배치되었기 때문에 → 공격자 객체가 권한 있는 kernel 객체 옆에 앉을 수 있었다. - Now:
- Kernel 객체들은 typed zones (
kalloc_type
)에서 할당된다. - 각 객체 타입(e.g.,
ipc_port_t
,task_t
,OSString
,OSData
)은 심지어 크기가 같더라도 전용 zone을 가진다. - 객체 타입 ↔ zone 간 매핑은 컴파일 시 kalloc_type 시스템에서 생성된다.
공격자는 더 이상 제어된 데이터(OSData
)가 같은 크기의 민감한 kernel 객체(task_t
) 옆에 위치한다고 보장할 수 없다.
2. Slabs and Per-CPU Caches
- 힙은 slabs(해당 zone을 위해 고정 크 청크로 잘라진 메모리 페이지)로 나뉜다.
- 각 zone은 경쟁을 줄이기 위해 per-CPU cache를 가진다.
- Allocation 경로:
- per-CPU cache 시도.
- 비어있으면 global freelist에서 가져옴.
- freelist가 비어있으면 새 slab(한 개 이상의 페이지)를 할당함.
- 이점: 이런 분산화는 할당이 서로 다른 CPU의 캐시에서 충족될 수 있으므로 heap sprays의 결정론성을 낮춘다.
3. Randomization inside zones
- zone 내에서 freed 요소들은 단순한 FIFO/LIFO 순서로 다시 제공되지 않는다.
- 최신 XNU는 encoded freelist pointers(Linux의 safe-linking 유사, 약 iOS 14 도입)를 사용한다.
- 각 freelist 포인터는 per-zone secret cookie로 XOR 인코딩되어 있다.
- 이는 공격자가 write primitive를 얻더라도 가짜 freelist 포인터를 위조하는 것을 방지한다.
- 일부 할당은 slab 내에서 배치가 무작위화되므로 스프레이가 인접성을 보장하지 않는다.
4. Guarded Allocations
- 특정 핵심 kernel 객체(e.g., credentials, task structures)는 guarded zones에서 할당된다.
- 이 zone들은 slab 사이에 guard pages(매핑되지 않은 메모리)를 삽입하거나 객체 주위에 redzones를 사용한다.
- guard page로의 어떠한 overflow도 fault를 트리거 → 조용한 손상 대신 즉시 panic을 유발한다.
5. Page Protection Layer (PPL) and SPTM
- freed 객체를 제어하더라도 kernel의 모든 메모리를 수정할 수 있는 것은 아니다:
- **PPL (Page Protection Layer)**은 특정 영역(e.g., code signing data, entitlements)이 심지어 kernel 자체에게도 read-only가 되도록 강제한다.
- A15/M2+ 디바이스에서는 이 역할이 SPTM (Secure Page Table Monitor) + **TXM (Trusted Execution Monitor)**로 대체/강화된다.
- 이러한 하드웨어 강제 레이어는 공격자가 단일 heap 손상에서 중요한 보안 구조물을 임의로 패치하는 것으로 승격하는 것을 불가능하게 한다.
- (추가/강화): 또한 kernel에서 포인터(특히 function pointers, vtables)를 보호하기 위해 **PAC (Pointer Authentication Codes)**가 사용되어 이를 위조하거나 손상하기 더 어렵게 만든다.
- (추가/강화): zones는 zone_require / zone enforcement를 강제할 수 있다; 즉, free된 객체는 올바른 typed zone을 통해서만 반환될 수 있다; 잘못된 cross-zone free는 panic을 유발하거나 거부될 수 있다. (Apple은 이 점을 그들의 메모리 안전 관련 글에서 암시한다)
6. Large Allocations
- 모든 할당이
kalloc_type
을 통하지는 않는다. - 매우 큰 요청(대략 ~16 KB 이상)은 typed zones를 우회하고 페이지 할당을 통해 **kernel VM (kmem)**에서 직접 제공된다.
- 이러한 경우는 덜 예측 가능하지만 또한 다른 객체와 slab을 공유하지 않으므로 덜 exploitable 하다.
7. Allocation Patterns Attackers Target
이러한 보호에도 불구하고 공격자들은 여전히 다음을 노린다:
- Reference count objects: retain/release 카운터를 조작할 수 있다면 use-after-free를 유발할 수 있다.
- Objects with function pointers (vtables): 하나를 손상시키는 것만으로도 여전히 control flow를 얻을 수 있다.
- Shared memory objects (IOSurface, Mach ports): 이들은 user ↔ kernel을 잇는 브리지이기 때문에 여전히 공격 대상이다.
하지만 — 이전과 달리 — 단순히 OSData
를 스프레이해서 task_t
옆에 오리라 기대할 수 없다. 성공하려면 type-specific bugs나 info leaks가 필요하다.
Example: Allocation Flow in Modern Heap
Suppose userspace calls into IOKit to allocate an OSData
object:
- Type lookup →
OSData
maps tokalloc_type_osdata
zone (size 64 bytes). - Check per-CPU cache for free elements.
- If found → return one.
- If empty → go to global freelist.
- If freelist empty → allocate a new slab (page of 4KB → 64 chunks of 64 bytes).
- Return chunk to caller.
Freelist pointer protection:
- Each freed chunk stores the address of the next free chunk, but encoded with a secret key.
- Overwriting that field with attacker data won’t work unless you know the key.
Comparison Table
Feature | Old Heap (Pre-iOS 15) | Modern Heap (iOS 15+ / A12+) |
---|---|---|
Allocation granularity | Fixed size buckets (kalloc.16 , kalloc.32 , etc.) | Size + type-based buckets (kalloc_type ) |
Placement predictability | High (same-size objects side by side) | Low (same-type grouping + randomness) |
Freelist management | Raw pointers in freed chunks (easy to corrupt) | Encoded pointers (safe-linking style) |
Adjacent object control | Easy via sprays/frees (feng shui predictable) | Hard — typed zones separate attacker objects |
Kernel data/code protections | Few hardware protections | PPL / SPTM protect page tables & code pages, and PAC protects pointers |
Allocation reuse validation | None (freelist pointers raw) | zone_require / zone enforcement |
Exploit reliability | High with heap sprays | Much lower, requires logic bugs or info leaks |
Large allocations handling | All small allocations managed equally | Large ones bypass zones → handled via VM |
Modern Userland Heap (iOS, macOS — type-aware / xzone malloc)
최근 Apple OS 버전들(특히 iOS 17+)에서 Apple은 더 안전한 userland allocator인 xzone malloc (XZM)을 도입했다. 이는 kernel의 kalloc_type
에 해당하는 user-space 아날로그로, 타입 인식, 메타데이터 분리, 메모리 태깅 보호를 적용한다.
Goals & Design Principles
- Type segregation / type awareness: *타입 또는 사용(pointers vs data)*에 따라 할당을 그룹화하여 type confusion 및 cross-type reuse를 방지한다.
- Metadata isolation: 힙 메타데이터(e.g. free lists, size/state bits)를 객체 페이로드와 분리하여 OOB 쓰기가 메타데이터를 손상시킬 가능성을 줄인다.
- Guard pages / redzones: 할당 주위에 unmapped 페이지나 패딩을 삽입하여 overflow를 적발한다.
- Memory tagging (EMTE / MIE): 하드웨어 태깅과 함께 동작하여 use-after-free, OOB, 무효 접근을 탐지한다.
- Scalable performance: 낮은 오버헤드를 유지하고 과도한 단편화를 피하며 초당 많은 할당을 낮은 지연으로 지원한다.
Architecture & Components
아래는 xzone allocator의 주요 요소들이다:
Segment Groups & Zones
- Segment groups는 주소 공간을 사용 범주별로 분할한다: 예:
data
,pointer_xzones
,data_large
,pointer_large
. - 각 segment group은 해당 카테고리의 할당을 호스트하는 segments(VM 범위)를 포함한다.
- 각 segment와 연관된 metadata slab(별도의 VM 영역)가 있어 해당 segment의 메타데이터(e.g. free/used bits, size classes)를 저장한다. 이 out-of-line (OOL) metadata는 메타데이터가 객체 페이로드와 섞이지 않도록 하여 overflow로 인한 손상을 완화한다.
- Segments는 chunks(슬라이스)로 잘려지고, 각 chunk는 다시 blocks(할당 단위)로 세분된다. 하나의 chunk는 특정 size class와 segment group에 묶여 있다(즉, chunk 내 모든 block은 동일한 크기 & 카테고리를 공유).
- 작은/중간 크기 할당은 고정 크 chunk를 사용하고, 큰/거대한 할당은 별도로 매핑할 수 있다.
Chunks & Blocks
- chunk는 한 size class 내 할당 전용 영역(종종 여러 페이지)이다.
- chunk 내부에서 blocks는 할당 가능한 슬롯이다. free된 block들은 metadata slab(예: 비트맵이나 out-of-line에 저장된 free lists)로 추적된다.
- chunks 사이(또는 내부)에 guard slices / guard pages가 삽입될 수 있어 OOB 쓰기를 포착한다.
Type / Type ID
- 모든 할당 지점(또는 malloc, calloc 등 호출)은 type identifier(
malloc_type_id_t
)와 연관되어 있으며, 이는 어떤 종류의 객체가 할당되는지를 인코딩한다. 이 type ID는 allocator에 전달되어 어느 zone/segment가 할당을 제공할지 선택한다. - 이 때문에 두 할당이 같은 크기라도 타입이 다르면 완전히 다른 zone으로 갈 수 있다.
- 초기 iOS 17 버전에서는 일부 API(e.g. CFAllocator)가 완전한 타입 인식을 하지 못했으며; Apple은 iOS 18에서 이러한 약점을 일부 해결했다.
Allocation & Freeing Workflow
여기 xzone에서 할당 및 해제의 높은 수준 흐름이 있다:
- malloc / calloc / realloc / typed alloc이 크기와 type ID와 함께 호출된다.
- allocator는 type ID를 사용해 적절한 segment group / zone을 선택한다.
- 해당 zone/segment 내에서 요청된 크기의 free block이 있는 chunk를 찾는다.
- local caches / per-thread pools 또는 metadata의 free block lists를 참조할 수 있다.
- 사용 가능한 free block이 없으면 zone에 새 chunk를 할당할 수 있다.
- metadata slab가 업데이트된다(free 비트가 클리어되고 bookkeeping 수행).
- 메모리 태깅(EMTE)이 적용되는 경우 반환된 block에 tag가 할당되고 metadata는 해당 블록의 “live” 상태를 반영하도록 업데이트된다.
free()
가 호출되면:
- block은 metadata에서 freed로 표시된다(OOL slab을 통해).
- block은 free list에 놓이거나 재사용을 위해 풀링될 수 있다.
- 선택적으로 block 내용이 지워지거나 poisoning되어 데이터 누수나 UAF exploit 가능성을 줄인다.
- 해당 block에 연관된 하드웨어 태그는 무효화되거나 재태그될 수 있다.
- 전체 chunk가 비게 되면(모든 block이 freed) allocator는 메모리 압력 하에서 해당 chunk를 reclaim(언맵하거나 OS로 반환)할 수 있다.
Security Features & Hardening
다음은 modern userland xzone에 내장된 방어 기능들이다:
Feature | Purpose | Notes |
---|---|---|
Metadata decoupling | Prevent overflow from corrupting metadata | Metadata lives in separate VM region (metadata slab) |
Guard pages / unmapped slices | Catch out-of-bounds writes | Helps detect buffer overflows rather than silently corrupting adjacent blocks |
Type-based segregation | Prevent cross-type reuse & type confusion | Even same-size allocations from different types go to different zones |
Memory Tagging (EMTE / MIE) | Detect invalid access, stale references, OOB, UAF | xzone works in concert with hardware EMTE in synchronous mode (“Memory Integrity Enforcement”) |
Delayed reuse / poisoning / zap | Reduce chance of use-after-free exploitation | Freed blocks may be poisoned, zeroed, or quarantined before reuse |
Chunk reclamation / dynamic unmapping | Reduce memory waste and fragmentation | Entire chunks may be unmapped when unused |
Randomization / placement variation | Prevent deterministic adjacency | Blocks in a chunk and chunk selection may have randomized aspects |
Segregation of “data-only” allocations | Separate allocations that don’t store pointers | Reduces attacker control over metadata or control fields |
Interaction with Memory Integrity Enforcement (MIE / EMTE)
- Apple의 MIE (Memory Integrity Enforcement)는 **Enhanced Memory Tagging Extension (EMTE)**를 주요 공격 표면 전반에서 항상 켜진 동기식 모드로 가져오는 하드웨어 + OS 프레임워크다.
- xzone allocator는 user space에서 MIE의 근본적인 기반이다: xzone을 통해 수행된 할당은 태그를 받고 하드웨어에 의해 접근이 검사된다.
- MIE에서는 allocator, 태그 할당, metadata 관리, 태그 기밀성 강제가 통합되어 메모리 오류(e.g. stale reads, OOB, UAF)가 즉시 포착되어 나중에 악용되는 것을 방지한다.
If you like, I can also generate a cheat-sheet or diagram of xzone internals for your book. Do you want me to do that next? ::contentReference[oai:20]{index=20}
(Old) Physical Use-After-Free via IOSurface
Ghidra Install BinDiff
Download BinDiff DMG from https://www.zynamics.com/bindiff/manual and install it.
Open Ghidra with ghidraRun
and go to File
--> Install Extensions
, press the add button and select the path /Applications/BinDiff/Extra/Ghidra/BinExport
and click OK and isntall it even if there is a version mismatch.
Using BinDiff with Kernel versions
- Go to the page https://ipsw.me/ and download the iOS versions you want to diff. These will be
.ipsw
files. - Decompress until you get the bin format of the kernelcache of both
.ipsw
files. You have information on how to do this on:
macOS Kernel Extensions & Kernelcache
- Open Ghidra with
ghidraRun
, create a new project and load the kernelcaches. - Open each kernelcache so they are automatically analyzed by Ghidra.
- Then, on the project Window of Ghidra, right click each kernelcache, select
Export
, select formatBinary BinExport (v2) for BinDiff
and export them. - Open BinDiff, create a new workspace and add a new diff indicating as primary file the kernelcache that contains the vulnerability and as secondary file the patched kernelcache.
Finding the right XNU version
If you want to check for vulnerabilities in a specific version of iOS, you can check which XNU release version the iOS version uses at [https://www.theiphonewiki.com/wiki/kernel]https://www.theiphonewiki.com/wiki/kernel).
For example, the versions 15.1 RC
, 15.1
and 15.1.1
use the version Darwin Kernel Version 21.1.0: Wed Oct 13 19:14:48 PDT 2021; root:xnu-8019.43.1~1/RELEASE_ARM64_T8006
.
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
tip
AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.