Ir para conteúdo
  • Construindo seu debugger - Parte 3: ptrace

       (2 análises)

    anderson_leite

    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:

    Até a próxima!

    Editado por Fernando Mercês


    Revisão: Fernando Mercês

    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.


    Visitante seijinz

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

    sensacional!

    antigamente era difícil achar artigos assim em português, só tinha nas revistas phrack e 29A

     

    parabéns

    Link para a análise
    Compartilhar em outros sites


  • Conteúdo Similar

×
×
  • Criar Novo...