O último jailbreak com 0day: Tachy0n
(blog.siguza.net)- 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 objetokalloc.16e 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
racoontinham restrições bem diferentes em permissões, sandbox e técnicas de spray - O exploit para o unc0ver obtinha repetidamente sobreposição de
OSDatae usavaIOBufferMemoryDescriptoreIOAcceleratorFamily2para 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_contextpara rastrear as operações de I/O enviadas- A estrutura contém os campos
io_waiter,io_issuedeio_completed
- A estrutura contém os campos
- O trabalho real era executado em uma thread separada, e quando todo o I/O terminava,
do_aio_completionliberava esse contexto - Quando nenhuma operação era agendada, a thread atual de
lio_listioprecisava 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_listiovoltava a verificarlio_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_listioalocalio_context- A operação termina e
do_aio_completionliberalio_context - O atacante realoca essa memória liberada com um objeto sob seu controle e faz parecer que
lio_context->io_issued == 0 lio_listiovê 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_contextficava 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_issuednã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_NULLna primeira entrada para que ela caísse emkalloc.16e fizesseio_issuedparecer 0 - Liberar o array OOL mach ports com o segundo free
- Fazer spray de dados controlados em
kalloc.16para substituir o array de mach ports por um ponteiro falso
- Provocar o primeiro free de
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
mmapemlockpara 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
__DATAdo kernel - Como não foi encontrado um alvo adequado, o suporte a A10/A11 no caminho de app não foi implementado
- O plano era vazar um ponteiro de memória compartilhada ou posicionar dados no segmento
- O caminho via
racoontinha 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
OSUnserializeXMLviaIOSurface::setValue - Em vez disso, era possível usar a chamada
OSUnserializeXMLdentro deRootDomainUserClient::secureSleepSystemOptionspara fazer spray de alguns objetos em forma de leak
- O
racoontinha 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
__DATAdo kernel como depósito de dados em endereço conhecido - No Spice, o escolhido foi
vm.swapfileprefix
- Sabendo o kernel slide, era possível usar um global sysctl no
- 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 dekernel_map - Isso permitia obter ponteiros de código do kernel e do heap e cobrir A7~A11
- No sandbox de app,
sysctl_procargsxera bloqueado, então esse mesmo método não funcionava
- Com o vazamento de informação de
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_listioe threads racer que desserializavamOSDatapor meio de IOSurface - O padrão era 4 freerer e 16 racer, com possibilidade de ajuste
- A camada inferior executava threads freerer que chamavam
- Os dados desserializados via IOSurface formavam um
OSDictionarycom várias entradasOSData- A posição correspondente a
io_issuedprecisava ser 0 - Valores mágicos como
0x41414141,0x69696969e a chavekeram usados para detectar overlap
- A posição correspondente a
- Depois da corrida, todos os valores
OSDataeram 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
kdentro do buffer divergiam, isso indicava que outro objetoOSDataapontava para o mesmo backing buffer, caracterizando overlap
- As funções
maybe_reyoinkeoverlapno código montavam essas informações de sobreposição e as repassavam para a camada superior - A camada superior usava os objetos
OSDatasobrepostos 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
OSDatacontendo o ponteiro de fake task port
- Liberava um
Posicionando dados controlados em um endereço de kernel conhecido
- O exploit podia ler como
OSDatao 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 deIOVirtualRange, e um únicoIOVirtualRangecabe exatamente emkalloc.16 - Porém, um
IOMemoryDescriptornormal usa_singleRangeem vez de alocação no heap quando há apenas um range
- O campo
IOBufferMemoryDescriptorera a exceção, pois chamavaIONew(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 umIOAccelContext2 - 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
- Ao abrir o userclient tipo 0 em
- O exploit usava o seguinte loop
- Abre
IOAccelContext2e obtém doisOSDatasobrepostos - Libera um dos
OSData - Conecta um
IOAccelSharedUserClient2já aberto comIOConnectAddClient() - Lê o
OSDatarestante e verifica se os primeiros 8 bytes são um ponteiro de kernel alinhado em página e se os 8 bytes seguintes são0x8000 - Se a condição não bater, fecha
IOAccelContext2e repete
- Abre
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 invocaIOAccelContext2::processSidebandBuffer- Ele lê uma estrutura localizada 0x10 bytes após o início da memória compartilhada
- A primeira estrutura é feita com
tok == 0x100e 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_requireaceitava páginas fora dezone_mape interpretava os primeiros0x20bytes 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
- Na época,
- 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
- A análise foi resumida em How to unc0ver a 0-day in 4 hours or less
- 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
kallocezalloc- 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
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.
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.
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.
É 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.
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.
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.
Não sei se é preciso passar por intermediários, ou se existe algum e-mail público que não vá simplesmente se perder no suporte de primeiro nível.
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.
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.
Pelo que sei, o unc0ver, o jailbreak distribuído que incluía isso, provavelmente era “semi-untethered”.