Ocorreu neste fim de semana o PlaidCTF que é uma competição de hacking organizada pelo grupo PPP [1]. Neste post será mostrada a solução para o desafio ezhp.
Descrição do problema
Luckily when you travel back in time, you still get to use all your knowledge from the present. With that knowledge in hand, breaking into this service (at 54.81.149.239:9174) owned by The Plague
O problema
O problema consistia em um binário ELF compilado para arquitetura x86 que quando executado mostrava o seguinte menu para gerenciamento de anotações:
![]() |
Imagem 1 – Menu de opções do binário |
Com uma rápida análise, ficou claro que o binário possuia um alocador de memória próprio para gerenciar essas anotações. Do ponto de vista de um atacante, alocadores de memória são bem atrativos, uma vez que eles geralmente possuem estruturas de gerenciamento que podem ocasionar problemas de corrupção de memória.
Afim de entender de forma geral como esse alocador funciona, foi realizada a sequência de tarefas:
- Criar uma anotação de 100 bytes e preencher com 100 caracteres “A”.
- Criar uma anotação de 10 bytes e preencher com 10 caracteres “B”.
- Criar uma anotação de 10 bytes e preencher com 10 caracteres “C”.
Para cada anotação criada é alocado um espaço de memória para armazenar os dados dessa anotação. Este espaço de memória também contém um cabeçalho para armazenar estruturas de gerenciamento utilizadas pelo alocador. Espaços de memória como esse, serão chamados de chunk, cada chunk possui a seguinte estrutura:
- tamanho do chunk e flag (4 bytes)
- ponteiro para o próximo chunk (4 bytes)
- ponteiro para o chunk anterior (4 bytes)
- espaço para os dado dos usuário (tamanho – 12 bytes)
Pontos a se observar na Imagem 2:
- O alocador reserva mais memória do que especificada pelo usuário. Isto é devido a um alinhamento feito pelo alocador e pela necessidade de alocar espaço para a estrutura de gerenciamento.
- Os chunks alocados estão ligados por uma lista duplamente encadeada.
- O bit menos significativo do tamanho do chunk determina se o chunk está em uso.
- Buffer overflow em um chunk pode corromper a estrutura interna e os dados dos chunks adjacentes.
Exploração
Antes de ser discutida a estratégia de exploração, é importante mostrar o pseudo código que realiza a remoção de uma anotação (liberação do chunk).
1 function liberar(endereco) { 2 chunk *atual; 3 chunk *anterior; 4 chunk *proximo; 5 6 if (endereco) { 7 atual = endereco2chunk(endereco); 8 anterior = atual->anterior; 9 proximo = atual->proximo; 10 if (anterior) 11 anterior->proximo = proximo; 12 if (proximo) 13 proximo->anterior = anterior; 14 ... 15 marca_liberado(atual); 16 } 17 }
Observa-se nas linhas 8 e 9 que são recuperadas as referências do chunk anterior e do próximo chunk, logo depois nas linhas 11 e 13 esses chunks tem ponteiros atualizados para refletir a remoção do chunk atual da lista encadeada. Com a corrupção dos ponteiros do chunk atual pode-se conseguir uma escrita controlada na memória.
- Alocacar 3 chunks (chunk #0, chunk #1 e chunk #2).
- Colocar o payload no chunk #0 e fazer overflow no mesmo para corromper os ponteiro do chunk #1.
- Remover o chunk #1.
Esta estratégia é basicamente a mesma usada para explorar vulnerabilidades em alocadores no passado [2].
![]() |
|
|
Uma vez conseguido realizar uma escrita controlada na memória o atacante pode tirar proveito de entradas na GOT [3][4] para redirecionar o fluxo de execução para o shellcode. Para este desafio foi sobrescrita a entrada responsável por chamar a função puts.
![]() |
Imagem 4 – Endereço na GOT que terá seu valor sobrescrito. |
![]() |
Imagem 5 – Valores na GOT antes e depois da corrupção de memória. O segundo valor (0x0804c018) é o endereço do payload na memória. |
Quando uma próxima chamada a função “puts” for realizada, o payload será execudado.
O formato do payload enviado para ser armazenado no chunk #0 foi:
- SHORTJMPCODE (2 bytes)
- NOPSLED + SHELLCODE (110 bytes)
- ENDEREÇO NA GOT – 8 (4 bytes)
- BYTE (1 bytes)
Como pode ser observado na Imagem 5, houve a necessidade de inserir no começo do payload um SHORTJMPCODE, o objetivo é pular o endereço que é escrito durante a atualização dos ponteiros. Vale mencionar que a proteção ASLR [5] não impede a exploração dessa vulnerabilidade porque é feita uma sobrescrita parcial de 1 byte no valor do ponteiro para o chunk anterior. O valor deste ponteiro é usado para especificar o endereço do payload.
![]() |
Imagem 3 – Exploração concretizada
|
Conclusão
Neste desafio ficou claro que implementar um alocador de memória próprio nem sempre é uma boa ideia. Como dito anteriormente, alocadores precisam de estruturas de gerenciamento que se não forem devidamente protegidas podem ocasionar corrupção de memória e consequentemente execução de código malicioso.
Referências
[1] http://ppp.cylab.cmu.edu/wordpress/?page_id=2
[2] http://phrack.org/issues/57/8
[3] https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries