Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
blanket — CVE-2018-4280: Vulnerabilidade de substituição de porta Mach no launchd no iOS 11.2.6 levando a escape da sandbox, escalada de privilégios e bypass de codesigning. | Kitploit
Ferramentas/GitHubGitHub/bazad/blanket
Escalada de PrivilégiosSegurança iOSAnálise de VulnerabilidadesExploraçãoPós-ExploraçãoSegurança MóvelExploração de Binários
GitHubbazad/blanket

blanket

CVE-2018-4280: Vulnerabilidade de substituição de porta Mach no launchd no iOS 11.2.6 levando a escape da sandbox, escalada de privilégios e bypass de codesigning.

Ver Repositório
26043há 7 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

blanket

Blanket é uma ferramenta de escape de sandbox voltada para iOS 11.2.6, embora a vulnerabilidade principal só tenha sido corrigida no iOS 11.4.1. Ela explora uma vulnerabilidade de substituição de porta Mach no launchd (CVE-2018-4280), bem como várias vulnerabilidades menores em outros serviços, para executar código dentro do processo ReportCrash, que não tem sandbox, roda como root e possui a permissão task_for_pid-allow. Isso concede ao blanket controle sobre todos os processos em execução no telefone, incluindo os críticos de segurança como amfid.

O exploit consiste em várias etapas. Este README explicará a vulnerabilidade principal e as etapas do escape de sandbox passo a passo.

Imitando serviços do sistema

Ao pesquisar relatórios de falhas no iOS, descobri uma vulnerabilidade de substituição de porta Mach no launchd. Ao travar de uma maneira específica, um processo pode fazer o kernel enviar uma mensagem Mach para o launchd que faz com que o launchd desaloque excessivamente um direito de envio para uma porta Mach em seu namespace IPC. Isso permite que um invasor se faça passar por qualquer serviço do launchd que ele possa consultar para o resto do sistema, o que abre inúmeras possibilidades de escalada de privilégios.

Essa vulnerabilidade também está presente no macOS, mas acioná-la no iOS é mais difícil devido a verificações no launchd que garantem que a mensagem de exceção Mach venha do kernel.

CVE-2018-4280: Desalocação excessiva de porta Mach do launchd ao lidar com mensagens de exceção EXC_CRASH

O launchd multiplexa vários manipuladores de mensagens Mach diferentes em sua porta principal, incluindo um manipulador MIG para mensagens de exceção. Se um processo enviar uma mensagem mach_exception_raise ou mach_exception_raise_state_identity para sua própria porta bootstrap, o launchd receberá e processará essa mensagem como uma exceção de nível de host.

Infelizmente, o tratamento dessas mensagens pelo launchd é defeituoso. Se o tipo de exceção for EXC_CRASH, o launchd desalocará as portas de thread e tarefa enviadas na mensagem e depois retornará KERN_FAILURE da rotina de serviço, fazendo com que o sistema MIG desaloque novamente as portas de thread e tarefa. (A suposição é que, se uma rotina de serviço retornar sucesso, ela assumiu a propriedade de todos os recursos na mensagem Mach, enquanto se a rotina de serviço retornar um erro, ela não assumiu a propriedade de nenhum dos recursos.)

