Linux interno: como / proc / self / mem grava em memória não gravável



A estranha peculiaridade do pseudo arquivo /proc/*/mem



está em sua semântica incisiva. As operações de gravação por meio desse arquivo serão bem-sucedidas mesmo se a memória virtual de destino estiver marcada como não gravável. Isso é intencional e é usado ativamente por projetos como o compilador Julia JIT ou o depurador rr.



Mas a questão é: o código privilegiado obedece às permissões de memória virtual? Até que ponto o hardware pode afetar o acesso à memória do kernel?



Tentaremos responder a essas perguntas e considerar as nuances da interação entre o sistema operacional e o hardware no qual ele é executado. Vamos explorar os limites do processador que podem afetar o kernel e ver como o kernel pode contorná-los.



Corrigir libc com / proc / self / mem



Como é essa semântica incisiva? Considere o código:



#include <fstream>
#include <iostream>
#include <sys/mman.h>

/* Write @len bytes at @ptr to @addr in this address space using
 * /proc/self/mem.
 */
void memwrite(void *addr, char *ptr, size_t len) {
  std::ofstream ff("/proc/self/mem");
  ff.seekp(reinterpret_cast<size_t>(addr));
  ff.write(ptr, len);
  ff.flush();
}

int main(int argc, char **argv) {
  // Map an unwritable page. (read-only)
  auto mymap =
      (int *)mmap(NULL, 0x9000,
                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

  if (mymap == MAP_FAILED) {
    std::cout << "FAILED\n";
    return 1;
  }

  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
  getchar();

  // Try to write to the unwritable page.
  memwrite(mymap, "\x40\x41\x41\x41", 4);
  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
  getchar();
  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
  getchar();

  // Try to writ to the text segment (executable code) of libc.
  auto getchar_ptr = (char *)getchar;
  memwrite(getchar_ptr, "\xcc", 1);

  // Run the libc function whose code we modified. If the write worked,
  // we will get a SIGTRAP when the 0xcc executes.
  getchar();
}

      
      





É /proc/self/mem



usado aqui para gravar em duas páginas de memória não graváveis. O primeiro contém o próprio código e o segundo pertence a libc



(a função getchar



). A última parte é de mais interesse: o código grava o byte 0xcc (um ponto de interrupção em aplicativos x86-64), que, se executado, fará com que o kernel forneça ao nosso processo um SIGTRAP. Isso muda literalmente o executável libc. E se na próxima ligação getchar



conseguirmos o SIGTRAP, saberemos que o registro foi um sucesso.



Isto é o que parece quando você executa o programa:





Trabalho! No meio, são impressas expressões que provam que o valor 0x41414140 foi escrito e lido com sucesso da memória. A última saída mostra que, após o patching, nosso processo recebeu um SIGTRAP como resultado de nossa chamada getchar



.



No vídeo:





Vimos como esse recurso funciona da perspectiva do espaço do usuário. Vamos cavar mais fundo. Para entender completamente como isso funciona, você precisa observar como o hardware impõe restrições de memória.



Equipamento



Na plataforma x86-64, há duas configurações de processador que controlam a capacidade do kernel de acessar a memória. Eles são usados ​​pela unidade de gerenciamento de memória (MMU).



A primeira configuração é o bit de proteção contra gravação (CR0.WP). A partir do manual da Intel (Volume 3, Seção 2.5), sabemos:



Proteção contra gravação (16º bit CR0). Se fornecido, evita que procedimentos de nível de supervisor gravem em páginas protegidas contra gravação. Se o bit estiver vazio, os procedimentos de nível de supervisor podem gravar em páginas protegidas contra gravação (independentemente das configurações de bit U / S; consulte as Seções 4.1.3 e 4.6).


Isso evita que o kernel grave em páginas protegidas contra gravação, o que é naturalmente permitido por padrão .



A segunda configuração é Prevenção de Acesso do Modo Supervisor (SMAP) (CR4.SMAP). A descrição completa no Volume 3, Seção 4.6, é detalhada. Resumindo, o SMAP priva completamente o kernel da capacidade de escrever ou ler da memória do espaço do usuário. Isso evita exploits que inundam o espaço do usuário com dados maliciosos que o kernel deve ler durante a execução.



Se o seu código do kernel usa apenas canais aprovados ( copy_to_user



etc.), então o SMAP pode ser ignorado com segurança, essas funções o usarão automaticamente antes e depois de acessar a memória. E quanto à proteção contra gravação?



Se CR0.WP não for especificado, a implementação do /proc/*/mem



kernel pode, de fato, gravar sem cerimônia na memória do espaço do usuário protegida contra gravação.



No entanto, CR0.WP é definido na inicialização e geralmente vive durante todo o tempo de operação dos sistemas. Neste caso, ao tentar escrever, será emitida uma falha de página. É mais uma ferramenta Copy-on-Write do que uma ferramenta de segurança, portanto, não impõe nenhuma restrição real ao kernel. Em outras palavras, o tratamento de falhas inconveniente é necessário, o que não é necessário para um determinado bit.



Vamos descobrir a implementação agora.



Como / proc / * / mem funciona



/proc/*/mem



Ele é implementado em fs / proc / base.c .



A estrutura file_operations



contém as funções do manipulador, e a função mem_rw () oferece suporte total ao manipulador de gravação. mem_rw()



usa access_remote_vm () para operações de escrita . E access_remote_vm()



faz isso:



  • Chama get_user_pages_remote()



    para encontrar um quadro físico que corresponda ao endereço virtual de destino.
  • Chamadas kmap()



    para marcar este quadro como gravável no espaço de endereço virtual do kernel.
  • Solicita copy_to_user_page()



    a execução final das operações de gravação.


Esta implementação ignora completamente o problema da capacidade do kernel de gravar em um espaço de usuário não gravável! O controle do kernel sobre o subsistema de memória virtual permite que a MMU seja completamente ignorada, permitindo que o kernel simplesmente grave em seu próprio espaço de endereço gravável. Portanto, a discussão de CR0.WP torna-se irrelevante.



Vejamos cada uma das etapas:



get_user_pages_remote ()



Para ignorar a MMU, o kernel precisa fazer manualmente o que a MMU faz no hardware do aplicativo. Primeiro, você precisa converter o endereço virtual de destino em um endereço físico. Isso é feito pela família de funções get_user_pages()



... Eles percorrem as tabelas de páginas e procuram quadros de memória física que correspondam a um determinado intervalo de endereços virtuais.



O chamador fornece o contexto e usa sinalizadores para alterar o comportamento get_user_pages()



. A bandeira FOLL_FORCE



que está sendo transmitida é especialmente interessante mem_rw()



. O sinalizador aciona check_vma_flags (lógica de verificação de acesso get_user_pages()



) para ignorar gravações em páginas não graváveis ​​e continuar a pesquisa. A semântica "incisiva" refere-se completamente a FOLL_FORCE



(meus comentários):



static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
        [...]
        if (write) { // If performing a write..
                if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..
                        if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..
                                return -EFAULT; // Return an error
        [...]
        return 0; // Otherwise, proceed with lookup
}

      
      





get_user_pages()



Ele também segue a semântica de cópia na gravação (CoW). Se uma gravação em uma tabela de página não gravável for especificada, uma falha de página será emulada chamando o handle_mm_fault



manipulador de erro da página principal. Isso inicia a rotina de processamento de cópia na gravação apropriada do_wp_page



, que copia a página conforme necessário. Portanto, se as entradas /proc/*/mem



forem executadas por mapeamento compartilhado privado, por exemplo, libc, elas serão visíveis apenas dentro do processo.



kmap ()



Depois que um quadro físico é encontrado, ele precisa ser mapeado para o espaço de endereço virtual do kernel, que é gravável. Isso é feito com a ajuda de kmap()



.



Em uma plataforma x86 de 64 bits, toda a memória física é mapeada por meio da área de mapeamento em linha do espaço de endereço virtual do kernel. Neste caso, o kmap()



funcionamento é muito simples: basta adicionar o endereço inicial do mapeamento linear ao endereço físico do quadro para calcular o endereço virtual para o qual esse quadro está mapeado.



Em uma plataforma x86 de 32 bits, o mapeamento em linha contém um subconjunto de memória física, portanto, uma função kmap()



pode precisar mapear um quadro alocando memória highmem e manipulando tabelas de página.



Em ambos os casos, o mapeamento de linha e o mapeamento de alta memória são executados com proteção. PAGE_KERNEL que permite a escrita.



copy_to_user_page ()



O último passo é executar a escrita. Isso é feito usando o copy_to_user_page()



que é essencialmente memcpy. Isso funciona porque o destino é um mapeamento gravável de kmap()



.



Discussão



Portanto, primeiro, o kernel, usando a tabela de página de memória pertencente ao programa, converte o endereço virtual de destino no espaço do usuário para o quadro físico correspondente. O kernel então mapeia esse quadro para seu próprio espaço virtual gravável. Finalmente, ele grava com memcpy simples.



Surpreendentemente, CR0.WP não é usado aqui. A implementação ignora elegantemente esse ponto, aproveitando o fato de que não é necessário acessar a memória por meio de um ponteiro de espaço do usuário . Como o kernel tem controle total sobre a memória virtual, ele pode simplesmente remapear o quadro físico em seu próprio espaço de endereço virtual com resoluções arbitrárias e fazer o que quiser com ele.



É importante observar que as permissões que protegem uma página da memória estão relacionadas ao endereço virtual usado para acessar essa página, não ao quadro físico associado à página . A notação de permissão de memória se refere exclusivamente à memória virtual, não à memória física.



Conclusão



Examinando os detalhes da semântica incisiva na implementação, /proc/*/mem



podemos refletir a relação entre o núcleo e o processador. À primeira vista, a capacidade do kernel de gravar em uma memória não gravável levanta a questão: até que ponto o processador pode afetar o acesso à memória do kernel? O manual descreve os mecanismos de controle que podem limitar as ações do kernel. Mas, em uma inspeção mais próxima, as limitações são superficiais, na melhor das hipóteses. Esses são obstáculos simples de contornar.



All Articles