Este desafio fornece tanto o executável como o código-fonte. Analisando o código, vemos que ele é mais complexo, no sentido de não ser apenas 1 ou 2 funções, porém sua temática é bem interessante, já que ele permite o usuário criar 8 níveis de "dungeons", escrever dados em cada um desses níveis, ler o que está nesses níveis, e como qualquer rogue-like, acessar tais níveis.
Para cada nível acessado, as opções são as mesmas, criar, escrever, ler, e acessar.
chall.c
#include <stdio.h>
#include <stdlib.h>
struct Level *start = NULL;
struct Level *prev = NULL;
struct Level *curr = NULL;
struct Level
{
struct Level *next[8];
char data[0x20];
};
int get_num()
{
char buf[0x10];
fgets(buf, 0x10, stdin);
return atoi(buf);
}
void create_level()
{
if (prev == curr) {
puts("We encourage game creativity so try to mix it up!");
return;
}
printf("Enter level index: ");
int idx = get_num();
if (idx < 0 || idx > 7) {
puts("Invalid index.");
return;
}
struct Level *level = malloc(sizeof(struct Level));
if (level == NULL) {
puts("Failed to allocate level.");
return;
}
level->data[0] = '\0';
for (int i = 0; i < 8; i++)
level->next[i] = NULL;
prev = level;
if (start == NULL)
start = level;
else
curr->next[idx] = level;
}
void edit_level()
{
if (start == NULL || curr == NULL) {
puts("No level to edit.");
return;
}
if (curr == prev || curr == start) {
puts("We encourage game creativity so try to mix it up!");
return;
}
printf("Enter level data: ");
fgets(curr->data, 0x40, stdin);
}
void test_level()
{
if (start == NULL || curr == NULL) {
puts("No level to test.");
return;
}
if (curr == prev || curr == start) {
puts("We encourage game creativity so try to mix it up!");
return;
}
printf("Level data: ");
write(1, curr->data, sizeof(curr->data));
putchar('\n');
}
void explore()
{
printf("Enter level index: ");
int idx = get_num();
if (idx < 0 || idx > 7) {
puts("Invalid index.");
return;
}
if (curr == NULL) {
puts("No level to explore.");
return;
}
curr = curr->next[idx];
}
void reset()
{
curr = start;
}
void menu()
{
puts("==================");
puts("1. Create level");
puts("2. Edit level");
puts("3. Test level");
puts("4. Explore");
puts("5. Reset");
puts("6. Exit");
int choice;
printf("Choice: ");
choice = get_num();
if (choice < 1 || choice > 6)
return;
switch (choice)
{
case 1:
create_level();
break;
case 2:
edit_level();
break;
case 3:
test_level();
break;
case 4:
explore();
break;
case 5:
reset();
break;
case 6:
exit(0);
}
}
void init()
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
// Add starting level
start = malloc(sizeof(struct Level));
start->data[0] = '\0';
for (int i = 0; i < 8; i++)
start->next[i] = NULL;
curr = start;
}
int main()
{
init();
puts("Welcome to the heap-like game engine!");
printf("A welcome gift: %p\n", main);
while (1)
menu();
return 0;
}
Analisando a fundo cada função, podemos identificar algumas coisas interessantes:
Temos um vazamento do endereço da main(), dessa forma já podemos burlar o PIE do executável.
Na função de escrever no nível, nós podemos escrever 0x20bytes a mais do que ele armazena.
A função de explorar não faz nenhuma verificação em relação ao endereço, ou seja, podemos acessar qualquer endereço que esteja na índice inserido.
Com isso uma ideia já fica em mente, com o overflow podemos sobrescrever o chunk da frente para que um de seus índices contenha um endereço da tabela .got, e dessa forma, podemos acessar esse endereço, ler o que tem nele para vazar um endereço da libc, e em seguida sobrescrever esse valor para ser a função system(). A questão fica, qual função da tabela .got devemos sobrescrever, por que ainda temos que passar /bin/sh para a função.
E é aí, que olhando para a função get_num(), identificamos nosso alvo, a função atoi() que é chamada como atoi(buf).
2. Exploit
Já sabendo o que fazer, a solução se torna bem simples, só falta descobrir o offset para chegar nos dados (curr->data, se curr for exatamente o endereço da .got, nós não escreveremos nela e nem vazaremos o endereço), e descobrir quantos bytes escrever até chegar no índice do próximo chunk. Com o pwndbg, e o comando vis_heap_chunks, podemos descobrir a quantidade de bytes.
Nota: No pwndbg os chunks saem coloridos facilitando a leitura.
Com isso descobrimos que devemos escrever 48 caracteres, e os próximos começarão a escrever nos índices daquele chunk. Agora, o offset podemos descobrir direto pelo assembly.
Note que antes de chamar a fgets, ele passa como buffer o curr + 0x40, e aí está o nosso offset.
Importante: Como em nenhum momento esse chunk é liberado, não devemos nos preocupar com o programa identificando um erro relacionado ao chunk corrompido.
Importante: O ideal para esse desafio é usar uma ferramenta como o pwninit que cria um novo executável vinculado com a libc informada, pois usando o executável normal e sem o linker, a libc utilizada será a do sistema. No caso, eu não usei o pwninit e por isso tive que abrir a libc separadamente na solução.