Jump to content
  • Sign in to follow this  

    Desativando LD_PRELOAD no Linux

       (1 review)

    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. :)
    Sign in to follow this  


    User Feedback

    Join the conversation

    You can post now and register later. If you have an account, sign in now to post with your account.

    Guest

    Fabiano Furtado

       1 of 1 member found this review helpful 1 / 1 member

    Artigo EXCELENTE! Sempre aprendo algo novo por aqui!

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

    Valeu!

    Share this review


    Link to review

  • Similar Content

    • By Candeer
      Olá, já faz um bom tempo desde do ultimo artigo sobre a construção de debuggers mas, sem mais delongas, vamos dar continuidade a esta série! 😀 
      Neste artigo iremos falar um pouco sobre uma chamada de sistema que é capaz de controlar quase todos os aspectos de um processo: a syscall PTRACE (process trace). Antes de continuarmos, vale ressaltar que todo o código utilizado neste artigo está disponível no repositório do Github.
      De acordo com o manual do Linux (man ptrace), a syscall ptrace é definida assim:
      "A syscall ptrace provê meios para que um processo (denominado "tracer") possa observar, controlar a execução de um outro processo (denominado "tracee"), examinar e modificar a memória e registradores do "tracee". É primariamente utilizado para a implementação de 'breakpoint debugging' e para rastreamento de syscalls".
      Em outras palavras, podemos utilizar a ptrace para controlar um outro processo sobre o qual termos permissões sobre!
      Por exemplo, execute:
      strace /bin/ls O comando "strace" acima, é utilizado para que se possa rastrear todas as syscalls que um programa realiza. Vale lembrar que toda a técnica utilizada para o rastreamento de syscalls envolve o conteúdo abordado nos artigos anteriores, então é de suma importância que você tenha lido (ou saiba) o primeiro artigo sobre Sinais e o segundo sobre Forks.
      Antes de começar a rastrear um dado comando, o strace precisa ter controle total sobre a execução do processo alvo, para isso é feito um fork do processo em questão e o mesmo é "traceado". Voltaremos neste assunto em breve.
      A wrapper da ptrace é definida em <sys/ptrace.h> e tem o seguinte protótipo:
      #include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); Onde o primeiro argumento request é um enum onde cada valor define uma ação em cima do "tracee", tais como TRACEME, GETEREGS, SETREGS e etc. O segundo argumento, pid, é o PID (Process Identification) do processo que queremos "tracear", o terceiro argumento addr é um endereço para alguma interação que a ser realizada da memória do processo "traceado" e o quarto e último argumento data é algum tipo de dado passado para o processo.
      Agora que você ja conhece o formato desta syscall, vamos fazer um pequeno breakdown do comando "strace".
      Execute:
      strace strace /bin/ls 2>&1 | grep -A2 clone Por mais bizarro que o comando acima pareça, o que vamos fazer aqui é rastrear todas as syscalls que o strace faz usando o próprio strace! Como a saída padrão do strace não é o stdout (dê uma lida em standart streams, caso esteja confuso) então é primeiro redirecionar a saída de erro para a saída padrão, para que seja possível rodar o grep no que queremos.
      Estamos buscando aqui, alguma chamada a syscall clone, que é sempre chamada quando é feito um fork. A chamada à ptrace vem logo em seguida:
      clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c4aa8ea10) = 16203 ptrace(PTRACE_SEIZE, 16203, NULL, 0) = 0 Nesse caso, o strace cria um processo filho e em seguida usa o ptrace com o argumento SEIZE para iniciar o rastreamento (tracing) de um processo sem interrompê-lo, como analisaremos em seguida. Dessa maneira o strace é capaz de interceptar cada chamada de sistema feita pelo processo!
      Dê uma olhada no comando ltrace, que diferente do strace, rastreia todas as chamadas à bibliotecas (libraries trace) e tente fazer o mesmo que fizemos acima!
      Algumas ações notáveis que podemos fazer com a ptrace:
      PTRACE_PEEKTEXT, PTRACE_PEEKDATA Ler uma word em um dado endereço. PTRACE_POKETEXT, PTRACE_POKEDATA Copiar uma word para um determinado endereço (injete dados na memória). PTRACE_GETREGS Ler os registradores de um processo, que será guardado na struct user_regs_struct em <sys/user.h>. PTRACE_SETREGS Escrever nos registradores de um processo (também no formato da struct acima). Execute "man ptrace" para uma abordagem mais detalhadas de todos os valores disponíveis. 👍
       
      Implementando um simples tracer
      Agora que já temos uma base de forks e uma ideia de como o ptrace funciona, podemos unificar os dois e tenho certeza que o ptrace irá ficar mais claro. A partir de agora ele é fundamental para a implementação do nosso debugger.
      O primeiro passo é definir o escopo de como será feito o nosso "tracer": vamos rastrear um processo que já esta sendo executado ou vamos criar um novo? Para o nosso debugger, iremos apenas criar um fork e trocar sua imagem de execução para a do programa que queremos debugar, usando uma das funções da família exec.
      Primeiro vamos usar a função execl, que faz parte do leque de funções exec (man 3 exec) que trocam a imagem do nosso processo por outra, ou seja, o nosso programa é realmente trocado por outro em uma execução.
      A função execl é definida como:
      #include <unistd.h> int execl(const char *pathname, const char *arg, ... /* (char *) NULL */); Onde o primeiro argumento pathname é caminho completo do nosso executável alvo e os demais argumentos, que podem ser vários, são os argumentos para o programa que será executado.
      Para seguir um padrão, o primeiro argumento que geralmente colocamos é o caminho do programa em questão (lembrem que no array argv a posição 0 guarda o nome do programa em si), o resto dos argumentos são opcionais e seguem no modelo de lista de argumentos que são delimitados por um argumento NULL, que geralmente usamos para finalizar a lista.
      Agora considere o seguinte exemplo:
      #include <unistd.h> #include <stdio.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; printf("First arg => %s\n", args[0]); execv(command, args); puts("Continua?\n"); return 0; } Compile com
      $ gcc -o exec exec.c $ ./exec /bin/ls -lah Este programa bem simples demonstra como a exec funciona.
      O que acabamos de criar aqui foi uma espécie de wrapper para qualquer comando: ele irá pegar o nome do comando e os seus respectivos argumentos e trocar sua execução atual pela a que você especificou.
      Note também a string "Continue?" que deveria ser impressa na tela. Esta nunca será impressa pois o nosso programa virou de fato, outro.
      Interessante, não? Usando um pouco de criatividade, podemos criar novos processos filhos combinando forks + exec, ou seja, criamos um fork do nosso processo e trocamos sua imagem por outra! Dessa maneira, por exemplo, temos total controle sobre o comando ls.
      Modificando um pouco o código acima e seguindo a ideia de forks, temos:
      #include <stdio.h> #include <sys/types.h> #include <sys/ptrace.h> #include <unistd.h> int main(int argc, char* const* argv) { if (argc < 3) { printf("Usage: %s <command> <args>\n", argv[0]); return 1; } const char* command = argv[1]; char* const* args = &argv[1]; pid_t child_pid = fork(); // Neste ponto, todas as variaveis sao copiadas para o nosso fork // o fork NAO recebe as mesmas variaveis, apenas uma cópia ;) if (!child_pid) { // Hora de transformar nosso fork em outro programa ptrace(PTRACE_TRACEME, NULL, NULL, NULL); execv(command, args); } char in; do { puts("Iniciar processo ? [y/n]: "); in = getchar(); } while (in != 'y'); ptrace(PTRACE_CONT, child_pid, NULL, NULL); return 0; } Compile
      $ gcc -o fork_exec fork_exec. $ ./fork_exec /bin/ls O programa acima realiza os primeiros passos do nosso tracer: é passado o caminho de um programa e os argumentos para o mesmo. Com isso criamos um fork e usamos o ptrace no própio fork com o argumento TRACEME. Este parâmetro indica que o este processo será "traced" pelo seu processo pai. Em seguida trocamos a nossa execução para o nosso programa alvo. Neste momento temos total controle sobre a execução, no exemplo acima, do comando ls.
      Quando um processo inicia sua execução com TRACEME + exec, o mesmo recebe um sinal de interrupção (SIGTRAP) até que o seu processo pai indique que ele deve continuar sua execução. Por isso, o nosso processo pai, que retém o PID do processo filho, usa o ptrace com o argumento CONT para que seja enviado o signal para dar continuidade de execução.
      E depois?
      Agora toda a comunicação entre os processos pai e o filho se dará via sinais e usaremos a syscall wait constantemente.
      Lembra que definimos acima algumas funções que podemos usar em conjunto com a ptrace? Para já irmos adiantando alguns artigos, vamos fazer um programa que mostra o estado dos registradores para um processo, passo a passo. Vamos usar dois parâmetros para a ptrace: GETREGS e STEP. Segue o código:
      #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/ptrace.h> #include <sys/user.h> #include <sys/wait.h> void display_regs(struct user_regs_struct* regs) {     printf("RIP: 0x%x\n", regs->rip);     printf("RBP: 0x%x\n", regs->rbp);     printf("RSP: 0x%x\n", regs->rsp); } int main(int argc, char* const* argv) {     if (argc < 2) {         fprintf(stderr, "Usage: %s <program_path>\n", argv[0]);         return 1;     }     const char* progName = argv[1];          pid_t child = fork();     if (!child) {         ptrace(PTRACE_TRACEME, NULL, NULL, NULL);         execl(progName, progName, NULL);     }          int status;     int options = 0;     int signal;     // Estrutura que mantem os registradores     struct user_regs_struct regs;     /// Capta primeiro sinal de parada do filho     waitpid(child, &status, 0);     signal = WSTOPSIG(status);     if (signal == SIGTRAP) {         printf("Processo alvo %s esperando pronto para iniciar\n\n", progName);     }          printf("Executando 10 instruções\n");     for (int i = 0; i < 10; ++i) {         printf("Passo: %d\n", i+1);         // Executa uma instrução         ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);         // Espera sinal do filho         waitpid(child, &status, 0);         // Copia o estado atual dos registradores         ptrace(PTRACE_GETREGS, child, NULL, &regs);         // Função local para imprimir os principais registradores         display_regs(&regs);         puts("\n\n");     }     puts("Continuando...\n");     /// Continua execução     ptrace(PTRACE_CONT, child, NULL, NULL);     waitpid(child, &status, 0);     printf("Filho saiu com %d\n", WIFEXITED(status));     return 0; }  
      Compile:
      $ gcc -o tracer tracer.c $ ./tracer /bin/ls O código acima, além de criar e rastrear o processo, executa as primeiras 10 instruções e copia os estados dos registradores em cada passo. Logo após, continua a execução do programa normalmente.
      A estrutura user_reg_struct, definida em <sys/user.h>, contém todos os registradores que estão disponíveis na sua arquitetura. O código foi escrito considerando um ambiente x86-64.
      Com o estudo da ptrace, fechamos toda a introdução para construirmos o nosso debugger de fato, que vamos começar a desenvolver no próximo artigo, incialmente com capacidade de por breakpoints, imprimir o atual estado dos registrados e executar instrução por instrução do processo.
      Qualquer dúvida ou correção sinta-se livre de por nos comentários!  😁
      Links úteis:
      Process control Process relationship Code injection with ptrace Sinais Fork Até a próxima!
    • By Candeer
      Olá! No artigo anterior falamos sobre Signals, que é de suma importância para a comunicação entre processos, mas para construir o nosso debugger precisamos muito mais do que apenas isso, precisamos de fato ter total controle sobre um dado processo e se possível controlar até o seu própio início.
      Neste artigo será explicado o que são forks e seu uso em desenvolvimento de aplicações em sistemas UNIX. Sem mais delongas, vamos prosseguir!!!😀
      Resumidamente a syscall fork é usada para a duplicação e criação de um processo. Quando um dado processo chama a função fork(), é criada uma cópia idêntinca de seus dados. Note que apenas uma cópia é feita, o processo filho não compartilha o mesmo espaço de memória do pai.
      A syscall fork retorna um PID que é usado para indetificar em qual processos estamos e também dar acesso ao ID do processo filho. Caso o PID seja 0 estamos executando no filho, caso seja qualquer outro somos o processo pai, isso ocorre pois o pai precisa saber o PID do filho, mas o filho não necessariamente precisa saber o seu própio (da mesma maneira que o seu processo não sabe o própio PID ao menos que o mesmo peça).
      Algo interessante de se notar é que os Init System usados para subir e gerenciar serviços de sua máquina trabalham dessa mesma maneira, você pode checar sua árvore de processo usando comando pstree:
      $ pstree Dessa maneira você tem uma representação bem visual de como está dividida a sua estrutura de processos 😀. Note que todos os processos são filhos do seu Init system (seja ele SystemV, Systemd, etc). Aconselho você explorar o comando pstree para uma visão bem mais detalhada do seu sistema! Outra abordagem é usar o própio comando ps:
      $ ps -ef Rode o comando acima (dependendo da quantidade de processos use um pipe para o less 😉) e com ele teremos uma visão mais detalhada. A coluna PID representa o ID do processo em si e a coluna PPID representa o "Parent Process ID", que nada mais é que o ID do processo pai. Note que o PID 1 é o seu Init System e os seus processos rodam como filho dele!

      Vale notar que o processo Pai do própio init é o PID 0, que é conhecido como "swapper" ou "scheduler", que é o processo responsavel para realização de paging. Paging é o sistema de gerenciamento de memória que salva os dados da RAM em uma memória secundária (HD, SSD e etc) e recupera em formato de páginas (outros PID também são filhos do propio PID 0 como PID 2 que gerencia todas as threads que rodam em Kernel Land(KThread) etc).
       
      Programando Forks
      A syscall fork está na lib  <unistd.h> (Unix Standard library) e tem a seguinte construção:
      #include <sys/types.h> #include <unistd.h> pid_t fork(void); Precisamos incluir a lib <sys/types.h> para que seja possivel acessar o tipo pid_t. A função fork não espera nenhum parâmetro para a sua construção e o código abaixo demonstra o quão simples é cria um fork.
      #include <stdio.h> // Acesso a syscall #include <unistd.h> // Acesso ao tipo variavel pid_t #include <sys/types.h> int main(void) { int x; printf("Processo normal...\n"); printf("Forking...\n"); sleep(5); pid_t pid = fork(); x = 40; if (pid == 0) { printf("Eu sou o processo filho meu PID: %d\n", pid); } else { printf("Eu sou o processo pai de %d\n", pid); } sleep(5); return 0; } Compile o código acima da seguinte forma:
      $ gcc -o fork fork.c $ ./fork Note que o código se "divide" a partir da chamada fork e um if  é usado para saber se estamos executando no pai ou no filho, note também que o pai sabe o PID e o filho não.
      Para melhor visualização o código acima roda por 10 segundos (por conta da chamada ao sleep com esse tempo de espera). Abra um outro terminal e rode o comando:
      $ watch -n1 pstree O comando acima vai executar o pstree a cada 1 segundo, desta forma você verá o exato momento da criação do fork.

      Comunicando-se com o processo fork
      Agora imagine que um  processo precisa esperar o seu filho terminar algum trabalho e dependendo do seu sinal o processo pai realiza alguma ação. A comunicação entre o processo pai e o filho se da por signals. O pai pode saber exatamente o estado do seu processo filho usando a syscall wait e waitpid, ambas na lib <sys/wait.h>:
      #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); A syscall wait espera que ao menos 1 de seus processos filho troque de estado, já a waitpid espera por um processo específico. Como sabemos exatamente qual processo queremos rastrear iremos usar esta call 😀:
      #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(void) { printf("Spliting work...\n"); pid_t pid = fork(); if (!pid) { int a = 0; for(int i = 0; i < 100000000; i++ ) { a += i*2 + 10 *i; } return 9; } int status; int signal; printf("Waiting child finish work...\n"); waitpid(pid, &status, 0); if (WIFEXITED(status)) { signal = WEXITSTATUS(status); printf("Child exited, status = %s\n", strsignal(signal)); } return 1; } Compile o código acima e execute:
      $ gcc -o work work.c $ ./work Spliting work... Waiting child finish work... Child exited, status = Killed Veja que após a chamada de fork nosso processo filho executa várias iterações e realiza um cálculo (um cálculo totalmente randômico) e após isso retorna 9. Este retorno em questão é apenas por motivos educativos (no artigo anterior falamos de sinais e como eles funcionam). O processo pai usa a syscall waitpid para esperar que qualquer signal seja enviada do pid especificado. Após receber um status é verificado se o fork saiu (WIFEXITED) e se sim, pegamos o signal enviado usando WEXITSTATUS(status da saída) e usamos a chamada strsignal(provida pela string.h) para recuperar uma versão em texto do signal. Nesse caso iremos recuperar o signal "KILLED", pois colocamos 9 apenas por razões educativas.
      Normalmente se tudo ocorreu bem colocamos 0 (inclusive é dessa maneira que sua shell avalia se o programa rodou certo).
      $./work && echo "Filho saiu com 0, tudo certo..." || echo "Filho saiu com 1, algo errado..." No caso acima a nossa shell irá criar um fork do nosso work, executar o nosso programa (que por sua vez também executa um fork mas não entra em questão aqui) e se o signal retornado pelo fork for 0 ele imprime uma mensagem, caso contrario ele imprime uma mensagem de erro, dessa maneira você pode orquestrar um shell scripting usando o própio retorno do processo 😉
      Tente mudar o retorno do fork acima e verifique seu status usando funções providas pela <sys/wait.h>. No exemplo acima usamos apenas a call WIFEXITED e WEXITSTATUS, mas existem várias outras.
      Forks são de extrema importância para criação e gerenciamento de processos e iremos usar forks para que seja possível executar o programa que queremos debugar, dessa maneira o software em questão vai ser filho do nosso debugger, o que nós da total controle sobre o mesmo.
      Comentarios são todos bem vindos e todos os códigos usados estão disponíveis no github! 😀

      Links úteis:
          Process Control
          fork
          wait
          Process State
          Fork Bomb - Cuidado com isso
    • By Candeer
      Olá, neste artigo compartilharei um pouco da minha pesquisa no desenvolvimento de debuggers. No momento estou trabalhando em um protótipo de debugger para Linux, mas nada tão avançado quanto um gdb ou radare (muitas coisas são necessárias para chegar neste nível de maturidade de software).
      O desenvolvimento de debuggers é uma atividade muito interessante, já que, em sua forma mais básica, pode ser resumido em uma série de chamadas de sistema (syscalls) para que seja possível o controle do processo a ser depurado (muitas vezes chamado de debuggee) e de seus recursos, mas não vamos colocar a carroça na frente dos cavalos e vamos em partes.
      Antes de começarmos a discutir detalhes mais específicos acerca da depuração de processos, é necessário um entendimento básico de como os mesmos se comunicam na plataforma que vamos desenvolver o tal debugger, no nosso caso, UNIX-like.
      Inter-process communication (IPC)
      IPC é uma forma que processos podem utilizar para se comunicar dentro de um sistema operacional. Existem diversas maneiras de comunicação: via sinais (signals), sockets, etc, mas para a criação de um debugger é apenas necessário usar sinais para a execução.
      Sinais funcionam como uma notificação que pode ser enviada à um processo específico para avisar que algum evento ocorreu.
      É possível também programar um processo para reagir aos sinais de maneira não padrão. Se você já teve um uso razoável de Linux, você provavelmente já enviou sinais à um processo. Por exemplo, quando você aperta Ctrl+C para interromper a execução de um processo, é enviado um sinal do tipo SIGINT, que nada mais é que uma abreviação para Signal Interruption. Se o processo em questão não está preparado para reagir a este sinal, o mesmo é terminado. Por exemplo, considere o seguinte código:
      #include <stdio.h> int main(void) { while(1) printf("hi\n"); return 0; } Ao compilar e executar o código acima e apertar Ctrl+C, o mesmo encerra como esperado, porém podemos verificar que um SIGINT foi enviado usando a ferramenta ltrace, que além de listar chamadas a bibliotecas também mostra os sinais enviados ao processo:
      $ gcc -o hello hello.c $ ltrace ./hello Rode o comando acima e aperte Ctrl+C para verificar o sinal enviado!
      Programando reações a sinais
      A capacidade de enviar sinais a um processo nos dá a possibilidade de saber o que esta acontecendo com algum processo específico que estejamos depurando.
      Para programar reações a algum tipo de sinal, podemos incluir a biblioteca signal, para que possamos usar a função e estrutura (struct) sigaction:
      struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };  
      int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); A struct sigaction nos permite adicionar handlers (tratadores) para nossos sinais, enviando o endereço de nossa função que realiza algum tipo de ação baseada no sinal enviado para o campo sa_handler(sigaction handler).
      Um handler neste contexto nada mais é que uma função que sempre vai ser chamada quando um dado sinal for enviado, dessa maneira podemos executar alguma ação quando recebermos um sinal.
      Já a função sigaction recebe o número do sinal, porém uma série de macros já são pré-definidas e podemos passar como argumento apenas o nome do sinal, como SIGINT por exemplo. A função recebe também a referência da struct previamente definida (struct sigaction) e, caso precise trocar um handler por outro, também recebe no último argumento (oldact) o handler anterior, para que possa ser feita a troca pelo novo. Como não é o nosso caso, vamos passar NULL neste último argumento.
      O código abaixo simula um uso de handlers de sinais, que imprime uma mensagem quando um sinal é enviado:
      #include <stdio.h> #include <signal.h> #include <unistd.h> // sleep void simple_handler(int sig) { printf("Hello SIGINT\n"); } int main() { struct sigaction sig_handler = { simple_handler }; sigaction(SIGINT, &sig_handler, NULL); sleep(1000); return 0; } Ao executar o código acima, aperte Ctrl+C e veja que será imprimido a mensagem do nosso handler!
      O manual da signal contém uma tabela com todos os sinais usados por sistemas POSIX.
      Para enviarmos sinais facilmente em sistemas UNIX podemos usar o comando kill:
      $ kill -l O comando acima mostra todos os sinais e seus respectivos números, com isso podemos fazer algo interessante. Por exemplo, rode o código acima em um terminal separado e use o kill para se comunicar com o seu processo, assim:
      $ ps ax | grep simple_signal $ kill -2 <pid> Primeiro buscamos o PID do nosso processo então usamos o kill que espera como primeiro argumento numero do sinal (listado em kill -l) e o segundo o PID do processo alvo.
      Ao enviar o sinal, podemos ver que o nosso código reage aos sinais que foram associados a um handler especifico! Tente criar handlers para vários sinais e teste usando o comando kill. 😃
      Abaixo um código para demonstrar um uso real de um software que escreve dados aleatórios nos arquivos temporários e antes de uma finalização abrupta, é deletado o que foi usado:
      #include <stdio.h> #include <signal.h> #include <unistd.h> // Log errors void fatal(const char* err_msg) { fprintf(stderr, "Error: %s\n", err_msg); } // Escreve algo random em um arquivo void random_work() { FILE* temp_files = fopen("/tmp/foo", "w"); if (!temp_files) { fatal("Cant open foo!"); } else { fprintf(temp_files, "%s", "Random random random!\n"); fclose(temp_files); } } // Handler para deleta arquivos criados void handler_termination(int sig) { // Verifica se existe usando a function access // Caso existe usa a syscall unlink para remover o arquivo if (access("/tmp/foo", R_OK) < 0) return; unlink("/tmp/foo"); printf("All clean! closing...\n"); } int main() { //struct sigaction que recebe a function handler_termination como valor do seu handler struct sigaction interruption_handler; interruption_handler.sa_handler = handler_termination; // Syscall sigaction que associa o nosso handler para um sinal especifico // O ultimo campo NULL, espera o handler anterior para que posso tornar o novo handler o default sigaction(SIGINT, &interruption_handler, NULL); random_work(); sleep(1000); handler_termination(0); return 0; } Dica: Dê uma olhada na tabela de sinais e crie handlers para o mesmo código acima!
      Para a construção do nosso debugger iremos focar mais no signal SIGTRAP, para que seja possível detectar se o nosso processo sofreu uma "trap" da CPU. Uma trap ocorre quando acontece alguma interrupção síncrona na execução, que faz o processo ficar parado até que o sistema operacional execute alguma ação. Isto será usado para implementar e interpretar breakpoints. Veremos tudo isso com mais detalhes em breve!
      Sinta-se livre para comentar e sugerir correções e melhorias. Até o próximo artigo!
      Links úteis:
      Syscall IPC CERO 11 – Linux Syscalls Syscalls, Kernel mode vs User mode Programação em C
    • By Fernando Mercês
      Apesar de ser um assunto que já foi tratado em muitos sites e fóruns, continuo a observar que alguns usuários do Windows devem ter mais atenção com o tipo de arquivo que aplica o famoso duplo-clique. Identificar se o arquivo é inofensivo ou se é uma ameaça pode ser mais fácil do que se imagina. Este artigo tem como intenção desmistificar as lendas sobre as extensões de arquivos maliciosos e alertar sobre as extensões perigosas.
      O sistema operacional Windows reconhece (deduz o conteúdo) dos arquvios por sua extensão. Mas o que é extensão de arquivo?
      Extensão de arquivo para o MS-Windows, comumente são os três caracteres após o ponto, no nome completo do arquivo. Por exemplo, documento001.txt, é um arquivo com extensão .TXT, logo, o Windows deduzirá que é um documento de texto e associará este documento à um certo programa, que poderá abrí-lo (recebendo seu caminho como parâmetro) mediante um duplo-clique neste documento.
      Confundiu? Bom, em termos práticos, cada extensão de arquivo que mereça, possui um determinado programa responsável por interpretar um duplo-clique num arquivo que possua tal extensão. Essas informações (quais extensões são abertas por qual programa) ficam no registro do Windows.
      Ainda com o exemplo do documento001.txt, vamos analisar sua associação: ao dar um duplo-clique nele, vemos que ele é aberto pelo Bloco de Notas (notepad.exe).
      A chave do registro responsável por armazenar as informações desta associação é, no Windows XP: HKEY_CLASSES_ROOT\txtfile\shell\open\command. Veja a imagem:

      Perceba que na coluna "Dados" há o caminho completo do notepad.exe (usando uma variável de sistema sim, mas não deixa de ser o caminho absoluto), seguido de %1. Já sabemos que é o Bloco de Notas (notepad.exe) que abrirá arquivos de texto, agora vamos entender o parâmetro.
      Eu escrevi mais acima que o caminho do arquivo a ser aberto era passado por parâmetro. É justamente isso que o "%1" faz. Essa variável armazena o caminho absoluto do arquivo que está sendo acessado, no instante do acesso. Portanto, se você clicou no arquivo documento001.txt e ele está em C:\docs\, esta variável conterá o valor C:\docs\documento001.txt. Isso informa ao Bloco de Notas onde está o arquivo.
      Agora que já sabemos o que é e como funciona a extensão, vamos aos riscos.
      Um arquivo executável precisa ter uma extensão de executável para ser executado. As extensões mais comuns de arquivos executáveis são: EXE, COM, BAT, VBS, MSI, SCR, arquivos do Office (porque podem conter macros, que são executáveis).
      Um vírus, obrigatoriamente, tem que ter uma dessas extensões. Ou seja, ele tem que ser um executável.
      O problema é que há certos disfarces utilizados pelos disseminadores de vírus. Um deles é colocar um nome de arquivo do tipo: "arquivo.jpg .exe". Assim mesmo, com vários espaços entre o .jpg e o .exe. A extensão deste arquivo de exemplo é .EXE e ele será executado como tal! Não é .jpg! O texto ".jpg" neste caso faz parte do nome do arquivo e não da extensão. A "técnica" de colocar espaços é para que os programas de email e webmail identifiquem um nome muito grande e exibam reticências após o .jpg, dando a impressão que é um arquivo de imagem.
      Detalhe que o ícone de um executável pode ser facilmente alterado para o ícone de uma imagem, o que aumenta as chances da vítima de ser enganada.
      Os vírus são programas. Logo, repito, são executáveis. Um arquivo de áudio puro, por exemplo, não pode ser vírus! A extensão .mp3 estaria associada ao Windows Media Player, ou Winamp, ou qualquer outro. Esses software não executam rotinas de executáveis. Só entendem fluxo de mídia, portanto, não executam as rotinas virais diretamente. Mas eles podem conter falhas que sejam exploradas através de payloads maliciosos de arquivos de mídia ou playlists (como os .m3u).
      Tenha certeza da extensão do arquivo e de sua procedência, e estará praticamente livre de ser infectado por um vírus anexo à um e-mail ou disponível para download. Mas lembre-se que os arquivos podem também estar zipados (.ZIP) ou compactados com outro compactador (RAR, LZIP, GZIP, LHA, JAR, etc). Dentro deles é que você deve examinar a extensão do arquivo.
    • By Fernando Mercês
      A maioria dos vírus e pragas virtuais compartilham de certos métodos de auto-inicialização com o SO. Isto inclui os spywares e seus similares.
      Os sistemas Windows possuem métodos para inicializar programas junto ao seu carregamento limitados. Na maioria das vezes os vírus iniciam justamente por eles e daí a importância de conhecê-los e saber gerenciá-los. Desta forma, o técnico pode remover manualmente muitas pragas, o que economizará tempo com scans de softwares antivírus e anti-spys, além de ser extremamente útil quando o vírus ataca estes softwares de proteção, impedindo sua inicialização.
      Primeiro vamos ver de que jeito um aplicativo pode ser iniciado junto ao Windows. A maior parte dos vírus age deste jeito.
      Em sistemas baseados em Windows NT, o que inclui os Windows 2000, XP e 2003, os métodos de inicialização de programas são:
      Através da pasta Inicializar do Menu Iniciar. No registro do sitema. Por serviço de sistema (o que inclui certa parte do registro). Na primeira maneira, basta colocar um atalho para o programa que se deseja executar na pasta Inicializar do Menu Iniciar. Por exemplo, na imagem abaixo, a cada inicialização do Windows, inicializaremos a Calculadora junto.

      Obviamente um vírus pode se aproveitar deste recurso e colocar um atalho para si neta pasta mas não é comum isso acontecer pois o vírus ficaria facilmente visível e uma das intenções de vírus complexos é passar despercebido ao usuário/técnico. De qualquer forma, não custa conferir.
      Agora vamos ao método mais usado, o registro do sistema. Aqui precisaremos explicar resumidamente como o registro destas versões do Windows funciona.
      O registro é um banco de dados que armazena informações essenciais sobre diversos softwares instalados no Windows, além de informações pertinentes ao próprio sistema. Por conta disto, é comum apelidar o registro de “alma do sistema”.
      Esse banco de dados possui, além de outros dados, chaves, sub-chaves e valores numa organização hierárquica (similar ao Windows Explorer).
      Para o artigo, precisaremos conhecer essas três chaves:
      HKEY_LOCAL_MACHINE – Esta é a chave mais importante do registro. Nela estão contidas informações sobre o PC (hardware instalado, softwares com sua opções e configurações e outros itens). Inclusive veremos que um programa pode ser inicializado por uma sub-chave desta chave. HKEY_USER – Nesta chave são definidas configurações personalizadas para cada usuário do sistema, já que as versões do Windows mais novas permitem logon simultâneo ou não de usuários diferentes. De maneira similar à chave anterior, um vírus pode inicializar-se junto ao SO somente para um usuário específico, usando uma sub-chave desta chave. HKEY_CURRENT_USER – Como o nome sugere, mantém informações sobre o usuário que está atualmente logado no sistema. Todo o registro é dinâmico mas esta chave merece uma definição de dinamismo especial pois muda os valores de suas sub-chaves completamente quando logamos com outro usuário no Windows. Não é difícil de deduzir que ela é um atalho para uma sub-chave de HKEY_USER, já que esta última mantém uma sub-chave para cada usuário cadastrado no sistema. Por exemplo, se logarmos com o usuário “Fernando”, esta chave será uma cópia da sub-chave HKEY_USER. SID (Security Identifier) é uma identificação única que cada usuário tem e o SO conhece os usuários através deste SID. Abaixo, o utilitário “regedit” (Registry Editor), usado para visualizar o conteúdo do registro do sitema.

      Perceba a igualdade entre as áreas destacadas em vermelho. É justamente o que falamos na explicação da chave HKEY_CURRENT_USER. Note o SID do meu suário também.
      Depois desta breve introdução ao registro do sistema, podemos partir para as sub-chaves que realmente importam na questão da remoção manual de vírus.
      São elas:
      HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRun HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRunOnce HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRunOnceEx HKEY_USERS\SOFTWAREMicrosoftWindowsCurrentVersionRun HKEY_USERS\SOFTWAREMicrosoftWindowsCurrentVersionRunOnce As duas últimas chaves acima dependem do SID do usuário mas se a suspeita de vírus for no usuário que está logado, você pode acessá-las pelo atalho como comentamos acima.
      Tudo o que estiver nestas chaves será inicializado junto ao sistema. Faça o teste: verifique o que tem nas sub-chaves de seu PC e veja os caminhos para os arquivos que incializam. No exemplo HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRun da máquina que usei, está assim.

      Como podemos ver, só há três valores, que são os caminhos absolutos dos executáveis que inicializam junto à máquina usada. Esta é uma máquina virtual. Numa máquina real, os valores normais são outros.
      Para remover programas da inicialização, basta remover os valores desejados.
      É importante salientar que muitos arquivos presentes nestas sub-chaves são essenciais ao sistema e não devem ser removidos. Cabe ao técnico saber identificar caminhos e nomes de executáveis suspeitos. Se você tiver dúvida quando à procedência de algum arquivo, pode digitar seu nome no site Process Library, que mantém uma lista atualizadas de processos (programas em execução) para nos ajudar a identificar se são do sistema ou não.
      Há ainda o método de serviços que podem ser utilizado por alguns vírus mais complexos. A tela de serviços você tem acesso indo no menu Iniciar > Executar, digitando services.msc e clicando no botão OK. Eis a da máquina que usamos para o artigo.

      Esses serviços são na verdade processos (programas) inicializados que recebem este nome pela capacidade de poderem ser muito melhor gerenciados pelo sistema operacional que um processo comum. Perceba a coluna “Status” na imagem. Um serviço pode ser iniciado, reiniciado, pausado ou parado e seu método de inicialização pode ser manual (quando clicado), automático (a cada inicialização do sistema) ou desativado. Um vírus obviamente se aproveitaria do método automático e poderíamos pará-lo e depois desativá-lo numa remoação manual.
      Assim como os processos comuns, a maioria dos serviços é essencial ao sistema, portanto, é bom que se faça uma pesquisa sobre ele (no Google e sites similares) antes de parar ou desativar um serviço suspeito ou não.
      Ao entrarmos nas propriedades de um serviço, vemos o caminho do executável ao qual ele se refere além de uma caixa drop-down para alterar o tipo de inicialização, como mostra a imagem abaixo.

      Com este básico conhecimento, muitos vírus podem ser removidos mas é claro que não basta. Recomendamos sempre um scan com um bom antivírus atualizado e com um anti-spy, mesmo após a remoção manual.
      Existem alguns programas que podem ajudar na identificação de vírus e na remoção manual. Abaixo segue uma lista com descrição:
      Process Explorer – monitora os processos em execuçao em tempo real, o que permite identificarmos se algum processo suspeito está sendo executado. O Process Explorer também mostra o que o processo está fazendo ou tentou fazer (quando bloqueado pelo SO). HiJackThis – gera uma lista e um arquivo de log com todos os processos que inicializam junto ao sistema, podendo ser utlizado inclusive para remover o que sejam julgados suspeitos (muito cuidado com seu julgamento). NOTA: O projeto original foi descontinuado, mas o usuário Polshyn Stanislav da Ucrânia o continuou. Gmer - uma aplicação que detecta e remove rootkits (dentro de um limite, claro). A ideia é procurar por uma lista de coisas escondidas, tais como processos, threads, serviços, arquivos etc. MSCONFIG – utilitário presente no Windows XP mas que pode ser copiado a partir do arquivo msconfig.exe para outros sitemas Windows. Ele mostra de forma interativa o que está sendo inicializado junto ao sistema além de permitir a consulta de outras informações. Você pode chamá-lo a partir do menu executar.
×
×
  • Create New...