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) 이것은 기본적인 보호 중 하나입니다: 모든 실행 코드(apps, dynamic libraries, JIT-ed code, extensions, frameworks, caches)는 Apple의 신뢰 루트로 이어지는 인증서 체인으로 암호학적 서명이 되어야 합니다. 런타임에서 바이너리를 메모리에 로드하기 전에(또는 특정 경계를 넘는 점프를 수행하기 전에) 시스템은 서명을 검사합니다. 코드가 수정되었거나(비트 플립, 패치) 서명이 없으면 로드가 실패합니다.
- Thwarts: 익스플로잇 체인의 “고전적 payload drop + execute” 단계; 임의 코드 주입; 악의적인 로직을 삽입하기 위해 기존 바이너리를 수정하는 것.
- Mechanism detail:
- Mach-O loader(및 dynamic linker)는 코드 페이지, 세그먼트, entitlements, team IDs, 그리고 서명이 파일 내용 전체를 커버하는지를 검사합니다.
- JIT 캐시나 동적으로 생성된 코드 같은 메모리 영역의 경우, Apple은 페이지가 서명되었거나 특수 API(예:
mprotect와 코드 서명 검사)를 통해 검증되도록 강제합니다. - 서명에는 entitlements와 식별자가 포함되며, OS는 특정 API나 권한 있는 기능이 특정 entitlements를 요구한다는 것을 강제합니다(위조 불가).
예시
익스플로잇이 프로세스에서 코드 실행을 얻고 shellcode를 힙에 쓰고 그곳으로 점프하려고 한다고 가정합시다. iOS에서는 해당 페이지가 실행 가능으로 표시되는 것과 동시에 코드 서명 제약을 만족해야 합니다. shellcode는 Apple의 인증서로 서명되지 않았기 때문에 점프가 실패하거나 시스템이 해당 메모리 영역을 실행 가능으로 만드는 것을 거부합니다.2. CoreTrust
Introduced around iOS 14+ era (or gradually in newer devices / later iOS) CoreTrust는 런타임 서명 검증을 수행하는 서브시스템으로, 로컬 사용자랜드의 신뢰 저장소에 의존하지 않고 Apple의 루트 인증서에 대해 바이너리(시스템 및 사용자 바이너리 포함)를 검증합니다.
- Thwarts: 설치 후 바이너리 변조, 시스템 라이브러리나 사용자 앱을 교체하거나 패치하려는 jailbreaking 기법; 신뢰된 바이너리를 악성 바이너리로 교체해 시스템을 속이는 시도.
- Mechanism detail:
- 로컬 신뢰 데이터베이스나 인증서 캐시를 신뢰하는 대신, CoreTrust는 Apple의 루트를 직접 참조하거나 안전한 체인 검증을 수행합니다.
- 기존 바이너리에 대한 수정(예: 파일시스템의 변경)이 감지되어 거부되도록 보장합니다.
- entitlements, team IDs, 코드 서명 플래그 및 기타 메타데이터를 바이너리 로드 시점에 연동시킵니다.
예시
jailbreak은 `SpringBoard`나 `libsystem`을 패치된 버전으로 교체하여 영구성을 얻으려 할 수 있습니다. 하지만 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는 쓰기 가능한 페이지(데이터용)는 실행 불가로, 실행 가능한 페이지는 쓰기 불가로 강제합니다. 단순히 heap이나 stack 영역에 shellcode를 쓰고 실행할 수 없습니다.
- Thwarts: 직접적인 shellcode 실행; 전형적인 buffer-overflow → 주입된 shellcode로 점프하는 방식.
- Mechanism detail:
- MMU / 메모리 보호 플래그(페이지 테이블을 통해)가 분리를 강제합니다.
- writable 페이지를 executable로 표시하려는 모든 시도는 시스템 검사를 유발하며(금지되거나 코드 서명 승인이 필요합니다).
- 많은 경우, 페이지를 실행 가능으로 만드는 것은 추가 제약이나 검사를 강제하는 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 등)의 베이스 주소를 프로세스 실행마다 무작위화합니다. gadget들의 주소는 실행 간에 이동합니다.
- Thwarts: ROP/JOP을 위한 gadget 주소 하드코딩; 정적 익스플로잇 체인; 알려진 오프셋으로의 블라인드 점프.
- Mechanism detail:
- 로드된 각 라이브러리/동적 모듈은 무작위 오프셋으로 rebased 됩니다.
- 스택 및 힙의 베이스 포인터는 (일정한 엔트로피 한도 내에서) 무작위화됩니다.
- 때때로 다른 영역들(예: mmap 할당)도 무작위화됩니다.
- 정보-disclosure 완화와 결합되어, 공격자는 런타임에 base 주소를 알아내기 위해 먼저 주소나 포인터를 leak해야 합니다.
예시
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 및 기타 커널 구조의 베이스를 무작위화합니다.
- Thwarts: 커널 코드나 데이터의 고정 위치에 의존하는 커널 수준 익스플로잇; 정적 커널 익스플로잇.
- Mechanism detail:
- 각 부팅마다 커널의 베이스 주소가 범위 내에서 무작위화됩니다.
task_structs,vm_map등과 같은 커널 데이터 구조도 재배치되거나 오프셋될 수 있습니다.- 공격자는 먼저 kernel 포인터를 leak하거나 정보 노출 취약점을 이용해 오프셋을 계산해야 커널 구조나 코드를 hijack할 수 있습니다.
예시
로컬 취약점이 `KERN_BASE + offset`에 있는 커널 함수 포인터를 손상시키려 합니다. 그러나 `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(aka AMCC)는 커널 텍스트 페이지의 무결성을(해시나 체크섬으로) 지속적으로 모니터링합니다. 허용된 창 밖에서 패치, 인라인 훅, 코드 변경을 탐지하면 커널 패닉이나 재부팅을 트리거합니다.
- Thwarts: 영구적인 커널 패치(커널 명령어 수정), 인라인 훅, 정적 함수 덮어쓰기.
- Mechanism detail:
- 하드웨어 또는 펌웨어 모듈이 커널 텍스트 영역을 모니터링합니다.
- 주기적이거나 온디맨드로 페이지를 재해시하고 예상 값과 비교합니다.
- 일치하지 않을 경우(정상적인 업데이트 창 밖에서) 기기를 패닉시켜 악의적 패치가 유지되지 않도록 합니다.
- 공격자는 탐지 창을 피하거나 합법적인 패치 경로를 사용해야 합니다.
예시
익스플로잇이 커널 함수 선두부(예: `memcmp`)를 패치해 호출을 가로채려 합니다. 그러나 KPP는 코드 페이지의 해시가 예상 값과 더 이상 일치하지 않음을 감지하고 커널 패닉을 일으켜 패치가 안정화되기 전에 장치를 크래시시킵니다.7. Kernel Text Read‐Only Region (KTRR)
Introduced in modern SoCs (post ~A12 / newer hardware) KTRR은 하드웨어로 강제되는 메커니즘입니다: 부팅 초기에 커널 텍스트가 잠기면 EL1(커널)에서 해당 코드 페이지들을 더 이상 쓸 수 없게 됩니다.
- Thwarts: 부팅 이후 커널 코드에 대한 어떠한 수정(예: 패치, 인플레이스 코드 주입)도 EL1 권한 레벨에서 수행하는 것을 방지합니다.
- Mechanism detail:
- 부팅 중(secure/bootloader 단계) 메모리 컨트롤러(또는 보안 하드웨어 유닛)가 커널 텍스트를 포함한 물리 페이지들을 read-only로 표시합니다.
- 익스플로잇이 커널 권한을 완전히 얻었다 하더라도 해당 페이지들을 써서 명령어를 패치할 수 없습니다.
- 이를 수정하려면 공격자는 먼저 부트 체인을 손상시키거나 KTRR 자체를 무력화해야 합니다.
예시
권한 상승 익스플로잇이 EL1로 점프해 커널 함수(예: syscall 핸들러)에 트램폴린을 쓰려 합니다. 그러나 해당 페이지들은 KTRR로 인해 read-only로 잠겨 있어 쓰기가 실패하거나 폴트가 발생하므로 패치가 적용되지 않습니다.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 모드에서 직접 읽을 수 없습니다.
- 많은 시스템에서 64비트 포인터의 모든 비트가 사용되지 않기 때문에(예: 48비트 주소 공간), 상위 비트는 효과 주소를 변경하지 않고 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 모드에서 접근 불가합니다.
-
PAC는 암호 함수(ARM은 QARMA를 제안)로 계산되며 다음을 사용합니다:
- 포인터 값(정규화된 부분)
- modifier(스택 포인터 같은 문맥 값)
- 비밀 키
- 내부 트윅 로직 결과 PAC가 포인터의 상위 비트에 저장된 값과 일치하면 인증이 성공합니다.
Instruction Families
명명 규칙은: PAC / AUT / XPAC, 그리고 도메인 문자입니다.
PACxx명령은 포인터에 서명(sign)하고 PAC를 삽입합니다.AUTxx명령은 인증 + 제거(검증 후 PAC 제거)를 수행합니다.XPACxx명령은 검증 없이 PAC를 제거합니다.
도메인 / 접미사:
| 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(authenticate-and-return)나BLRAA(authenticate & branch) 같은 결합 형태가 ARM 확장/컴파일러 지원에서 존재합니다.- modifier가 0인 형태도 있음:
PACIZA/PACIZB등(modifier가 암묵적으로 0).
Modifiers
modifier의 주요 목적은 PAC를 특정 문맥에 고정(bind)하는 것입니다. 동일한 주소가 다른 문맥에서 서명되면 서로 다른 PAC를 가지게 되어 포인터 재사용을 방지합니다. 해시에 salt를 추가하는 것과 유사합니다.
따라서:
- modifier는 PAC 계산에 혼합되는 문맥 값(다른 레지스터)입니다. 일반적인 선택지는 stack pointer(
SP), frame pointer, 또는 어떤 객체 ID입니다. - SP를 modifier로 사용하는 것은 리턴 주소 서명에 흔히 사용됩니다: PAC는 특정 스택 프레임에 묶입니다. LR을 다른 프레임에서 재사용하려 하면 modifier가 달라 PAC 검증이 실패합니다.
- 동일한 포인터 값이 다른 modifier로 서명되면 서로 다른 PAC를 갖습니다.
- modifier는 반드시 비밀일 필요는 없지만, 이상적으로는 공격자가 제어하지 않는 값이어야 합니다.
- 의미 있는 modifier가 없는 경우에는 일부 명령이 0 또는 암묵적 상수를 사용합니다.
Apple / iOS / XNU Customizations & Observations
- Apple의 PAC 구현은 부팅별 diversifiers를 포함하여 부팅마다 키나 트윅이 달라지게 하여 부팅 간 재사용을 방지합니다.
- 또한 PAC가 user 모드에서 서명되어 kernel 모드에서 쉽게 재사용되지 않도록 하는 cross-domain mitigations를 포함합니다.
- Apple Silicon(M1) 리버스 엔지니어링에서는 아홉 가지 modifier 타입과 키 제어를 위한 Apple 전용 시스템 레지스터가 존재하는 것이 드러났습니다.
- Apple은 return address signing, kernel 데이터의 pointer 무결성, signed thread contexts 등 많은 커널 서브시스템에서 PAC를 사용합니다.
- Google Project Zero는 강력한 메모리 읽기/쓰기 프리미티브가 있는 경우 A 키에 대해 kernel PAC를 위조할 수 있음을 보였으나(주로 A12-era 장치 대상), Apple은 많은 경로를 패치했습니다.
- Apple 시스템에서는 일부 키가 커널 전체에 걸쳐 글로벌이지만, 사용자 프로세스는 부팅별 또는 프로세스별 무작위성을 가질 수 있습니다.
PAC Bypasses
- Kernel-mode PAC: theoretical vs real bypasses
- 커널 PAC 키와 로직은 엄격히 제어되므로(특권 레지스터, diversifiers, 도메인 분리), 임의로 서명된 커널 포인터를 위조하는 것은 매우 어렵습니다.
- Azad의 2020 “iOS Kernel PAC, One Year Later“는 iOS 12-13에서 몇몇 부분적 우회(서명 gadget 재사용, 서명된 상태의 재사용, 보호되지 않은 간접 분기)를 발견했지만, 완전한 일반 우회는 없었다고 보고했습니다. bazad.github.io
- Apple의 “Dark Magic” 커스터마이제이션은 취약 표면을 더 좁혔습니다(도메인 전환, per-key 활성화 비트). i.blackhat.com
- Apple silicon(M1/M2)에서 보고된 알려진 kernel PAC bypass CVE-2023-32424 같은 사례가 있습니다. i.blackhat.com
- 그러나 이러한 우회는 종종 매우 특정한 gadget이나 구현 버그에 의존하며, 일반 목적의 우회는 아닙니다.
따라서 kernel 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로 서명된 포인터를 추출하거나 재사용하여 PAC를 우회하려고 시도합니다.
- “No Clicks Required” 발표는 shared cache 위에 오라클을 구축해 상대적 주소를 추론하고 이를 서명된 포인터와 결합해 PAC를 우회하는 방법을 설명합니다. saelo.github.io
- 또한 사용자 공간의 shared 라이브러리에서 가져온 함수 포인터 임포트는 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 로더와 동적 재배치 로직은 복잡하며 때때로 재배치를 수행하기 위해 페이지를 임시로 read/write로 매핑한 뒤 다시 read-only로 전환합니다. 공격자는 이러한 창을 악용합니다. Synacktiv의 발표는 dynamic relocations를 통한 타이밍 기반 PAC 우회인 “Operation Triangulation“을 설명합니다. Synacktiv
- DYLD 페이지는 현재 SPRR / VM_FLAGS_TPRO 같은 보호로 보호되지만(몇몇 보호 플래그), 이전 버전은 더 약한 보호를 가졌습니다. Synacktiv
- WebKit 익스플로잇 체인에서는 DYLD 로더가 종종 PAC 우회의 대상입니다. 발표 자료는 많은 PAC 우회가 DYLD 로더(재배치, interposer 훅)를 겨냥했다고 언급합니다. Synacktiv
2.4 NSPredicate / NSExpression / ObjC / SLOP
- userland 익스플로잇 체인에서 Objective-C 런타임 메서드들(
NSPredicate,NSExpression,NSInvocation)은 제어 호출을 눈에 띄지 않게 전달하는 용도로 사용됩니다. - PAC 도입 이전의 오래된 iOS에서는 fake NSInvocation 객체를 사용해 제어를 주입하는 익스플로잇이 있었습니다. PAC가 도입되면서 수정이 필요했지만 SLOP(SeLector Oriented Programming) 기법은 PAC 환경에서도 확장되어 사용됩니다. Project Zero
- 원래 SLOP 기술은 가짜 invocation을 만들어 ObjC 호출을 체인하는 방식이었고, ISA나 selector 포인터가 항상 완전히 PAC로 보호되지 않는다는 점을 악용했습니다. Project Zero
- 포인터 인증이 부분적으로 적용되는 환경에서는 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>
버퍼 오버플로우는 스택의 반환 주소를 덮어쓴다. 공격자는 목표 gadget 주소를 쓰지만 올바른 PAC를 계산하지 못한다. 함수가 반환될 때 CPU의 `AUTIA` 명령은 PAC 불일치로 인해 오류를 발생시킨다. 체인이 실패한다.
Project Zero의 A12 (iPhone XS)에 대한 분석은 Apple의 PAC가 어떻게 사용되는지와 공격자가 메모리 read/write 프리미티브를 가졌을 때 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)을 삽입하여 간접 분기가 오직 그 위치들로만 성공하도록 한다.
- **직접 분기 / 호출**(즉 고정 주소 `B`, `BL`)은 BTI의 제한을 받지 않는다. 가정은 코드 페이지가 신뢰되며 공격자가 이를 변경할 수 없다는 것이므로(따라서 직접 분기는 안전하다).
- 또한, **RET / return** 명령은 일반적으로 PAC나 return signing 메커니즘으로 반환 주소가 보호되기 때문에 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 table에서 오는 대상)인 블록에만 삽입된다는 것을 보여준다.
#### BTI + PAC synergy
PAC는 포인터 값(출처)을 보호한다 — 간접 호출/반환 체인이 변조되지 않았음을 보장한다.
BTI는 유효한 포인터라 하더라도 오직 적절히 표시된 진입점으로만 향해야 함을 보장한다.
결합하면, 공격자는 올바른 PAC를 가진 유효한 포인터와 해당 대상에 BTI가 배치되어 있어야 한다. 이는 exploit gadget을 구성하는 난이도를 증가시킨다.
#### Example
<details>
<summary>예시</summary>
공격자가 `0xABCDEF`에 있는 gadget으로 피벗하려고 하는데 그 주소가 `BTI c`로 시작하지 않는 경우를 생각해보자. CPU는 `blr x0`을 실행할 때 대상을 확인하고 유효한 landing pad가 없으므로 오류를 발생시킨다. 따라서 많은 gadget이 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이 명시적으로 비활성화되지 않는 한 접근을 차단한다.
- 취지: 커널이 속임수로 조작되더라도, 커널은 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을 유발하지 않을 수 있다**. 다시 말해, 사용자 페이지가 실행 가능하지만 읽기 권한이 없으면, 커널의 읽기 시도가 PAN을 우회할 수 있다. 아키텍처는 “EL0에서 접근 가능”을 판정할 때 읽기 권한을 요구하지 않고 단지 실행 가능만으로 다루는 경우가 있어 특정 구현에서 PAN 우회가 발생한다.
- 이로 인해, iOS / XNU가 execute-only user 페이지를 허용하면(예: 일부 JIT 또는 코드 캐시 설정), 커널은 PAN이 활성화된 상태에서도 해당 페이지에서 우연히 읽어올 수 있다. 이는 일부 ARMv8+ 시스템에서 알려진 미묘한 exploitable 영역이다.
#### PXN (Privileged eXecute Never)
- **PXN**은 페이지 테이블 플래그(페이지 테이블 엔트리, leaf 또는 block 엔트리)에 있는 비트로, 해당 페이지가 **privileged 모드에서 실행 불가**함을 나타낸다(즉 EL1에서 실행할 수 없음).
- PXN은 커널(또는 어떤 privileged 코드)이 user-space 페이지로 분기하여 그곳에서 명령을 실행하는 것을 방지한다. 결과적으로 커널 수준의 제어 흐름이 사용자 메모리로 리디렉션되는 것을 차단한다.
- PAN과 결합하면:
1. 기본적으로 커널은 사용자 데이터를 읽거나 쓰지 못한다 (PAN)
2. 커널은 사용자 코드를 실행할 수 없다 (PXN)
- ARMv8 페이지 테이블 포맷에서 leaf 엔트리에는 `PXN` 비트(및 unprivileged execute-never인 `UXN`)가 속성 비트로 존재한다.
따라서 가설적으로 커널에 손상된 함수 포인터가 사용자 메모리를 가리키더라도, PXN 비트는 분기 시 오류를 발생시킨다.
#### Memory-permission model & how PAN and PXN map to page table bits
PAN / PXN이 어떻게 작동하는지 이해하려면 ARM의 변환 및 권한 모델을 봐야 한다(단순화):
- 각 페이지 또는 블록 엔트리는 읽기/쓰기, privileged vs unprivileged를 위한 **AP[2:1]**와 실행 불가 제한을 위한 **UXN / PXN** 비트 등을 포함하는 속성 필드를 가진다.
- PSTATE.PAN이 1(활성)일 때, 하드웨어는 수정된 의미론을 강화한다: “EL0에서 접근 가능한”으로 표시된 페이지에 대한 privileged 접근은 금지되고(오류 발생) 차단된다.
- 앞서 언급한 버그 때문에, 읽기 권한 없이 실행만 가능한 페이지는 특정 구현에서 “EL0에서 접근 가능한”으로 간주되지 않아 PAN을 우회할 수 있다.
- 페이지의 PXN 비트가 설정되면, 더 높은 권한 레벨에서 온 명령 페치라도 실행이 금지된다.
#### Kernel usage of PAN / PXN in a hardened OS (e.g. iOS / XNU)
강화된 커널 설계(예: Apple이 사용할 수 있는 설계)에서는:
- 커널은 기본적으로 PAN을 활성화한다(따라서 privileged 코드가 제약을 받는다).
- 사용자 버퍼를 합법적으로 읽거나 써야 하는 경로(예: syscall buffer copy, I/O, 사용자 포인터의 read/write)에서는 커널이 일시적으로 **PAN을 비활성화**하거나 사용자 메모리 접근을 허용하는 특수 명령을 사용한다.
- 사용자 메모리 접근을 마친 후에는 반드시 PAN을 다시 활성화해야 한다.
- PXN은 페이지 테이블을 통해 강제된다: 사용자 페이지는 PXN = 1로 설정되어(커널이 실행할 수 없음), 커널 페이지는 PXN이 설정되어 있지 않다(커널 코드 실행 가능).
- 커널은 사용자 메모리 영역으로 실행 흐름이 들어가지 않도록 보장해야 한다(그렇게 되면 PXN을 우회할 수 있음) — 따라서 “사용자 제어 shellcode로 점프”하는 익스플로잇 체인은 차단된다.
앞서 언급한 execute-only 페이지를 통한 PAN 우회 때문에, 실제 시스템에서는 Apple이 execute-only user 페이지를 비활성화하거나 명세의 약점을 우회하는 패치를 적용할 수 있다.
#### Attack surfaces, bypasses, and mitigations
- **execute-only 페이지를 통한 PAN 우회**: 앞서 논의했듯, 명세의 허점으로 인해 읽기 권한이 없는 execute-only 사용자 페이지는 일부 구현에서 “EL0에서 접근 가능”으로 간주되지 않아 PAN이 차단하지 못할 수 있다. 이는 공격자에게 “execute-only” 섹션을 통해 데이터를 주입할 비정상적인 경로를 제공한다.
- **일시적 윈도우 익스플로잇**: 커널이 PAN을 필요 이상으로 오래 비활성화하면, 레이스나 악의적 경로가 그 창을 이용해 의도치 않은 사용자 메모리 접근을 수행할 수 있다.
- **재활성화 누락**: 코드 경로가 PAN을 다시 활성화하는 것을 잊으면, 이후의 커널 작업이 잘못해서 사용자 메모리에 접근할 수 있다.
- **PXN의 잘못된 구성**: 페이지 테이블이 사용자 페이지에 PXN을 설정하지 않거나 사용자 코드 페이지를 잘못 매핑하면, 커널이 사용자 공간 코드를 실행하도록 속을 수 있다.
- **추측 실행 / 사이드채널**: 추측 실행 우회와 유사하게, 마이크로아키텍처적 부작용이 PAN / PXN 검사에서 일시적 위반을 초래할 수 있다(다만 이런 공격은 CPU 설계에 크게 의존한다).
- **복잡한 상호작용**: JIT, shared memory, just-in-time code regions 같은 고급 기능에서는 커널이 특정 메모리 접근이나 사용자 매핑 영역에서의 실행을 허용할 필요가 있으며, PAN/PXN 제약 하에서 이를 안전하게 설계하는 것은 비자명하다.
#### Example
<details>
<summary>코드 예시</summary>
다음은 사용자 메모리 접근 주위에서 PAN을 활성/비활성화하는 것을 보여주는 설명적 의사-어셈블리 시퀀스와 오류가 발생할 수 있는 방법들이다.
</details>
// 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
If the kernel had **not** set PXN on that user page, then the branch might succeed — which would be insecure.
If the kernel forgets to re-enable PAN after user memory access, it opens a window where further kernel logic might accidentally read/write arbitrary user memory.
If the user pointer is into an execute-only page (user page with only execute permission, no read/write), under the PAN spec bug, `ldr W2, [X1]` might **not** fault even with PAN enabled, enabling a bypass exploit, depending on implementation.
</details>
<details>
<summary>예시</summary>
A kernel vulnerability tries to take a user-provided function pointer and call it in kernel context (i.e. `call user_buffer`). Under PAN/PXN, that operation is disallowed or faults.
</details>
---
### 11. **Top Byte Ignore (TBI) / Pointer Tagging**
**Introduced in ARMv8.5 / newer (or optional extension)**
TBI means the top byte (most-significant byte) of a 64-bit pointer is ignored by address translation. This lets OS or hardware embed **tag bits** in the pointer’s top byte without affecting the actual address.
- TBI stands for **Top Byte Ignore** (sometimes called *Address Tagging*). It is a hardware feature (available in many ARMv8+ implementations) that **ignores the top 8 bits** (bits 63:56) of a 64-bit pointer when performing **address translation / load/store / instruction fetch**.
- In effect, the CPU treats a pointer `0xTTxxxx_xxxx_xxxx` (where `TT` = top byte) as `0x00xxxx_xxxx_xxxx` for the purposes of address translation, ignoring (masking off) the top byte. The top byte can be used by software to store **metadata / tag bits**.
- This gives software “free” in-band space to embed a byte of tag in each pointer without altering which memory location it refers to.
- The architecture ensures that loads, stores, and instruction fetch treat the pointer with its top byte masked (i.e. tag stripped off) before performing the actual memory access.
Thus TBI decouples the **logical pointer** (pointer + tag) from the **physical address** used for memory operations.
#### Why TBI: 사용 사례 및 동기
- **Pointer tagging / metadata**: You can store extra metadata (e.g. object type, version, bounds, integrity tags) in that top byte. When you later use the pointer, the tag is ignored at hardware level, so you don’t need to strip manually for the memory access.
- **Memory tagging / MTE (Memory Tagging Extension)**: TBI is the base hardware mechanism that MTE builds on. In ARMv8.5, the **Memory Tagging Extension** uses bits 59:56 of the pointer as a **logical tag** and checks it against an **allocation tag** stored in memory.
- **Enhanced security & integrity**: By combining TBI with pointer authentication (PAC) or runtime checks, you can force not just the pointer value but also the tag to be correct. An attacker overwriting a pointer without the correct tag will produce a mismatched tag.
- **Compatibility**: Because TBI is optional and tag bits are ignored by hardware, existing untagged code continues to operate normally. The tag bits effectively become “don’t care” bits for legacy code.
#### Example
<details>
<summary>예시</summary>
A function pointer included a tag in its top byte (say `0xAA`). An exploit overwrites the pointer low bits but neglects the tag, so when the kernel verifies or sanitizes, the pointer fails or is rejected.
</details>
---
### 12. **Page Protection Layer (PPL)**
**Introduced in late iOS / modern hardware (iOS ~17 / Apple silicon / high-end models)** (some reports show PPL circa macOS / Apple silicon, but Apple is bringing analogous protections to iOS)
- PPL is designed as an **intra-kernel protection boundary**: even if the kernel (EL1) is compromised and has read/write capabilities, **it should not be able to freely modify** certain **sensitive pages** (especially page tables, code-signing metadata, kernel code pages, entitlements, trust caches, etc.).
- It effectively creates a **“kernel within the kernel”** — a smaller trusted component (PPL) with **elevated privileges** that alone can modify protected pages. Other kernel code must call into PPL routines to effect changes.
- This reduces the attack surface for kernel exploits: even with full arbitrary R/W/execute in kernel mode, exploit code must also somehow get into the PPL domain (or bypass PPL) to modify critical structures.
- On newer Apple silicon (A15+ / M2+), Apple is transitioning to **SPTM (Secure Page Table Monitor)**, which in many cases replaces PPL for page-table protection on those platforms.
Here’s how PPL is believed to operate, based on public analysis:
#### Use of APRR / permission routing (APRR = Access Permission ReRouting)
- Apple hardware uses a mechanism called **APRR (Access Permission ReRouting)**, which allows page table entries (PTEs) to contain small indices, rather than full permission bits. Those indices are mapped via APRR registers to actual permissions. This allows dynamic remapping of permissions per domain.
- PPL leverages APRR to segregate privilege within kernel context: only the PPL domain is permitted to update the mapping between indices and effective permissions. That is, when non-PPL kernel code writes a PTE or tries to flip permission bits, the APRR logic disallows it (or enforces read-only mapping).
- PPL code itself runs in a restricted region (e.g. `__PPLTEXT`) which is normally non-executable or non-writable until entry gates temporarily allow it. The kernel calls PPL entry points (“PPL routines”) to perform sensitive operations.
#### Gate / Entry & Exit
- When the kernel needs to modify a protected page (e.g. change permissions of a kernel code page, or modify page tables), it calls into a **PPL wrapper** routine, which does validation and then transitions into the PPL domain. Outside that domain, the protected pages are effectively read-only or non-modifiable by the main kernel.
- During PPL entry, the APRR mappings are adjusted so that memory pages in the PPL region are set to **executable & writable** within PPL. Upon exit, they are returned to read-only / non-writable. This ensures that only well-audited PPL routines can write to protected pages.
- Outside PPL, attempts by kernel code to write to those protected pages will fault (permission denied) because the APRR mapping for that code domain doesn’t permit writing.
#### Protected page categories
The pages that PPL typically protects include:
- Page table structures (translation table entries, mapping metadata)
- Kernel code pages, especially those containing critical logic
- Code-sign metadata (trust caches, signature blobs)
- Entitlement tables, signature enforcement tables
- Other high-value kernel structures where a patch would allow bypassing signature checks or credentials manipulation
The idea is that even if the kernel memory is fully controlled, the attacker cannot simply patch or rewrite these pages, unless they also compromise PPL routines or bypass PPL.
#### Known Bypasses & Vulnerabilities
1. **Project Zero’s PPL bypass (stale TLB trick)**
- A public writeup by Project Zero describes a bypass involving **stale TLB entries**.
- The idea:
1. Allocate two physical pages A and B, mark them as PPL pages (so they are protected).
2. Map two virtual addresses P and Q whose L3 translation table pages come from A and B.
3. Spin a thread to continuously access Q, keeping its TLB entry alive.
4. Call `pmap_remove_options()` to remove mappings starting at P; due to a bug, the code mistakenly removes the TTEs for both P and Q, but only invalidates the TLB entry for P, leaving Q’s stale entry live.
5. Reuse B (page Q’s table) to map arbitrary memory (e.g. PPL-protected pages). Because the stale TLB entry still maps Q’s old mapping, that mapping remains valid for that context.
6. Through this, the attacker can put writable mapping of PPL-protected pages in place without going through PPL interface.
- This exploit required fine control of physical mapping and TLB behavior. It demonstrates that a security boundary relying on TLB / mapping correctness must be extremely careful about TLB invalidations and mapping consistency.
- Project Zero commented that bypasses like this are subtle and rare, but possible in complex systems. Still, they regard PPL as a solid mitigation.
2. **Other potential hazards & constraints**
- If a kernel exploit can directly enter PPL routines (via calling the PPL wrappers), it might bypass restrictions. Thus argument validation is critical.
- Bugs in the PPL code itself (e.g. arithmetic overflow, boundary checks) can allow out-of-bounds modifications inside PPL. Project Zero observed that such a bug in `pmap_remove_options_internal()` was exploited in their bypass.
- The PPL boundary is irrevocably tied to hardware enforcement (APRR, memory controller), so it's only as strong as the hardware implementation.
#### Example
<details>
<summary>코드 예제</summary>
Here’s a simplified pseudocode / logic showing how a kernel might call into PPL to modify protected pages:
</details>
```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.
예시
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 / 대체 / 향후
- 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 비밀 태그 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 leaked (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.
예시
``` 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 width limitation**: 태그에 사용할 수 있는 비트 수(예: 4비트 또는 작은 도메인)가 제한되어 네임스페이스가 좁습니다.
- **Side-channel leaks**: 태그 비트가 캐시나 speculative execution 등을 통해 leaked될 수 있다면, 공격자가 유효한 태그를 알아내어 우회할 수 있습니다. Apple의 tag confidentiality enforcement는 이를 완화하려는 목적입니다.
- **Performance overhead**: 각 로드/스토어 시 태그 검사가 추가 비용을 발생시키므로, Apple은 하드웨어 최적화를 통해 오버헤드를 낮춰야 합니다.
- **Compatibility & fallback**: 오래된 하드웨어나 EMTE를 지원하지 않는 부품에서는 폴백이 필요합니다. Apple은 MIE가 지원되는 기기에서만 활성화된다고 주장합니다.
- **Complex allocator logic**: 할당자는 태그 관리, retagging, 경계 정렬, 태그 충돌 회피 등을 처리해야 합니다. 할당자 로직의 버그는 취약점을 유발할 수 있습니다.
- **Mixed memory / hybrid areas**: 일부 메모리는 비태그(레거시)로 남아 있을 수 있어 상호운용성이 복잡해집니다.
- **Speculative / transient attacks**: 많은 마이크로아키텍처 보호와 마찬가지로, speculative execution이나 micro-op fusion이 검사를 일시적으로 우회하거나 태그 비트를 누출할 가능성이 있습니다.
- **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.|
Because Apple controls both the hardware and software stack, it can enforce EMTE tightly, avoid performance pitfalls, and close side-channel holes.
---
## 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**
- 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).
2. **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.
3. **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.
4. **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.
5. **Tag confidentiality enforcement**
- Apple must prevent tag values being leaked (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.
6. **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.
Because EMTE is built into MIE, Apple uses EMTE in synchronous mode across key attack surfaces, not as opt-in or debugging mode.
---
## 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`, ...).
This process involves multiple layers of exception propagation and handling before reaching user space or being converted to a 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`.
```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를 등록할 수 있다.
They are defined by the API:
task_set_exception_ports()
thread_set_exception_ports()
host_set_exception_ports()
각 예외 포트는 다음을 가진다:
- mask (어떤 예외를 받고 싶은지)
- port name (메시지를 받을 Mach 포트)
- behavior (커널이 메시지를 보내는 방식)
- flavor (어떤 스레드 상태를 포함할지)
디버거와 예외 처리
debugger(예: LLDB)는 대상 task 또는 thread에 exception port를 설정하며, 보통 task_set_exception_ports()를 사용한다.
예외가 발생하면:
- Mach 메시지가 디버거 프로세스로 전송된다.
- 디버거는 예외를 처리(resume, 레지스터 수정, 명령어 건너뛰기)할지 처리하지 않을지 결정할 수 있다.
- 디버거가 처리하지 않으면 예외는 다음 레벨로 전파된다 (task → host).
EXC_BAD_ACCESS의 흐름
-
스레드가 잘못된 포인터를 역참조 → CPU가 Data Abort를 발생시킨다.
-
커널 트랩 핸들러가
exception_triage(EXC_BAD_ACCESS, ...)를 호출한다. -
메시지가 전송된다:
-
Thread 포트 → (디버거가 브레이크포인트를 가로챌 수 있음).
-
디버거가 무시하면 → Task 포트 → (프로세스 수준 핸들러).
-
무시되면 → Host 포트 (보통 ReportCrash).
- 아무도 처리하지 않으면 →
bsd_exception()이SIGSEGV로 변환한다.
PAC 예외
Pointer Authentication(PAC)이 실패(서명 불일치)하면, 특수한 Mach 예외가 발생한다:
EXC_ARM_PAC(타입)- 코드에는 세부사항이 포함될 수 있음(예: 키 타입, 포인터 타입).
바이너리에 플래그 **TFRO_PAC_EXC_FATAL**가 설정되어 있으면, 커널은 PAC 실패를 치명적(fatal) 으로 처리하여 디버거 인터셉션을 우회한다. 이는 공격자가 디버거를 이용해 PAC 체크를 우회하는 것을 방지하기 위한 것으로 platform binaries에 대해 활성화된다.
Software Breakpoints
소프트웨어 브레이크포인트(int3 on x86, brk on ARM64)는 고의적인 폴트를 발생시키는 방식으로 구현된다.
디버거는 exception port를 통해 이를 포착한다:
- 명령 포인터 또는 메모리를 수정한다.
- 원래 명령을 복원한다.
- 실행을 재개한다.
이 동일한 메커니즘으로 PAC 예외를 “잡을” 수 있다 — **단, TFRO_PAC_EXC_FATAL**이 설정된 경우에는 디버거에 도달하지 않는다.
BSD 시그널로의 변환
아무 핸들러도 예외를 받아들이지 않으면:
-
커널이
task_exception_notify() → bsd_exception()을 호출한다. -
이는 Mach 예외를 시그널로 매핑한다:
| Mach 예외 | 시그널 |
|---|---|
| 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→ 저수준 트랩 핸들러. -
osfmk/mach/exc.h→ 예외 코드와 구조체. -
osfmk/kern/task.c→ Task 예외 포트 설정.
Old Kernel Heap (Pre-iOS 15 / Pre-A12 era)
커널은 zone allocator(kalloc)를 사용했으며 고정 크기 “존(zone)“으로 나뉘어 있었다.
각 존은 단일 크기 클래스의 할당만 저장했다.
From the screenshot:
| Zone Name | Element Size | Example Use |
|---|---|---|
default.kalloc.16 | 16 바이트 | 매우 작은 커널 구조체, 포인터. |
default.kalloc.32 | 32 바이트 | 작은 구조체, 객체 헤더. |
default.kalloc.64 | 64 바이트 | IPC 메시지, 아주 작은 커널 버퍼. |
default.kalloc.128 | 128 바이트 | OSObject의 일부 같은 중간 크기 객체. |
| … | … | … |
default.kalloc.1280 | 1280 바이트 | 큰 구조체, IOSurface/그래픽 메타데이터. |
작동 방식:
- 각 할당 요청은 가장 가까운 존 크기로 올림(rounded up) 된다.
(예: 50바이트 요청은kalloc.64존에 할당된다). - 각 존의 메모리는 **프리리스트(freelist)**에 보관되었다 — 커널이 해제한 청크는 해당 존으로 돌아갔다.
- 64바이트 버퍼를 오버플로우하면 동일한 존의 다음 객체를 덮어쓰게 된다.
이 때문에 heap spraying / feng shui가 매우 효과적이었다: 같은 크기 클래스의 할당을 뿌려 객체 이웃을 예측할 수 있었다.
프리리스트
각 kalloc 존 내부에서, 해제된 객체들은 시스템으로 바로 반환되지 않고 사용 가능한 청크들의 연결 리스트인 프리리스트로 들어갔다.
-
청크가 해제되면, 커널은 그 청크의 시작 부분에 포인터를 썼다 → 동일한 존의 다음 자유 청크의 주소를 가리킴.
-
존은 첫 번째 자유 청크를 가리키는 HEAD 포인터를 유지했다.
-
할당은 항상 현재 HEAD를 사용했다:
-
HEAD를 팝(해당 메모리를 호출자에게 반환).
-
HEAD = HEAD->next로 업데이트(해제된 청크의 헤더에 저장된 값).
-
해제는 청크를 프리리스트에 다시 푸시했다:
-
freed_chunk->next = HEAD -
HEAD = freed_chunk
따라서 프리리스트는 해제된 메모리 자체 내부에 구축된 단순한 연결 리스트였다.
정상 상태:
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를 덮어쓴다.
Then, on the next allocation of that size:
-
allocator는 손상된 chunk를 꺼낸다.
-
공격자가 제공한 “next” pointer를 따른다.
-
arbitrary memory를 가리키는 포인터를 반환하여 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 디자인은 하드닝 이전에 익스플로잇을 매우 효과적으로 만들었습니다: heap sprays로 인한 예측 가능한 이웃, raw pointer freelist 링크, 그리고 타입 분리가 없어서 공격자가 UAF/overflow 버그를 임의의 kernel memory 제어로 확대할 수 있었습니다.
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.
그렇게 함으로써 메모리 손상이 발생했을 때 공격자는 제어된 데이터로 victim 객체를 신뢰성 있게 덮어쓸 수 있습니다.
단계:
- Spray allocations (fill the holes)
- 시간이 지나면 kernel heap은 단편화됩니다: 이전에 해제된 객체들 때문에 어떤 zone에는 구멍들이 생깁니다.
- 공격자는 먼저 많은 더미 할당을 해서 이 구멍들을 채워 힙을 “빽빽하게(packed)” 만들어 예측 가능하게 만듭니다.
- Force new pages
- 구멍들이 채워지면 다음 할당은 zone에 새로 추가된 페이지에서 나오게 됩니다.
- 새 페이지는 객체들이 흩어져 있지 않고 한곳에 모이게 합니다.
- 이는 공격자가 이웃 제어를 훨씬 더 잘 하게 해줍니다.
- Place attacker objects
- 공격자는 이제 다시 스프레이를 해서 그 새 페이지들에 공격자가 제어하는 많은 객체들을 생성합니다.
- 이 객체들은 모두 같은 zone에 속하므로 크기와 배치가 예측 가능합니다.
- Free a controlled object (make a gap)
- 공격자는 의도적으로 자신의 객체 중 하나를 해제합니다.
- 이렇게 하면 allocator가 동일한 크기의 다음 할당에 재사용할 “구멍”이 생성됩니다.
- Victim object lands in the hole
- 공격자는 커널이 victim 객체(손상시키려는 객체)를 할당하도록 유도합니다.
- 그 구멍이 freelist에서 첫 번째 사용 가능한 슬롯이므로 victim은 공격자가 해제한 정확한 위치에 배치됩니다.
- Overflow / UAF into victim
- 이제 공격자는 victim 주위에 공격자 제어 객체를 가집니다.
- 자신의 객체에서 overflow 하거나 해제된 객체를 재사용함으로써 victim의 메모리 필드를 선택한 값으로 신뢰성 있게 덮어쓸 수 있습니다.
작동 원인:
- Zone allocator의 예측성: 같은 크기의 할당은 항상 같은 zone에서 나옵니다.
- Freelist 동작: 새 할당은 가장 최근에 해제된 청크를 먼저 재사용합니다.
- Heap sprays: 공격자가 예측 가능한 내용으로 메모리를 채워 배치를 제어합니다.
- 최종 결과: 공격자는 victim 객체가 어디에 배치될지와 그 옆에 어떤 데이터가 놓일지를 제어합니다.
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이 있었고, 해당 크기의 모든 객체는 거기에 배치되었습니다 → 공격자 객체가 권한 있는 커널 객체 옆에 위치할 수 있었습니다. - Now:
- 커널 객체들은 typed zones (
kalloc_type)에서 할당됩니다. ipc_port_t,task_t,OSString,OSData등 각 객체 타입은 크기가 같더라도 자체 전용 zone을 가집니다.- 객체 타입 ↔ zone 매핑은 컴파일 시 kalloc_type system으로 생성됩니다.
공격자는 이제 제어된 데이터(OSData)가 동일 크기의 민감한 커널 객체(task_t) 옆에 반드시 놓이도록 보장할 수 없습니다.
2. Slabs and Per-CPU Caches
- 힙은 slabs(해당 zone을 위해 고정 크기 청크로 잘라진 페이지)로 나뉩니다.
- 각 zone은 경쟁을 줄이기 위해 per-CPU cache를 가집니다.
- 할당 경로:
- per-CPU cache 시도.
- 비어 있으면 global freelist에서 가져옴.
- freelist가 비어 있으면 새 slab(하나 이상 페이지)를 할당.
- 이점: 이러한 분산은 할당이 서로 다른 CPU의 캐시에서 충족될 수 있으므로 heap sprays의 결정론성을 낮춥니다.
3. Randomization inside zones
- zone 내에서 해제된 요소들은 단순한 FIFO/LIFO 순서로 반환되지 않습니다.
- 최신 XNU는 encoded freelist pointers(Linux의 safe-linking과 유사, ~iOS 14 도입)를 사용합니다.
- 각 freelist 포인터는 per-zone secret cookie로 XOR 인코딩됩니다.
- 이는 공격자가 쓰기 원시(write primitive)를 얻더라도 가짜 freelist 포인터를 위조하지 못하게 합니다.
- 일부 할당은 slab 내에서 배치가 무작위화되어 스프레이가 인접성을 보장하지 못합니다.
4. Guarded Allocations
- 특정 중요한 커널 객체(예: credentials, task 구조체)는 guarded zones에서 할당됩니다.
- 이러한 zone들은 slab 사이에 guard pages(맵되지 않은 메모리)를 삽입하거나 객체 주위에 redzones를 사용합니다.
- guard page로의 overflow는 fault를 유발 → 즉시 panic(정지)을 발생시키며 조용한 손상을 방지합니다.
5. Page Protection Layer (PPL) and SPTM
- 해제된 객체를 제어하더라도 모든 커널 메모리를 수정할 수는 없습니다:
- **PPL (Page Protection Layer)**은 특정 영역(예: code signing 데이터, entitlements)이 커널 자체에서도 read-only로 강제되게 합니다.
- A15/M2+ 기기에서는 이 역할이 SPTM (Secure Page Table Monitor) + **TXM (Trusted Execution Monitor)**로 대체/강화됩니다.
- 이러한 하드웨어 강제 레이어는 공격자가 단일 힙 손상에서 중요한 보안 구조의 임의 패치로 상승하는 것을 어렵게 만듭니다.
- (추가/강화): 또한 커널은 포인터(특히 함수 포인터, vtables)를 보호하기 위해 **PAC (Pointer Authentication Codes)**를 사용하여 이를 위조하거나 손상시키기 어렵게 합니다.
- (추가/강화): zones는 zone_require / zone enforcement를 시행할 수 있어, 잘못된 cross-zone free는 panic을 일으키거나 거부될 수 있습니다. (Apple은 이 점을 메모리 안전성 관련 게시물에서 암시합니다)
6. Large Allocations
- 모든 할당이
kalloc_type을 통하지는 않습니다. - 매우 큰 요청(대략 16 KB 이상)은 typed zones를 우회하고 페이지 할당을 통해 **kernel VM (kmem)**에서 직접 제공됩니다.
- 이런 할당은 덜 예측 가능하지만, 다른 객체와 slab를 공유하지 않기 때문에 익스플로잇 가능성도 낮습니다.
7. Allocation Patterns Attackers Target
이러한 보호에도 불구하고 공격자들은 여전히 다음을 찾습니다:
- Reference count objects: retain/release 카운터를 손댈 수 있다면 use-after-free를 유발할 수 있습니다.
- Objects with function pointers (vtables): 이를 손상시키면 여전히 제어 흐름을 얻을 수 있습니다.
- Shared memory objects (IOSurface, Mach ports): user ↔ kernel을 연결하는 브리지가 되므로 여전히 공격 대상입니다.
하지만 — 과거와 달리 — 단순히 OSData를 스프레이해서 task_t 옆에 오게 할 수는 없습니다. 성공하려면 타입별 버그나 **정보 누출(info leak)**이 필요합니다.
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)
최근 Apple OS 버전(특히 iOS 17+)에서 Apple은 더 안전한 유저랜드 allocator인 xzone malloc(XZM)을 도입했습니다. 이는 커널의 kalloc_type에 대응하는 유저 공간의 구현으로, 타입 인식, 메타데이터 분리, 메모리 태깅 보호를 적용합니다.
Goals & Design Principles
- Type segregation / type awareness: *타입 또는 용도(포인터 대 데이터)*별로 할당을 그룹화하여 타입 혼동(type confusion)과 크로스-타입 재사용을 방지.
- Metadata isolation: 힙 메타데이터(예: free lists, size/state 비트)를 객체 페이로드와 분리하여 OOB 쓰기가 메타데이터를 손상시킬 가능성을 줄임.
- Guard pages / redzones: 오버플로우를 잡기 위해 할당 주위에 맵되지 않은 페이지 또는 패딩을 삽입.
- 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에는 해당 segment의 메타데이터(예: free/used 비트, 사이즈 클래스)를 저장하는 metadata slab(별도의 VM 영역)가 연결됩니다. 이 out-of-line (OOL) metadata는 메타데이터가 객체 페이로드와 섞이지 않도록 하여 오버플로우로 인한 손상을 완화합니다.
- Segments는 chunks(슬라이스)로 잘려지며, 각 chunk는 다시 blocks(할당 단위)로 세분화됩니다. 하나의 chunk는 특정 사이즈 클래스와 segment group에 묶입니다(즉 해당 chunk의 모든 block은 동일한 크기와 카테고리를 공유).
- 작은/중간 크기 할당에는 고정 크기 chunks를 사용하고, 큰/거대한 할당은 별도로 매핑할 수 있습니다.
Chunks & Blocks
- chunk는 특정 그룹 내에서 하나의 사이즈 클래스를 위해 전용된 영역(종종 여러 페이지)입니다.
- chunk 내부의 blocks는 할당 가능한 슬롯입니다. 해제된 block은 metadata slab(예: 비트맵 또는 out-of-line free list)를 통해 추적됩니다.
- chunk 사이(또는 내부)에 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(예: CFAllocator)가 완전히 타입 인식되지 않았고, Apple은 iOS 18에서 이러한 약점을 일부 보완했습니다.
Allocation & Freeing Workflow
다음은 xzone에서 할당 및 해제가 작동하는 고수준 흐름입니다:
- malloc / calloc / realloc / typed alloc이 크기와 type ID와 함께 호출됩니다.
- allocator는 type ID를 사용해 올바른 segment group / zone을 선택합니다.
- 해당 zone/segment 내에서 요청한 크기의 free blocks가 있는 chunk를 찾습니다.
- 로컬 캐시 / per-thread pools 또는 metadata의 free block lists를 참조할 수 있습니다.
- 사용 가능한 free block이 없으면 해당 zone에 새 chunk를 할당할 수 있습니다.
- metadata slab가 업데이트됩니다(free 비트 클리어, 회계 처리).
- 메모리 태깅(EMTE)이 적용되는 경우 반환된 block에 tag가 할당되고 metadata는 해당 블록이 “라이브” 상태임을 반영하도록 업데이트됩니다.
free()호출 시:
- block은 metadata에서 해제 상태로 표시됩니다(OOL slab 통해).
- block은 재사용을 위해 free list에 놓이거나 풀에 보관될 수 있습니다.
- 선택적으로 데이터 유출이나 use-after-free 악용을 줄이기 위해 블록 내용을 지우거나 poison 처리할 수 있습니다.
- 블록과 연관된 하드웨어 태그는 무효화되거나 재태깅될 수 있습니다.
- 전체 chunk가 비어 있으면(모든 block이 해제) allocator는 메모리 압박 하에서 해당 chunk를 회수(언맵하거나 OS로 반환) 할 수 있습니다.
Security Features & Hardening
다음은 현대 유저랜드 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)는 하드웨어 + OS 프레임워크로, **Enhanced Memory Tagging Extension (EMTE)**를 핵심 공격 표면에 대해 항상 활성화된 동기식 모드로 제공합니다.
- xzone allocator는 유저 공간에서 MIE의 근간으로 동작합니다: xzone을 통해 이루어지는 할당은 태그를 받고, 접근은 하드웨어에 의해 검사됩니다.
- MIE에서 allocator, 태그 할당, metadata 관리, 태그 기밀성 강화는 통합되어 메모리 오류(예: 오래된 참조, 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
.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 helper & watcher modules
- Watcher anti-analysis: 전용 watcher 바이너리가 장치를 지속적으로 프로파일링하며 리서치 환경이 감지되면 킬체인을 중단합니다. 이 바이너리는
security.mac.amfi.developer_mode_status,diagnosticd콘솔의 존재, 로케일US또는IL, 탈옥 흔적(예: Cydia),bash,tcpdump,frida,sshd,checkrain같은 프로세스, 모바일 AV 앱(McAfee, AvastMobileSecurity, NortonMobileSecurity), 사용자 지정 HTTP 프록시 설정 및 사용자 지정 루트 CA를 검사합니다. 어떤 검사라도 실패하면 추가 페이로드 전달이 차단됩니다. - Helper surveillance hooks: helper 컴포넌트는 다른 단계와
/tmp/helper.sock을 통해 통신한 뒤 DMHooker 및 UMHooker라는 훅 세트를 로드합니다. 이 훅들은 VOIP 오디오 경로를 가로채며(녹음은/private/var/tmp/l/voip_%lu_%u_PART.m4a에 저장됨), 시스템 전체 키로거를 구현하고 UI 없이 사진을 촬영하며, 이러한 동작이 보통 발생시킬 알림을 억제하기 위해 SpringBoard에 훅을 겁니다. 따라서 helper는 Predator와 같은 더 무거운 임플란트를 배포하기 전에 은밀한 검증 및 경량 감시 계층으로 작동합니다.
iMessage/Media Parser Zero-Click Chains
Imessage Media Parser Zero Click Coreaudio Pac Bypass
References
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을 제출하여 해킹 트릭을 공유하세요.
HackTricks

