1 pontos por GN⁺ 2025-05-25 | 2 comentários | Compartilhar no WhatsApp
  • tachy0n foi um exploit de elevação de privilégio no kernel que funcionava no iOS 13.0~13.5 e foi incluído como 0day no unc0ver v5.0.0 em 23 de maio de 2020, mirando diretamente a versão mais recente do iOS na época
  • O núcleo do exploit era uma condição de corrida Lightspeed em lio_listio, que permitia liberar duas vezes um objeto kalloc.16 e fazer com que objetos diferentes apontassem para a mesma memória
  • Um bug da mesma família já havia sido usado anteriormente no jailbreak/untether Spice voltado ao iOS 11, mas o sandbox de app e o ambiente racoon tinham restrições bem diferentes em permissões, sandbox e técnicas de spray
  • O exploit para o unc0ver obtinha repetidamente sobreposição de OSData e usava IOBufferMemoryDescriptor e IOAcceleratorFamily2 para montar um fake task e uma fake mach port
  • Depois do iOS 14, a Apple passou a bloquear a própria estratégia de exploit com separação do allocator, sequestering, PAC e hardening por objeto, e o conhecimento público sobre exploits de kernel do iOS ficou muito atrás do que existia na era do iOS 13

Por que a divulgação do tachy0n foi incomum

  • tachy0n era um exploit antigo que afetava do iOS 13.0 ao 13.5 e foi divulgado em 23 de maio de 2020 como parte do unc0ver v5.0.0
  • Pelos padrões da época, era um kernel LPE local comum, mas foi incomum por ter sido publicado como um jailbreak 0day que afetava a versão mais recente do iOS
  • Cerca de uma semana após a divulgação do exploit, a Apple distribuiu um patch específico para esse bug
  • O bug era 0day no iOS 13.5, mas antes disso já havia sido explorado como 1day
  • Pwn20wnd encontrou um 0day alcançável a partir do sandbox de app, e o ponto de partida foram testes de regressão sobre 1days conhecidos
    • O SockPuppet do iOS 12 havia sido corrigido no iOS 12.3, mas reapareceu no iOS 12.4
    • Esse caso expôs a falta de testes de regressão da Apple para esse tipo de bug, e Pwn20wnd implementou alguns testes de regressão para 1days conhecidos e obteve um acerto

Lightspeed: condição de corrida em lio_listio

  • O bug central do tachy0n era o bug Lightspeed da Synacktiv, relacionado ao CVE-2020-9859 e possivelmente ao CVE-2018-4344
  • A vulnerabilidade estava na syscall lio_listio, usada para realizar I/O de arquivo assíncrona ou em lote
  • O kernel alocava uma estrutura aio_lio_context para rastrear as operações de I/O enviadas
    • A estrutura contém os campos io_waiter, io_issued e io_completed
  • O trabalho real era executado em uma thread separada, e quando todo o I/O terminava, do_aio_completion liberava esse contexto
  • Quando nenhuma operação era agendada, a thread atual de lio_listio precisava liberar o contexto
  • O problema era que essa verificação tinha uma condição de corrida
    • Uma operação podia ser enviada para outra thread, mas ser concluída antes do momento da checagem, fazendo com que o contexto já tivesse sido liberado
    • Nesse caso, lio_listio voltava a verificar lio_context, que já havia se tornado um dangling pointer

Como o double free leva ao exploit

  • A sequência necessária para o exploit era a seguinte
    • lio_listio aloca lio_context
    • A operação termina e do_aio_completion libera lio_context
    • O atacante realoca essa memória liberada com um objeto sob seu controle e faz parecer que lio_context->io_issued == 0
    • lio_listio vê isso e libera de novo o objeto do atacante
    • A mesma memória é então realocada com outro objeto, fazendo com que duas alocações diferentes apontem para a mesma região de memória
  • Em dispositivos 64-bit, lio_context ficava na menor zone, kalloc.16
  • Antes do iOS 14, independentemente do tipo do objeto, o mesmo local de alocação era compartilhado por tamanho
    • Objetos C++, arrays de ponteiros e buffers de dados fornecidos pelo usuário podiam reutilizar a memória uns dos outros dentro do mesmo bucket de tamanho
  • Esse double free funcionava de forma diferente de um double free comum, que sem realocação intermediária costuma levar facilmente a um estado fatal
    • lio_context->io_issued não vira 0 enquanto a alocação está ativa
    • Depois da liberação, o allocator sobrescreve os primeiros 8 bytes com um valor canário e um ponteiro da freelist ou um valor XOR com o endereço do objeto
    • Portanto, o segundo free só acontece se houver uma realocação intermediária e se os bytes 4~7 forem 0
  • Na prática, o exploit podia tentar essa corrida repetidas vezes, e era raro em uso real que outro objeto do sistema zerasse por acaso os bytes necessários para disparar o double free

