Segurança

Patch Diffing CVE-2024-23265: Vulnerabilidade de Corrupção de Memória no Kernel do iOS | 8kSec

O artigo analisa a vulnerabilidade de segurança CVE-2024-23265 no kernel do iOS, corrigida na versão 17.4, e detalha o processo de comparação entre versões para identificar as alterações implementadas pela Apple para resolver este problema de segurança.

Compartilhar
Patch Diffing CVE-2024-23265: iOS Kernel Bug | 8kSec

Introdução

Neste blog, analisaremos a vulnerabilidade CVE-2024-23265, uma falha de corrupção de memória no kernel do iOS. Este exercício de aprendizado ajudará a entender o processo de diferenciação IPSW e identificar onde o patch provavelmente corrigiu a vulnerabilidade.

CVE-2024-23265 é uma falha de corrupção de memória no kernel do iOS, corrigida na versão 17.4. A correção foi feita melhorando os mecanismos de bloqueio para prevenir condições de corrida que poderiam levar à terminação inesperada do sistema ou a escritas não autorizadas na memória do kernel.

  • Versão Vulnerável: iOS 17.3.1 (Build 21D61)
  • Versão Corrigida: iOS 17.4 (Build 21E219)

Vamos explorar como o problema subjacente foi abordado no nível do kernel e como analisar tais correções usando técnicas práticas de diferenciação.

Extração e Diferenciação dos KEXTs do Kernelcache com IPSW

Continuando da série IPSW Walkthrough publicada no blog 8ksec, usaremos a ferramenta IPSW tool.

Para começar, baixe os arquivos IPSW relevantes diretamente via terminal usando os seguintes comandos:

ipsw download ipsw --device iPhone15,4 --build 21D61  ipsw download ipsw --device iPhone15,4 --build 21E219

Eles correspondem às versões vulneráveis (iOS 17.3.1) e corrigidas (iOS 17.4) associadas à CVE-2024-23265.

Agora, para extrair o kernelcache dos arquivos IPSW baixados, use os seguintes comandos:

ipsw extract --kernel iPhone15,4_17.3.1_21D61_Restore.ipsw ipsw extract --kernel iPhone15,4_17.4_21E219_Restore.ipsw

Ao extrair, podemos inspecionar as versões do kernel para confirmar diferenças entre os builds:

ipsw kernel version 21D61__iPhone15,4/kernelcache.release.iPhone15,4 --json | jq .
{   "kernel": {     "darwin": "23.3.0",     "date": "2023-12-20T17:32:11Z",     "xnu": "10002.82.4~3",     "type": "RELEASE",     "arch": "ARM64",     "cpu": "T8120"   },   "llvm": {} }
ipsw kernel version 21E219__iPhone15,4/kernelcache.release.iPhone15,4 --json | jq .
{   "kernel": {     "darwin": "23.4.0",     "date": "2024-02-16T20:44:20Z",     "xnu": "10063.102.14~10",     "type": "RELEASE",     "arch": "ARM64",     "cpu": "T8120"   },   "llvm": {} }

Também podemos extrair todos os KEXTs de ambos os kernelcaches para inspeção detalhada usando:

ipsw kernel extract kernelcache.release.iPhone15,4 --all --output KEXTs_21D61
ipsw kernel extract kernelcache.release.iPhone15,4 --all --output KEXTs_21E219

No entanto, para o propósito de fazer a comparação (diffing), extrair todos os KEXTs é desnecessário. Uma rápida comparação entre as duas versões do kernelcache pode ser feita diretamente com:

Ao contrário de ferramentas como o plugin BinExport para Ghidra e a extensão BinDiffHelper, que permitem uma análise completa da cadeia BinDiff, bem como a reconstrução de assembly baseada em IA é uma opção emergente, este exemplo adota uma abordagem mais rápida e leve: exportar código descompilado do Ghidra e compará-lo usando ferramentas como Meld.