Aqui está o código da rotina de serviço do launchd para mensagens mach_exception_raise, descompilado usando IDA/Hex-Rays e levemente editado para legibilidade:```C kern_return_t __fastcall catch_mach_exception_raise( // (a) The service routine is mach_port_t exception_port, // called with values directly mach_port_t thread, // from the Mach message mach_port_t task, // sent by the client. The exception_type_t exception, // thread and task ports could mach_exception_data_t code, // be arbitrary send rights. mach_msg_type_number_t codeCnt) { __int64 __stack_guard; // ST28_8@1 kern_return_t kr; // w0@1 MAPDST kern_return_t result; // w0@4 __int64 codes_left; // x25@6 mach_exception_data_type_t code_value; // t1@7 int pid; // [xsp+34h] [xbp-44Ch]@1 char codes_str[1024]; // [xsp+38h] [xbp-448h]@7

root@kitploit:~
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
    _os_assumes_log(kr);
    _os_avoid_tail_call();
}
if ( current_audit_token.val[5] )                   // (b) If the message was sent by
{                                                   //     a process with a nonzero PID
    result = KERN_FAILURE;                          //     (any non-kernel process),
}                                                   //     the message is rejected.
else
{
    if ( codeCnt )
    {
        codes_left = codeCnt;
        do
        {
            code_value = *code;
            ++code;
            __snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
            --codes_left;
        }
        while ( codes_left );
    }
    launchd_log_2(
        0LL,
        3LL,
        "Host-level exception raised: pid = %d, thread = 0x%x, "
            "exception type = 0x%x, codes = { %s }",
        pid,
        thread,
        exception,
        codes_str);
    kr = deallocate_port(thread);                   // (c) The "thread" port sent in
    if ( kr )                                       //     the message is deallocated.
    {
        _os_assumes_log(kr);
        _os_avoid_tail_call();
    }
    kr = deallocate_port(task);                     // (d) The "task" port sent in the
    if ( kr )                                       //     message is deallocated.
    {
        _os_assumes_log(kr);
        _os_avoid_tail_call();
    }
    if ( exception == EXC_CRASH )                   // (e) If the exception type is
        result = KERN_FAILURE;                      //     EXC_CRASH, then KERN_FAILURE
    else                                            //     is returned. MIG will
        result = 0;                                 //     deallocate the ports again.
}
*__stack_chk_guard_ptr;
return result;

}

root@kitploit:~
Isto é o que o código faz:

1. Esta função é a rotina de serviço Mach para mensagens de exceção `mach_exception_raise`: ela é
   invocada diretamente pelo sistema Mach quando o launchd processa uma mensagem de exceção Mach
   `mach_exception_raise`. Os argumentos para a rotina de serviço são analisados da mensagem Mach e,
   portanto, são controlados pelo remetente da mensagem.
2. Em (b), o launchd verifica se a mensagem de exceção Mach foi enviada pelo kernel. O token de
   auditoria do remetente contém o PID do processo de envio no campo 5, que será zero apenas para o
   kernel. Se a mensagem não foi enviada pelo kernel, ela é rejeitada.
3. As portas de thread e tarefa da mensagem são desalocadas explicitamente em (c) e (d).
4. Em (e), o launchd verifica se o tipo de exceção é `EXC_CRASH` e retorna `KERN_FAILURE` se
   for. A intenção é garantir que não sejam tratadas mensagens `EXC_CRASH`, presumivelmente para que
   o ReportCrash seja invocado como o manipulador de corpos. No entanto, retornar `KERN_FAILURE` neste ponto
   fará com que as portas de tarefa e thread sejam desalocadas novamente quando a mensagem de exceção for
   limpa posteriormente. Isso significa que essas duas portas serão desalocadas em excesso.

Para que esta vulnerabilidade seja útil, queremos liberar o direito de envio do launchd para um serviço Mach
que ele oferece, para que possamos então personificar esse serviço para o resto do sistema. Isso
significa que precisaremos que as portas de tarefa e thread na mensagem de exceção sejam, na verdade, direitos de envio
para a porta do serviço Mach que queremos liberar no launchd. Então, depois de enviarmos ao launchd a mensagem de exceção maliciosa e liberarmos a porta do serviço, tentaremos fazer com que o mesmo nome de porta seja reutilizado, mas desta vez para uma porta Mach para a qual detemos o direito de recepção. Dessa forma, quando um cliente pedir ao launchd
que lhe dê um direito de envio para a porta Mach do serviço, o launchd lhe dará, em vez disso, um direito de envio para a nossa porta, permitindo-nos personificar esse serviço para o cliente. Depois disso, existem muitas rotas diferentes para obter privilégios de sistema.


### Acionando a vulnerabilidade

Para realmente acionar a vulnerabilidade, precisaremos contornar a verificação de que a mensagem foi
enviada pelo kernel. Isso ocorre porque, se enviarmos a mensagem de exceção diretamente para o launchd, ela
será simplesmente descartada. De alguma forma, precisamos fazer com que o kernel envie uma mensagem de exceção "maliciosa" contendo um direito de envio Mach para um serviço de sistema em vez das portas reais de thread e tarefa.

Acontece que existe uma armadilha Mach, `task_set_special_port`, que pode ser usada para definir um direito de envio personalizado a ser usado no lugar da porta de tarefa real em certas situações. Uma dessas situações é quando o kernel gera uma mensagem de exceção em nome de uma tarefa: em vez de colocar o direito de envio de tarefa real na mensagem de exceção, o kernel usará o direito de envio fornecido por `task_set_special_port`. Mais especificamente, se uma tarefa chamar `task_set_special_port` para definir um valor personalizado para sua porta especial `TASK_KERNEL_PORT` e então a tarefa falhar, a mensagem de exceção gerada pelo kernel terá um direito de envio para a porta personalizada, não a porta de tarefa real, no campo "task". Uma API equivalente, `thread_set_special_port`, pode ser usada para definir uma porta personalizada no campo "thread" da mensagem de exceção gerada.

Devido a esse comportamento, na verdade não é nada difícil fazer com que o kernel gere uma mensagem de exceção "maliciosa" contendo uma porta de serviço Mach no lugar da porta de tarefa e thread. No entanto, ainda precisamos garantir que a mensagem de exceção que geramos seja entregue ao launchd.

Mais uma vez, garantir que o kernel entregue a mensagem de exceção "maliciosa" ao launchd não é difícil se você conhecer a API correta. A função `thread_set_exception_ports` definirá qualquer direito de envio Mach como a porta para a qual as mensagens de exceção nesta thread são entregues. Assim, tudo o que precisamos fazer é invocar `thread_set_exception_ports` com a porta bootstrap, e então qualquer exceção que gerarmos fará com que o kernel envie uma mensagem de exceção para o launchd.

A última peça do quebra-cabeça é obter o tipo de exceção correto. A vulnerabilidade só será acionada para exceções `EXC_CRASH`. Um pouco de tentativa e erro revela que podemos facilmente gerar exceções `EXC_CRASH` chamando a função padrão `abort`.

Assim, em resumo, podemos usar APIs existentes e bem documentadas para fazer com que o kernel gere uma mensagem de exceção `EXC_CRASH` maliciosa em nosso nome e a entregue ao launchd, acionando a vulnerabilidade e liberando a porta do serviço Mach:

1. Use `thread_set_exception_ports` para definir o launchd como o manipulador de exceções para esta thread.
2. Chame `bootstrap_look_up` para obter a porta do serviço que queremos personificar do launchd.
3. Chame `task_set_special_port`/`thread_set_special_port` para usar essa porta de serviço em vez das portas reais de tarefa e thread em mensagens de exceção.
4. Chame `abort`. O kernel enviará uma mensagem de exceção `EXC_CRASH` para o launchd, mas as portas de tarefa e thread na mensagem serão a porta de serviço alvo.
5. O launchd processará a mensagem de exceção e liberará a porta de serviço.


### Executando código após a falha

Há um problema com a estratégia acima: chamar `abort` matará nosso processo. Se quisermos ser capazes de executar qualquer código após acionar a vulnerabilidade, precisamos de uma maneira de realizar a falha em outro processo.

(Com outros tipos de exceção, um processo poderia realmente se recuperar da exceção. A maneira como um processo se recuperaria é definir seu manipulador de exceção de thread como o launchd e seu manipulador de exceção de tarefa como ele mesmo. Após o launchd processar e falhar ao manipular a exceção, o kernel enviaria a exceção para o manipulador de tarefa, que redefiniria o estado da thread e informaria ao kernel que a exceção foi tratada. No entanto, um processo não pode capturar suas próprias exceções `EXC_CRASH`, então precisamos de dois processos.)

Uma estratégia é primeiro explorar uma vulnerabilidade em outro processo no iOS e forçar esse processo a definir suas portas de kernel e falhar. No entanto, para uma prova de conceito, é mais fácil criar uma extensão de aplicativo.

Extensões de aplicativo, introduzidas no iOS 8, fornecem uma maneira de empacotar alguma funcionalidade de um aplicativo para que ela esteja disponível fora do aplicativo. O código de uma extensão de aplicativo é executado em um processo separado e em sandbox. Isso torna muito fácil iniciar um processo que definirá suas portas especiais, registrará o launchd como seu manipulador de exceção para `EXC_CRASH` e então chamará `abort`.

Não há uma maneira suportada para um aplicativo iniciar programaticamente sua própria extensão de aplicativo e se comunicar com ela. No entanto, Ian McDowell escreveu um [ótimo artigo][Multi-Process iOS App Using NSExtension] descrevendo como usar a API privada `NSExtension` para iniciar e se comunicar com um processo de extensão de aplicativo. Usei uma estratégia quase idêntica aqui. A única diferença é que precisamos comunicar uma porta Mach ao processo de extensão de aplicativo, o que envolve registrar um serviço dummy no launchd ao qual a extensão de aplicativo se conecta.

[Multi-Process iOS App Using NSExtension]: https://ianmcdowell.net/blog/nsextension/


### Prevenindo a reutilização de portas no launchd

Um desafio que você notaria se executasse o exploit conforme descrito é que ocasionalmente você não conseguiria readquirir a porta liberada. A razão para isso é que o kernel rastreia as entradas IPC livres de um processo em uma lista livre, e assim um nome de porta recém-liberado será reutilizado (com um número de geração diferente) quando uma nova porta for alocada na tabela IPC. Portanto, só realocaremos o nome da porta que queremos se o launchd não reutilizar aquele slot de entrada IPC para outra porta primeiro.

A maneira de contornar isso é enterrar o slot de entrada IPC livre no fundo da lista livre, para que, se o launchd alocar novas portas, esses outros slots sejam usados primeiro. Como fazemos isso? Podemos registrar um monte de serviços Mach dummy no launchd com portas para as quais detemos o direito de recepção. Quando chamamos `abort`, o manipulador de exceção será acionado primeiro, e então o estado do processo, incluindo as portas Mach, será limpo. Quando o launchd receber a exceção `EXC_CRASH`, ele liberará inadvertidamente a porta de serviço alvo, colocando o slot de entrada IPC correspondente a esse nome de porta no topo da lista livre. Em seguida, quando o restante das portas Mach da nossa extensão de aplicativo for destruído, o launchd receberá notificações e liberará as portas de serviço dummy, enterrando o slot de entrada IPC alvo atrás dos slots das portas recém-liberadas. Assim, desde que o launchd aloque menos portas do que o número de serviços dummy que registramos, o slot alvo ainda estará na lista livre, o que significa que ainda podemos fazer com que o launchd realoque o slot com o mesmo nome de porta do serviço original.

A limitação dessa estratégia é que precisamos da entidade `com.apple.security.application-groups` para registrar serviços no launchd. Existem outras maneiras de armazenar portas Mach no launchd, mas usar grupos de aplicativos é certamente a mais fácil e é suficiente para esta prova de conceito.


### Personificando o serviço liberado

Depois de gerar a extensão de aplicativo crashante e liberar um direito de envio Mach no launchd, precisamos realocar aquele nome de porta Mach com um direito de envio para o qual detemos o direito de recepção. Dessa forma, qualquer mensagem que o launchd enviar para aquele nome de porta será recebida por nós, e sempre que o launchd compartilhar aquele nome de porta com um cliente, o cliente receberá um direito de envio para a nossa porta. Em particular, se conseguirmos liberar o direito de envio do launchd para um serviço Mach, então qualquer processo que solicitar aquele serviço do launchd receberá um direito de envio para a nossa própria porta em vez da porta de serviço real. Isso nos permite personificar o serviço ou realizar um ataque man-in-the-middle, inspecionando todas as mensagens que o cliente envia para o serviço.

Fazer com que o nome da porta liberada seja reutilizado para que se refira a uma porta que possuímos também é bastante simples, dado que já decidimos usar a entidade de grupos de aplicativos: basta registrar serviços Mach dummy no launchd até que um deles reutilize o nome da porta original. Precisaremos fazer isso em lotes, registrando um grande número de serviços dummy juntos, verificando se algum reutilizou com sucesso o nome da porta liberada e, em seguida, cancelando o registro deles. A razão é que precisamos ter certeza de que nossos registros percorrem todo o caminho de volta na lista livre de portas IPC para recuperar o nome da porta enterrado que queremos.

Podemos verificar se conseguimos reutilizar com sucesso o nome da porta liberada procurando o serviço original com `bootstrap_look_up`: se ele retornar uma de nossas portas de serviço registradas, estamos prontos.

Uma vez que conseguimos registrar um novo serviço que obtém o mesmo nome de porta que o original, qualquer cliente que procurar o serviço original no launchd receberá um direito de envio para a nossa porta, não para a porta de serviço real. Assim, estamos efetivamente personificando o serviço original para o resto do sistema (ou pelo menos, para aqueles processos que procuram o serviço após nosso ataque).


Estágio 1: Obtendo a porta host-priv
---------------------------------------------------------------------------------------------------

Uma vez que temos a capacidade de personificar serviços arbitrários do sistema, o próximo passo é obter a porta host-priv. Esta etapa é direta e não é afetada pelas mudanças no iOS 11.3. A ideia de alto nível deste ataque é personificar o SafetyNet, falhar o ReportCrash e, em seguida, recuperar a porta host-priv da porta de tarefa do ReportCrash moribundo enviada na mensagem de exceção.


### Sobre ReportCrash e SafetyNet

O ReportCrash é responsável por gerar relatórios de falha no iOS. Este único binário na verdade oferece 4 serviços diferentes (cada um em um processo diferente, embora nem todos possam estar em execução em um determinado momento):

1. `com.apple.ReportCrash` é responsável por gerar relatórios de falha para processos que falham. É o manipulador de exceção em nível de host para exceções `EXC_CRASH`, `EXC_GUARD` e `EXC_RESOURCE`.
2. `com.apple.ReportCrash.Jetsam` lida com relatórios Jetsam.
3. `com.apple.ReportCrash.SimulateCrash` cria relatórios para falhas simuladas.
4. `com.apple.ReportCrash.SafetyNet` é o manipulador de exceção registrado para o serviço `com.apple.ReportCrash`.

Os que nos interessam são `com.apple.ReportCrash` e `com.apple.ReportCrash.SafetyNet`, doravante referidos simplesmente como ReportCrash e SafetyNet. Ambos são serviços baseados em MIG e executam efetivamente o mesmo código.

Quando o ReportCrash é iniciado, ele procura o serviço SafetyNet no launchd e define a porta retornada como o manipulador de exceção em nível de tarefa. A intenção parece ser que, se o próprio ReportCrash falhasse, um processo separado geraria o relatório de falha para ele. No entanto, este caminho de código parece defunto: o ReportCrash registra o SafetyNet para mensagens `mach_exception_raise`, embora tanto o ReportCrash quanto o SafetyNet manipulem apenas mensagens `mach_exception_raise_state_identity`. Apesar disso, ambos os serviços ainda estão presentes e acessíveis de dentro da sandbox de contêiner do iOS.


### Primitivas de manipulação do ReportCrash

Para realizar o ataque a seguir, precisamos ser capazes de manipular o ReportCrash (ou SafetyNet) para se comportar da maneira que queremos. Especificamente, precisamos das seguintes capacidades: iniciar o ReportCrash sob demanda, forçar o ReportCrash a sair, falhar o ReportCrash e garantir que o ReportCrash não saia enquanto o estamos usando. Aqui descreverei como alcançamos cada objetivo.

Para iniciar o ReportCrash, simplesmente precisamos enviar a ele uma mensagem Mach: o launchd o iniciará sob demanda. No entanto, devido ao seu design peculiar, qualquer tipo de mensagem, exceto `mach_exception_raise_state_identity`, fará com que o ReportCrash pare de responder a novas mensagens e eventualmente saia. Portanto, precisamos enviar uma mensagem `mach_exception_raise_state_identity` se quisermos que ele permaneça vivo depois.

Para fazer o ReportCrash sair, podemos simplesmente enviar a ele qualquer outro tipo de mensagem Mach.

Existem muitas maneiras de falhar o ReportCrash. A mais fácil é provavelmente enviar uma mensagem `mach_exception_raise_state_identity` com a porta de thread definida como `MACH_PORT_NULL`.

Finalmente, precisamos garantir que o ReportCrash não saia enquanto o estamos usando. Cada mensagem `mach_exception_raise_state_identity` que ele processa faz com que ele inicie outra thread para ouvir a próxima mensagem enquanto a thread original gera o relatório de falha. O ReportCrash sairá depois que todas as threads pendentes que estão gerando um relatório de falha terminarem. Assim, se conseguirmos parar uma dessas threads enquanto ela está no processo de gerar um relatório de falha, podemos impedi-la de sair.

A maneira mais fácil que encontrei para fazer isso foi enviar uma mensagem `mach_exception_raise_state_identity` com uma porta personalizada nos campos de tarefa e thread. Quando o ReportCrash tentar gerar um relatório de falha, ele chamará `task_policy_get` na porta "task", o que fará com que ele envie uma mensagem Mach para a porta que enviamos e aguarde uma resposta. Mas como a porta "task" é apenas uma porta Mach comum, podemos simplesmente não responder à mensagem Mach, e o ReportCrash aguardará indefinidamente pelo retorno de `task_policy_get`.


### Extraindo host-priv do ReportCrash

Para o primeiro estágio do exploit, o plano de ataque é relativamente direto:

1. Inicie o serviço SafetyNet e force-o a permanecer vivo durante a duração do nosso ataque.
2. Use a primitiva de personificação de serviço do launchd para personificar o SafetyNet. Isso nos dá uma nova porta na qual podemos receber mensagens destinadas ao serviço SafetyNet real.
3. Faça qualquer instância existente do ReportCrash sair. Dessa forma, podemos garantir que o ReportCrash procure nossa porta SafetyNet na próxima etapa.
4. Inicie o ReportCrash. O ReportCrash procurará o SafetyNet no launchd e definirá a porta resultante, que é a porta falsa do SafetyNet para a qual possuímos o direito de recepção, como o destino para mensagens `EXC_CRASH`.
5. Acione uma falha no ReportCrash. Depois de ver que não há manipuladores registrados para o tipo de exceção original, o ReportCrash entrará na fase de morte do processo. Neste ponto, o XNU verá que o ReportCrash registrou a porta falsa do SafetyNet para receber exceções `EXC_CRASH`, então gerará uma mensagem de exceção e a enviará para essa porta.
6. Em seguida, ouvimos a porta falsa do SafetyNet para a mensagem `EXC_CRASH`. Ela será do tipo `mach_exception_raise`, o que significa que conterá a porta de tarefa do ReportCrash.
7. Finalmente, usamos `task_get_special_port` na porta de tarefa do ReportCrash para obter a porta host do ReportCrash. Como o ReportCrash não está em sandbox e é executado como root, esta é a porta host-priv.

No final deste estágio da fuga da sandbox, acabamos com uma porta host-priv utilizável. Isso por si só demonstra que esta é uma questão de segurança grave.


Estágio 2: Escapando da sandbox
---------------------------------------------------------------------------------------------------

Embora tenhamos a porta host-priv, nosso objetivo é escapar completamente da sandbox e executar código como root com a entidade `task_for_pid-allow`. O primeiro passo para alcançar isso é simplesmente escapar da sandbox.

Tecnicamente falando, não há razão para precisarmos obter a porta host-priv antes de escapar da sandbox: essas duas etapas são independentes e podem ocorrer em qualquer ordem. No entanto, este estágio deixará o sistema instável se ele ou os estágios subsequentes falharem, então vale a pena colocá-lo mais tarde.

O ataque de alto nível é usar a mesma vulnerabilidade do launchd novamente para personificar um serviço de sistema. No entanto, desta vez nosso objetivo é personificar um serviço para o qual um cliente enviará sua porta de tarefa em uma mensagem Mach. É fácil encontrar por experimentação no iOS 11.2.6 que, se personificarmos `com.apple.CARenderServer` (doravante CARenderServer) hospedado pelo backboardd e nos comunicarmos com `com.apple.DragUI.druid.source`, o daemon druid sem sandbox enviará sua porta de tarefa em uma mensagem Mach para a porta de serviço falsa.

Esta etapa do exploit está quebrada no iOS 11.3 porque o druid não envia mais sua porta de tarefa na mensagem Mach para o CARenderServer. Apesar disso, estou confiante de que esta vulnerabilidade ainda pode ser usada para escapar da sandbox. Uma maneira de fazer isso é procurar serviços sem sandbox que confiam na entrada de outros serviços. Esses tipos de "vulnerabilidades" nunca seriam exploráveis sem a capacidade de substituir serviços do sistema, o que significa que provavelmente são uma superfície de ataque de baixa prioridade, tanto interna quanto externamente à Apple.


### Falhando o druid

Assim como com o ReportCrash, precisamos ser capazes de forçar o druid a reiniciar caso ele já esteja em execução para que ele procure nossa porta falsa do CARenderServer no launchd. Decidi usar um bug no libxpc que já estava programado para ser corrigido para esse fim.

Ao examinar o libxpc, encontrei uma leitura fora dos limites que poderia ser usada para forçar qualquer serviço XPC a falhar:```C
void _xpc_dictionary_apply_wire_f
(
        OS_xpc_dictionary *xdict,
        OS_xpc_serializer *xserializer,
        const void *context,
        bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *)
)
{
...
    uint64_t count = (unsigned int)*serialized_dict_count;
    if ( count )
    {
        uint64_t depth = xserializer->depth;
        uint64_t index = 0;
        do
        {
            const char *key = _xpc_serializer_read(xserializer, 0, 0, 0);
            size_t keylen = strlen(key);
            _xpc_serializer_advance(xserializer, keylen + 1);
            if ( !applier_fn(key, xserializer, context) )
                break;
            xserializer->depth = depth;
            ++index;
        }
        while ( index < count );
    }
...
}

