COVIL HACKER

, ! .


» COVIL HACKER » Vulnerabilidades/exploração de software » Artigo Exploração CVE-2022-42703 - ataque de retorno na pilha


Artigo Exploração CVE-2022-42703 - ataque de retorno na pilha

1 2 2

1

Este artigo detalha a exploração do CVE-2022-42703 (versão P0 2351 - corrigida em 5 de setembro de 2022), um bug que Jann Horn descobriu no subsistema de gerenciamento de memória (MM) do kernel do Linux que faz com que ele seja usado após a liberação em a estrutura anon_vma . Como o bug é muito complexo (é difícil para mim decifrá-lo, é claro!), uma futura postagem no blog irá descrevê-lo por completo. Por enquanto, a entrada do rastreador de problemas, este artigo LWN explicando o que é anon_vma e o commit que causou o erro são ótimas fontes para obter mais contexto.

Configuração

O acionamento bem-sucedido da vulnerabilidade subjacente faz com que o folio->mapping aponte para um objeto anon_vma liberado. A chamada madvise(..., MADV_PAGEOUT) pode então ser usada para reativar o acesso ao anon_vma liberado em folio_lock_anon_vma_read() :

struct anon_vma *folio_lock_anon_vma_read(struct folio *folio,

                      struct rmap_walk_control *rwc)

{

    struct anon_vma *anon_vma = NULL;

    struct anon_vma *root_anon_vma;

    unsigned long anon_mapping;

    rcu_read_lock();

    anon_mapping = (unsigned long)READ_ONCE(folio->mapping);

    if ((anon_mapping & PAGE_MAPPING_FLAGS) != PAGE_MAPPING_ANON)

        goto out;

    if (!folio_mapped(folio))

        goto out;

    // anon_vma is dangling pointer

    anon_vma = (struct anon_vma *) (anon_mapping - PAGE_MAPPING_ANON);

    // root_anon_vma is read from dangling pointer

    root_anon_vma = READ_ONCE(anon_vma->root);

    if (down_read_trylock(&root_anon_vma->rwsem)) {

[...]

        if (!folio_mapped(folio)) { // false

[...]

        }

        goto out;

    }

    if (rwc && rwc->try_lock) { // true

        anon_vma = NULL;

        rwc->contended = true;

        goto out;

    }

[...]

out:

    rcu_read_unlock();

    return anon_vma; // return dangling pointer

}




Uma possível técnica de exploração é permitir que a função retorne um ponteiro anon_vma e tente forçar as operações subsequentes a fazer algo útil. Em vez disso, decidimos usar a chamada down_read_trylock() dentro da função para corromper a memória no endereço escolhido, o que podemos fazer se pudermos controlar o ponteiro root_anon_vma que é lido do anon_vma liberado.

