Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions sekai-2026/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
### Python ###
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]
**/*$py.class

# C extensions
**/*.so

# Distribution / packaging
**/.Python
**/build/
**/develop-eggs/
**/dist/
**/downloads/
**/eggs/
**/.eggs/
**/lib/
**/lib64/
**/parts/
**/sdist/
**/var/
**/wheels/
**/share/python-wheels/
**/*.egg-info/
**/.installed.cfg
**/*.egg
**/MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
**/*.manifest
**/*.spec

# Installer logs
**/pip-log.txt
**/pip-delete-this-directory.txt

# Unit test / coverage reports
**/htmlcov/
**/.tox/
**/.nox/
**/.coverage
**/.coverage.*
**/.cache
**/nosetests.xml
**/coverage.xml
**/*.cover
**/*.py,cover
**/.hypothesis/
**/.pytest_cache/
**/cover/

# Translations
**/*.mo
**/*.pot

# Django stuff:
**/*.log
**/local_settings.py
**/db.sqlite3
**/db.sqlite3-journal

# Flask stuff:
**/instance/
**/.webassets-cache

# Scrapy stuff:
**/.scrapy

# Sphinx documentation
**/docs/_build/

# PyBuilder
**/.pybuilder/
**/target/

# Jupyter Notebook
**/.ipynb_checkpoints

# IPython
**/profile_default/
**/ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
**/.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
**/__pypackages__/

# Celery stuff
**/celerybeat-schedule
**/celerybeat.pid

# SageMath parsed files
**/*.sage.py

# Environments
**/.env
**/.venv
**/env/
**/venv/
**/ENV/
**/env.bak/
**/venv.bak/

# Spyder project settings
**/.spyderproject
**/.spyproject

# Rope project settings
**/.ropeproject

# mkdocs documentation
**//site

# mypy
**/.mypy_cache/
**/.dmypy.json
**/dmypy.json

# Pyre type checker
**/.pyre/

# pytype static type analyzer
**/.pytype/

# Cython debug symbols
**/cython_debug/

### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
**/poetry.toml

# ruff
**/.ruff_cache/
3 changes: 3 additions & 0 deletions sekai-2026/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SekaiCTF 2026