O problema é que o uso de um strlen não verificado em dados controlados pelo atacante permite que a chave da entrada do dicionário serializado se estenda além do final do buffer de dados. Isso significa que o serviço XPC que desserializa o dicionário irá travar, seja quando strlen desreferenciar memória fora dos limites ou quando _xpc_serializer_advance tentar avançar o serializador além do final dos dados fornecidos.

Esse bug já havia sido corrigido no iOS 11.3 Beta quando o descobri, então não o reportei à Apple. O exploit está disponível como um projeto independente no meu repositório xpc-crash.

Para usar esse bug para travar o druid, simplesmente precisamos enviar ao serviço druid uma mensagem XPC malformada de modo que a chave do dicionário seja não terminada e se estenda até o último byte da mensagem.

Obtendo o task port do druid

Obter o task port do druid no iOS 11.2.6 usando nossa primitiva de personificação de serviço é fácil:

  1. Use a capacidade de personificação de serviço Mach para personificar o CARenderServer.
  2. Envie uma mensagem para o serviço druid para que ele inicie.
  3. Se não obtivermos o task port do druid após alguns segundos, mate o druid usando o bug XPC e reinicie-o.
  4. O druid nos enviará seu task port na porta falsa do CARenderServer.

Contornando as restrições de task port de binários da plataforma

