[ Inicio ] [ Hacking ] [ CTFs ] [ Rant ]
.:: Brenn0 Weblog ::.

Título : Level 6 - Return-to-libc [OverTheWire CTF - Narnia]
Autor : brennords
Data : 24/12/2017
            

Rodar narnia6 não oferece muitas informações:

Aparentemente ele exige dois argumentos e, se os dois forem passados, o primeiro é impresso.

No código fonte encontramos respostas sobre o comportamento do programa e como ele é explorável.

São recebidos dois argumentos, b1 e b2 e, de forma não muito normal, imprime b1 usando fp, que no começo do código é declarado e setado como um ponteiro para a função puts.

O que chama a atenção é a forma com que os argumentos são salvos nos vetores de char b1 e b2: usa-se a função strcpy. Já falei outras vezes sobre como a mesma é insegura, pois faz cópias sem verificar o espaço disponível no destino, o que pode causar buffer overflows.

É exatamente o que acontece nesse caso. Qualquer vetor de char maior que 8 que for passado para o programa como qualquer um dos dois argumentos vai ultrapassar o buffer e causar problemas.

Observe o que aconte se forem passados dois argumentos de 20 caracteres:

Muito bom. Se controlamos EIP, controlamos o programa.

Mas, desde o começo, acreditei que esse level não seria mais um simples stack overflow como os anteriores. Costumo usar o comado checksec no peda assim que carrego o binário e já havia descoberto o que o diferencia dos binários anteriores:

O bit NX está setado! Ou seja, pôr shellcode na stack e jogar a execução do programa pra lá não vai funcionar, porque quando esta forma de proteção está habilitada, as regiões da memória são separadas entre as que contém código executável e as que servem apenas para o armazenamento de dados. Como a stack foi criada apenas para guardar valores, faz sentido que nada que esteja nela seja executável. Você pode aprender mais sobre NX aqui e usando o google.

Depois de observar esse fator, lembrei de uma das técnicas utilizadas para burlar esse mecanismo de segurança: a return-to-libc.

Já que não é possível inserir e executar nosso próprio código, que tal usar o código que já está no executável?

Essa é uma proposição bem ampla e vai depender das limitações impostas pelo programa e da criatividade e habilidade do cara que estiver explorando. E noob do jeito que sou, usarei a forma mais básica e comum. Mas deixe-me tentar explicar essa porra melhor (tô meio como sono mas tentarei).

No cabeçalho do código pode-se ver os includes:

#include stdio.h
#include stdlib.h
#include string.h

Quando um programador dá um #include nessas porra, insere no seu programa funções já prontas de outros arquivos. Tipo a primeira, stdio.h, que é uma das bibliotecas padrão do C e seu nome é uma sigla para “standard input and output”, traz consigo a função printf que faz o trabalho sujo por trás da impressão de algum dado na tela. As bibliotecas são uma forma de não ter que reescrever o mesmo código sempre, sabe? Por isso existem essas já conhecidas e padronizadas.

Ou seja, além do código fonte vísivel, tem mais um bocado de funções disponíveis para execução.

Consegue ver onde podemos chegar?

O segundo #include do código fonte do desafio é o da stdlib.h. Ela é mais uma das bibliotecas padrão e é a que interessa para nós porque nela está a função system, meu pirraia. E caso não saibas, essa função tem a capacidade de executar comandos no sistema.

Consegue ver melhor?

Voltando ao código do chall, ao fazer uma melhor análise enquanto crashava o bagui, acabei sacando que quando se estouram os buffers, estourando de maneira calculada, eu conseguia sobreescrever o ponteiro fp. É, aquele puto criado naquela linha meio complicada, que me fez viajar um pouco para entende-lo, começou a ter algum sentido para existir. Suponho que seja a forma que o criador do desafio tenha encontrado para facilitar a execução de um ataque tipo return2libc.

Não conseguiu ver o porquê?

Observe a linha 10. fp é chamado usando b1 como argumento. Essa porra tá praticamente dada. Pense bem, se é possível corromper o ponteiro fp para apontar para qualquer lugar/função que quisermos (leia-se: system), ela já vai estar pronta para receber b1 como argumento (leia-se: /bin/sh). E, ao executar system(“bin/sh”), teremos uma shell.

Para começar, era preciso saber o endereço de system. O que não é dificil de descobrir.

Após setar um breakpoint em main e rodar o programa só para vê-lo parar, é só dar um “p system” para descobrir o endereço.

Com o endereço para onde queria levar a execução do programa, comecei os testes para descobrir os exatos valores do overflow. Apanhei um pouco porque noob é noob e de vez em quando ainda me enrolo brincando na stack.

Após algumas tentativas, encontrei uma forma meio… Talvez estranha de explorar a falha:

Para o primeiro argumento, b1, enviei oito A’s, o que já ultrapassa o limite reservado na memória para o mesmo. Após os A’s, mando o endereço de system, que vai sobreescrever o ponteiro fp.

Mas e quando fp for chamar b1? Vai ter A’s e um endereço ao invés do argumento que preciso.

Foi aí que me enrolei um pouco por não conseguir entender bem o que havia acontecido quando alinhei o segundo argumento e peguei uma shell.

Depois de um tempo no gdb, percebi que, ao estourar o buffer de b2 com oito C’s, e enviar a string “/bin/sh” logo depois, aparentemente estava sobreescrevendo b1 com o valor “/bin/sh”.

Ou seja, parecia uma gambiarra massa. Mas de fato funcionava e o objetivo foi alcançado.

A shell com privilégios de narnia7 foi obtida com sucesso.