- **Event:** [SekaiCTF 2026](https://ctf.sekai.team) ([CTFTime](https://ctftime.org/event/3113/))
73 changes: 73 additions & 0 deletions sekai-2026/pwn/ppp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
FROM --platform=linux/amd64 alpine:3.20 AS readflag

RUN apk add --no-cache gcc musl-dev python3
WORKDIR /src
COPY src/readflag.c /src/readflag.c
COPY flag.txt /src/flag.txt
RUN python3 -c 'from pathlib import Path; src = Path("/src/readflag.c").read_text(); flag = Path("/src/flag.txt").read_text().rstrip("\n"); Path("/src/readflag_patched.c").write_text(src.replace("SEKAI{REPLACE_ME}", flag))' \
&& gcc /src/readflag_patched.c -static -O2 -s -o /readflag \
&& chmod 4511 /readflag

FROM ubuntu:20.04 AS app

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool pkg-config m4 git \
libssl-dev zlib1g-dev libcurl4-openssl-dev \
socat ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build

ENV CFG="--enable-shared --disable-static --prefix=/usr/local"
RUN git clone https://github.com/libimobiledevice/libplist && \
cd libplist && git checkout 32428abacb909988e8e960a8845a6430b17b6a60 && \
./autogen.sh --without-cython $CFG && make -j"$(nproc)" && make install

RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && \
cd libimobiledevice-glue && git checkout da770a7687f35fbb981db4d7b47b1b032cd5c2c7 && \
./autogen.sh $CFG && make -j"$(nproc)" && make install

RUN git clone https://github.com/libimobiledevice/libusbmuxd && \
cd libusbmuxd && git checkout 93eb168bf6b07472d17781328c21df0c60300524 && \
./autogen.sh $CFG && make -j"$(nproc)" && make install

RUN git clone https://github.com/libimobiledevice/libtatsu && \
cd libtatsu && ./autogen.sh $CFG && make -j"$(nproc)" && make install

RUN git clone https://github.com/libimobiledevice/libimobiledevice && \
cd libimobiledevice && git checkout fa0f79190142bc309307967c058f89c1b36eb6b8 && \
./autogen.sh --without-cython $CFG && make -j"$(nproc)" && make install

RUN echo /usr/local/lib > /etc/ld.so.conf.d/local.conf && ldconfig

COPY src/afc_list.c /build/afc_list.c
RUN mkdir -p /app && \
gcc -O0 -fstack-protector-all -no-pie -o /app/afc_list /build/afc_list.c \
$(pkg-config --cflags --libs libimobiledevice-1.0) -lpthread

COPY --from=readflag /readflag /readflag

# nsjail uses one-way pipes; socat gives afc_list a bidirectional fd 0
RUN printf '#!/bin/sh\nexec socat STDIO EXEC:/app/afc_list\n' > /app/run

RUN useradd -m ctf \
&& chmod 755 /app/afc_list /app/run \
&& chown ctf:ctf /app/afc_list /app/run \
&& chown root:root /readflag \
&& chmod 4511 /readflag

FROM ghcr.io/es3n1n/jail:latest

COPY --from=app / /srv
COPY hook.sh /jail/hook.sh
RUN chown root:root /srv/readflag \
&& chmod 4511 /srv/readflag

ENV JAIL_PORT=5000 \
JAIL_TIME=120 \
JAIL_MEM=256M \
JAIL_CPU=1000 \
JAIL_PIDS=16 \
JAIL_TMP_SIZE=0 \
JAIL_SYSCALLS=personality
60 changes: 60 additions & 0 deletions sekai-2026/pwn/ppp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Writeup: ppp (SekaiCTF 2026)

- **Event:** [SekaiCTF 2026](https://ctf.sekai.team) ([CTFTime](https://ctftime.org/event/3113/))
- **Challenge:** ppp
- **Category:** pwn
- **Solved by:** [dopri](https://github.com/DoPri/)
- **Remote:** `ppp.chals.sekai.team:1337`

This was a 0-day challenge. The vulnerability is a real, previously unknown bug in [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice) (8k stars on GitHub), a open-source library for communicating with iOS devices over the AFC (Apple File Conduit) protocol.
The challenge binary (`afc_list`) is a thin, "safe" wrapper around the real library. Ehe exploitable bug lives in libimobiledevice's own `afc_receive_data()` function, not in challenge-specific code.

The Dockerfile pins specific commits of the libimobiledevice stack:

| Library | Commit |
| --------------------- | ------------------------------------------ |
| libplist | `32428abacb909988e8e960a8845a6430b17b6a60` |
| libimobiledevice-glue | `da770a7687f35fbb981db4d7b47b1b032cd5c2c7` |
| libusbmuxd | `93eb168bf6b07472d17781328c21df0c60300524` |
| libtatsu | `60a39f36d719344360ec2e87563ed43f61f0530f` |
| libimobiledevice | `fa0f79190142bc309307967c058f89c1b36eb6b8` |

The binary is linked against glibc 2.31 on Ubuntu 20.04, compiled with `-O0 -fstack-protector-all -no-pie`, and presents an interactive `afc>` prompt with commands like `devinfo` and `read`.
A SUID `/readflag` binary on the remote prints the flag when invoked as `/readflag sekai ppp`.

## Binary Security

```
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
```

## Initial Analysis

The AFC protocol uses a 0x28-byte header containing a magic (`CFA6LPAA`), `entire_length`, `this_length`, a packet number, and an operation code. The bug is in libimobiledevice's `afc_receive_data()`: it allocates a receive buffer with `malloc(entire_length - 0x28)` and then reads `this_length - 0x28` bytes into it. The two length fields are independently attacker-controlled and the function does not verify that `this_length <= entire_length`.
When `entire_length` is set to exactly `0x28`, the allocation becomes `malloc(0)`, which returns a minimal 0x20-sized tcache chunk. If `this_length` then exceeds `0x28`, the subsequent `recv` writes `this_length - 0x28` bytes into this undersized buffer, producing a heap overflow of arbitrary length.

The `devinfo` command parses the received data as a sequence of null-terminated key-value string pairs. Internally it calls `strdup` on each string and later `free`s the copies. This gives precise control over the number and size of heap allocations from the 0x20 tcache bin.

The `read` command opens a file handle, reads data via `afc_receive_data()`, then closes the handle. Each step exchanges AFC packets with the client, so we control the overflow trigger by responding with a crafted `this_length` in the data packet.

The nsjail configuration sets `persona_addr_no_randomize: true`, which disables ASLR inside the sandbox. All addresses are therefore deterministic, though the exact libc base differs a bit between local and remote environments.
Since ASLR is disabled but the exact libc base varies between environments, the exploit brute-forces a 2 MiB range around the expected base in 0x1000 steps until it finds the correct offset.

## Solution

The exploit in `solve.py` performs a GOT overwrite via tcache poisoning:

1. **Heap shaping.** Send 10 `devinfo` requests, each with two 8-byte null-terminated strings. Each devinfo allocates and frees several 0x20-sized chunks through `strdup`/`free`, populating the 0x20 tcache free list with a chain of chunks at known addresses.

2. **Heap overflow.** Issue a `read /x` command and respond to the file-read AFC packet with `entire_length = 0x28` (triggering `malloc(0)`) and `this_length = 0x28 + overflow_size`. The overflow payload writes 0x10 bytes of padding, then preserves the adjacent chunk's size field (`0x21`) and overwrites its `fd` pointer with `free@GOT` (`0x404070`). After this chunk is freed back to tcache, the chain becomes: `chunk_A -> chunk_B -> free@GOT`.

3. **GOT overwrite.** Send a final `devinfo` with multiple null-terminated strings. The internal `strdup` calls allocate from the poisoned 0x20 tcache. After `chunk_A` and `chunk_B`, the third allocation returns `free@GOT`. The `strdup` writes the string's content there, which we previously set to `system@GOT`.

4. **Command execution.** One of the other strings in the same devinfo payload is `"/readflag sekai ppp"`. When the devinfo handler later frees this string, `free("/readflag sekai ppp")` dispatches through the overwritten GOT entry and calls `system("/readflag sekai ppp")`, which prints the flag.
Binary file added sekai-2026/pwn/ppp/afc_list
Binary file not shown.
18 changes: 18 additions & 0 deletions sekai-2026/pwn/ppp/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
challenge:
build:
context: .
dockerfile: Dockerfile
image: sekai-ppp:dist
privileged: true
ports:
- "${PORT:-5000}:5000"
environment:
JAIL_PORT: "5000"
JAIL_TIME: "300"
JAIL_CPU: "1000"
JAIL_MEM: "1G"
JAIL_PIDS: "128"
JAIL_TMP_SIZE: "16M"
JAIL_SYSCALLS: "personality"
TIMEOUT: "300"
1 change: 1 addition & 0 deletions sekai-2026/pwn/ppp/flag.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SEKAI{REDACTED}
13 changes: 13 additions & 0 deletions sekai-2026/pwn/ppp/hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu

tmp="${nsjail_cfg}.ppp"
sed '1,/nosuid:true/s/nosuid:true/nosuid:false/' "$nsjail_cfg" > "$tmp"
printf '\n' >> "$tmp"
cat >> "$tmp" <<'EOF'
disable_no_new_privs: true
persona_addr_no_randomize: true
uidmap { inside_id: "0" outside_id: "" count: 1 }
gidmap { inside_id: "0" outside_id: "" count: 1 }
EOF
mv "$tmp" "$nsjail_cfg"
Binary file added sekai-2026/pwn/ppp/libc.so.6
Binary file not shown.
7 changes: 7 additions & 0 deletions sekai-2026/pwn/ppp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "ppp"
version = "0.1.0"
requires-python = "~=3.12.0"
dependencies = [
"pwntools~=4.15.0",
]
Loading