Às vezes, uma necessidade pode aumentar para iniciar uma thread em um processo separado, e a necessidade não é necessariamente mal-intencionada. Por exemplo, alguém pode querer substituir as funções da biblioteca ou colocar algum código entre o executável e uma função de biblioteca. No entanto, o Linux não oferece uma chamada de sistema que faria qualquer coisa semelhante à API do Windows CreateRemoteThread, apesar do fato de eu ver pessoas à procura de tal funcionalidade.
Você pode pesquisar por “equivalente a CreateRemoteThread em Linux” e ver que pelo menos 90% dos resultados acabam em algo como “por que você iria querer fazer isso?”. Há um certo tipo de pessoas em fóruns pensando se eles não têm uma resposta, então, provavelmente, isso não existe e ninguém jamais precisou disso. Outros realmente acreditam que, se eles sabem por quê, eles podem te dizer como fazer isso de outra maneira. O último caso é por vezes verdade, mas, na maioria das vezes, a solução que está sendo solicitada é a única aceitável, e é isso que as pessoas se recusam a entender.
Então, vamos dizer que você precisa injetar uma thread em um processo em execução, por qualquer razão (pode ser que deseje realizar uma “injeção de DLL” da forma como o Linux – isso é com você). Entretanto, não há uma chamada de sistema específica para permitir isso, há uma abundância de outras chamadas de sistema e funções de biblioteca que “felizmente” irão ajudá-lo.
ptrace() inevitável
Na primeira vez em que você dá uma olhada no ptrace(), é um pouco assustadora (como ioctl()) – uma função, muitas solicitações possíveis e vai saber quando e qual parâmetro está sendo ignorado. Na prática, é bastante simples. Essa função é utilizada por depuradores e nos casos em que um tem que controlar a execução de um processo por qualquer razão. Nós usaremos essa função para a injeção de thread neste artigo.
A primeira coisa você vai querer fazer é anexar ao processo de destino:
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
PTRACE_ATTACH – pedido para anexar a um processo em execução;
pid – a ID do processo que você deseja anexar.
Se o valor de retorno for igual ao pid do processo de destino – voilà, você está conectado. Se for -1, no entanto, isso significa que ocorreu um erro e você precisa verificar errno para saber o que aconteceu. Você deve ter em mente que em determinados sistemas você pode não ser capaz de se conectar a um processo que não é descendente do conector ou que não tenha sido especificado como tracer (usando prctl()). Por exemplo, no Ubuntu, desde o Ubuntu 10.10, essa é exatamente a situação. Se você quiser mudar isso, então você precisa localizar o arquivo ptrace.conf e definir scope ptrace para 0.
Já que eu estou usando o Ubuntu e só posso anexar a um processo filho (a menos que eu queira alguma dor de cabeça adicional), é isso que vou abordar neste artigo.
Preparativos
O primeiro passo, assim como no caso do Windows, é que você precisa escrever um injector. Ele carregará o processo victim, injetar o shellcode e a saída. Essa é a parte mais simples, e a estrutura do tal carregador seria assim:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
int main(int argc, char** argv)
{
pid_t pid;
int status;
if(0 == (pid = fork()))
{
// We are in the child process, so we just ptrace() and execl()
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl(*(argv+1), NULL, NULL);
}
else
{
// We are in the parent (injector)
ptrace(PTRACE_SETOPTIONS, pid, PTRACE_O_TRACEEXEC, NULL);
// Wait for exec in the child
waitpid(pid, &status, 0);
// The rest of the code comes here
}
return 0;
}
Como você pode ver, os forks de carregamento se ramificam e então se comportam de acordo com o valor de retorno da função fork(). Se ele retorna 0, isso significa que estamos no processo filho (na verdade, você deve verificar se ele retornou -1, o que indicaria um erro), caso contrário, ele é um pid do processo filho e estamos no pai.
FIlho
O código filho não possui muitas coisas para fazer. Tudo o que precisa ser feito é dizer ao sistema operacional que ele pode ser rastreado e substituí-lo com o victim executável chamando execl().
Pai
Em caso de pai, a situação é muito diferente e muito mais complicada. Você deve dizer ao OS, que você deseja receber uma notificação com os problemas do processo vítima quando você executar o sys_execve. Você faz isso chamando ptrace() com PTRACE_SETOPTIONS e PTRACE_O_TRACEEXEC. Então você simplesmente usa waitpid().
Quando waitpid() retorna (e você deve verificar o valor de retorno para -1, o que significa erro), não é ainda o melhor momento para iniciar a injeção. Especialmente, considerando que você pode não ter ideia do que é o que processo de vítima. O próximo passo é aguardar uma chamada de sistema ocorrer ao dizer ao OS (e seria bom pular algumas chamadas de sistema, de modo que o processo vítima possa inicializar corretamente):
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
Seguido por um loop:
while(1)
{
if(-1 == waitpid(pid, &status, 0))
{
//Some error occurred. Print a message and
break;
}
if(WIFEXITED(status))
{
//The victim process has terminated. Print a message and
break;
}
if(WIFSTOPPED(status))
{
// Here comes the actual injection code. Actually, all its stages.
}
if(WIFSIGNALED(status))
{
// The victim process received a signal and terminated. Print a message and
break;
}
// All done.
return 0;
}
Injeção
Você deve introduzir uma variável para contar estágios. Vamos nomeá-la passo.
Stage 0 (stage = 0)
Eu não mencionei, mas ptrace() irá notificá-lo duas vezes durante uma chamada de sistema. Na primeira vez, um pouco antes da chamada de sistema (assim você pode inspecionar registros); a segunda notificação chegaria logo após a conclusão de chamadas do sistema (para que você possa inspecionar o valor de retorno). Portanto, dessa vez não fizermos nada, apenas retomar o processo vítima rastreado:
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
e incrementar a variável stage.
Stage 1 (stage = 1)
Registros do backup do processo vítima, parte do código do processo vítima que seria substituído por seu script e, finalmente, injetar o seu script.
Use ptrace (PTRACE_GETREGS, pid, NULL, regs), onde regs é um ponteiro para a estrutura user_regs (declarado em sys/user.h). O conteúdo dos registradores da vítima seria copiado lá.
Use ptrace (PTRACE_PEEKTEXT, pid, address_in_victim, NULL) para copiar o código executável do processo vítima (para fazer um backup) e ptrace (PTRACE_POKETEXT, pid, address_in_victim, shellcode), onde address_in_victim é o que seu nome sugere (você obtém o valor inicial do RIP de vítima em 64 ou EIP em sistemas de 32 bits). Script, no entanto, contém bytes do código a ser injetado empacotado em um valor unsigned long. Você, muito provavelmente, teria que fazer essas chamadas para várias iterações, como eu acho que o seu script seria, no máximo, de 8 bytes.
O início do seu script irá alocar memória para a função thread (a menos que você esteja executando o código que já está lá).
start:
mov rax, 9 ;sys_mmap
mov rdi, 0 ;requested address
mov rsi, 0x1000 ;one page
mov rdx, 7 ;PROT_READ | PROT_WRITE | PROT_EXEC
mov r10, 0x22 ;MAP_ANON | MAP_PRIVATE
mov r8, -1 ;fd
mov r9, 0 ;offset
syscall
db 0xCC
Incremente a variável stage. Retome o processo de vítima com
ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
Stage 2 (stage = 2)
Ignore todas as paradas até
0xCC == (unsigned char)(ptrace(PTRACE_PEEKTEXT, pid,
ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, regs.rip), NULL), NULL) & 0xFF
o que significaria que você atingiu o seu ponto de interrupção. Verifique o registo rax de vítima para valor de retorno
retval = ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, regs.rax), NULL);
e abortar, caso ele contenha um código de erro.
Você precisa incrementar o ponteiro de instrução (RIP/EIP) antes de deixar vítima retomar:
ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, regs.rip),
ptrace(PTRACE_PEEKUSER,pid, offsetof(struct user, regs.rip), NULL) + 1);
Incremente contador de estágios e
ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
Stage 3 (stage = 3)
Depois de alocar a memória, o seu script deve copiar a função thread lá e, na verdade, criar uma thread.
Você deve, novamente, ignorar todas as paradas desde que
0xCC != (unsigned char)(ptrace(PTRACE_PEEKTEXT, pid,
ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, regs.rip), NULL), NULL) & 0xFF
Assim que chegar a esse ponto de interrupção, você sabe que a thread foi iniciada e o injetor fez o que ele foi escrito para fazer.
Agora você tem que restaurar vítima ao seu estado inicial de pré-injeção, restaurando os valores dos registros:
ptrace(PTRACE_SETREGS, pid, NULL, regs);
e, o que é ainda mais importante, você tem que restaurar o código de backup, copiando de volta os unsigned longs armazenados.
A última coisa seria retirar do processo vítima:
ptrace(PTRACE_DETACH, pid, NULL, NULL);
Nesse ponto, o injetor pode seguramente sair, deixando vítima continuar a execução.
Voilà! Você tem apenas uma thread injetada em outro processo.
P.S.: Injeção de objeto compartilhado (como injeção de DLL)
Embora a injeção de código executável seja bastante simples, a injeção de objeto compartilhado é outra história diferente. Apesar do fato de que o kernel do Linux fornece a chamada de sistema sys_uselib, ele pode não estar disponível em alguns sistemas. Nesse caso, você tem algumas opções:
- Verifique se vítima utiliza as funções libdl (dlopen(), dlsym() e dlclose(), analise a imagem e obtenha endereços de funções relevantes. No entanto, nem todo programa usa libdl.
- Utilize a chamada de sistema sys_uselib. No entanto, ela pode não estar disponível.
- Escreva o seu próprio carregador de objeto compartilhado. Isso pode ser trabalhoso, mas você seria capaz de reutilizá-lo sempre que precisar.
Espero que este artigo tenha sido útil. Vejo vocês na próxima.
?
Texto original disponível em http://syprog.blogspot.com.br/2012/03/linux-threads-through-magnifier-remote.html