Files
Serdtse-Sysoly/finals/zcrx-broker-pwn-hard/WRITEUP.md
T
2026-09-17 00:50:07 +03:00

42 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<h1 align="center">ZCRX Broker</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-PWN-blueviolet" alt="PWN"/>
<img src="https://img.shields.io/badge/difficulty-hard-critical" alt="hard"/>
</p>
ZCRX Broker управляет буферами приёма без копирования. В раздатке есть ELF `zcrx-broker` без PIE. У сервиса собственный пул памяти. Два отложенных события могут вернуть один блок в список свободных дважды. Добиваемся выделения одного адреса двум объектам. Тогда запись в новый blob изменит поля действующего объекта прав.
## Решение
Создаём `niov view` через пункт `1`, label любой. Новый view имеет `refs = 1`. Для view `0` дважды вызываем `2) arm lifecycle event`: сначала путь `scrub`, затем `refill`. Оба события запоминают одно и то же состояние с одной ссылкой.
В обоих снимках счётчик ссылок равен единице. Поэтому при выборе `3) drain lifecycle events` сервис обрабатывает оба события и дважды возвращает один и тот же блок в пул. В списке свободных теперь две записи с одним адресом — **double free в самописном аллокаторе**.
Выделяем capability через пункт `4`: она занимает первую запись. Вторую пока оставляем в пуле. У capability есть закодированный обработчик, который можно прочитать через `5) inspect capability`, cap `0`. Значение `encoded_handler` сохраняем числом в `encoded_audit`.
Обработчик по умолчанию — `audit_handler`, кодирование — XOR с pointer guard. Адреса функций берём из `readelf -sW public/zcrx-broker`, поскольку PIE выключен. Восстанавливаем guard и кодируем адрес экспорта:
```python
ptr_guard = encoded_audit ^ audit_handler
encoded_export = export_handler ^ ptr_guard
```
Пункт `6) allocate blob` заберёт вторую запись того же блока. Blob и живая capability окажутся в одной памяти. Сначала готовим 176-байтную подделку.
```python
import struct
payload = struct.pack("<QQQ", 0x4341505245433031, encoded_export, 0x4558504f52542121)
payload += b"/broker/export\x00".ljust(48, b"\x00")
payload = payload.ljust(176, b"\x00")
```
Выбираем `6) allocate blob`, на запрос `blob bytes:` отправляем строку `176`, затем на `blob payload:` передаём payload без добавленного перевода строки. Его поля задают `magic`, `encoded_handler`, `rights` и нуль-терминированный путь. Пункт `7) commit capability`, cap `0`, декодирует обработчик тем же guard. Проверки проходят, управление уходит в `export_handler`, и сервис печатает флаг.
[Солвер](solve/solve.py).
## Флаг
`caplag{double_returned_niov_shared_the_capability}`