Uso anterior no Spice

  • O mesmo bug também foi usado no jailbreak/untether Spice, voltado ao iOS 11.x
  • O Spice foi desenvolvido pela equipe de Jake Blair junto com Sparkey e littlelailo, numa época em que a versão mais recente era o iOS 13.x
  • O objetivo era criar mach port forgery tanto no ambiente de app quanto no ambiente racoon
    • Em exploits de kernel para iOS antes do iOS 14, fazer o kernel interpretar um valor controlado pelo usuário como ponteiro de mach port tornava as etapas seguintes decisivamente mais fáceis
  • O fluxo básico para gerar mach port forgery com Lightspeed era o seguinte
    • Provocar o primeiro free de lio_context
    • Fazer spray de mach messages com descritores OOL mach ports de tamanho 1 ou 2
    • Colocar MACH_PORT_NULL na primeira entrada para que ela caísse em kalloc.16 e fizesse io_issued parecer 0
    • Liberar o array OOL mach ports com o segundo free
    • Fazer spray de dados controlados em kalloc.16 para substituir o array de mach ports por um ponteiro falso

Diferenças de restrição entre o sandbox de app e o racoon

  • Em A7~A9(X), como não havia PAN, era possível usar apenas mmap e mlock para fazer o kernel dereferenciar endereços de userland como se fossem ponteiros de kernel
  • Houve tentativa de dar suporte também a A10 e A11, mas no sandbox de app não foi possível encontrar um vazamento adequado de endereço de kernel e um alvo para posicionar dados controlados, então isso não foi concluído
  • Entre os 1days que se pretendia usar estavam o vazamento de informação da stack do kernel de Ian Beer e a fuga de sandbox do backboardd
    • O plano era vazar um ponteiro de memória compartilhada ou posicionar dados no segmento __DATA do kernel
    • Como não foi encontrado um alvo adequado, o suporte a A10/A11 no caminho de app não foi implementado
  • O caminho via racoon tinha condições diferentes
    • Ele era executado como root, mas com um sandbox mais rígido que o de apps comuns
    • Como não havia acesso a IOSurface, não dava para usar o spray comum de OSUnserializeXML via IOSurface::setValue
    • Em vez disso, era possível usar a chamada OSUnserializeXML dentro de RootDomainUserClient::secureSleepSystemOptions para fazer spray de alguns objetos em forma de leak
  • O racoon tinha um perfil de sandbox que permitia todas as leituras e escritas de sysctl, além de privilégios de root
    • Sabendo o kernel slide, era possível usar um global sysctl no __DATA do kernel como depósito de dados em endereço conhecido
    • No Spice, o escolhido foi vm.swapfileprefix
  • Para obter o kernel slide, foi usado o CVE-2018-4413 do panicall
    • Com o vazamento de informação de sysctl_procargsx, era possível vazar quase uma página inteira de memória de kernel não inicializada de kernel_map
    • Isso permitia obter ponteiros de código do kernel e do heap e cobrir A7~A11
    • No sandbox de app, sysctl_procargsx era bloqueado, então esse mesmo método não funcionava

Estrutura do exploit tachy0n para o unc0ver

  • Como o alvo do unc0ver era A8~A13, não dava para ignorar A10+ nem depender de dereferência em userland
  • O exploit foi projetado em duas camadas para lidar com a etapa de corrupção de memória, que podia falhar
    • A camada inferior executava threads freerer que chamavam lio_listio e threads racer que desserializavam OSData por meio de IOSurface
    • O padrão era 4 freerer e 16 racer, com possibilidade de ajuste
  • Os dados desserializados via IOSurface formavam um OSDictionary com várias entradas OSData
    • A posição correspondente a io_issued precisava ser 0
    • Valores mágicos como 0x41414141, 0x69696969 e a chave k eram usados para detectar overlap
  • Depois da corrida, todos os valores OSData eram verificados
    • Se um objeto tivesse o valor mágico alterado, considerava-se que ele havia sido tomado por outro objeto do sistema e ele era marcado para limpeza posterior
    • Se a chave e o valor k dentro do buffer divergiam, isso indicava que outro objeto OSData apontava para o mesmo backing buffer, caracterizando overlap
  • As funções maybe_reyoink e overlap no código montavam essas informações de sobreposição e as repassavam para a camada superior
  • A camada superior usava os objetos OSData sobrepostos para montar uma fake mach port
    • Liberava um OSData
    • Fazia spray de uma mach message com descritor OOL port
    • Liberava o outro OSData
    • Realocava com um novo OSData contendo o ponteiro de fake task port

