Ir para conteúdo
  • Desativando LD_PRELOAD no Linux

       (2 análises)

    Fernando Mercês

    preloading é um recurso suportado pelo runtime loader  de binários ELF implementado na glibc (GNU C Library), mais especificamente no arquivo rtld.c. Ele consiste em carregar uma biblioteca antes de todas as outras durante o carregamento de um programa executável. Assim é possível injetar funções em programas, inspecionar as funções existentes, etc. Por exemplo, considere o programa ola.c abaixo:

    #include <stdio.h>
    
    void main() {
    	printf("ola, mundo do bem!");
    }

    Ao compilar e rodar, a saída é conforme o esperado:

    $ gcc -o ola ola.c
    $ ./ola
    ola, mundo do bem!

    A função printf() foi utilizada com sucesso pois este binário foi implicitamente linkado com a glibc graças ao gcc. Veja:

    $ ldd ola
    linux-vdso.so.1 (0x00007ffe4892b000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a3a2dd000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f8a3a692000)

    E portanto a função printf() é resolvida. Até aí nenhuma novidade. Agora, para usar o recurso do preloading, temos que criar uma biblioteca, que será carregada antes da glibc (libc6). A ideia é fazer com o que o binário chame a nossa printf() e não a da glibc. Isso pode ser chamado de usermode hook (incompleto, porém, já que eu repassei o argumento para a função puts() ao invés da printf() original da glibc). Considere o código em hook.c:

    #include <stdio.h>
    
    int printf(const char *format, ...) {
        puts("hahaha sua printf tah hookada!");
        return puts(format);
    }

    O protótipo da printf() é o mesmo do original (confira no manual). Eu não reimplementei tudo o que precisaria para ela aqui, somente o básico para ajudar na construção do artigo. E como expliquei antes, o hook não está completo uma vez que eu passo o que recebo na minha printf() para a função puts() da glibc. O ideal seria passar para a printf() original mas para isso eu precisaria buscar o símbolo, declarar um ponteiro de função, etc. E o assunto desde artigo não é hooking de funções.

    Por hora vamos compilar a biblioteca:

    $ gcc -shared -fPIC -o hook.so hook.c
    $ ldd hook.so
    linux-vdso.so.1 (0x00007ffffadb8000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f011dfbc000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f011e572000)

    E agora precisamos instruir o loader a carregá-la antes de todas as outras quando formos executar o nosso programa (ola). Há pelo menos duas formas de acordo com a documentação oficial:

    1. Definir uma variável de ambiente LD_PRELOAD contendo o endereço de uma ou mais bibliotecas para serem carregadas.
    2. Colocar o path de uma ou mais bibliotecas num arquivo /etc/ld.so.preload (caminho e nome são fixos aqui).

    Então vamos testar. Primeiro uma execução normal, depois com a variável LD_PRELOAD setada e finalmente com o recurso do arquivo /etc/ld.so.preload:

    ## Execução normal
    $ ./ola
    ola, mundo do bem!
    
    ## Com caminho em variável de ambiente
    $ export LD_PRELOAD=$PWD/hook.so
    $ ./ola
    hahaha sua printf tah hookada!
    ola, mundo do bem!
    
    ## Com caminho em arquivo
    $ unset LD_PRELOAD
    # echo $PWD/hook.so > /etc/ld.so.preload
    $ ./oi
    hahaha sua printf tah hookada!
    hello world

    Percebe o perigo? Não é à toa que existem vários malware para Linux utilizando este recurso. Alguns exemplos são os rootkits Jynx, Azazel e Umbreon. Além disso, algumas vulnerabilidades como a recente CVE-2016-6662 do MySQL dependem deste recurso para serem exploradas com sucesso. É razoável então um administrador que não utilize este recurso num servidor em produção querer desabilitá-lo, certo?

    Desabilitando o preloading

    Não há mecanismo no código em questão da glibc que permita desabilitar este recurso. Pelo menos eu não achei. Uma saída é alterar os fontes e recompilar, mas a glibc demora tanto pra ser compilada que eu desisti e optei por fazer engenheira reversa no trecho necessário e verificar quão difícil seria um patch. Analisando o fonte do rtld.c fica fácil ver que a função do_preload() retorna o número de bibliotecas a serem carregadas no preloading. Primeiro a checagem é feita na variável de ambiente LD_PRELOAD:

    index.php?app=core&module=system&controldo_preload_var.thumb.png.1394c656c9f480366af7f15131c7c125.png

    O número de bibliotecas é armazenado na variável npreloads., que mais tarde alimenta uma repetição para de fato carregar as bibliotecas.

    Mais abaixo, o vemos que o trecho de código que busca o arquivo /etc/ld.so.preload também usa a do_preload():

    do_preload_file.thumb.png.97a99cec288ede68a5e7b0295413e048.png

    Sendo assim veio a ideia de encontrar essa função no loader (no meu caso /lib64/ld-linux-x86-64.so.2 – mas pode estar em /lib para sistemas x86 também) e patchear lá diretamente.

    PS.: Apesar de o código ser parte da glibc, a biblioteca do loader é compilada separadamente e tem um nome tipo ld-linux-$ARCH.so.2, onde $ARCH é a arquitetura da máquina. No meu caso, x86-64.

    Fiz uma cópia do arquivo /lib64/ld-linux-x86-64.so.2 para o diretório $HOME para começar a trabalhar.  Pelo visto ela é compilada sem os símbolos, o que elimina a hipótese de achar a função por nome de forma fácil:

    $ nm ld-linux-x86-64.so.2
    nm: ld-linux-x86-64.so.2: no symbols

    Sem problemas. Com o HT Editor, um editor de binários com suporte a disassembly, abri o arquivo e busquei pela string “/etc/ld.so.preload” já que ela é fixa na função, que deve referenciá-la. A ideia foi chegar no trecho de código que chama a função do_preload(). Os passos são:

    • Abrir a biblioteca no hte:
    $ hte ld-linux-x86-64.so.2

    No hte, facilita se mudarmos o modo de visualização para elf/image com a tecla [F6]. Depois é só usar [F7] para buscar pela string ASCII /etc/ld.so.preload:

    hte_search_string.thumb.png.84007dfe7325e1d2d7942951344db013.png

    Após achar a string percebemos que ela é referenciada (; xref) em 4 lugares diferentes. Um desses trechos de código também deve chamar a função do_preload() que é a que queremos encontrar.

    hte_xref.thumb.png.5ecf814222e60f5d904a88b02626bf2a.png

    Depois de analisar cada um deles, percebemos que tanto na r4294 quando na r4302 logo depois da referência à string tem uma CALL para uma função em 0xae0 que ao seguir com o hte (apertando [ENTER] no nome dela) é mostrada abaixo:

    hte_sub_ae0.thumb.png.0d4fd026c0ae55f1227b2b6e17b724b1.png

    Se comparamos com o código da função do_preload() vemos que se trata dela:

    do_preload.thumb.png.00f818186b788670a33e41379e83b9ed.png

    A ideia é forçar que ela retorne 0, assim quando ela for chamada seja pelo trecho de código que carrega as bibliotecas a partir da variável LD_PRELOAD ou pelo trecho responsável por ler o arquivo /etc/ld.so.preload, ela vai sempre retornar 0 e vai fazer com que o loader não carregue as bibliotecas. Para isso, desça até o trecho de código do salto em 0xb37. Perceba que ele salta para 0xb56 onde o registrador EAX é zerado com um XOR, e depois o registrador AL (parte baixa de AX, que por sua vez é a parte baixa de EAX) é setado para 1 pela instrução SETNZ caso a condição em 0x58 não seja atendida (linha 675 no código-fonte). Só precisamos fazer com que esta instrução SETNZ em 0xb5e não seja executada para controlar o retorno da função.

    hte_salto.thumb.png.2ffbd69b21cc46e2b1521934a67b46fd.png

    Ao pressionar [F4], entramos no modo de edição. Há várias maneiras de fazer com que esta instrução em 0xb5e não execute, mas vou fazer a mais clássica: NOPar seus 3 bytes. No modo de edição, substitua os bytes da instrução SETNZ AL (0f 95 c0) por 3 NOP’s (90 90 90), ficando assim:

    hte_nop.thumb.png.fe50a33da8f84745d300c0214707920b.png

    Dessa forma, o EAX é zerado em 0xb56, a comparação ocorre em 0xb58 mas ele não é mais alterado, tendo seu conteúdo zerado até o retorno da função. [F2] para salvar.

    Agora para testar vou usar duas técnicas combinadas. A primeira é de declarar uma variável de ambiente só para o contexto de um processo. A outra é de usar o loader como se fosse um executável (sim, ele pode receber o caminho de um binário ELF por parâmetro!). Veja:

    $ LD_PRELOAD=$PWD/hook.so ./ld-linux-x86-64.so.2 ./ola
    Inconsistency detected by ld.so: rtld.c: 1732: dl_main: Assertion `i == npreloads' failed!

    Para nosso azar, o loader checa o número de funções a serem carregadas dentro de uma repetição, fora da função do_preload(). Precisamos achar essa confirmação (assertion) para patchear também. Usando a mesma técnica de buscar pela string primeiro (nesse caso busquei pela string “npreloads” exibida no erro) você chega na referência r3148:

    hte_npreloads.thumb.png.c34887a5fd8f6a69c390368c72c0a512.png

    Que te leva diretamente para a repetição da assert():

    hte_assert_loop.thumb.png.028baa77c849222afb9759e0f009b3d4.png

    Comparando com o fonte:

    assert_npreloads.thumb.png.e150b41ce0b2c8a60e30946ccb85aad5.png

    Para o salto em 0x3134 sempre acontecer e a CALL de erro em 0x3154 não executar, resolvi patchear a instrução JZ para que sempre pule para 0x2d60. No modo de edição dá pra ver que há um JMP negativo (salto para trás) em 0x315f de 5 bytes, conforme a figura:

    hte_assert.thumb.png.d6acb8859adf8d2532de5c68a931a1a9.png

    Podemos usá-lo só para copiar o opcode. ;)

    Como em 0x3134 temos 6 bytes, NOPamos o primeiro e copiamos o opcode do JMP negativo (que é 0xe9), ficando assim:

    hte_assert_patched.thumb.png.e1c7f69eb8b221d3c819621b98051f88.png

    Após salvar e testar, voilà:

    ## Com variável de ambiente
    $ LD_PRELOAD=$PWD/hook.so ./ld-linux-x86-64.so.2 ./ola
    ola, mundo do bem!
    
    ## Com arquivo
    # echo $PWD/hook.so > /etc/ld.so.preload
    $ ./ld-linux-x86-64.so.2 ./ola
    ola, mundo do bem!

    Agora se você for bravo o suficiente é só substituir o loader original para desativar completamente o recurso de preloading e ficar livre de ameaças que abusam dele.

    Fica também o desafio para quem quiser automatizar este processo de alguma maneira e/ou trabalhar na versão de 32-bits do loader.  O Matheus Medeiros fez um script maneiro para automatizar o patch! Valeu, Matheus!

    Patches de código e recompilação seriam melhores opções, de fato, mas quis mostrar uma maneira usando engenharia reversa por três motivos:

    1. Se automatizada, pode ser mais fácil de ser colocada em prática em um ambiente em produção.
    2. Recompilar a glibc demora muito. Se alguém souber de uma maneira de recompilar somente o loader, por favor, me avise!
    3. Engenharia Reversa é divertido. :)

    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.


    Fabiano Furtado

       1 de 1 membros acharam esta análise útil 1 / 1 membro

    Artigo EXCELENTE! Sempre aprendo algo novo por aqui!

    Vou postar um comentário, pois estou com algumas dúvidas.

    Valeu!

    Link para a análise
    Compartilhar em outros sites


  • Conteúdo Similar

×
×
  • Criar Novo...