Use o seguinte script do Ghidra para exportar todas as funções descompiladas:

#@category Export
#@keybinding #@menupath #@toolbar from ghidra.app.decompiler import DecompInterface from ghidra.util.task import ConsoleTaskMonitor import os # Altere para onde deseja que a saída seja escrita output_file = "" decomp = DecompInterface() decomp.openProgram(currentProgram) fm = currentProgram.getFunctionManager() functions = fm.getFunctions(True) with open(output_file, "w") as out: out.write("// Funções descompiladas de: "{}nn".format(currentProgram.getName())) for function in functions: results = decomp.decompileFunction(function, 60, ConsoleTaskMonitor()) if results and results.decompileCompleted(): sig = function.getName() body = results.getDecompiledFunction().getC() out.write("// Função: "{} em {}n".format(sig, function.getEntryPoint())) out.write(body) out.write("nn") else: out.write("// Falha ao descompilar função: "{} em {}nn".format(function.getName(), function.getEntryPoint()))

Agora, para exportar o código descompilado diretamente via terminal, podemos usar Ghidra’s analyzeHeadless:

analyzeHeadless   -process com.apple.driver.AppleDiskImages2 -scriptPath ~/ghidra_scripts -postScript DumpAllToSingleFile.py --noanalysis

Após gerar os arquivos de código para ambos os projetos, podemos começar a compará-los.

Identificando o Patch via Comparação Descompilada

Agora estamos prontos para a comparação:

meld version1.c version2.c

Depois de uma inspeção, mudanças significativas começam a aparecer por volta da linha ~8170:

Vista de comparação do Meld mostrando diferenças no código descompilado AppleDiskImages2 entre iOS 17.3.1 e 17.4 em torno da linha 8170

Vista de comparação do Meld destacando a nova cláusula de verificação guardada que verifica lVar4 contra -1 na versão patchada iOS 17.4

Ao aplicar a simbolização, muitos símbolos internos permanecem não resolvidos. Esta função específica parece implementar verificações de limites de ponteiros e lógica semelhante à de uma tabela de páginas em AppleDiskImages2.kext, mas seu nome exato e papel não estão presentes nos símbolos disponíveis.

Diferenças-chave incluem um novo cláusula de guarda. No iOS 17.3.1:

if (lVar4 != 0) {
  return lVar4;
}

No iOS 17.4:

if ((lVar4 != 0) && (lVar4 != -1)) {
  return lVar4;
}
// ......
lVar5 = 0;
if (lVar4 != -1) {
  lVar5 = lVar4;
}

Isto adiciona uma verificação explícita contra -1 (0xffffffffffffffff), prevenindo a volta de um ponteiro potencialmente envenenado ou inválido. Esta correção mitigou o retorno de ponteiros não validados, abordando padrões comuns de condições de corrida em estruturas apertadas de ponteiros ou mapas de alocação.

Outra mudança que podemos ver afeta a lógica de término do loop:

if (uVar8 == *(uint *)(lVar5 + 0xc)) {
  lVar5 = 0;
}

Em vez de retornar diretamente lVar4, a lógica atualizada garante:

  • Retorno nulo em falha (falha limpa)
  • Apenas resultados válidos e não--1 são propagados

Isto reforça a verificação de limites e validação da integridade de memória.

Condições Provavelmente Vulneráveis no iOS 17.3.1

Até este ponto, as condições provavelmente vulneráveis em 17.3.1 podem ser:

  • *(long *)(uVar3 + (uVar1 & 0xfffffff8)) pode retornar -1:
    • Não validado
    • Retornado diretamente ao chamador
  • Caso a estrutura de dados subjacente seja modificada concorrentemente:
    • Um valor -1 estalado pode ser retornado
    • Race com desalocação leva a UAF ou exposição de ponteiro inválido
  • Índice controlado pelo usuário (param_1[1]) impulsiona o loop