Uma vez que temos o task port do druid, ainda precisamos descobrir como executar código dentro do processo druid.

O problema é que o XNU protege os task ports de binários da plataforma contra modificações por binários não pertencentes à plataforma. A defesa é implementada na função task_conversion_eval, que é chamada por convert_port_to_locked_task e convert_port_to_task_with_exec_token:```C kern_return_t task_conversion_eval(task_t caller, task_t victim) { /* * Tasks are allowed to resolve their own task ports, and the kernel is * allowed to resolve anyone's task port. */ if (caller == kernel_task) { return KERN_SUCCESS; }

root@kitploit:~
if (caller == victim) {
	return KERN_SUCCESS;
}

/*
 * Only the kernel can can resolve the kernel's task port. We've established
 * by this point that the caller is not kernel_task.
 */
if (victim == kernel_task) {
	return KERN_INVALID_SECURITY;
}

#if CONFIG_EMBEDDED /* * On embedded platforms, only a platform binary can resolve the task port * of another platform binary. / if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) { #if SECURE_KERNEL return KERN_INVALID_SECURITY; #else if (cs_relax_platform_task_ports) { return KERN_SUCCESS; } else { return KERN_INVALID_SECURITY; } #endif / SECURE_KERNEL / } #endif / CONFIG_EMBEDDED */

root@kitploit:~
return KERN_SUCCESS;

}

root@kitploit:~
As rotinas de conversão MIG que dependem dessas funções, incluindo `convert_port_to_task` e
`convert_port_to_map`, portanto, falharão quando as chamarmos na tarefa do druid. Por exemplo,
`mach_vm_write` não nos permitirá manipular a memória do druid.

No entanto, ao olhar para o arquivo MIG `osfmk/mach/task.defs` no XNU, notei algo
interessante:```C
/*
 *	Returns the set of threads belonging to the target task.
 */