Posicionando dados controlados em um endereço de kernel conhecido

  • O exploit podia ler como OSData o conteúdo realocado pelo array de descritores OOL ports e assim vazar o ponteiro bruto de kernel de uma mach port
  • Nas etapas seguintes, isso era usado para vazar o endereço da task port e da service port de IOSurfaceRoot, mas o problema central era obter de forma estável o endereço de kernel de um buffer controlável
  • Um candidato encontrado no código-fonte do XNU foi IOMemoryDescriptor
    • O campo _ranges é um array de IOVirtualRange, e um único IOVirtualRange cabe exatamente em kalloc.16
    • Porém, um IOMemoryDescriptor normal usa _singleRange em vez de alocação no heap quando há apenas um range
  • IOBufferMemoryDescriptor era a exceção, pois chamava IONew(IOAddressRange, 1) mesmo para um único range, realizando uma alocação no heap
  • Um lugar conveniente para alocar isso arbitrariamente e mapeá-lo no espaço de endereçamento do usuário era a interface AGX de IOAcceleratorFamily2
    • Ao abrir o userclient tipo 0 em IOGraphicsAccelerator2, obtém-se um IOAccelContext2
    • Com ::clientMemoryForType(), era possível mapear três memory descriptors
    • O tipo 0 tinha 0x8000 bytes, o que era usado para identificar o descriptor vítima
  • O exploit usava o seguinte loop
    • Abre IOAccelContext2 e obtém dois OSData sobrepostos
    • Libera um dos OSData
    • Conecta um IOAccelSharedUserClient2 já aberto com IOConnectAddClient()
    • Lê o OSData restante e verifica se os primeiros 8 bytes são um ponteiro de kernel alinhado em página e se os 8 bytes seguintes são 0x8000
    • Se a condição não bater, fecha IOAccelContext2 e repete

Memória pageable, fake port e zone_require

  • Mesmo após mapear o memory descriptor no processo e descobrir o endereço de kernel, ainda restava o problema de que a memória era criada como kIOMemoryPageable
  • Como a fake mach port e o fake task object podiam ser acessados com preempção desativada, o kernel precisava fazer fault-in dessa página antes
  • Isso era resolvido chamando duas vezes o external method 2 de IOAccelContext2::submit_data_buffers, que indiretamente invoca IOAccelContext2::processSidebandBuffer
    • Ele lê uma estrutura localizada 0x10 bytes após o início da memória compartilhada
    • A primeira estrutura é feita com tok == 0x100 e cobre a página inteira, para avançar também sobre a segunda página
    • A segunda página podia então ser usada depois para armazenar os dados do objeto falso
  • As etapas seguintes levavam à criação de fake task, fake port, OOL descriptor switcheroo e um primitive de leitura arbitrária
  • Também era necessário contornar zone_require
    • Na época, zone_require aceitava páginas fora de zone_map e interpretava os primeiros 0x20 bytes da página como se fossem metadados
    • Inserindo o índice correto de zone, era possível usá-la como uma espécie de passe livre para a zone desejada
    • Por isso, eram necessárias duas páginas: uma para task e outra para mach port
  • Hoje esse exploit está publicado no GitHub

Análise e patch após a divulgação

  • A divulgação de um exploit 0day completo para a versão assinada mais recente chamou atenção na cena de jailbreak do iOS
  • Brandon Azad, então no Project Zero, identificou a vulnerabilidade e avisou a Apple em até 4 horas após a publicação do exploit
  • Seis dias após a divulgação do exploit, a Synacktiv publicou um novo texto argumentando que o fix original no iOS 12 criou um memory leak, e que a tentativa de corrigir esse memory leak pode ter reintroduzido o bug original
  • Nove dias após a divulgação do exploit, a Apple lançou o patch
  • Depois disso, o XNU ganhou um teste de regressão para esse bug
  • Cinquenta e quatro dias após a divulgação, uma versão revertida chamada “tardy0n” foi incluída no jailbreak Odyssey, também voltada ao iOS 13.0~13.5