Isto aponta para uma possível condição de corrida, permitindo que condições controladas pelo usuário bypassem verificações de segurança e retornem referências de memória maliciosas ou corrompidas. Se esta função for invocada via um caminho IOUserClient::externalMethod ou equivalente, um ator malicioso pode:

  • Race atualizações à estrutura de dados subjacente
  • Obrigar o retorno de -1
  • Iniciar corrupção da memória do kernel ou acesso não autorizado a ponteiros

Esses achados se alinham estreitamente com o aviso CVE-2024-23265:

"Melhorias de bloqueio prevenem escritas não autorizadas na memória do kernel ou terminação inesperada do sistema."

O retorno de -1 como um ponteiro pode disparar ambos os modos de falha.

Rastreando o Impacto

Analisando a função vulnerável no iOS 17.3.1 dentro do Ghidra, inspecionamos as referências ao seu uso:

Uma referência notável leva a __ZN20AppleDiskImageDevice15boostAnyRequestEh:

undefined8 __ZN20AppleDiskImageDevice15boostAnyRequestEh(long param_1, uint param_2)

A função AppleDiskImageDevice::boostAnyRequest(uint8_t level):

  • Chama diretamente a função vulnerável 9 vezes
  • Itera sobre param_1 + 0xf8 usando param_1[1] como índice do loop (provavelmente influenciado pelo usuário)
  • Aplique lógica envolvendo bandeiras QoS e buffers de memória residentes no kernel
  • Likely manipulando algum tipo de fila de I/O ou lista de buffers em trânsito
  • Não tem verificação de limites para valores desreferenciados (antes da correção)

Outros referências:

void FUN_fffffff028a13b98(long param_1, long param_2)

Esta função: - Também chama a função vulnerável 3 vezes - Preenche uma estrutura em param_2 + 0x780 - Sugere coleta de metadados ou enumeração de extensões de uma estrutura de suporte - Parece ser um caminhar estruturado das mapeações da imagem do disco, escrevendo-as na memória do usuário.

Sua principal iteração é:

while ((local_48 != local_60 || local_40 != local_58) && (*(uint *)(param_2 + 0x780)

Rastreamento da Acessibilidade do Usuário

Para determinar se o usuário pode alcançar boostAnyRequest() ou FUN_fffffff028a13b98, verificamos a presença de referências externalMethod ou tabelas IOExternalMethodDispatch. Esses definem como IOUserClient expõe seletores de método ao usuário. Se qualquer uma dessas funções é referenciada nessas tabelas ou em declarações condicionais vinculadas a índices de seletores, isso confirma a acessibilidade da syscall via IOConnectCallMethod.

Sem sucesso nas referências estáticas, procuramos por evidências em string:

Resultados da busca de strings no Ghidra mostrando o símbolo IOReturn AppleDiskImageDevice::boostAnyRequest(uint8_t) no binário do KEXT AppleDiskImages2

A string IOReturn AppleDiskImageDevice::boostAnyRequest(uint8_t) confirma que boostAnyRequest é um método formalmente declarado com um símbolo visível, não anônimo ou in-line. Embora isso não prove que está exposto via IOExternalMethodDispatch, seu nome e estrutura são consistentes com métodos que são despachados através de externalMethod em subclasses de IOUserClient.

Consultando boostAnyRequest, encontramos a seguinte função:

Visão do Ghidra de referências cruzadas mostrando a função que chama boostAnyRequest, operando sobre param_1 + 0xf8

Esta função chama boostAnyRequest(...) se FUN_fffffff028a0c98c(...) == 0.

Ela opera em um campo de objeto a partir de param_1 + 0xf8, provavelmente referenciando o AppleDiskImages2UserClient ou uma estrutura relacionada.

Em seguida, ela chama DIDeviceRequestPool::GetRequest(…) que interessantemente se encaixa no nome visto ao procurar por externalMethod na árvore de símbolos: e.g. DIDeviceCreatorUserClient::externalMethod(…):

Árvore de símbolos do Ghidra mostrando referência a DIDeviceCreatorUserClient::externalMethod indicando acessibilidade do usuário ao código vulnerável

Isto nos diz que a chamada para boostAnyRequest não é independente. Em vez disso, ela é:

  • Likely uma subrotina em um usuário ou em sua classe auxiliar
  • Chamado como parte de um caminho de solicitação, provavelmente a partir do externalMethod de uma DIDevice*UserClient subclass
  • Implica que chamando algum seletor levará à GetRequest(...), o qual por sua vez pode chamar boostAnyRequest(...) se uma condição for atendida

Há também 4 referências cruzadas para __ZN20AppleDiskImageDevice10GetRequestEmhPFbPvES0_:

  • FUN_fffffff028a0f990
  • AppleDiskImageDevice::doEjectMediaEv
  • FUN_fffffff028a10444
  • FUN_fffffff028a107e4

Todos chamam:

AppleDiskImageDevice::GetRequest(...)

→ o qual chama condicionalmente:

AppleDiskImageDevice::boostAnyRequest(…)

→ o qual contém a chamada vulnerável para:

FUN_fffffff028a156c8

Essas funções não aparecem em tabelas de despacho padrão, o que significa que a KEXT provavelmente usa um despacho manual dentro da função externalMethod(...), como um switch na seleção ou uma matriz personalizada de ponteiros para funções. Isso torna a análise estática mais difícil e força a verificação em tempo real para encontrar seleções válidas.

O despacho provavelmente é feito por meio de um switch personalizado (param_2) ou tabela indireta, com cada valor do seletor mapeando para uma dessas funções.

A seguir, tentamos identificar o mapeamento dos seletores varrendo a binária em busca de referências escalares aos endereços das funções vulneráveis, mas não encontramos correspondências, confirmando que o despacho é dinâmico e não tabular.

Resultado da varredura binária no Ghidra mostrando nenhuma referência escalar ao endereço da função vulnerável, confirmado despacho dinâmico

Conclusão

Com a comparação do AppleDiskImages2 KEXT entre iOS 17.3.1 e 17.4, identificamos uma vulnerabilidade de corrupção de memória no kernel (CVE-2024-23265), causada por falta de verificação do valor sentinela -1 em um loop que desreferencia ponteiros do kernel.

A função vulnerável é chamada repetidamente na AppleDiskImageDevice::boostAnyRequest(uint8_t), provavelmente exposta via um método user client. A Apple corrigiu isso adicionando bloqueio e guardas -1, indicando uma condição de corrida clássica onde uma tabela de ponteiros ou lista de extensões pode ser modificada durante a iteração, levando a uma desreferência inválida.

Para explorar essa vulnerabilidade, primeiro identifique o IOService apropriado via ioreg, depois use fuzzing ou força bruta para valores de seletor com IOConnectCallMethod para disparar o caminho do código vulnerável (Referência), após isso, forneça entrada elaborada para manipular o estado interno do loop ou referências à memória, então induza uma condição de corrida na estrutura subjacente usando concorrência e finalmente observe o comportamento do kernel em busca de evidências de corrupção de memória, desreferência inválida de ponteiro ou instabilidade no sistema (Consulte Análise de Kernel Panic da 8ksec).

Sobre o Autor — Sergio Lopez (s3rg0x)

Sergio Lopez (LinkedIn) é um Red Teamer e Pesquisador de Segurança, com experiência em segurança da IA, análise de vulnerabilidades do kernel, shellcoding, design de quadros de comando C2 e desenvolvimento de malware evasivo.

Ele apresentou em Black Hat e várias outras conferências de segurança líderes, e possui múltiplas certificações da indústria incluindo OSEP, OSWE, GREM, CRTO e eCPPT.

Sergio é apaixonado por avançar a comunidade de segurança e está sempre ansioso para contribuir com pesquisas e compartilhamento de conhecimento.

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa