hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-ipc-inter-process-communication/macos-thread-injection-via-task-port.md

13 KiB

Injeção de Thread no macOS via Porta de Tarefa

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Este post foi copiado de https://bazad.github.io/2018/10/bypassing-platform-binary-task-threads/ (que contém mais informações)

Código

1. Sequestro de Thread

A primeira coisa que fazemos é chamar task_threads() na porta da tarefa para obter uma lista de threads na tarefa remota e, em seguida, escolher uma delas para sequestrar. Ao contrário dos frameworks tradicionais de injeção de código, não podemos criar uma nova thread remota porque thread_create_running() será bloqueado pela nova mitigação.

Em seguida, podemos chamar thread_suspend() para interromper a execução da thread.

Neste ponto, o único controle útil que temos sobre a thread remota é pará-la, iniciá-la, obter seus valores de registro e definir seus valores de registro. Assim, podemos iniciar uma chamada de função remota definindo os registros x0 a x7 na thread remota para os argumentos, definindo pc para a função que queremos executar e iniciando a thread. Neste ponto, precisamos detectar o retorno e garantir que a thread não trave.

Existem algumas maneiras de fazer isso. Uma maneira seria registrar um manipulador de exceção para a thread remota usando thread_set_exception_ports() e definir o registro de endereço de retorno, lr, para um endereço inválido antes de chamar a função; dessa forma, após a execução da função, uma exceção seria gerada e uma mensagem seria enviada para nossa porta de exceção, momento em que podemos inspecionar o estado da thread para recuperar o valor de retorno. No entanto, para simplificar, copiei a estratégia usada no exploit triple_fetch de Ian Beer, que era definir lr para o endereço de uma instrução que entraria em loop infinito e, em seguida, verificar repetidamente os registros da thread até que pc apontasse para essa instrução.

2. Portas Mach para comunicação

O próximo passo é criar portas Mach por meio das quais podemos nos comunicar com a thread remota. Essas portas Mach serão úteis posteriormente para ajudar na transferência de direitos de envio e recebimento arbitrários entre as tarefas.

Para estabelecer uma comunicação bidirecional, precisaremos criar dois direitos de recebimento Mach: um na tarefa local e outro na tarefa remota. Em seguida, precisaremos transferir um direito de envio para cada porta para a outra tarefa. Isso dará a cada tarefa uma maneira de enviar uma mensagem que pode ser recebida pela outra.

Vamos primeiro nos concentrar em configurar a porta local, ou seja, a porta para a qual a tarefa local possui o direito de recebimento. Podemos criar a porta Mach como qualquer outra, chamando mach_port_allocate(). O truque é obter um direito de envio para essa porta na tarefa remota.

Um truque conveniente que podemos usar para copiar um direito de envio da tarefa atual para uma tarefa remota usando apenas um primitivo de execução básico é armazenar um direito de envio para nossa porta local na porta especial THREAD_KERNEL_PORT da thread remota usando thread_set_special_port(); em seguida, podemos fazer a thread remota chamar mach_thread_self() para recuperar o direito de envio.

Em seguida, configuraremos a porta remota, que é praticamente o inverso do que acabamos de fazer. Podemos fazer a thread remota alocar uma porta Mach chamando mach_reply_port(); não podemos usar mach_port_allocate() porque este último retorna o nome da porta alocada na memória e ainda não temos um primitivo de leitura. Depois de termos uma porta, podemos criar um direito de envio chamando mach_port_insert_right() na thread remota. Em seguida, podemos armazenar a porta no kernel chamando thread_set_special_port(). Finalmente, de volta à tarefa local, podemos recuperar a porta chamando thread_get_special_port() na thread remota, dando-nos um direito de envio para a porta Mach acabada de alocar na tarefa remota.

Neste ponto, criamos as portas Mach que usaremos para comunicação bidirecional.

3. Leitura/escrita básica de memória

Agora vamos usar o primitivo de execução para criar primitivos básicos de leitura e escrita de memória. Esses primitivos não serão usados para muita coisa (em breve vamos atualizar para primitivos muito mais poderosos), mas são um passo fundamental para nos ajudar a expandir nosso controle sobre o processo remoto.

Para ler e escrever memória usando nosso primitivo de execução, estaremos procurando por funções como estas:

uint64_t read_func(uint64_t *address) {
return *address;
}
void write_func(uint64_t *address, uint64_t value) {
*address = value;
}

Eles podem corresponder ao seguinte código assembly:

_read_func:
ldr     x0, [x0]
ret
_write_func:
str     x1, [x0]
ret

Uma rápida análise de algumas bibliotecas comuns revelou alguns bons candidatos. Para ler a memória, podemos usar a função property_getName() da biblioteca Objective-C runtime:

const char *property_getName(objc_property_t prop)
{
return prop->name;
}

Acontece que prop é o primeiro campo de objc_property_t, então isso corresponde diretamente à função hipotética read_func acima. Só precisamos realizar uma chamada de função remota com o primeiro argumento sendo o endereço que queremos ler, e o valor de retorno será os dados nesse endereço.

Encontrar uma função pré-existente para escrever na memória é um pouco mais difícil, mas ainda existem ótimas opções sem efeitos colaterais indesejados. No libxpc, a função _xpc_int64_set_value() tem a seguinte desmontagem:

__xpc_int64_set_value:
str     x1, [x0, #0x18]
ret

Assim, para realizar uma escrita de 64 bits no endereço address, podemos realizar a chamada remota:

_xpc_int64_set_value(address - 0x18, value)

Com essas primitivas em mãos, estamos prontos para criar memória compartilhada.

4. Memória compartilhada

Nosso próximo passo é criar memória compartilhada entre a tarefa remota e local. Isso nos permitirá transferir dados entre os processos com mais facilidade: com uma região de memória compartilhada, a leitura e gravação arbitrária de memória é tão simples quanto uma chamada remota para memcpy(). Além disso, ter uma região de memória compartilhada nos permitirá configurar facilmente uma pilha para que possamos chamar funções com mais de 8 argumentos.

Para facilitar as coisas, podemos reutilizar os recursos de memória compartilhada do libxpc. O libxpc fornece um tipo de objeto XPC, OS_xpc_shmem, que permite estabelecer regiões de memória compartilhada por meio do XPC. Ao reverter o libxpc, determinamos que OS_xpc_shmem é baseado em entradas de memória Mach, que são portas Mach que representam uma região de memória virtual. E como já mostramos como enviar portas Mach para a tarefa remota, podemos usar isso para configurar facilmente nossa própria memória compartilhada.

Primeiro, precisamos alocar a memória que compartilharemos usando mach_vm_allocate(). Precisamos usar mach_vm_allocate() para que possamos usar xpc_shmem_create() para criar um objeto OS_xpc_shmem para a região. xpc_shmem_create() cuidará de criar a entrada de memória Mach para nós e armazenará o direito de envio Mach para a entrada de memória no objeto OS_xpc_shmem opaco no deslocamento 0x18.

Depois de obtermos a porta da entrada de memória, criaremos um objeto OS_xpc_shmem no processo remoto que representa a mesma região de memória, permitindo-nos chamar xpc_shmem_map() para estabelecer o mapeamento de memória compartilhada. Primeiro, realizamos uma chamada remota para malloc() para alocar memória para o OS_xpc_shmem e usamos nossa primitiva de gravação básica para copiar o conteúdo do objeto OS_xpc_shmem local. Infelizmente, o objeto resultante não está totalmente correto: seu campo de entrada de memória Mach no deslocamento 0x18 contém o nome da tarefa local para a entrada de memória, não o nome da tarefa remota. Para corrigir isso, usamos o truque thread_set_special_port() para inserir um direito de envio para a entrada de memória Mach na tarefa remota e, em seguida, sobrescrevemos o campo 0x18 com o nome da entrada de memória remota. Neste ponto, o objeto remoto OS_xpc_shmem é válido e o mapeamento de memória pode ser estabelecido com uma chamada remota para xpc_shmem_remote().

5. Controle total

Com a memória compartilhada em um endereço conhecido e uma primitiva de execução arbitrária, basicamente terminamos. Leituras e gravações arbitrárias de memória são implementadas chamando memcpy() para e da região compartilhada, respectivamente. Chamadas de função com mais de 8 argumentos são realizadas colocando argumentos adicionais além dos primeiros 8 na pilha, de acordo com a convenção de chamada. A transferência arbitrária de portas Mach entre as tarefas pode ser feita enviando mensagens Mach pelas portas estabelecidas anteriormente. Podemos até transferir descritores de arquivo entre os processos usando fileports (um agradecimento especial a Ian Beer por demonstrar essa técnica em triple_fetch!).

Em resumo, agora temos controle total e fácil sobre o processo vítima. Você pode ver a implementação completa e a API exposta na biblioteca threadexec.

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