O ambiente de exploit mudou após o iOS 14

  • O iOS 14 mostrou uma mudança na estratégia de segurança de kernel da Apple
  • Antes do iOS 14, fosse o primitive inicial um heap overflow, over-release de objeto C++ ou type confusion, o alvo seguinte quase sempre era uma mach port
  • Uma das maiores mudanças no iOS 14 envolveu o allocator kalloc e zalloc
    • O zone map foi dividido em vários intervalos “kheap”
    • Dados controlados pelo usuário e objetos do kernel passaram a ser separados em heaps diferentes
    • Objetos de kernel passaram a usar sequestering, fazendo com que uma página de endereço virtual alocada para uma zone específica não fosse reutilizada por outra zone antes de um reboot
    • A memória física pode ser liberada, mas o intervalo de memória virtual não é reutilizado por outro tipo de objeto, bloqueando na prática type confusion entre objetos de kernel
  • Guard pages, posições iniciais de alocação por zone variando a cada boot e refinamentos posteriores reduziram muito a confiabilidade de ataques cross-zone
  • A Apple deixou de apenas bloquear bugs individuais e passou a bloquear estratégias de exploit
    • Se um exploit usa uma struct de kmsg como alvo de corrupção, essa struct passa a ser assinada
    • Se um pipe buffer é usado como interface estável de leitura/escrita no kernel, os ponteiros relevantes passam a ser protegidos por PAC
    • Se surge uma técnica que usa um objeto não relacionado como vítima, aquele tipo de objeto passa a receber hardening
  • Com isso, em desenvolvimento de exploit, a estratégia de exploit passou a valer mais do que um 0day inicial de corrupção de memória

A ruptura no conhecimento público

  • Antes do iOS 14, avalia-se que o conhecimento público sobre pesquisa de segurança em iOS era quase equivalente ao conhecimento privado
  • Depois do iOS 14, salvo exceções, o compartilhamento de informação praticamente parou
  • Mesmo a poucas semanas do beta do iOS 19, segue não havendo exploit público de kernel para iOS 18 ou iOS 17
  • As notas de segurança da Apple às vezes registram vulnerabilidades exploradas ativamente, mas as informações públicas não acompanham o ritmo da pesquisa privada
  • O fato de que a divulgação do tachy0n aconteceu há apenas 5 anos mostra o quanto a área de exploits de kernel para iOS mudou rapidamente

2 comentários

 
ndrgrd 2025-05-26

Embora o hardware da Apple seja excelente, o software é cheio de mecanismos feitos para manter o usuário na coleira.
Mesmo que você só queira fazer um app que você mesmo criou e compilou rodar apenas no seu próprio dispositivo, ainda precisa de uma assinatura de 100 dólares.

