Reverse Engineering · Dynamic Linking

Hijacking a Shared Library: OverTheWire Maze

Where Utumno was about the stack, the Maze levels are about the dynamic linker. The trick that unlocked several of them: a setuid binary calls an ordinary libc function like puts, and I get to decide which puts it actually runs. Override the symbol, and the program does my bidding with its elevated privileges.

Note: Educational writeup of a public OverTheWire wargame in a sandboxed VM. No real system is targeted and no passwords are shown.

The observation

The vulnerable program is setuid to the next level's user and, somewhere in its flow, calls a common libc function — here puts. Symbol resolution for that call goes through the dynamic linker, which means if I can get my definition of puts loaded ahead of the real one, my code runs inside the privileged process. That's the whole game: not smashing memory, just answering "which function is puts?" with a function of my own.

The payload: a malicious puts

My replacement keeps the same signature so the linker is happy to bind to it — but instead of printing its argument, it opens the password file that only the elevated user can read, and prints that:

#include <stdlib.h>
#include <stdio.h>

int puts(const char *s) {
    FILE *fp;
    fp = fopen("/etc/maze_pass/maze2", "r");   // readable only as the target user
    char pass[11] = {0};
    fread(pass, sizeof(char), 10, fp);
    printf("%s", pass);
    return 10;
}

Because the binary runs with the target user's privileges, the fopen succeeds even though my own shell can't read that file. When the program calls puts(...), control lands in my function, it reads the protected file, and prints the password to stdout. The overridden call effectively becomes "leak the next credential."

Why the override binds

The dynamic linker resolves each imported symbol the first time it's used, walking the loaded objects in a defined order. By getting my object loaded early — ahead of libc in that search order — my puts is the first match, so every puts call in the target resolves to mine. The real libc puts is never consulted. Same name, same signature, completely different behaviour, and it all happens before main even reaches the call.

The elegant part is how little there is to it. There's no shellcode, no offset to measure, no ASLR to defeat — just an understanding that a symbol name is a promise the linker keeps to whichever object gets there first. Control the search order and you control the function.

Chaining it

Each level hands you the password to the next, so the same override — repointed at the appropriate /etc/maze_pass/mazeN file — carries you up the ladder. The mechanics stay identical; only the target path changes. That repetition is the point: once the linker-hijack idea clicks, the level stops being a puzzle and becomes a template.

Takeaways

Two lessons stuck with me:

Coming off Utumno's byte-level shellcode work, Maze was a nice change of altitude: same goal (run my code with more privilege), a completely different mechanism (linking, not overflow). Together they're a good pair for building intuition about the two big surfaces of a native process — its stack and its symbol table.

OverTheWire is a legal, free security-training platform. This documents a technique on an intentionally vulnerable practice binary in an isolated VM — not guidance against any real system.
Prev: OverTheWire Utumno Next: CantCrack keygen