Aventura com ptrace (2)

Em Habré já escreveu sobre interceptação de chamadas de sistema por meio de ptrace ; Alexa escreveu sobre este post muito mais detalhado, que eu decidi traduzir.


Por onde começar


A comunicação entre o programa depurado e o depurador ocorre usando sinais. Isso complica muito as coisas já difíceis; por diversão, você pode ler a seção BUGS do man ptrace .

Há pelo menos duas maneiras diferentes de iniciar a depuração:

  1. ptrace(PTRACE_TRACEME, 0, NULL, NULL) tornará o pai do processo atual um depurador para ele. Nenhuma assistência é necessária dos pais; man aconselha gentilmente: “Um processo provavelmente não deve fazer essa solicitação se seus pais não esperam rastreá-la.” (Em outro lugar do homem, você viu a frase “provavelmente não deveria” ?) Se o processo atual já tiver um depurador, a chamada falhará.
  2. ptrace(PTRACE_ATTACH, pid, NULL, NULL) tornará o processo atual um depurador para o pid . Se o pid já tiver um depurador, a chamada falhará. SIGSTOP enviado ao processo de depuração e não continuará funcionando até que o depurador o descongele.

Esses dois métodos são completamente independentes; Você pode usar um ou outro, mas não faz sentido combiná-los. É importante observar que PTRACE_ATTACH não é instantâneo: depois que ptrace(PTRACE_ATTACH) é chamado, geralmente waitpid(2) é PTRACE_ATTACH para aguardar até que PTRACE_ATTACH "funcione".

Você pode iniciar o processo filho em depuração usando PTRACE_TRACEME seguinte maneira:

 static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); /*   ,   . */ if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); /*  . */ execvp(argv[0], argv); /*     . */ die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; /* ,       . */ if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } /*      ptrace,    . */ /* *  ,      *  ,      . *    --  API  ptrace! */ /*       PTRACE_SYSCALL. */ } /* (argc, argv) --    ,    . */ void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); } 

Sem uma chamada de raise(SIGSTOP) , pode ser que o execvp(3) seja executado antes que o processo pai esteja pronto para isso; e as ações do depurador (por exemplo, interceptando chamadas do sistema) não serão iniciadas no início do processo.

Quando a depuração é iniciada, cada ptrace(PTRACE_SYSCALL, pid, NULL, NULL) irá "descongelar" o processo depurado até a primeira entrada na chamada do sistema e depois até a chamada do sistema sair.

Montador telecinético


ptrace(PTRACE_SYSCALL) não retorna nenhuma informação ao depurador; ele simplesmente promete que o processo que está sendo depurado será interrompido duas vezes a cada chamada do sistema. Para obter informações sobre o que está acontecendo com o processo depurado - por exemplo, em qual sistema o sistema parou -, é necessário subir em uma cópia de seus registros armazenados pelo kernel em um struct user em um formato que depende da arquitetura específica. (Por exemplo, em x86_64, o número da chamada estará no campo regs.orig_rax , o primeiro parâmetro passado estará em regs.rdi , etc.) Alexa comenta: “parece que você está escrevendo código de montagem em C que funciona com os registros do processador remoto.”

Em vez da estrutura descrita em sys/user.h , pode ser mais conveniente usar as constantes de índice definidas em sys/reg.h :

 #include <sys/reg.h> /*    . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /*      . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif } 

Nesse caso, duas paradas do processo depurado - na entrada da chamada do sistema e na saída dela - não diferem de forma alguma do ponto de vista do depurador; para que o próprio depurador se lembre do estado de cada um dos processos depurados: se houver vários, ninguém garante que um par de sinais de um processo chegue em uma linha.

Descendentes


Uma das opções do ptrace , PTRACE_O_TRACECLONE , garante que todos os filhos do processo depurado sejam depurados automaticamente quando saírem do fork(2) . Um ponto sutil adicional aqui é que os descendentes tomados para depuração se tornam "pseudo-filhos" do depurador, e o waitpid responderá não apenas à interrupção de "filhos imediatos", mas também para parar de depurar "pseudo-filhos". O homem alerta sobre isso: “A configuração do sinalizador WCONTINUED ao chamar waitpid (2) não é recomendada: o estado“ continuado ”é por processo e o consumo pode confundir o verdadeiro pai do rastreado”. - ou seja, “Pseudo-filhos” têm dois pais que podem esperar que parem. Para o programador do depurador, isso significa que waitpid(-1) aguardará não apenas os filhos imediatos pararem, mas também qualquer processo de depuração.

Signals


(Conteúdo bônus de um tradutor: esta informação não está no artigo em inglês)
Como já mencionado no início, a comunicação entre o programa depurado e o depurador ocorre usando sinais. Um processo recebe o SIGSTOP quando um depurador está conectado a ele e, em seguida, o SIGTRAP toda vez que algo interessante acontece no processo que está sendo depurado - por exemplo, uma chamada do sistema ou um sinal externo. O depurador, por sua vez, recebe o SIGCHLD cada vez que um dos processos depurados (não necessariamente o filho imediato) "congela" ou "congela".

ptrace(PTRACE_SYSCALL) processo depurado é realizado chamando ptrace(PTRACE_SYSCALL) (antes do primeiro sinal ou chamada do sistema) ou ptrace(PTRACE_CONT) (antes do primeiro sinal). Quando os sinais SIGSTOP/SIGCONT também são usados ​​para finalidades não relacionadas à depuração, podem surgir problemas com o ptrace : se o depurador "descongela" o processo depurado que recebeu o SIGSTOP , do lado de fora parecerá que o sinal foi ignorado; se o depurador não "descongelar" o processo que está sendo depurado, o SIGCONT externo não poderá "descongelar" o processo.

Agora, a parte divertida: o Linux proíbe os processos de depuração , mas não impede a criação de loops quando um pai e um filho depuram um ao outro. Nesse caso, quando um dos processos recebe qualquer sinal externo, ele "congela" via SIGTRAP - o SIGCHLD enviado para o segundo processo e também "congela" via SIGTRAP . É impossível tirar esses "co-depuradores" do impasse enviando o SIGCONT de fora; a única maneira é matar ( SIGKILL ) a criança, e o pai sairá da depuração e "congelará". (Se você matar um pai, o filho morrerá com ele.) Se o filho PTRACE_O_EXITKILL opção PTRACE_O_EXITKILL , o pai depurado por ele também morrerá.

Agora você sabe como implementar um par de processos que, ao receber qualquer sinal, congelam para sempre e morrem apenas juntos. Por que isso pode ser necessário na prática, não vou explicar :-)

Source: https://habr.com/ru/post/pt439882/


All Articles