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

Título : Level 2 - Explorando um stack overflow como se fosse anos 90 [OverTheWire CTF – Narnia]
Autor : brennords
Data : 03/01/2017
            

Continuando a viagem por narnia, chegou a hora de passar do level2.

Logado no ssh do ctf e dentro de /narnia, executei o chall pela primeira vez:

narnia2@melinda:/narnia$ ./narnia2
Usage: ./narnia2 argument

Executando com o argumento:

narnia2@melinda:/narnia$ ./narnia2 argumento
argumentonarnia2@melinda:/narnia$

Aparentemente ele só imprime o argumento passado e nada mais. Pode-se confirmar o comportamento checando o código fonte:

Simples.

Na primeira linha da função main, temos um vetor de chars chamado buf que tem o tamanho necessário para guardar 128 caracteres. Logo depois tem um if simples que verifica se foi ou não passado um argumento para o programa, mas essa parte é dispensável. A linha mais importante é a strcpy(buf,argv[1]);

strcpy é uma função famosa por seu potencial perigoso. O que ela faz é copiar o segundo argumento para o primeiro. Mas ao contrário da sua parente strncpy, a função não te dá a opção de avisar o quanto se pode copiar para a variável de destino. No casso do chall, strcpy apenas supõe que buf é grande o suficiente para receber o valor que vier de argv[1].

Apesar de não haver nada de errado em usar strcpy (sei lá que motivo alguém teria), não dá pra usar a mesma quando se está tratando de dados que podem variar de tamanho e muito menos para entradas de usuários pela razão óbvia: a variavel de destino pode ter um tamanho menor do que a entrada de dados, o que causaria um bom e velho buffer overflow.

Hora de ver um buffer se estourando na prática:

narnia2@melinda:/narnia$ ./narnia2 `python -c 'print A*150'`
Segmentation fault

Apesar do erro Segmentation fault não significar exatamente que o que aconteceu foi mesmo um problema de buffer overflow, já vamos confirmar que o erro foi causado por terem sido passados 150 A’s como argumento.

narnia2@melinda:/narnia$ gdb -q narnia2
Reading symbols from narnia2...(no debugging symbols found)...done.
(gdb) source /usr/local/peda/peda.py
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : disabled
PIE : disabled
RELRO : disabled
gdb-peda$

O comando checksec do peda serve para verificar se o binário tem determinadas proteções. Nem vou entrar no mérito de explica-las porque ainda estou engatinhando por essas águas (engatinhar na água?), mas foi bom saber que não há nenhuma proteção ativa e que o programa pode ser explorado como se estivessemos nos anos 90.

No write-up do level0 falei de forma básica pra caaralho de como o stack se parecia e funcionava. Irei supôr que saibas o que tentei ensinar e agilizar este post pulando as explicações que fiz lá.

Agora quer saber a razão do segmentation fault?

É bastante informação nova se você não conhece sobre registradores. Por um momento pensei em detalhar tudo usando minhas próprias palavras, mas por que reescrever o que já foi escrito e publicado por aí? Recomendo este bom link que dá uma aula sobre os registradores, o stack e tudo mais que você precisaria para entender o resto da exploração neste write-up (lá chega até a ter informação suficiente para que se possa resolver o chall sem precisar de write-up).

Agora voltando…

O segmentation fault acontece quando o programa tenta acessar um endereço de memória proibido para ele, seja por questões de permissões ou por questão de existência. No caso acima, pode-se comprovar que é o segundo caso.

Quando insiro 150 B’s e o programa só tem reservado no stack espaço para 128 desses caracteres, acabo sobreescrevendo seja lá o que estiver no próprio stack depois desses 128 bytes. Se você seguiu meu conselho e deu uma lida aqui antes de continuar, sabe bem o que é um stack frame. Sabe também que, quando uma função é chamada, o programa “pula” para o determinado endereço que as instruções da função estão e que, no fim dessa função, o programa precisa saber para onde voltar. Para voltar, o software usa um endereço de retorno, que é sempre salvo no stack assim que se chama alguma função. Esse endereço fica bem abaixo do stack frame.

Logo, assim que 128 B’s lotam o espaço reservado pela variavel buf, os outros 22 B’s sobreescrevem o que vem em seguida e entre esses dados sobreescritos está o endereço de retorno.

Créditos pela imagem: http://www.crimesciberneticos.com/2011/03/exploiting-buffer-overflow.html

É importante que entendas sobre o funcionamento do stack, então, reforço que não ignores este link e pesquise mais pela internet se houver dúvidas.

Assim que a função termina seu trabalho, o programa vai buscar no stack o endereço de retorno salvo anteriormente (estou abstraindo todas instruções em asm responsáveis por pôr isso em prática). Mas o que acontece se os B’s extras sobreescreveram e corromperam esse endereço? O programa não tem como saber e vai tentar retornar sua execução de toda a forma. E é por isso que no dump acima EIP (o registrador responsável por guardar o endereço da próxima instrução a ser executada) tem o valor 0x42424242: o endereço de retorno apontava para 4 B’s hexadecimais. E 0x42424242 não é um endereço válido, logo, causa para um segmentation fault.

Se controlamos o endereço de retorno, controlamos o processo. E se controlamos o processo, podemos obriga-lo a fazer nossas vontades. E a vontade aqui é ganhar uma shell com privilegios narnia3.

Ok, mas como fazer isso?

Basicamente o que precisa ser feito é: inserir instruções no processo e fazer com que o endereço de retorno aponte para onde estiver essas instruções. Vamos substituir o endereço de retorno original pelo endereço de nosso shellcode.

 Mas antes de usar qualquer shellcode, precisa-se saber onde o mesmo estará na memória. Há varias formas de se descobrir isso. Usei o gdb e o peda para pôr um breakpoint no momento que a função main chama strcpy, afinal, tanto o endereço de origem, quanto o endereço de destino da nossa string envenenada serão passados como argumento para a função.

Logo ali na linha 32 pode-se ver os “Guessed arguments”.

arg[0]: 0xffffd4c0 --0x2c (',')
arg[1]: 0xffffd741 ('B' repeats 150 times)

Como a documentação de strcpy nos conta, temos aí o primeiro argumento 0xffffd4c0 como sendo o destino da cópia da string e 0xffffd741 a origem.

Usei o comando ‘n’ de next para executar e parar na instrução seguinte sem entrar na função (use ‘s’ de step e veja a diferença). E com o comando x/s [endereço] vejo que os B’s estão bem guardados nos dois endereços que peguei de strcpy.

Agora é só pôr nesse endereço o shellcode e sobreescrever o endereço de retorno.

Para mudar o endereço de retorno, preciso saber qual parte dos 150 B’s sobreescreveu o mesmo. Também há várias técnicas para descobrir. Muitas vezes consigo calcular o tamanho do stack frame analisando o asm da função, dessa forma, adiciono + 4 ao tamanho do frame e sobreescrevo exatamente o endereço de retorno.

As instruções responsáveis pela formação do stack frame de main são as 4 primeiras (de main+0 a main+6). A primeira, push ebp, salva o valor que aponta para a base do stack (ebp) no próprio stack. A segunda, mov ebp,esp, envia o valor da base do stack (ebp) para esp, conhecido como stack pointer e responsável por apontar para o topo do stack. A terceira linha, faz isso aqui (começando a ter preguiça de contar tudo). E a quarta, sub esp,0x90, subtrai do stack pointer atual o valor 90h que em decimal é 140.

Isso me parece bem mais fácil de entender com a ajuda de desenhos e tal. Vou deixar este link que dá uma aula melhor que a minha.

O que mais importante se tira da interpretação dessas instruções é: o tamanho que a função main reservou para os dados é de 140.

É possível provar facilmente se esse valor está correto. Enviarei 140 B’s + 4 R’s como argumento para o programa e ver o que acontece.

Agora temos a faca e o queijo na mão.

Usei o mesmo shellcode usado no chall anterior e comecei a montar o exploit. Subtrai os 48 bytes do shellcode e preferi, ao invés de B’s, usar um nopsled para preencher o espaço restante do buffer porque é coisa de leet. O exploit ficou: \x90 * 92 + shellcode + endereço de retorno.

Ainda somei uns bytes ao endereço que peguei sendo argumento de destino para o buffer só para ver o nopsled em ação e ao invés de usar 0xffffd4c0, usei 0xffffd510 (faça suas modificações e analise com o gdb se souber).