routine task_threads(
		target_task	: task_inspect_t;
	out	act_list	: thread_act_array_t);

A função task_threads, que enumera as threads em uma tarefa, na verdade recebe um task_inspect_t em vez de um task_t, o que significa que o MIG a converte usando convert_port_to_task_inspect em vez de convert_port_to_task. Uma rápida olhada em convert_port_to_task_inspect revela que esta função não realiza a verificação task_conversion_eval, o que significa que podemos chamá-la com sucesso em binários de plataforma. Isso é interessante porque as threads retornadas não são direitos thread_inspect_t, mas sim direitos completos thread_act_t. Em outras palavras, task_threads promove um direito de tarefa não modificável em direitos de thread modificáveis. E como não existe um thread_conversion_eval equivalente, isso significa que podemos usar as APIs de thread do Mach para modificar as threads em uma tarefa, mesmo que essa tarefa seja um binário de plataforma.

Para aproveitar isso, escrevi uma biblioteca chamada threadexec que constrói uma capacidade completa de chamada de função sobre a API de threads do Mach. O projeto threadexec por si só foi um empreendimento significativo, mas como é apenas indiretamente relevante para este exploit, vou abrir mão de uma explicação detalhada do seu funcionamento interno.

Estágio 3: Instalando um novo manipulador de exceções em nível de host

Depois de obtermos o porto host-priv e a execução de código sem sandbox dentro do druid, o próximo estágio da fuga completa da sandbox é instalar um novo manipulador de exceções em nível de host. Este processo é direto dadas as nossas capacidades atuais:

  1. Obter o manipulador de exceções em nível de host atual para EXC_BAD_ACCESS chamando host_get_exception_ports.
  2. Alocar um porto Mach que será o novo manipulador de exceções em nível de host para EXC_BAD_ACCESS.
  3. Enviar o porto host-priv e um direito de envio para o porto Mach que acabamos de alocar para o druid.
  4. Usando nosso contexto de execução no druid, fazer o druid chamar host_set_exception_ports para registrar nosso porto Mach como o manipulador de exceções em nível de host para EXC_BAD_ACCESS.

Após este estágio, sempre que um processo acessar um endereço de memória inválido (e também não tiver um manipulador de exceções registrado), uma mensagem de exceção EXC_BAD_ACCESS será enviada para o nosso novo porto manipulador de exceções. Isso nos dará o porto de tarefa de qualquer processo que esteja falhando, e como EXC_BAD_ACCESS é uma exceção recuperável, desta vez podemos usar o porto de tarefa para executar código.

Estágio 4: Obtendo o porto de tarefa do ReportCrash

O próximo estágio é disparar uma exceção EXC_BAD_ACCESS no ReportCrash para que seu porto de tarefa seja enviado em uma mensagem de exceção para o nosso novo porto manipulador de exceções:

  1. Quebrar o ReportCrash usando a técnica descrita anteriormente. Isso fará com que o ReportCrash gere uma exceção EXC_BAD_ACCESS. Como o ReportCrash não tem um manipulador de exceções registrado para EXC_BAD_ACCESS (lembre-se que o SafetyNet está registrado para EXC_CRASH), a exceção será entregue ao manipulador de exceções em nível de host.
  2. Ouvir mensagens de exceção no nosso porto manipulador de exceções do host.
  3. Quando recebermos a mensagem de exceção para o ReportCrash, salvar os portos de tarefa e thread. Suspender a thread que está falhando e retornar KERN_SUCCESS para indicar ao kernel que a exceção foi tratada e o ReportCrash pode ser retomado.
  4. Usar os portos de tarefa e thread para estabelecer um contexto de execução dentro do ReportCrash, assim como fizemos com o druid.