Controlar o ponteiro root_anon_vma significa restaurar um anon_vma liberado com memória controlada pelo invasor. As estruturas struct anon_vma são alocadas de seu próprio cache kmalloc, o que significa que não podemos simplesmente desalocar uma e restaurá-la com outro objeto. Em vez disso, retornamos a página de laje anon_vma associada de volta ao alocador de página do kernel seguindo uma estratégia muito semelhante à descrita aqui - ( https://googleprojectzero.blogspot.com/ … nel-memory .html). Ao liberar todos os objetos anon_vma na página slab e, em seguida, despejar a lista parcial livre de percpu da página slab, podemos chamar a memória virtual anteriormente associada a anon_vma para ser retornada ao alocador de página. Em seguida, pulverizamos os buffers de canal para retornar um anon_vma liberado com memória controlada pelo invasor.

Até agora, discutimos como transformar nosso uso pós-lançamento em uma chamada down_read_trylock() em um ponteiro controlado pelo invasor. A implementação de down_read_trylock() se parece com isso:



struct rw_semaphore {

    atomic_long_t count;

    atomic_long_t owner;

    struct optimistic_spin_queue osq; /* spinner MCS lock */

    raw_spinlock_t wait_lock;

    struct list_head wait_list;

};

...

static inline int __down_read_trylock(struct rw_semaphore *sem)

{

    long tmp;

    DEBUG_RWSEMS_WARN_ON(sem->magic != sem, sem);

    tmp = atomic_long_read(&sem->count);

    while (!(tmp & RWSEM_READ_FAILED_MASK)) {

        if (atomic_long_try_cmpxchg_acquire(&sem->count, &tmp,

                            tmp + RWSEM_READER_BIAS)) {

            rwsem_set_reader_owned(sem);

            return 1;

        }

    }

    return 0;

}

Foi útil emular down_read_trylock() no unicórnio para ver como ele se comporta com diferentes valores sem->count. Supondo que esse código funcione com memória inerte e imutável, ele incrementará sem->count em 0x100 se os 3 bits menos significativos e o bit mais significativo não forem definidos. Isso significa que é difícil alterar o ponteiro do kernel e não podemos alterar nenhum valor que não esteja alinhado a 8 bytes (porque eles terão um ou mais dos três bits inferiores definidos). Além disso, esse semáforo é desbloqueado posteriormente, fazendo com que qualquer gravação que façamos seja cancelada em um futuro próximo. Além disso, no momento não temos uma estratégia definida para definir um slide KASLR ou determinar os endereços de quaisquer objetos que possamos querer sobrescrever com nosso novo primitivo. Acontece que

Corrupção de pilha

No Linux x86-64, quando a CPU executa certas interrupções e exceções, ela muda para a pilha apropriada, que é mapeada para um endereço virtual estático e não aleatório, com uma pilha diferente para diferentes tipos de exceções. Uma breve documentação sobre essas pilhas e sua estrutura pai cpu_entry_area pode ser encontrada aqui - https://docs.kernel.org/x86/pti.html. Essas pilhas são mais comumente usadas ao fazer login no kernel a partir do userland, mas também são usadas para exceções que ocorrem no modo kernel. Recentemente, vimos entradas KCTF nas quais os invasores usam a cpu_entry_area não aleatória para acessar dados em um endereço virtual conhecido na memória acessível pelo kernel, mesmo com SMAP e KASLR presentes. Você também pode usar essas pilhas para falsificar dados controlados maliciosamente em um endereço virtual de kernel conhecido. Isso funciona porque o conteúdo do registro de propósito geral da tarefa do invasor é enviado diretamente para essa pilha quando há uma mudança do modo de usuário para o modo kernel devido a uma dessas exceções. Isso também acontece quando o próprio kernel lança uma exceção de tabela de pilha de interrupção e muda para a pilha de exceção, exceto que neste caso os núcleos GPR são colocados em seu lugar. Esses registros enviados são usados ​​posteriormente para restaurar o estado do kernel depois que uma exceção foi tratada. No caso de uma exceção lançada pelo ambiente do usuário, o conteúdo do registro é restaurado da pilha de tarefas.

Um exemplo de exceção IST é uma exceção de banco de dados que pode ser acionada por um invasor por meio de um ponto de interrupção de hardware cujos registros associados são descritos aqui - https://pdos.csail.mit.edu/6.828/2004/r … 86/s12_02. htm. Pontos de interrupção de hardware podem ser acionados por vários tipos de acesso à memória, como leitura, gravação e busca de instruções. Esses pontos de interrupção de hardware podem ser definidos usando ptrace(2) e persistidos durante a execução do modo kernel no contexto de uma tarefa, como durante uma chamada do sistema. Isso significa que um ponto de interrupção de hardware definido por um invasor pode ser ativado no modo kernel, como durante uma chamada copy_to/from_user. A exceção resultante salvará e restaurará o contexto do kernel usando a pilha de exceção não aleatória acima, e esse contexto do kernel é um alvo excepcionalmente bom para nossa primitiva de entrada arbitrária.

Qualquer um dos registradores que copy_to/from_user está usando ativamente durante o processamento do ponto de interrupção de hardware pode ser corrompido usando nossa primitiva de gravação aleatória para sobrescrever seus valores salvos na pilha de exceção. Nesse caso, o tamanho da chamada copy_user é um alvo intuitivo. O valor do tamanho é armazenado permanentemente no registrador rcx, que será armazenado no mesmo endereço virtual toda vez que um ponto de interrupção de hardware for atingido. Depois de corromper este registro salvo com nossa primitiva de gravação aleatória, o kernel restaurará rcx da pilha de exceção assim que retornar para copy_to/from_user. Como rcx especifica o número de bytes que copy_user deve copiar, essa corrupção resultará em

...causando corrupção de pilha

A estratégia de ataque começa da seguinte forma:

1. O processo Y bifurca-se do processo X.

2. O processo X monitora o processo Y e, em seguida, define um ponto de interrupção de hardware em um endereço virtual conhecido [addr] no processo Y.

3. Processo Y faz muitas chamadas uname(2), que chamam copy_to_user do buffer de pilha do kernel em [addr]. Isso faz com que o kernel execute constantemente o watchpoint de hardware e entre no manipulador de exceções do banco de dados, usando a pilha de exceções do banco de dados para salvar e restaurar o estado copy_to_user.

4. Muitas gravações aleatórias são executadas ao mesmo tempo no local conhecido do valor armazenado rcx da pilha de exceções do banco de dados, que é o comprimento armazenado copy_to_user do processo Y.

https://forumupload.ru/uploads/001b/c9/09/2/t442246.png



A pilha de exceções do banco de dados raramente é usada, portanto, é improvável que estraguemos qualquer estado inesperado do kernel com uma exceção de banco de dados fictícia enviando spam para nossa primitiva de gravação arbitrária. A tecnologia também é atrevida, mas perder uma corrida significa apenas bagunçar os dados obsoletos da pilha. Nesse caso, simplesmente tentamos novamente. Na minha experiência, raramente leva mais do que alguns segundos para vencer uma corrida com sucesso.

Na alteração bem-sucedida do valor de comprimento, o kernel copiará a maior parte da pilha da tarefa atual de volta ao espaço do usuário, incluindo o cookie da pilha local da tarefa e os endereços de retorno. Podemos então reverter nossa técnica e atacar com copy_from_user. Em vez de copiar muitos bytes da pilha de tarefas do kernel para a área do usuário, forçamos o kernel a copiar muitos bytes da área do usuário para a pilha de tarefas do kernel! Novamente usamos a chamada de sistema prctl(2), que executa uma operação copy_from_user.call no buffer de pilha do kernel. Agora, alterando o valor do comprimento, estamos criando uma condição de estouro de buffer de pilha nesta função que não existia antes. Como já mesclamos o cookie de pilha e o slide KASLR, é muito fácil ignorar ambas as proteções e sobrescrever o endereço de retorno.

https://forumupload.ru/uploads/001b/c9/09/2/t660298.png

A conclusão da cadeia ROP do kernel é deixada como um exercício para o leitor.

Obtendo um slide KASLR com pré-busca

Ao relatar esse bug para a equipe de segurança do kernel do Linux, propusemos começar a randomizar a localização do percpu cpu_entry_area (CEA) e, portanto, a exceção associada e as pilhas de chamadas do sistema. Essa é uma defesa eficaz contra invasores, mas não é suficiente para impedir que um invasor local tire vantagem. 6 anos atrás Daniel Gruss et al. descobriu um método novo e mais confiável para usar o canal do lado do relógio TLB em processadores x86. Seus resultados mostraram que as instruções de pré-busca executadas no modo de usuário são removidas com atrasos diferentes estatisticamente significativos, dependendo se o endereço virtual solicitado para pré-busca foi mapeado ou não, mesmo que esse endereço virtual tenha sido mapeado apenas no modo kernel.https://docs.kernel.org/x86/pti.html - foi útil para proteger este canal lateral, no entanto, a maioria dos processadores modernos agora possui proteção integrada contra o Meltdown, para o qual o kPTI foi projetado especificamente para resolver e, portanto, o kPTI ( que tem implicações de desempenho significativas) está desativado em microarquiteturas modernas. Esta solução significa que é novamente possível aproveitar o canal lateral de pré-busca para contornar não apenas o KASLR, mas também a proteção de randomização da área de entrada da CPU, mantendo o método de exploração de corrupção de pilha CEA viável contra CPUs X86 modernas.

Surpreendentemente, existem poucos exemplos rápidos e confiáveis ​​dessa técnica de desvio de pré-busca KASLR disponível no domínio do código aberto, então decidi escrever um.

Execução

A essência de uma implementação eficiente desse método é ler sequencialmente o contador de carimbos de data/hora do processador antes e depois da pré-busca. Daniel Gruss gentilmente forneceu um código-fonte altamente eficiente e aberto para isso. A única alteração que fiz (por sugestão de Yann Horn) foi passar a usar lfence em vez de cpuid como a instrução de serialização, pois cpuid é emulado em ambientes VM. Na prática, também ficou claro que não há necessidade de executar nenhuma rotina de limpeza de cache para observar o efeito do canal lateral. Simplesmente cronometrar cada tentativa de pré-busca é suficiente.

A geração de tempos de pré-busca para todos os 512 slots KASLR possíveis gera muitos dados difusos que precisam ser analisados. Para minimizar o ruído, várias amostras de cada endereço testado são coletadas e o valor mínimo desse conjunto de amostras é usado nos resultados como um valor representativo do endereço. Este teste foi executado principalmente na CPU Tiger Lake, portanto, não foram necessárias mais de 16 amostras por slot para obter resultados excepcionalmente confiáveis. A identificação de um intervalo de tempo de pré-busca mínimo de baixa resolução restringe o escopo da pesquisa, evitando falsos positivos para o código de detecção de borda de resolução mais alta que encontra o endereço exato onde a pré-busca despenca no tempo de execução. O resultado desses esforços é o PoC,

https://forumupload.ru/uploads/001b/c9/09/2/t523645.png


Esse código de pré-busca realmente funciona para encontrar a localização das regiões CEA aleatórias no patch sugerido por Peter Zilstra. No entanto, o caminho para esse resultado leva a um código que demonstra outro problema muito importante - o KASLR está completamente comprometido em x86 contra invasores locais, e tem estado assim nos últimos anos e estará no futuro indefinido. Atualmente, não há planos para resolver os inúmeros problemas de microarquitetura que levam a canais de terceiros como este. Mais trabalho é necessário nesta área para preservar a integridade do KASLR ou, alternativamente, pode ser hora de reconhecer que o KASLR não é mais uma defesa eficaz contra hackers locais e desenvolver um código de segurança e medidas para proteger as consequências,

Conclusão

Esta exploração demonstra uma técnica altamente segura e independente que permite que uma ampla variedade de primitivas de gravação arbitrárias não controladas executem o código do kernel em plataformas x86. Embora essa técnica de exploração possa ser protegida de um contexto remoto, um invasor em um contexto local pode usar canais secundários de microarquitetura conhecidos para ignorar as proteções atuais. O trabalho adicional nesta área pode ser útil para complicar ainda mais a exploração, como executar a randomização na pilha para que o deslocamento da pilha de estado armazenado mude com cada exceção IST recebida. No entanto, por enquanto, esta continua sendo uma estratégia de exploração viável e poderosa no Linux x86. Traduzido

Fonte: https://googleprojectzero.blogspot.com/ … ttack.html

0

2


» COVIL HACKER » Vulnerabilidades/exploração de software » Artigo Exploração CVE-2022-42703 - ataque de retorno na pilha


|