Ir para conteúdo
  • Como hookei a execve() em x86-64 num LSM

       (0 análises)

    Fernando Mercês

    Este foi um problema que tive há bastante tempo, quando precisei fazer com que o módulo de kernel do antivírus Trend Micro ServerProtect funcionasse em versões de 64-bit do Linux. Acontece que este módulo utiliza o LSM (Linux Security Module) framework. Até aí tudo bem, foi como o jeito que programadores do módulo usaram para hookar a execve() nesta arquitetura, essencial para um antivírus por motivos óbvios: toda vez que um arquivo fosse executado no sistema, este seria escaneado por vírus.

    Apesar de um pouco defasado, decidi escrever agora pois me animei quando fui ao 11º Encontro da comunidade C/C++ Brasil [3]. De fato, atualmente um LSM não pode mais ser carregado num kernel rodando, como um LKM (Loadable Kernel Module) e isso tira toda a praticidade deste patch em específico, mas a técnica utilizada pode ser útil em outras ocasiões. Ou não.:)

    Indo direto ao ponto, a estrutura default_security_ops não era mais acessível diretamente na versão do kernel que trabalhei e por isso o seguinte trecho de código não funcionava num contexto LSM:

     unsigned long addr = kallsyms_lookup_name("default_security_ops");

    Minha intenção não era reescrever todo o módulo, então tive que dar um jeito rápido. A ideia foi buscar uma função exportada que manipulasse a estrutura default_security_ops, assim eu teria acesso ao endereço dela. Várias funções o fazem, mas buscando pela mais simples/menor encontrei a reset_security_ops(), presente em security/linux.c:

    void reset_security_ops(void)
    {
    	security_ops = &default_security_ops;
    }

    Essa função só faz uma coisa: coloca o endereço da estrutura que quero em outra. Sendo assim, é uma perfeita candidata para minha gambiarra.:)

    Pelo tamanho daria para tentar advinhar quão distante está o endereço da estrutura default_security_ops do endereço da função reset_security_ops(), mas vamos disassemblar só pra garantir:

    1. Extraindo a imagem do kernel

    O kernel fica comprimido em /boot e no próprio fonte existe um script para descomprimi-lo chamado extract-vmlinux. Usei da seguinte maneira:

    $ sudo cp /boot/vmlinuz-$(uname -r)-generic vmlinuz # trabalhar com um backup, só pra garantir ;)
    $ sudo chown $(whoami): vmlinuz
    $ /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux vmlinuz > vmlinux

    O arquivo vmlinux (com “x” ao invés de “z”) criado é a imagem descomprimida do kernel, que precisamos disassemblar.

    2. Disassemblando a imagem

    Por padrão a imagem do kernel não contém símbolos, então minha tentativa com o gdb foi frustrada:

    $ gdb -q ./vmlinux
    Reading symbols from ./vmlinux...(no debugging symbols found)...done.
    (gdb) disassemble reset_security_ops
    No symbol table is loaded.  Use the "file" command.

    Mas nem tudo está perdido. No kernel rodando, dá pra ver o endereço das funções no arquivo System.map:

    # grep reset_security_ops /boot/System.map-$(uname -r)
    ffffffff812d6be0 T reset_security_ops

    Sabendo o endereço, voltei ao gdb e pedi pra printar 8 intruções começando neste endereço, mas não antes de setar a sintaxe para Intel.:)

    (gdb) set disassembly-flavor intel
    (gdb) x/8i 0xffffffff812d6be0
        0xffffffff812d6be0:  call   0xffffffff81731480
        0xffffffff812d6be5:  push   rbp
        0xffffffff812d6be6:  mov    QWORD PTR [rip+0xcdd14f],0xffffffff81c80100        # 0xffffffff81fb3d40
        0xffffffff812d6bf1:  mov    rbp,rsp
        0xffffffff812d6bf4:  pop    rbp
        0xffffffff812d6bf5:  ret    
        0xffffffff812d6bf6:  nop    WORD PTR cs:[rax+rax*1+0x0]
        0xffffffff812d6c00:  call   0xffffffff81731480
    (gdb)

    Essa call no início apontar para um ret e honestamente eu não sei por que ela existe:

     
    (gdb) x/i 0xffffffff81731480
           0xffffffff81731480:  ret  
      

    3. Contando os bytes

    Lembrando que esta função simplesmente implementa a atribuição do endereço que queremos (default_security_ops) para uma variável, fica fácil perceber que a varíavel (security_ops) é RIP+0xcdd14f e o endereço da estrutura que queremos é 0xffffffff81c80100. Mas claro, a ideia aqui é fazer de forma genérica, então não posso trabalhar com esse endereço fixo. Bem, admitindo que essa função não mude, é razoável dizer que o endereço é um número de 64-bits que tem seu primeiro byte em uma posição fixa a partir do endereço da função reset_security_ops(). Isto está longe de ser uma solução profissional, mas resolveu meu problema na época.:) Ao invés do x/i (examine as instruction), vou usar o disassemble pra poder contar melhor os bytes:

    (gdb) disassemble /r 0xffffffff812d6be0, 0xffffffff812d6bff
        Dump of assembler code from 0xffffffff812d6be0 to 0xffffffff812d6bf4:
           0xffffffff812d6be0:  e8 9b a8 45 00                      call   0xffffffff81731480
           0xffffffff812d6be5:  55                                  push   rbp
           0xffffffff812d6be6:  48 c7 05 4f d1 cd 00 00 01 c8 81    mov    QWORD PTR [rip+0xcdd14f],0xffffffff81c80100
           0xffffffff812d6bf1:  48 89 e5                            mov    rbp,rsp
           0xffffffff812d6bf5:  c3                                  ret
        End of assembler dump.

    Para disassemblar sem símbolo você precisa dizer ao gdb até onde ir, por isso fui até 0xffffffff812d6bff, mas cortei o que veio depois do ret no dump. Se contarmos, vamos perceber que o número começa em (endereço da função) + 13 e tem 4 bytes. Só pra confirmar:

     (gdb) x/x 0xffffffff812d6be0 + 13
        0xffffffff812d6bed: 0x81c80100

    Agora vamos seguir (#medo)!

    4. Implementação

    Ficou deste jeito:

    unsigned long default_security_ops = 0xffffffff00000000;
    unsigned long _reset_security_ops = 0;
    
    _reset_security_ops = kallsyms_lookup_name("reset_security_ops");
    memcpy(&default_security_ops, (void *) (_reset_security_ops + 13), 4);

    A variável default_security_ops já é inicializada com sua parte alta toda setada, pois só a parte baixa do endereço está no assembly. A kallsyms_lookup_name() me dá o endereço da função reset_security_ops(). A memcpy() então copia os 4 bytes que compõem o segundo operando da instrução mov, que é o nosso endereço. O resultado abaixo foi obtido com este código implementado em um módulo:

    # grep default_security_ops /boot/System.map-3.13.0-44-generic
    ffffffff81c80100 d default_security_ops
    
    # rmmod teste; make >/dev/null && insmod teste.ko && dmesg | tail -1
    [15999.471619] default_security_ops address: 0xffffffff81c80100

    Os endereços batem, no entanto, essa coisa não foi para produção (graças ao Divino) e não recomendo que utilizem nada parecido em nenhum sistema crítico, mas será que dá pra aplicar essa técnica de “contagem de bytes” para outras coisas?:)


    Revisão: Leandro Fróes

    Feedback do Usuário

    Participe da conversa

    Você pode postar agora e se cadastrar mais tarde. Se você tem uma conta, faça o login para postar com sua conta.
    Nota: Sua postagem exigirá aprovação do moderador antes de ficar visível.

    Visitante

    • Isso não será mostrado para outros usuários.
    • Adicionar um análise...

      ×   Você colou conteúdo com formatação.   Remover formatação

        Apenas 75 emojis são permitidos.

      ×   Seu link foi automaticamente incorporado.   Mostrar como link

      ×   Seu conteúdo anterior foi restaurado.   Limpar o editor

      ×   Não é possível colar imagens diretamente. Carregar ou inserir imagens do URL.


  • Conteúdo Similar

×
×
  • Criar Novo...