Se você é um desenvolvedor que usa apps open source pequenos ou médios e compila para uso próprio,
fazer jailbreak explorando vulnerabilidades e usar sideload nos dispositivos da Apple é menos prático do que simplesmente usar Android.

 
GN⁺ 2025-05-25
Opiniões no Hacker News
  • É impressionante que a forma de vencer uma empresa de US$ 1 trilhão tenha sido uma tarefa simples e tediosa na qual a Apple é especialmente fraca: testes de regressão.
    SockPuppet, uma das grandes vulnerabilidades usadas para jailbreak no iOS 12, foi encontrada por Ned Williamson, do Project Zero, reportada à Apple, corrigida no iOS 12.3 e depois divulgada no rastreador de bugs do Project Zero.
    Mas ela reapareceu no iOS 12.4 como se nunca tivesse sido corrigida, provavelmente porque a Apple criou um fork separado do XNU para aquela versão e acabou não aplicando o patch.
    Isso era um forte sinal de que não havia testes de regressão para esse tipo de vulnerabilidade, e automatizar apenas algumas 1-days conhecidas já teria permitido que a Pwn acertasse em cheio.
    Fico curioso para saber quantas organizações mantêm uma fazenda de CI rodando continuamente vulnerabilidades antigas contra novas versões de projetos como Linux, FreeBSD, OpenWRT e OpenSSH.

    • Testes de regressão são um procedimento padrão de QA para verificar se bugs já corrigidos não voltaram.
      Há 20 anos, na faculdade, fiz QA voluntário na Mozilla, e havia um conjunto crescente de testes de regressão, principalmente para bugs do motor de JavaScript e de renderização/layout.
      Como criávamos casos de teste mínimos para reproduzir e confirmar correções, também era fácil colocá-los no pipeline de build.
      Bugs são inevitáveis, mas o pior cenário é um bug que foi corrigido com tempo e dinheiro voltar à vida.
      Organizações que valorizam qualidade certamente investem em testes de regressão, mas muitas organizações não respeitam QA: ou não fazem, ou terceirizam para o fornecedor mais barato.
      É realmente estranho que a Apple não tenha testes de regressão para jailbreak, historicamente uma das categorias de bugs de maior visibilidade.
      Hoje em dia a Mozilla pode receber várias críticas, mas no começo dos anos 2000 já operava um QA e CI/CD bastante sólidos com ferramentas como Tinderbox e Bugzilla.
      Quando DevOps virou moda e popularizou esse tipo de prática, eu achava que todo mundo já fazia isso, mas era engano meu.
    • Se aqui “projetos” incluir também agências de inteligência, é seguro presumir que pelo menos as agências do G10, além de Rússia, China, Coreia do Norte e inúmeros grupos privados, fazem esse tipo de trabalho.
    • O problema fundamental parece ser que muitas organizações isolaram o trabalho de segurança em um fluxo separado e em uma classificação separada de bugs.
      É uma situação meio Lei de Conway criada pela separação entre segurança e desenvolvimento de funcionalidades.
      Mesmo que existam procedimentos de build/release e um conjunto maduro de testes de regressão, a estrutura organizacional interna provavelmente faz com que esses problemas de segurança não entrem ali.
    • Não lembro o nome, mas já vi um projeto FOSS em que cada issue tinha um diretório de casos de teste.
      Havia facilmente milhares deles, e acho que talvez fosse o SQLite.
      É uma abordagem a ser imitada.
      Se as correções não forem backportadas, parece provável que os testes também não sejam.
  • Ao ver termos como kheap separation, mitigação de task port, SSV e SPTM, tive aquela sensação de estar conversando bem com um amigo em uma língua estrangeira e, de repente, a conversa virar uma explicação de neurocirurgia ou física nuclear, fazendo a compreensão despencar de um penhasco.
    Foi parecido com a vez em que tentei interpretar uma conversa sobre reforma de alto-forno.
    É uma pena que jailbreak já não seja tão ativo quanto antes.
    Quase não fiz nada prático com meu iPad com jailbreak, mas era divertido; hoje eu gostaria de instalar um app de tethering, o UTM e alguma solução para JIT.
    O SideStore também parecia promissor, mas minha conta já foi uma conta paga de Apple Developer, então ainda tenho 10 apps IDs que não expiram; por isso não consigo instalar apps como o UTM sem criar uma nova conta ou voltar a pagar.

    • Eu usava um iPhone 4 antigo com jailbreak, e essa era praticamente a única forma de usar o iPhone como aparelho principal.
      Depois que perdi isso, voltei para o Android, que por volta daquela época já tinha alcançado bastante em termos de recursos básicos.
  • Ouvi dizer que hoje a Apple paga US$ 1 milhão por um jailbreak, e isso deve ser o piso do preço no livre mercado.

  • Se isso for verdade, a Apple usou uma estratégia impressionante.
    Se ela fecha todos os caminhos para obter root no dispositivo, pode corrigir vulnerabilidades encontradas de graça por desenvolvedores de jailbreak.

    • Mas não é totalmente assim.
      Segundo o texto, a comunidade privada ainda tem exploits, e a Apple os corrige.
      Parece que apenas a comunidade pública, ou esse desenvolvedor, por algum motivo já não funciona mais assim.
  • A melhor frase de todo o texto foi: “Também gostaria de agradecer a quem desfez o patch desse bug no iOS 13.0. Isso também foi uma atitude muito legal.”

  • “Nem consigo imaginar onde estaremos daqui a 5 anos”, mas eu consigo.
    O iMessage ainda permitirá roubo de dispositivos, contas e dados.

  • O texto não dizia se era tethered ou untethered.

    • tachy0n é uma elevação local de privilégios (LPE), então essa classificação não se aplica muito bem.
      Pelo que sei, o unc0ver, o jailbreak distribuído que incluía isso, provavelmente era “semi-untethered”.