iOS Exploiting
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) 이것은 기본적인 보호 중 하나로: 모든 실행 가능한 코드(앱, dynamic libraries, JIT-ed code, extensions, frameworks, caches)는 Apple의 신뢰 루트에 연결된 인증서 체인으로 암호학적으로 서명되어야 한다. 런타임에서 바이너리를 메모리에 로드하기 전에(또는 특정 경계 간에 점프를 하기 전에) 시스템이 서명을 검사한다. 코드가 수정되었거나(비트 플립, 패치) 서명되지 않았다면 로드가 실패한다.
- 막는 것: 익스플로잇 체인에서의 “클래식 payload drop + execute” 단계; 임의 코드 주입; 기존 바이너리를 변경해 악성 로직을 삽입하는 것.
- 메커니즘 상세:
- Mach-O 로더(및 dynamic linker)는 코드 페이지, 세그먼트, entitlements, team IDs, 그리고 서명이 파일의 내용을 포함하는지 등을 검사한다.
- JIT 캐시나 동적으로 생성된 코드 같은 메모리 영역에 대해서는, Apple은 페이지가 서명되었거나 특별한 API(예:
mprotectwith code-sign checks)를 통해 검증되도록 요구한다. - 서명에는 entitlements와 식별자가 포함되며, OS는 특정 API나 권한 있는 기능이 특정 entitlements을 요구한다는 것을 강제한다(위조 불가).
예시
익스플로잇이 프로세스에서 코드 실행을 얻어 힙에 shellcode를 쓰고 그곳으로 점프하려고 한다고 가정하자. iOS에서는 그 페이지가 실행 가능하도록 표시되는 것뿐만 아니라 코드 서명 제약도 만족해야 한다. shellcode는 Apple의 인증서로 서명되어 있지 않기 때문에 점프가 실패하거나 시스템이 해당 메모리 영역을 실행 가능으로 만드는 것을 거부한다.2. CoreTrust
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust는 바이너리(시스템 및 유저 바이너리 포함)의 런타임 서명 검증을 Apple의 루트 인증서에 대하여 수행하는 서브시스템으로, 로컬의 userland 신뢰 저장소에 의존하지 않는다.
- 막는 것: 설치 후 바이너리 변조, 시스템 라이브러리나 유저 앱을 교체/패치하려는 jailbreaking 기법; 신뢰된 바이너리를 악성 바이너리로 대체해 시스템을 속이는 시도.
- 메커니즘 상세:
- CoreTrust는 로컬 신뢰 DB나 인증서 캐시를 신뢰하는 대신 Apple의 루트를 직접 참조하거나 안전한 체인에서 중간 인증서를 검증한다.
- 기존 바이너리에 대한 변경(예: 파일시스템 상의 변경)이 감지되어 거부되도록 보장한다.
- entitlements, team IDs, 코드 서명 플래그 및 기타 메타데이터를 로드 시 바이너리에 연동한다.
예시
jailbreak는 `SpringBoard`나 `libsystem`을 패치된 버전으로 교체해 영구성을 얻으려 할 수 있다. 그러나 OS의 로더나 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는 쓰기 가능(데이터)으로 표시된 페이지는 실행 불가이고, 실행 가능으로 표시된 페이지는 쓰기 불가임을 강제한다. 따라서 힙이나 스택에 shellcode를 쓰고 실행하는 것이 불가능하다.
- 막는 것: 직접적인 shellcode 실행; 고전적인 버퍼 오버플로우 → 주입된 shellcode로 점프.
- 메커니즘 상세:
- MMU / 메모리 보호 플래그(페이지 테이블을 통해)가 분리를 강제한다.
- 쓰기 가능한 페이지를 실행 가능으로 표시하려는 시도는 시스템 검사를 유발하며(금지되거나 코드 서명 승인이 필요).
- 많은 경우 페이지를 실행 가능으로 만들려면 추가 제약이나 검사를 강제하는 OS API를 거쳐야 한다.
예시
오버플로우로 shellcode를 힙에 쓴 공격자가 `mprotect(heap_addr, size, PROT_EXEC)`을 시도한다. 그러나 시스템은 거부하거나 새로운 페이지가 코드 서명 제약을 통과해야 한다고 검증한다(그리고 shellcode는 통과하지 못한다).4. Address Space Layout Randomization (ASLR)
Introduced in iOS ~4–5 era (roughly iOS 4–5 timeframe) ASLR은 주요 메모리 영역(라이브러리, heap, stack 등)의 베이스 주소를 각 프로세스 실행 시 무작위화한다. 가젯들의 주소는 실행마다 바뀐다.
- 막는 것: ROP/JOP을 위한 가젯 주소 하드코딩; 정적 익스플로잇 체인; 알려진 오프셋으로의 블라인드 점프.
- 메커니즘 상세:
- 로드된 각 라이브러리/동적 모듈은 무작위 오프셋으로 rebased된다.
- 스택 및 힙의 베이스 포인터는(일정한 엔트로피 제한 내에서) 무작위화된다.
- 때때로 다른 영역(예: mmap 할당)도 무작위화된다.
- information-leak mitigations와 결합하면, 공격자는 런타임에 베이스 주소를 알아내기 위해 먼저 주소를 leak해야 한다.
예시
ROP 체인은 `0x….lib + offset`에 있는 가젯을 기대한다. 그러나 `lib`는 실행마다 다르게 재배치되므로 하드코딩된 체인은 실패한다. 익스플로잇은 모듈의 베이스 주소를 알아내기 위해 먼저 주소를 leak해야 가젯 주소를 계산할 수 있다.5. Kernel Address Space Layout Randomization (KASLR)
Introduced in iOS ~ (iOS 5 / iOS 6 timeframe) 유저 ASLR과 유사하게, KASLR은 부팅 시 커널 텍스트 및 다른 커널 구조의 베이스를 무작위화한다.
- 막는 것: 커널 코드나 데이터의 고정 위치에 의존하는 커널 수준 익스플로잇; 정적 커널 익스플로잇.
- 메커니즘 상세:
- 부팅마다 커널의 베이스 주소가(범위 내에서) 무작위화된다.
task_structs,vm_map등과 같은 커널 데이터 구조도 재배치되거나 오프셋이 변경될 수 있다.- 공격자는 먼저 커널 포인터를 leak하거나 정보 공개 취약점을 이용해 오프셋을 계산해야 커널 구조나 코드를 조작할 수 있다.
예시
로컬 취약점이 `KERN_BASE + offset`에 있는 커널 함수 포인터(예: vtable)를 오염시키려 한다. 그러나 `KERN_BASE`가 알려져 있지 않으므로 공격자는 먼저 (예: 읽기 프리미티브로) 이를 leak해야 올바른 주소를 계산할 수 있다.6. Kernel Patch Protection (KPP / AMCC)
Introduced in newer iOS / A-series hardware (post around iOS 15–16 era or newer chips) KPP(또는 AMCC)는 커널 텍스트 페이지의 무결성을 지속적으로 모니터링(해시 또는 체크섬)한다. 허용된 윈도우 외에서 패치(인라인 훅, 코드 수정)를 감지하면 커널 패닉이나 재부팅을 트리거한다.
- 막는 것: 지속적인 커널 패칭(커널 명령어 수정), 인라인 훅, 정적 함수 덮어쓰기.
- 메커니즘 상세:
- 하드웨어나 펌웨어 모듈이 커널 텍스트 영역을 모니터링한다.
- 주기적이거나 필요 시 페이지를 재해시하고 기대값과 비교한다.
- 정당한 업데이트 윈도우 밖에서 불일치가 발생하면 장치를 패닉시켜(악성 패치를 안정화시키는 것을 방지) 재부팅한다.
- 공격자는 탐지 윈도우를 피하거나 합법적인 패치 경로를 사용해야 한다.
예시
익스플로잇이 커널 함수 프롤로그(예: `memcmp`)를 패치해 호출을 가로채려 한다. 그러나 KPP는 코드 페이지의 해시가 기대값과 일치하지 않음을 감지하고 커널 패닉을 발생시켜 장치를 크래시시킨다.7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR은 하드웨어로 강제되는 메커니즘으로: 부팅 초기에 커널 텍스트가 잠기면 EL1(커널)에서 해당 코드 페이지를 더 이상 쓸 수 없게 된다.
- 막는 것: 부팅 후 커널 코드 수정(예: 패치, 인플레이스 코드 주입) — EL1 권한 수준에서의 쓰기.
- 메커니즘 상세:
- 부팅(secure/bootloader 단계) 동안 메모리 컨트롤러(또는 보안 하드웨어 유닛)가 커널 텍스트를 포함하는 물리적 페이지를 읽기 전용으로 표시한다.
- 익스플로잇이 완전한 커널 권한을 얻어도 해당 페이지에 쓰기는 불가능하다.
- 이를 수정하려면 부트 체인을 손상시키거나 KTRR 자체를 우회해야 한다.
예시
권한 상승 익스플로잇이 EL1로 점프해 커널 함수에 트램폴린을 쓰려고 한다. 그러나 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)에 저장되며 유저 모드에서 직접 읽을 수 없다.
- 많은 시스템에서 64비트 포인터의 모든 비트가 사용되지 않기 때문에(예: 48비트 주소공간) 상위 비트가 남아 PAC를 저장하는 데 사용될 수 있다.
Architectural Basis & Key Types
-
ARMv8.3은 다섯 개의 128-bit 키(각각 두 개의 64-bit 시스템 레지스터로 구현)를 도입한다.
-
APIAKey — instruction pointers 용 (도메인 “I”, 키 A)
-
APIBKey — 두 번째 instruction pointer 키 (도메인 “I”, 키 B)
-
APDAKey — data pointers 용 (도메인 “D”, 키 A)
-
APDBKey — data pointers 용 (도메인 “D”, 키 B)
-
APGAKey — generic 키, 포인터가 아닌 데이터나 기타 일반 용도로 서명
-
이 키들은 특권 시스템 레지스터에 저장되며(EL1/EL2 등에서만 접근 가능), 유저 모드에서는 접근할 수 없다.
-
PAC는 암호학적 함수(ARM은 QARMA를 제안)로 계산되며 사용되는 요소:
- 포인터 값(정규화된 부분)
- modifier(컨텍스트 값, 예: salt)
- 비밀 키
- 내부 트윅 로직 결과 PAC가 포인터의 상위 비트에 저장된 값과 일치하면 인증이 성공한다.
Instruction Families
명명 규칙은: PAC / AUT / XPAC, 그 다음 도메인 문자이다.
PACxx명령은 포인터에 서명하고 PAC를 삽입한다AUTxx명령은 인증 + 스트립(검증 후 PAC 제거)을 수행한다XPACxx명령은 검증 없이 PAC를 제거한다
Domains / suffixes:
| 어셈블리 명령어 | 의미 / 도메인 | 키 / 도메인 | 어셈블리에서의 예제 사용법 |
|---|---|---|---|
| PACIA | instruction pointer에 APIAKey로 서명 | “I, A” | PACIA X0, X1 — X1을 modifier로 사용해 X0의 포인터를 서명 |
| PACIB | instruction pointer에 APIBKey로 서명 | “I, B” | PACIB X2, X3 |
| PACDA | data pointer에 APDAKey로 서명 | “D, A” | PACDA X4, X5 |
| PACDB | data pointer에 APDBKey로 서명 | “D, B” | PACDB X6, X7 |
| PACG / PACGA | APGAKey로 generic(비포인터) 서명 | “G” | PACGA X8, X9, X10 (X10을 modifier로 사용해 X9에 서명하고 결과를 X8에 저장) |
| AUTIA | APIA로 서명된 instruction pointer 인증 및 PAC 제거 | “I, A” | AUTIA X0, X1 — X1을 modifier로 사용해 X0의 PAC를 검사하고 제거 |
| AUTIB | APIB 도메인 인증 | “I, B” | AUTIB X2, X3 |
| AUTDA | APDA로 서명된 data pointer 인증 | “D, A” | AUTDA X4, X5 |
| AUTDB | APDB로 서명된 data pointer 인증 | “D, B” | AUTDB X6, X7 |
| AUTGA | generic / blob (APGA) 인증 | “G” | AUTGA X8, X9, X10 (generic 검증) |
| XPACI | PAC 제거(검증 없음, instruction 도메인) | “I” | XPACI X0 — X0의 PAC를 제거 (instruction 도메인) |
| XPACD | PAC 제거(검증 없음, data 도메인) | “D” | XPACD X4 — X4의 data 포인터에서 PAC 제거 |
일부 특수/별칭 형태:
PACIASP는PACIA X30, SP의 축약형(링크 레지스터를 SP를 modifier로 서명)AUTIASP는AUTIA X30, SP(SP로 링크 레지스터 인증)RETAA,RETAB(인증 후 반환) 또는BLRAA(인증 후 브랜치)와 같은 결합형이 ARM 확장/컴파일러 지원에 존재한다.- modifier가 0인 제로-모디파이어 변형도 존재:
PACIZA/PACIZB등(모디파이어가 암묵적으로 0).
Modifiers
modifier의 주된 목적은 PAC를 특정 컨텍스트에 바인딩해 동일한 주소라도 다른 컨텍스트에서 다른 PAC를 생성하게 하는 것이다. 이는 포인터를 다른 프레임이나 객체에서 단순히 재사용하는 것을 방지한다. 해시에 salt를 추가하는 것과 유사하다.
따라서:
- modifier는 컨텍스트 값(다른 레지스터)으로 PAC 계산에 혼합된다. 일반적인 선택: stack pointer(
SP), frame pointer, 또는 객체 ID. - SP를 modifier로 사용하는 것은 return address 서명에 흔히 쓰인다: PAC는 특정 스택 프레임에 묶인다. 다른 프레임에서 LR을 재사용하면 modifier가 달라져 PAC 검증이 실패한다.
- 동일한 포인터 값이라도 다른 modifier로 서명하면 다른 PAC가 생성된다.
- modifier는 반드시 비밀일 필요는 없지만, 이상적으로는 공격자가 제어할 수 없는 값이어야 한다.
- 의미 있는 modifier가 없는 경우 일부 명령 형태는 암묵적 0 등을 사용한다.
Apple / iOS / XNU Customizations & Observations
- Apple의 PAC 구현은 부팅별 diversifiers를 포함하여 키나 트윅이 부팅마다 바뀌어 부팅 간 재사용을 막는다.
- 또한 사용자 모드에서 서명된 PAC가 커널 모드에서 쉽게 재사용되지 않도록 크로스 도메인 완화책을 포함한다.
- Apple Silicon(M1) 리버스 엔지니어링 결과, 9개의 modifier 타입과 키 제어를 위한 Apple-특정 시스템 레지스터가 있음이 드러났다.
- Apple은 커널의 여러 서브시스템에 PAC를 사용: 리턴 주소 서명, 커널 데이터의 포인터 무결성, 서명된 쓰레드 컨텍스트 등.
- Google Project Zero는 강력한 메모리 읽기/쓰기 프리미티브가 있을 경우 A 키로 커널 PAC를 위조할 수 있음을 A12 시대 디바이스에서 보였으나, 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” 커스터마이제이션은 (도메인 전환, 키별 활성화 비트 등) 공격 표면을 더 좁혔다. i.blackhat.com
- Apple silicon(M1/M2)에서 알려진 커널 PAC 우회 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는 시스템 프레임워크와 라이브러리의 큰 사전 연결된 블롭이다. 매우 널리 공유되기 때문에 shared cache 내부의 함수 포인터는 미리 서명되어 여러 프로세스에서 사용된다. 공격자는 이미 서명된 포인터를 “PAC oracle“로 타깃팅한다.
- 일부 우회 기법은 shared cache에 있는 A-key로 서명된 포인터를 추출해 재사용하는 것을 시도한다.
- “No Clicks Required” 발표는 shared cache 위에 오라클을 구축해 상대적 주소를 추론하고 서명된 포인터와 결합해 PAC를 우회하는 방법을 설명한다. saelo.github.io
- 또한 userspace에서 shared libraries로부터 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의
dlsym로직에는: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 로더와 동적 재배치 로직은 복잡하고 때때로 재배치를 수행하기 위해 페이지를 일시적으로 read/write로 매핑한 뒤 다시 read-only로 바꾼다. 공격자는 이런 타이밍 창을 악용한다. Synacktiv의 발표는 dynamic relocations를 통한 타이밍 기반 PAC 우회(“Operation Triangulation”)를 설명한다. Synacktiv
- DYLD 페이지는 이제 SPRR / VM_FLAGS_TPRO 같은 보호로 보호되지만 이전 버전은 약한 보호를 가졌다. Synacktiv
- WebKit 익스플로잇 체인에서는 DYLD 로더가 종종 PAC 우회의 타깃이 된다(재배치, interposer 훅 등). Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- 유저랜드 익스플로잇 체인에서 Objective-C 런타임 메소드들(
NSPredicate,NSExpression,NSInvocation)은 통제된 메모리에서 제어 호출을 은밀히 전달하는 데 사용된다. - PAC 도입 이전의 iOS에서는 fake NSInvocation 객체를 사용해 제어를 임의의 selector로 호출하는 익스플로잇이 있었다. PAC가 도입되면서 수정이 필요했지만 SLOP(SeLector Oriented Programming) 기법은 PAC 환경에서도 확장되어 사용된다. Project Zero
- 원래 SLOP 기법은 가짜 invocation을 만들어 ObjC 호출을 체이닝했는데; 우회는 ISA나 selector 포인터가 완전히 PAC로 보호되지 않는 경우를 이용했다. Project Zero
- 포인터 인증이 부분적으로만 적용되는 환경에서는 메소드/셀렉터/타겟 포인터가 항상 PAC 보호를 받지 않아 우회 여지가 있다.
Example Flow
예시 서명 및 인증
``` ; 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 overwrites a return address on the stack. The attacker writes the target gadget address but cannot compute the correct PAC. When the function returns, the CPU’s `AUTIA` instruction faults because the PAC mismatch. The chain fails.
Project Zero’s analysis on A12 (iPhone XS) showed how Apple’s PAC is used and methods of forging PACs if an attacker has a memory read/write primitive.
</details>
### 9. **분기 대상 식별 (BTI)**
**ARMv8.5(이후 하드웨어)에서 도입됨**
BTI는 **간접 분기 대상**을 검사하는 하드웨어 기능입니다: `blr` 또는 간접 호출/점프를 실행할 때, 대상은 **BTI landing pad**(`BTI j` 또는 `BTI c`)로 시작해야 합니다. landing pad가 없는 gadget 주소로 점프하면 예외가 발생합니다.
LLVM의 구현은 BTI 명령의 세 가지 변형과 그것이 분기 유형에 어떻게 매핑되는지에 대해 언급합니다.
| BTI 변형 | 허용하는 것(어떤 분기 유형) | 일반적인 배치 / 사용 사례 |
|-------------|----------------------------------------|-------------------------------|
| **BTI C** | 콜 스타일의 간접 분기의 대상(예: `BLR`, 또는 `BR`이 X16/X17을 사용한 경우) | 간접 호출될 수 있는 함수의 진입부에 둠 |
| **BTI J** | 점프 스타일 분기의 대상(예: tail call에 사용되는 `BR`) | jump table이나 tail-call로 도달 가능한 블록의 시작에 배치 |
| **BTI JC** | C와 J 둘 다로 동작 | 콜이나 점프 분기 어느 쪽에서도 대상으로 지정될 수 있음 |
- branch target enforcement로 컴파일된 코드에서는 컴파일러가 각 유효한 간접 분기 대상(함수 시작부나 점프로 도달 가능한 블록)에 BTI 명령(C, J, 또는 JC)을 삽입하여 간접 분기가 오직 그 지점들로만 성공하도록 합니다.
- **직접 분기 / 호출**(즉 고정 주소 `B`, `BL`)은 BTI로 **제한되지 않습니다**. 가정은 코드 페이지가 신뢰되며 공격자가 이를 변경할 수 없다는 것이므로(따라서 직접 분기는 안전함)입니다.
- 또한, **RET / return** 명령은 일반적으로 BTI로 제한되지 않는데, 반환 주소는 PAC 또는 return signing 메커니즘으로 보호되기 때문입니다.
#### 메커니즘과 강제
- 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 table에서 오는 대상)에만 삽입된다는 것을 보여줍니다.
#### BTI + PAC 시너지
PAC는 포인터 값(출처)을 보호하여 간접 호출/복귀 체인이 변조되지 않았음을 보장합니다.
BTI는 유효한 포인터라도 올바르게 표시된 진입점으로만 향해야 함을 보장합니다.
결합되면 공격자는 올바른 PAC를 가진 유효한 포인터와 대상 위치에 BTI가 존재해야 합니다. 이는 exploit gadget을 구성하는 난이도를 높입니다.
#### 예시
<details>
<summary>예시</summary>
익스플로잇이 `0xABCDEF`에 있는 gadget으로 피벗하려 하는데 그 주소는 `BTI c`로 시작하지 않습니다. CPU는 `blr x0`을 실행할 때 대상을 검사하고 유효한 landing pad가 없기 때문에 오류를 발생시킵니다. 따라서 많은 gadget이 BTI 접두사를 포함하지 않는 한 사용 불가능해집니다.
</details>
### 10. **Privileged Access Never (PAN) & Privileged Execute Never (PXN)**
**더 최근의 ARMv8 확장 / iOS 지원(하드닝된 커널용)에서 도입됨**
#### PAN (Privileged Access Never)
- **PAN**은 **ARMv8.1-A**에 도입된 기능으로, **privileged 코드**(EL1 또는 EL2)가 **user-accessible(EL0)**로 표시된 메모리를 **읽거나 쓰는 것**을 PAN이 명시적으로 비활성화되어 있지 않는 한 방지합니다.
- 아이디어는: 커널이 속이거나 침해되더라도, 먼저 PAN을 *클리어*하지 않으면 임의로 user-space 포인터를 역참조할 수 없게 하여 **`ret2usr`** 스타일 익스플로잇이나 사용자 제어 버퍼의 오용 위험을 줄입니다.
- PAN이 활성화되어 있을 때(PSTATE.PAN = 1), "EL0에서 접근 가능한" 가상 주소에 접근하는 privileged load/store 명령은 권한 오류를 발생시킵니다.
- 커널은 정당하게 user-space 메모리에 접근해야 할 때(예: 유저 버퍼로/로부터 데이터 복사) **일시적으로 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 또는 코드 캐시 설정), 커널은 PAN이 활성화된 상태에서도 그들로부터 읽을 수 있게 되어 의도치 않은 취약 지점이 될 수 있습니다.
#### PXN (Privileged eXecute Never)
- **PXN**은 페이지 테이블 엔트리(leaf 또는 block 엔트리)의 플래그로, 해당 페이지가 **privileged 모드에서 실행 불가**함을 나타냅니다(즉 EL1에서 실행 불가).
- PXN은 커널(또는 어떤 privileged 코드)이 user-space 페이지에서 명령을 실행하거나 점프하는 것을 방지합니다. 결과적으로 커널 수준의 제어 흐름이 user 메모리로 리디렉션되는 것을 차단합니다.
- PAN과 결합하면 다음을 보장합니다:
1. 커널은 기본적으로 user-space 데이터를 읽거나 쓸 수 없다 (PAN)
2. 커널은 user-space 코드를 실행할 수 없다 (PXN)
- ARMv8의 페이지 테이블 형식에서, 리프 엔트리들은 속성 비트에 `PXN` 비트(및 unprivileged execute-never를 위한 `UXN`)를 가지고 있습니다.
따라서 커널에 user 메모리를 가리키는 손상된 함수 포인터가 있어 브랜치하려 해도, PXN 비트가 설정되어 있으면 예외가 발생합니다.
#### 메모리 권한 모델 및 PAN/PXN이 페이지 테이블 비트에 매핑되는 방식
PAN / PXN이 어떻게 동작하는지 이해하려면 ARM의 변환 및 권한 모델(단순화)을 볼 필요가 있습니다:
- 각 페이지 또는 블록 엔트리는 읽기/쓰기, privileged vs unprivileged 같은 접근 권한을 위한 **AP[2:1]** 필드와 실행 금지를 위한 **UXN / PXN** 비트를 포함한 속성 필드를 가집니다.
- PSTATE.PAN이 1(PAN 활성)일 때, 하드웨어는 수정된 의미를 적용합니다: "EL0에서 접근 가능한"으로 표시된 페이지에 대한 privileged 접근은 금지됩니다(오류 발생).
- 앞에서 언급한 버그 때문에, 읽기 권한 없이 실행만 가능한 페이지는 일부 구현에서 “EL0에서 접근 가능”으로 간주되지 않을 수 있어 PAN을 우회할 수 있습니다.
- 페이지의 PXN 비트가 설정되면, 더 높은 권한 수준에서의 명령 페치라도 실행이 금지됩니다.
#### 하드닝된 OS(예: iOS / XNU)에서 PAN / PXN의 커널 사용
하드닝된 커널 디자인(Apple이 사용할 수 있는 설계)에서는:
- 커널은 기본적으로 PAN을 활성화합니다(따라서 privileged 코드가 제약됨).
- 유효하게 user-space 메모리를 읽거나 써야 하는 경로(예: syscall 버퍼 복사, I/O, 사용자 포인터 읽기/쓰기)에서는 커널이 일시적으로 **PAN을 비활성화**하거나 권한을 무시하는 특수 명령을 사용합니다.
- 사용자 페이지에는 PXN = 1을 설정하여(페이지 테이블을 통해) 커널이 실행하지 못하게 하고, 커널 페이지에는 PXN을 설정하지 않습니다.
- 커널은 어떤 코드 경로도 user 메모리 영역으로의 실행 흐름을 초래하지 않도록 보장해야 합니다(그렇지 않으면 PXN을 우회하게 됨). 따라서 “user 제어 shellcode로 점프”에 의존하는 익스플로잇 체인은 차단됩니다.
앞서 언급한 execute-only 페이지를 통한 PAN 우회 때문에 실제 시스템에서는 Apple이 execute-only user 페이지를 비활성화하거나 명세 약점을 우회하는 패치를 적용할 수 있습니다.
#### 공격 표면, 우회 및 완화
- **execute-only 페이지를 통한 PAN 우회**: 앞서 설명한 것처럼 명세상의 간극 때문에 읽기 권한이 없는 실행 전용 user 페이지는 일부 구현에서 “EL0에서 접근 가능”으로 간주되지 않아 PAN이 차단하지 못합니다. 이는 공격자가 “execute-only” 섹션을 통해 데이터를 공급하는 비정상적인 경로를 제공합니다.
- **일시적 창(Temporal window) 익스플로잇**: 커널이 필요 이상으로 PAN을 비활성화하는 경우, 경쟁 상태나 악의적 경로가 그 창을 이용해 의도치 않은 user 메모리 접근을 수행할 수 있습니다.
- **재활성화 잊음**: 코드 경로가 PAN을 다시 활성화하는 것을 잊으면 이후의 커널 작업이 잘못해서 user 메모리에 접근할 수 있습니다.
- **PXN 오설정**: 페이지 테이블이 user 페이지에 PXN을 설정하지 않거나 user 코드 페이지를 잘못 매핑하면, 커널이 user-space 코드를 실행하도록 속을 수 있습니다.
- **스펙큘레이션 / 사이드채널**: 스펙큘레이션 우회와 마찬가지로, PAN / PXN 검사에 대한 일시적 위반을 초래하는 마이크로아키텍처적 부작용이 있을 수 있습니다(하지만 이러한 공격은 CPU 설계에 매우 의존적임).
- **복잡한 상호작용**: JIT, shared memory, just-in-time code 영역과 같은 고급 기능에서는 커널이 user-mapped 영역에서 특정 메모리 접근이나 실행을 허용해야 할 필요가 있고; PAN/PXN 제약 하에서 이를 안전하게 설계하는 것은 까다롭습니다.
#### 예시
<details>
<summary>코드 예제</summary>
Here are illustrative pseudo-assembly sequences showing enabling/disabling PAN around user memory access, and how a fault might occur.
// 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
</details>
<details>
<summary>Example</summary>
커널 취약점이 사용자 제공 함수 포인터를 가져와 커널 컨텍스트에서 호출하려고 시도합니다(예: `call user_buffer`). PAN/PXN 하에서는 해당 동작이 허용되지 않거나 예외가 발생합니다.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI는 64비트 포인터의 최상위 바이트를 주소 변환에서 무시하는 기능입니다. 이를 통해 OS나 하드웨어는 포인터 최상위 바이트에 **tag 비트들**을 삽입해도 실제 주소에 영향을 주지 않고 메타데이터를 보관할 수 있습니다.
- TBI는 **Top Byte Ignore**의 약자입니다(가끔 *Address Tagging*이라 불립니다). 많은 ARMv8+ 구현에서 제공되는 하드웨어 기능으로, 64비트 포인터의 최상위 8비트(비트 63:56)를 주소 변환 / load/store / instruction fetch 시 **무시**합니다.
- 결과적으로 CPU는 포인터 `0xTTxxxx_xxxx_xxxx`(여기서 `TT` = 최상위 바이트)를 주소 변환 목적상 `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)이 손상되어 읽기/쓰기 권한을 갖게 되더라도, 특정 민감한 페이지들(특히 페이지 테이블, code-signing 메타데이터, 커널 코드 페이지, entitlements, trust caches 등)을 **임의로 수정할 수 없어야 합니다**.
- 이는 사실상 **“커널 내부의 작은 신뢰된 컴포넌트”**를 생성합니다 — 더 높은 권한을 가진 소규모 구성요소(PPL)만이 보호된 페이지를 수정할 수 있습니다. 다른 커널 코드들은 변경을 위해 PPL 루틴을 호출해야 합니다.
- 이로 인해 커널 익스플로잇의 공격 표면이 줄어듭니다: 커널 모드에서 임의 R/W/execute를 얻었다 하더라도, 중요한 구조를 수정하려면 추가로 PPL 도메인을 침해하거나 PPL을 우회해야 합니다.
- 최신 Apple silicon(A15+ / M2+)에서는 페이지 테이블 보호를 위해 많은 경우 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 routines”)을 호출합니다.
#### Gate / Entry & Exit
- 커널이 보호된 페이지(예: 커널 코드 페이지의 권한 변경 또는 페이지 테이블 수정)를 변경해야 할 때, 검증을 수행한 뒤 PPL 도메인으로 전환하는 **PPL 래퍼** 루틴을 호출합니다. PPL 외부에서는 보호된 페이지가 사실상 읽기 전용 또는 수정 불가능합니다.
- PPL 진입 시 APRR 매핑이 조정되어, PPL 영역의 메모리 페이지가 PPL 내에서는 **실행 가능 & 쓰기 가능** 상태가 됩니다. 종료 시에는 다시 읽기 전용/비쓰기 상태로 복원됩니다. 이를 통해 잘 검토된 PPL 루틴만이 보호된 페이지에 쓸 수 있게 보장합니다.
- PPL 외부에서 비-PPL 커널 코드가 해당 보호 페이지에 쓰기를 시도하면 APRR 매핑상 그 코드 도메인이 쓰기를 허용하지 않기 때문에 fault(권한 거부)가 발생합니다.
#### Protected page categories
PPL이 일반적으로 보호하는 페이지들은 다음을 포함합니다:
- 페이지 테이블 구조(translation table entries, 매핑 메타데이터)
- 핵심 로직을 포함한 커널 코드 페이지
- code-sign 메타데이터(trust caches, 서명 블랍)
- entitlement 테이블, 서명 집행 테이블
- 서명 검사 우회나 자격 증명 조작을 허용할 수 있는 기타 고가치 커널 구조
아이디어는 커널 메모리가 완전히 통제된다 하더라도 공격자가 단순히 이러한 페이지를 패치하거나 다시 쓰지 못하도록 하는 것입니다. 이를 위해서는 PPL 루틴을 침해하거나 PPL을 우회해야 합니다.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- Project Zero의 공개 보고서는 **stale TLB entries**를 이용한 우회를 설명합니다.
- 개념은 다음과 같습니다:
1. 두 개의 물리 페이지 A와 B를 할당하고 이를 PPL 페이지로 표시합니다.
2. L3 translation table 페이지가 A와 B에서 온 두 개의 가상 주소 P와 Q를 매핑합니다.
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/매핑 일관성에 의존하는 보안 경계가 TLB 무효화와 매핑 일관성에 대해 매우 주의해야 함을 보여줍니다.
- Project Zero는 이러한 우회가 미묘하고 드물지만 복잡한 시스템에서는 가능하다고 언급했습니다. 그럼에도 불구하고 그들은 PPL을 강력한 완화책으로 평가합니다.
2. **Other potential hazards & constraints**
- 커널 익스플로잇이 직접 PPL 래퍼를 호출해 PPL에 진입할 수 있다면 제한을 우회할 수 있습니다. 따라서 인수 검증(argument validation)이 중요합니다.
- PPL 코드 자체의 버그(예: 산술 오버플로우, 경계 검사 실패)는 PPL 내부에서의 OOB 수정으로 이어질 수 있습니다. Project Zero는 `pmap_remove_options_internal()`의 버그가 그들의 우회에서 악용되었음을 관찰했습니다.
- PPL 경계는 하드웨어 집행(APRR, 메모리 컨트롤러)에 불가분하게 연결되어 있으므로 하드웨어 구현만큼 강력합니다.
#### Example
<details>
<summary>Code Example</summary>
여기 간단화한 의사코드 / 로직이 있습니다. 커널이 보호된 페이지를 수정하기 위해 PPL로 호출하는 방식을 보여줍니다:
```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.
예시
kernel exploit는 entitlement table을 덮어쓰거나 kernel signature blob을 수정해 code-sign enforcement를 비활성화하려 시도합니다. 해당 페이지가 PPL로 보호되어 있기 때문에, PPL 인터페이스를 거치지 않으면 쓰기가 차단됩니다. 따라서 kernel code execution이 있어도 code-sign 제약을 우회하거나 credential 데이터를 임의로 수정할 수 없습니다. On iOS 17+ 특정 기기는 PPL로 관리되는 페이지를 더 격리하기 위해 SPTM을 사용합니다.PPL → SPTM / 대체 / 향후
- 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
다음은 Apple의 MIE 구성에서 EMTE가 작동하는 고수준 설명입니다:
- Tag assignment
- 메모리가 할당될 때(예: in kernel or user space via secure allocators), 해당 블록에 secret tag가 할당됩니다.
- 사용자나 kernel에 반환되는 pointer는 그 tag를 상위 비트에 포함합니다 (using TBI / top byte ignore mechanisms).
- Tag checking on access
- pointer를 사용해 load 또는 store가 실행될 때마다 하드웨어는 pointer의 tag가 메모리 블록의 tag(allocation tag)와 일치하는지 검사합니다. 불일치하면 즉시 fault가 발생합니다(동기식이므로).
- 동기식이기 때문에 “delayed detection” 창은 존재하지 않습니다.
- Retagging on free / reuse
- 메모리가 freed되면 allocator는 블록의 tag를 변경합니다(따라서 오래된 포인터의 오래된 tag는 더 이상 일치하지 않습니다).
- 따라서 use-after-free 포인터는 오래된 tag를 가지므로 접근 시 mismatch가 발생합니다.
- Neighbor-tag differentiation to catch overflows
- 인접한 할당에는 서로 다른 tag가 부여됩니다. buffer overflow가 이웃 메모리로 흘러 들어가면 tag mismatch로 fault가 발생합니다.
- 이는 경계를 넘는 작은 오버플로우를 잡아내는 데 특히 효과적입니다.
- Tag confidentiality enforcement
- Apple은 tag 값이 leaked되지 않도록 해야 합니다(공격자가 tag를 알게 되면 올바른 tag를 가진 포인터를 조작할 수 있기 때문입니다).
- 이를 위해 tag 비트의 side-channel leakage를 방지하는 보호장치(microarchitectural / speculative controls)를 포함합니다.
- Kernel and user-space integration
- Apple은 EMTE를 user-space뿐 아니라 kernel / OS-critical components에도 사용합니다 (kernel을 메모리 손상으로부터 보호하기 위해).
- 하드웨어/OS는 kernel이 user space를 대신해 실행될 때에도 tag 규칙이 적용되도록 보장합니다.
예시
``` 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>
#### 제한 사항 및 과제
- **블록 내부 오버플로우 (Intrablock overflows)**: 오버플로우가 동일한 할당 내에 머물러(경계를 넘지 않음) 태그가 동일하게 유지되면, 태그 불일치(tag mismatch)가 이를 잡아내지 못한다.
- **태그 폭 제한 (Tag width limitation)**: 태그에 사용 가능한 비트가 몇 비트(예: 4비트 또는 작은 도메인)뿐이어서 네임스페이스가 제한된다.
- **Side-channel leaks**: 태그 비트가 leaked될 수 있다면(캐시 / speculative execution을 통해), 공격자가 유효한 태그를 알아내어 우회할 수 있다. Apple의 tag confidentiality enforcement는 이를 완화하려는 목적이다.
- **성능 오버헤드 (Performance overhead)**: 각 load/store마다 태그 검사가 추가 비용을 발생시키므로, Apple은 오버헤드를 낮추기 위해 하드웨어를 최적화해야 한다.
- **호환성 및 폴백 (Compatibility & fallback)**: 구형 하드웨어나 EMTE를 지원하지 않는 구성에서는 폴백이 필요하다. Apple은 MIE가 지원되는 기기에서만 활성화된다고 주장한다.
- **복잡한 할당자 로직 (Complex allocator logic)**: 할당자는 태그 관리, retagging, 경계 정렬, mis-tag 충돌 회피 등을 처리해야 한다. 할당자 로직의 버그는 취약점을 유발할 수 있다.
- **혼합 메모리 / 하이브리드 영역 (Mixed memory / hybrid areas)**: 일부 메모리는 untagged(레거시) 상태로 남아 있어 상호운용성이 더 까다로워질 수 있다.
- **Speculative / transient attacks**: 다른 많은 마이크로아키텍처 보호와 마찬가지로 speculative execution이나 micro-op fusions가 일시적으로 검사를 우회하거나 tag bits를 leak할 수 있다.
- **지원되는 영역으로 제한 (Limited to supported regions)**: Apple은 EMTE를 범용으로 적용하지 않고 선택적 고위험 영역(커널, security-critical subsystems)에만 적용할 수 있다.
---
## 표준 MTE와 비교한 주요 향상점 / 차이점
다음은 Apple이 강조하는 개선점 및 변경사항이다:
| 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 취약점을 더 잘 봉쇄할 수 있다.
---
## EMTE의 실제 동작 방식 (Apple / MIE)
아래는 Apple의 MIE 구성에서 EMTE가 어떻게 동작하는지에 대한 상위 수준 설명이다:
1. **Tag assignment**
- 메모리가 할당될 때(예: 커널 또는 사용자 공간에서 secure allocators를 통해), 해당 블록에 **secret tag**가 할당된다.
- 사용자나 커널에 반환되는 포인터는 해당 태그를 상위 비트에 포함한다(TBI / top byte ignore 메커니즘 사용).
2. **Tag checking on access**
- 포인터를 사용해 load 또는 store가 실행될 때마다 하드웨어는 포인터의 태그가 메모리 블록의 태그(할당 태그)와 일치하는지 확인한다. 불일치하면 즉시 폴트가 발생한다(동기 방식).
- 동기 방식이므로 “지연된 탐지” 창은 존재하지 않는다.
3. **Retagging on free / reuse**
- 메모리가 해제될 때 할당자는 블록의 태그를 변경한다(이로 인해 이전 태그를 가진 오래된 포인터는 더 이상 일치하지 않음).
- 따라서 use-after-free 포인터는 stale 태그를 가지게 되어 접근 시 불일치가 발생한다.
4. **Neighbor-tag differentiation to catch overflows**
- 인접한 할당에는 서로 다른 태그를 부여한다. 버퍼 오버플로우가 이웃 메모리로 흘러들어가면 태그 불일치로 폴트가 발생한다.
- 이는 경계를 넘는 작은 오버플로우를 탐지하는 데 특히 강력하다.
5. **Tag confidentiality enforcement**
- Apple은 태그 값이 leak되는 것을 방지해야 한다(공격자가 태그를 알게 되면 올바른 태그를 가진 포인터를 생성할 수 있기 때문).
- 이를 위해 태그 비트의 side-channel leak을 방지하는 보호장치(마이크로아키텍처/ speculative 제어 등)를 포함한다.
6. **Kernel and user-space integration**
- Apple은 EMTE를 사용자 공간뿐만 아니라 커널/OS-중요 구성요소에도 적용하여 커널을 메모리 손상으로부터 보호한다.
- 하드웨어/OS는 커널이 사용자 공간을 대신해 실행될 때에도 태그 규칙이 적용되도록 보장한다.
EMTE가 MIE에 통합되어 있기 때문에 Apple은 EMTE를 주요 공격 표면 전반에 걸쳐 동기 모드로 적용하며, 이를 선택적 또는 디버깅 용도로만 사용하지 않는다.
---
## XNU의 예외 처리
예외(**exception**)가 발생할 경우(예: `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, `EXC_CRASH`, `EXC_ARM_PAC` 등), XNU 커널의 **Mach layer**가 해당 예외가 UNIX 스타일의 **signal**(예: `SIGSEGV`, `SIGBUS`, `SIGILL` 등)로 변환되기 전에 이를 가로채는 책임을 진다.
이 과정은 사용자 공간에 도달하거나 BSD signal로 변환되기 전에 여러 계층의 예외 전파 및 처리 단계를 포함한다.
### 예외 흐름 (High-Level)
1. **CPU가 동기 예외를 트리거함** (예: 잘못된 포인터 역참조, PAC 실패, 불법 명령어 등).
2. **저수준 트랩 핸들러**가 실행된다 (`trap.c`, `exception.c` in XNU source).
3. 트랩 핸들러는 Mach 예외 처리의 핵심인 **`exception_triage()`**를 호출한다.
4. `exception_triage()`는 예외를 어떻게 라우팅할지 결정한다:
- 먼저 **thread's exception port**로 보낸다.
- 그다음 **task's exception port**로 보낸다.
- 그런 다음 **host's exception port**로 보낸다(종종 `launchd`나 `ReportCrash`).
만약 이 포트들 중 어느 것도 예외를 처리하지 않으면, 커널은:
- **BSD signal로 변환**(사용자 공간 프로세스의 경우).
- **Panic**(커널 공간 예외의 경우).
### 핵심 함수: `exception_triage()`
함수 `exception_triage()`는 Mach 예외를 가능한 핸들러 체인 상위로 라우팅하여 하나가 처리하거나 최종적으로 치명적일 때까지 전달한다. 해당 함수는 `osfmk/kern/exception.c`에 정의되어 있다.
```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와 같은 시그널로 변환됩니다.
Exception Ports
각 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를 포함할지)
디버거와 예외 처리
A debugger (예: LLDB)는 대상 task나 thread에 exception port를 설정하며, 보통 task_set_exception_ports()를 사용한다.
예외가 발생하면:
- Mach 메시지가 디버거 프로세스로 전송된다.
- 디버거는 예외를 handle(재개, 레지스터 수정, 명령어 스킵)할지 not handle 할지 결정할 수 있다.
- 디버거가 처리하지 않으면 예외는 다음 수준으로 전파된다 (task → host).
EXC_BAD_ACCESS의 흐름
-
스레드가 잘못된 포인터를 역참조 → CPU가 Data Abort를 발생시킨다.
-
커널 트랩 핸들러가
exception_triage(EXC_BAD_ACCESS, ...)를 호출한다. -
메시지가 전송된다:
-
Thread port → (디버거가 breakpoint를 가로챌 수 있음).
-
디버거가 무시하면 → Task port → (프로세스 수준 핸들러).
-
무시되면 → Host port (보통 ReportCrash).
- 아무도 처리하지 않으면 →
bsd_exception()이SIGSEGV로 변환한다.
PAC 예외
Pointer Authentication (PAC)이 실패(서명 불일치)하면, 특수한 Mach 예외가 발생한다:
EXC_ARM_PAC(타입)- 코드에는 세부사항(예: 키 타입, 포인터 타입)이 포함될 수 있다.
바이너리에 플래그 **TFRO_PAC_EXC_FATAL**가 설정되어 있으면, 커널은 PAC 실패를 fatal로 처리하여 디버거가 가로채는 것을 우회한다. 이는 공격자가 디버거를 사용해 PAC 검사를 우회하는 것을 방지하기 위한 것으로, platform binaries에 대해 활성화되어 있다.
소프트웨어 브레이크포인트
소프트웨어 브레이크포인트(int3 on x86, brk on ARM64)는 고의적인 fault를 일으켜 구현된다.
디버거는 exception port를 통해 이를 잡아낸다:
- instruction pointer나 메모리를 수정한다.
- 원래 명령어를 복원한다.
- 실행을 재개한다.
이 동일한 메커니즘으로 PAC 예외를 “잡을” 수 있다 — 단, TFRO_PAC_EXC_FATAL가 설정된 경우에는 절대 디버거에 도달하지 않는다.
BSD 시그널로의 변환
아무 핸들러도 예외를 수용하지 않으면:
-
커널은
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) |
XNU 소스의 주요 파일
-
osfmk/kern/exception.c→exception_triage(),exception_deliver_*()의 핵심. -
bsd/kern/kern_sig.c→ 시그널 전달 로직. -
osfmk/arm64/trap.c→ 저수준 trap 핸들러. -
osfmk/mach/exc.h→ 예외 코드와 구조체. -
osfmk/kern/task.c→ Task exception port 설정.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
커널은 고정 크기 “zones“로 나뉘는 zone allocator(kalloc)를 사용했다.
각 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.64zone에 할당된다). - 각 zone의 메모리는 freelist에 보관되었다 — 커널이 해제한 청크는 그 zone으로 돌아갔다.
- 64바이트 버퍼를 오버플로우하면, 같은 zone의 다음 객체를 덮어쓰게 된다.
이 때문에 heap spraying / feng shui가 매우 효과적이었다: 동일한 사이즈 클래스의 할당을 뿌려 객체 이웃을 예측할 수 있었다.
freelist
각 kalloc zone 내에서 해제된 객체는 시스템으로 직접 반환되지 않고, 사용 가능한 청크들의 linked list인 freelist로 들어갔다.
-
청크가 해제될 때, 커널은 그 청크의 시작 부분에 포인터를 썼다 → 동일 zone의 다음 free 청크의 주소.
-
zone은 첫 번째 free 청크를 가리키는 HEAD 포인터를 유지했다.
-
할당은 항상 현재 HEAD를 사용했다:
-
HEAD를 팝(pop) (해당 메모리를 호출자에게 반환).
-
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 악용
Because the first 8 bytes of a free chunk = freelist pointer, an attacker could corrupt it:
-
Heap overflow로 인접한 freed chunk에 침투 → 그 “next” pointer를 덮어쓸 수 있다.
-
Use-after-free로 freed object에 쓰기 → 그 “next” pointer를 덮어쓸 수 있다.
Then, on the next allocation of that size:
-
The allocator pops the corrupted chunk.
-
Follows the attacker-supplied “next” pointer.
-
Returns a pointer to arbitrary memory, enabling fake object primitives or 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 design made exploitation highly effective pre-hardening: predictable neighbors from heap sprays, raw pointer freelist links, and no type separation allowed attackers to escalate UAF/overflow bugs into arbitrary kernel memory control.
Heap Grooming / Feng Shui
The goal of heap grooming is to shape the heap layout so that when an attacker triggers an overflow or use-after-free, the target (victim) object sits right next to an attacker-controlled object.
That way, when memory corruption happens, the attacker can reliably overwrite the victim object with controlled data.
Steps:
- Spray allocations (fill the holes)
- Over time, the kernel heap gets fragmented: some zones have holes where old objects were freed.
- The attacker first makes lots of dummy allocations to fill these gaps, so the heap becomes “packed” and predictable.
- Force new pages
- Once the holes are filled, the next allocations must come from new pages added to the zone.
- Fresh pages mean objects will be clustered together, not scattered across old fragmented memory.
- This gives the attacker much better control of neighbors.
- Place attacker objects
- The attacker now sprays again, creating lots of attacker-controlled objects in those new pages.
- These objects are predictable in size and placement (since they all belong to the same zone).
- Free a controlled object (make a gap)
- The attacker deliberately frees one of their own objects.
- This creates a “hole” in the heap, which the allocator will later reuse for the next allocation of that size.
- Victim object lands in the hole
- The attacker triggers the kernel to allocate the victim object (the one they want to corrupt).
- Since the hole is the first available slot in the freelist, the victim is placed exactly where the attacker freed their object.
- Overflow / UAF into victim
- Now the attacker has attacker-controlled objects around the victim.
- By overflowing from one of their own objects (or reusing a freed one), they can reliably overwrite the victim’s memory fields with chosen values.
Why it works:
- Zone allocator predictability: allocations of the same size always come from the same zone.
- Freelist behavior: new allocations reuse the most recently freed chunk first.
- Heap sprays: attacker fills memory with predictable content and controls layout.
- End result: attacker controls where the victim object lands and what data sits next to it.
Modern Kernel Heap (iOS 15+/A12+ SoCs)
Apple hardened the allocator and made heap grooming much harder:
1. From Classic kalloc to kalloc_type
- Before: a single
kalloc.<size>zone existed for each size class (16, 32, 64, … 1280, etc.). Any object of that size was placed there → attacker objects could sit next to privileged kernel objects. - Now:
- Kernel objects are allocated from typed zones (
kalloc_type). - Each type of object (e.g.,
ipc_port_t,task_t,OSString,OSData) has its own dedicated zone, even if they’re the same size. - The mapping between object type ↔ zone is generated from the kalloc_type system at compile time.
An attacker can no longer guarantee that controlled data (OSData) ends up adjacent to sensitive kernel objects (task_t) of the same size.
2. Slabs and Per-CPU Caches
- The heap is divided into slabs (pages of memory carved into fixed-size chunks for that zone).
- Each zone has a per-CPU cache to reduce contention.
- Allocation path:
- Try per-CPU cache.
- If empty, pull from the global freelist.
- If freelist is empty, allocate a new slab (one or more pages).
- Benefit: This decentralization makes heap sprays less deterministic, since allocations may be satisfied from different CPUs’ caches.
3. Randomization inside zones
- Within a zone, freed elements are not handed back in simple FIFO/LIFO order.
- Modern XNU uses encoded freelist pointers (safe-linking like Linux, introduced ~iOS 14).
- Each freelist pointer is XOR-encoded with a per-zone secret cookie.
- This prevents attackers from forging a fake freelist pointer if they gain a write primitive.
- Some allocations are randomized in their placement within a slab, so spraying doesn’t guarantee adjacency.
4. Guarded Allocations
- Certain critical kernel objects (e.g., credentials, task structures) are allocated in guarded zones.
- These zones insert guard pages (unmapped memory) between slabs or use redzones around objects.
- Any overflow into the guard page triggers a fault → immediate panic instead of silent corruption.
5. Page Protection Layer (PPL) and SPTM
- Even if you control a freed object, you can’t modify all of kernel memory:
- PPL (Page Protection Layer) enforces that certain regions (e.g., code signing data, entitlements) are read-only even to the kernel itself.
- On A15/M2+ devices, this role is replaced/enhanced by SPTM (Secure Page Table Monitor) + TXM (Trusted Execution Monitor).
- These hardware-enforced layers mean attackers can’t escalate from a single heap corruption to arbitrary patching of critical security structures.
- (Added / Enhanced): also, PAC (Pointer Authentication Codes) is used in the kernel to protect pointers (especially function pointers, vtables) so that forging or corrupting them becomes harder.
- (Added / Enhanced): zones may enforce zone_require / zone enforcement, i.e. that an object freed can only be returned through its correct typed zone; invalid cross-zone frees may panic or be rejected. (Apple alludes to this in their memory safety posts)
6. Large Allocations
- Not all allocations go through
kalloc_type. - Very large requests (above ~16 KB) bypass typed zones and are served directly from kernel VM (kmem) via page allocations.
- These are less predictable, but also less exploitable, since they don’t share slabs with other objects.
7. Allocation Patterns Attackers Target
Even with these protections, attackers still look for:
- Reference count objects: if you can tamper with retain/release counters, you may cause use-after-free.
- Objects with function pointers (vtables): corrupting one still yields control flow.
- Shared memory objects (IOSurface, Mach ports): these are still attack targets because they bridge user ↔ kernel.
But — unlike before — you can’t just spray OSData and expect it to neighbor a task_t. You need type-specific bugs or info leaks to succeed.
Example: Allocation Flow in Modern Heap
Suppose userspace calls into IOKit to allocate an OSData object:
- Type lookup →
OSDatamaps tokalloc_type_osdatazone (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)
In recent Apple OS versions (especially iOS 17+), Apple introduced a more secure userland allocator, xzone malloc (XZM). This is the user-space analog to the kernel’s kalloc_type, applying type awareness, metadata isolation, and memory tagging safeguards.
Goals & Design Principles
- Type segregation / type awareness: group allocations by type or usage (pointer vs data) to prevent type confusion and cross-type reuse.
- Metadata isolation: separate heap metadata (e.g. free lists, size/state bits) from object payloads so that out-of-bounds writes are less likely to corrupt metadata.
- Guard pages / redzones: insert unmapped pages or padding around allocations to catch overflows.
- Memory tagging (EMTE / MIE): work in conjunction with hardware tagging to detect use-after-free, out-of-bounds, and invalid accesses.
- Scalable performance: maintain low overhead, avoid excessive fragmentation, and support many allocations per second with low latency.
Architecture & Components
Below are the main elements in the xzone allocator:
Segment Groups & Zones
- Segment groups partition the address space by usage categories: e.g.
data,pointer_xzones,data_large,pointer_large. - Each segment group contains segments (VM ranges) that host allocations for that category.
- Associated with each segment is a metadata slab (separate VM area) that stores metadata (e.g. free/used bits, size classes) for that segment. This out-of-line (OOL) metadata ensures that metadata is not intermingled with object payloads, mitigating corruption from overflows.
- Segments are carved into chunks (slices) which in turn are subdivided into blocks (allocation units). A chunk is tied to a specific size class and segment group (i.e. all blocks in a chunk share the same size & category).
- For small / medium allocations, it will use fixed-size chunks; for large/huges, it may map separately.
Chunks & Blocks
- A chunk is a region (often several pages) dedicated to allocations of one size class within a group.
- Inside a chunk, blocks are slots available for allocations. Freed blocks are tracked via the metadata slab — e.g. via bitmaps or free lists stored out-of-line.
- Between chunks (or within), guard slices / guard pages may be inserted (e.g. unmapped slices) to catch out-of-bounds writes.
Type / Type ID
- Every allocation site (or call to malloc, calloc, etc.) is associated with a type identifier (a
malloc_type_id_t) which encodes what kind of object is being allocated. That type ID is passed to the allocator, which uses it to select which zone / segment to serve the allocation. - Because of this, even if two allocations have the same size, they may go into entirely different zones if their types differ.
- In early iOS 17 versions, not all APIs (e.g. CFAllocator) were fully type-aware; Apple addressed some of those weaknesses in iOS 18.
Allocation & Freeing Workflow
Here is a high-level flow of how allocation and deallocation operate in xzone:
- malloc / calloc / realloc / typed alloc is invoked with a size and type ID.
- The allocator uses the type ID to pick the correct segment group / zone.
- Within that zone/segment, it seeks a chunk that has free blocks of the requested size.
- It may consult local caches / per-thread pools or free block lists from metadata.
- If no free block is available, it may allocate a new chunk in that zone.
- The metadata slab is updated (free bit cleared, bookkeeping).
- If memory tagging (EMTE) is in play, the returned block gets a tag assigned, and metadata is updated to reflect its “live” state.
- When
free()is called:
- The block is marked as freed in metadata (via OOL slab).
- The block may be placed into a free list or pooled for reuse.
- Optionally, block contents may be cleared or poisoned to reduce data leaks or use-after-free exploitation.
- The hardware tag associated with the block may be invalidated or re-tagged.
- If an entire chunk becomes free (all blocks freed), the allocator may reclaim that chunk (unmap it or return to OS) under memory pressure.
Security Features & Hardening
These are the defenses built into 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’s MIE (Memory Integrity Enforcement) is the hardware + OS framework that brings Enhanced Memory Tagging Extension (EMTE) into always-on, synchronous mode across major attack surfaces.
- xzone allocator is a fundamental foundation of MIE in user space: allocations done via xzone get tags, and accesses are checked by hardware.
- In MIE, the allocator, tag assignment, metadata management, and tag confidentiality enforcement are integrated to ensure that memory errors (e.g. stale reads, OOB, UAF) are caught immediately, not exploited later.
- 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
.ipswfiles. - Decompress until you get the bin format of the kernelcache of both
.ipswfiles. 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 BinDiffand 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.
JSKit-Based Safari Chains and PREYHUNTER Stagers
Renderer RCE abstraction with JSKit
- Reusable entry: Recent in-the-wild chains abused a WebKit JIT bug (patched as CVE-2023-41993) purely to gain JavaScript-level arbitrary read/write. The exploit immediately pivots into a purchased framework called JSKit, so any future Safari bug only needs to deliver the same primitive.
- Version abstraction & PAC bypasses: JSKit bundles support for a wide range of iOS releases together with multiple, selectable Pointer Authentication Code bypass modules. The framework fingerprints the target build, selects the appropriate PAC bypass logic, and verifies every step (primitive validation, shellcode launch) before progressing.
- Manual Mach-O mapping: JSKit parses Mach-O headers directly from memory, resolves the symbols it needs inside dyld-cached images, and can manually map additional Mach-O payloads without writing them to disk. This keeps the renderer process in-memory only and evades code-signature checks tied to filesystem artifacts.
- Portfolio model: Debug strings such as “exploit number 7” show that the suppliers maintain multiple interchangeable WebKit exploits. Once the JS primitive matches JSKit’s interface, the rest of the chain is unchanged across campaigns.
Kernel bridge: IPC UAF -> code-sign bypass pattern
- Kernel IPC UAF (CVE-2023-41992): The second stage, still running inside the Safari context, triggers a kernel use-after-free in IPC code, re-allocates the freed object from userland, and abuses the dangling pointers to pivot into arbitrary kernel read/write. The stage also reuses PAC bypass material previously computed by JSKit instead of re-deriving it.
- Code-signing bypass (CVE-2023-41991): With kernel R/W available, the exploit patches the trust cache / code-signing structures so unsigned payloads execute as
system. The stage then exposes a lightweight kernel R/W service to later payloads. - Composed pattern: This chain demonstrates a reusable recipe that defenders should expect going forward:
WebKit renderer RCE -> kernel IPC UAF -> kernel arbitrary R/W -> code-sign bypass -> unsigned system stager
PREYHUNTER 헬퍼 및 워처 모듈
-
Watcher anti-analysis: 전용 watcher 바이너리가 장치를 지속적으로 프로파일링하고 연구 환경이 감지되면 킬체인을 중단합니다.
security.mac.amfi.developer_mode_status,diagnosticd콘솔의 존재, 로케일US또는IL, Cydia 같은 jailbreak 흔적,bash,tcpdump,frida,sshd,checkrain같은 프로세스, 모바일 AV 앱(McAfee, AvastMobileSecurity, NortonMobileSecurity), 사용자 설정 HTTP 프록시, 사용자 설정 루트 CA 등을 검사합니다. 어떤 검사라도 실패하면 추가 페이로드 전달을 차단합니다. -
Helper surveillance hooks: 헬퍼 컴포넌트는
/tmp/helper.sock을 통해 다른 단계와 통신한 후 DMHooker 및 UMHooker라는 훅 세트를 로드합니다. 이 훅들은 VOIP 오디오 경로를 탭(녹음은/private/var/tmp/l/voip_%lu_%u_PART.m4a에 저장), 시스템 전체 키로거 구현, UI 없이 사진 캡처, 그리고 이러한 동작들이 보통 발생시킬 알림을 억제하기 위해 SpringBoard를 훅합니다. 따라서 헬퍼는 Predator 같은 더 무거운 임플란트를 투하하기 전의 은밀한 검증 및 경미한 감시 레이어로 작동합니다. -
HiddenDot indicator suppression in SpringBoard: 커널 수준 코드 인젝션으로 Predator는
SBSensorActivityDataProvider._handleNewDomainData:(센서 활동의 집계 지점)를 훅합니다. 훅은 Objective-Cself포인터(x0)를 0으로 만들어 호출이[nil _handleNewDomainData:newData]가 되게 하여 카메라/마이크 업데이트를 무시하고 녹색/주황색 점을 억제합니다. -
Mach exception-based hooking flow (DMHooker): 훅은
EXC_BREAKPOINT+ exception ports로 구현되며, 이후thread_set_state가 레지스터를 변조하고 실행이 재개됩니다. 반환 코드2는 “변경된 스레드 상태로 계속“을 의미합니다. -
PAC-aware redirection for camera access checks:
mediaserverd에서 패턴 스캔(예:memmem)으로CMCapture.framework내부의FigVideoCaptureSourceCreateWithSourceInfo근처에 있는 비공개 루틴을 찾습니다. 훅은 미리 서명된 PAC 캐시된 반환 주소를 사용하도록 리디렉션하려3을 반환하여 PAC를 만족시키면서 체크를 우회합니다. -
VoIP capture pipeline in
mediaserverd:AudioConverterNew와AudioConverterConvertComplexBuffer+52를 훅하여 버퍼를 탭하고, 버퍼 크기로 샘플레이트를 추정하고, NEON으로 float32 PCM → int16으로 변환하며, 4채널을 스테레오로 다운믹스하고ExtAudioFileWrite()로 저장합니다. VoIP 모듈 자체는 지표를 억제하지 않으므로 운영자는 HiddenDot를 별도로 활성화해야 합니다.
WebKit DFG Store-Barrier UAF + ANGLE PBO OOB (iOS 26.1)
Webkit Dfg Store Barrier Uaf Angle Oob
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
References
- https://www.jamf.com/blog/predator-spyware-ios-recording-indicator-bypass-analysis/
- Google Threat Intelligence – Intellexa zero-day exploits continue
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을 제출하여 해킹 트릭을 공유하세요.