Neste ponto, temos execução de código dentro de um processo sem sandbox, root, com task_for_pid-allow.

Estágio 5: Restaurando o manipulador de exceções em nível de host original

Os próximos dois estágios não são estritamente necessários, mas devem ser realizados de qualquer forma.

Depois que tivermos execução de código dentro do ReportCrash, devemos redefinir o manipulador de exceções em nível de host para EXC_BAD_ACCESS usando o druid:

  1. Enviar o porto manipulador de exceções em nível de host antigo para o druid.
  2. Chamar host_set_exception_ports no druid para re-registrar o manipulador de exceções em nível de host antigo para EXC_BAD_ACCESS.

Isso impedirá que nosso porto manipulador de exceções receba mensagens de exceção de outros processos que estejam falhando.

Estágio 6: Corrigindo o launchd

A última etapa é restaurar os danos que causamos ao launchd quando liberamos portos de serviço em seu namespace IPC para nos passar por eles:

  1. Chamar task_for_pid no ReportCrash para obter o porto de tarefa do launchd.
  2. Para cada serviço que nos passamos:
    1. Obter o nome do launchd para o direito de envio ao porto de serviço falso. Este é o nome original do porto de serviço real.
    2. Destruir o porto de serviço falso, desregistrando o serviço falso do launchd.
    3. Chamar mach_port_insert_right no ReportCrash para inserir o porto de serviço real no espaço IPC do launchd sob o nome original.

Após esta etapa, o sistema deve estar novamente totalmente funcional. Após uma exploração bem-sucedida, não deve haver necessidade de reinicialização forçada do dispositivo, pois o exploit repara todos os danos por si só.

Pós-exploração

O Blanket também inclui uma carga útil de pós-exploração que contorna o amfid e gera um shell bind. Esta seção descreverá como isso é alcançado.

Gerando um processo de carga útil

Mesmo depois de obter execução de código no ReportCrash, usar essa capacidade não é fácil: estamos limitados a realizar chamadas de função individuais de dentro do processo, o que torna doloroso executar tarefas complexas. Idealmente, gostaríamos de uma maneira de executar código nativamente com os privilégios do ReportCrash, seja injetando código no ReportCrash ou gerando um novo processo com os mesmos (ou maiores) privilégios.

O Blanket escolhe a rota de geração de processos. Usamos task_for_pid e nosso status de binário de plataforma no ReportCrash para obter o porto de tarefa do launchd e criar uma nova thread dentro do launchd que possamos controlar. Em seguida, usamos essa thread para chamar posix_spawn para iniciar nosso binário de carga útil. O binário de carga útil pode ser assinado com entitlements restritos, incluindo task_for_pid-allow, para conceder capacidades adicionais.

Contornando o amfid

Para que o iOS aceite nosso binário recém-gerado, precisamos contornar a verificação de assinatura de código. Várias estratégias foram discutidas ao longo dos anos, mas a estratégia atual mais comum é registrar um manipulador de exceções para o amfid e, em seguida, realizar um patch de dados para que o amfid falhe ao tentar chamar MISValidateSignatureAndCopyInfo. Isso nos permite falsificar a implementação dessa função para fingir que a assinatura de código é válida.

No entanto, existe outra abordagem que acredito ser mais robusta e flexível: em vez de modificar o amfid, podemos simplesmente registrar um novo porto amfid no kernel.

O kernel mantém controle de qual porto enviar mensagens para o amfid usando um porto especial do host chamado HOST_AMFID_PORT. Se tivermos execução de código root sem sandbox, podemos definir este porto para um novo valor. A Apple protegeu contra este ataque verificando se a resposta a uma solicitação de validação realmente veio do amfid: o cdhash do remetente é comparado ao cdhash do amfid. No entanto, isso não impede que a mensagem seja enviada para um processo diferente do amfid; apenas impede que a resposta venha de um processo que não seja o amfid. Se montarmos um triângulo onde o kernel envia mensagens para nós, geramos a resposta e a passamos para o amfid, e então o amfid envia a resposta para o kernel, conseguiremos contornar a verificação do remetente.

Há inúmeras vantagens nesta abordagem, das quais a maior é provavelmente o acesso a flags adicionais na rotina de serviço verify_code_directory. Embora o amfid não use todas elas, existem muitas outras flags de saída que o amfid poderia definir para controlar o comportamento da verificação de assinatura de código. Aqui está um protótipo parcial de verify_code_directory:```C kern_return_t verify_code_directory( mach_port_t amfid_port, amfid_path_t path, uint64_t file_offset, int32_t a4, int32_t a5, int32_t a6, int32_t * entitlements_valid, int32_t * signature_valid, int32_t * unrestrict, int32_t * signer_type, int32_t * is_apple, int32_t * is_developer_code, amfid_a13_t a13, amfid_cdhash_t cdhash, audit_token_t audit);

root@kitploit:~
De particular interesse para desenvolvedores de jailbreak é o parâmetro `is_apple`. Este parâmetro não parece ser usado pelo amfid, mas, se definido, fará com que o kernel defina a flag de codesigning `CS_PLATFORM_BINARY`, que concede privilégios de binário de plataforma ao aplicativo. Em particular, isso significa que o aplicativo agora pode usar portas de tarefa para modificar binários de plataforma diretamente.


Brechas usadas neste ataque
---------------------------------------------------------------------------------------------------

Este ataque aproveita várias brechas que não são vulnerabilidades de segurança por si só, mas minimizam a eficácia de várias mitigações de exploração. Nem todas precisam ser fechadas juntas, já que algumas são parcialmente redundantes, mas vale a pena listá-las de qualquer forma.

No kernel:

1. `task_threads` pode promover um `task_inspect_t` apenas de inspeção para um `thread_act_t` com capacidade de modificação.
2. Não existe `thread_conversion_eval` para desempenhar o papel de `task_conversion_eval` para threads.
3. Um binário não-plataforma pode usar um direito `task_inspect_t` para um binário de plataforma.
4. Mensagens de exceção para processos sem sandbox podem ser entregues a processos com sandbox, mesmo que isso forneça uma maneira de escapar da sandbox. Não está claro se existe uma correção limpa para esta brecha.
5. Execução de código sem sandbox, a porta host-priv e a capacidade de travar um processo `task_for_pid-allow` podem ser combinadas para construir uma solução alternativa para `task_for_pid`. (A solução alternativa é: chamar `host_set_exception_ports` para definir um novo manipulador de exceções a nível de host, então travar o processo `task_for_pid-allow` para receber sua porta de tarefa e executar código com a permissão.)

Em extensões de aplicativo:

1. Extensões de aplicativo que compartilham um grupo de aplicativos podem se comunicar usando mensagens Mach, apesar da documentação sugerir que a comunicação entre o aplicativo host e a extensão do aplicativo deveria ser impossível.


Correções e mitigações recomendadas
---------------------------------------------------------------------------------------------------

Recomendo as seguintes correções, aproximadamente em ordem de importância:

1. Apenas desalocar portas Mach nas rotinas de serviço do launchd ao retornar `KERN_SUCCESS`. Isso corrigirá a vulnerabilidade de substituição de porta Mach.
2. Fechar a brecha `task_threads` que permite que um binário não-plataforma use a porta de tarefa de um binário de plataforma para obter execução de código.
3. Corrigir problemas de travamento no ReportCrash.
4. O conjunto de serviços Mach acessíveis de dentro da sandbox do contêiner deve ser minimizado. Não vejo uma razão legítima para a maioria dos aplicativos iOS se comunicarem com o ReportCrash ou SafetyNet.
5. O maior número possível de processos deve ter sandbox. Não tenho certeza se o druid precisa estar sem sandbox para funcionar corretamente, mas se não, ele deve ser colocado em uma sandbox apropriada.
6. Código morto deve ser eliminado. O SafetyNet não parece estar desempenhando sua funcionalidade pretendida. Se não for mais necessário, provavelmente deveria ser removido.
7. Fechar a solução alternativa baseada em `host_set_exception_ports` para `task_for_pid`. Por exemplo, considere se vale a pena restringir `host_set_exception_ports` ao root ou restringir a usabilidade da porta host-priv em algumas configurações. Isso viola o design elegante baseado em capacidades do Mach, mas `host_set_exception_ports` pode ser um alvo promissor para abuso.
8. Considere se vale a pena adicionar `task_conversion_eval` a `task_inspect_t`.


Executando o Blanket
---------------------------------------------------------------------------------------------------

O Blanket deve funcionar em qualquer dispositivo com iOS 11.2.6.

1. Baixe o projeto:   ```
   git clone https://github.com/bazad/blanket
   cd blanket
  1. Baixe e compile a biblioteca threadexec, que é necessária para o blanket injetar código em processos e tarefas: ``` git clone https://github.com/bazad/threadexec cd threadexec make ARCH=arm64 SDK=iphoneos EXTRA_CFLAGS='-mios-version-min=11.1 -fembed-bitcode' cd ..
    root@kitploit:~
  2. Baixe o iOS binpack de Jonathan Levin, que contém os binários que serão usados pelo bind shell. Se você alterar o payload para fazer outra coisa, não precisará do binpack. ``` mkdir binpack curl http://newosxbook.com/tools/binpack64-256.tar.gz | tar -xf- -C binpack
    root@kitploit:~
  3. Abra o Xcode e configure o projeto. Você precisará alterar o identificador de assinatura e especificar uma permissão personalizada de grupo de aplicativos.
  4. Edite o arquivo headers/config.h e altere APP_GROUP para o identificador de grupo de aplicativos que você especificou anteriormente.

Depois disso, você deve conseguir compilar e executar o projeto no dispositivo.

Se o blanket for bem-sucedido, ele executará o binário do payload (código-fonte em blanket_payload/blanket_payload.c), que por padrão cria um bind shell na porta 4242. Você pode se conectar a essa porta com netcat e executar comandos de shell arbitrários.

Créditos

Muitos agradecimentos a Ian Beer e Jonathan Levin por suas excelentes pesquisas sobre segurança e internals do iOS.

Cronologia

Descobri esta vulnerabilidade em janeiro de 2018 e comecei a desenvolver o exploit no final de fevereiro. Reportei este problema à Apple em 13 de abril. A Apple atribuiu a vulnerabilidade de substituição de porta Mach no launchd como CVE-2018-4280, e foi corrigida no iOS 11.4.1 e macOS 10.13.6 em 9 de julho.

Licença

Blanket é distribuído sob a licença MIT.


Brandon Azad

Baixar ferramenta