Files
Tajna-tretej-stolicy/pwn-навигация/WRITEUP.md
2026-04-22 10:58:32 +03:00

71 lines
3.8 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">Навигация</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-PWN-blueviolet" alt="PWN"/>
<img src="https://img.shields.io/badge/points-946-orange" alt="946 pts"/>
</p>
Сервис на Go, но парсер зовётся через CGo — и именно в этой части расположено классическое переполнение буфера. В глобальной структуре `Parser` лежит 64-байтное поле имени и три указателя на функции:
```c
typedef struct {
char name[64];
int (*validate)(const uint8_t*, size_t);
void* (*transform)(const uint8_t*, size_t, size_t*);
void (*cleanup)(void*);
} Parser;
```
Сначала указатели выставляются на дефолтные реализации, а потом имя копируется через `strcpy()` без проверки длины. Намечаем вектор: имя длиннее 64 байт ляжет поверх указателя `validate`. А записать туда мы хотим адрес `win()`.
## Решение
Протокол бинарный — 16-байтный little-endian заголовок:
```text
magic = 0xDEADBEEF (4 байта)
type (4 байта)
length (4 байта)
id (4 байта)
```
Команда `STATUS` выдаёт диагностику с адресами всех интересных функций, включая сам `win()` — ASLR снимается одним запросом:
```text
STATUS worker=... cache=0x... parser=0x... win=0x...
```
Дальше отправляем `EXEC` с пейлоадом:
```mermaid
block-beta
columns 10
N["A × 64<br/>64 B<br/>→ name[64]"]:7
V["p64(win)<br/>8 B<br/>→ validate"]:1
X["transform + cleanup<br/>нетронуты"]:2
classDef base fill:#e0e7ff,stroke:#6366f1,color:#312e81
classDef hit fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef muted fill:#f3f4f6,stroke:#9ca3af,color:#4b5563
class N base
class V hit
class X muted
```
Тут нюанс: `strcpy` остановится на первом нулевом байте, а в non-PIE x86-64 адрес `win()` выглядит как `0x00000000004xxxxx` — в начале у него нули. Но в little-endian значимые младшие байты идут первыми, а старшие нули и так уже лежат на нужном месте с момента установки `default_validate`. Поэтому [partial overwrite](https://ir0nstone.gitbook.io/notes/types/stack/partial-overwrites) перепишет только значимую часть, старшие нули оставит как есть — и указатель получается корректный.
Триггер — обычный `PARSE`. Внутри `parse_data()` сервис по-прежнему зовёт `active_parser.validate(data, len)`, но теперь `validate` указывает не на дефолтную реализацию, а на `win()`, которая лезет в `/tmp/flag` и кладёт содержимое в глобальный буфер. Остаётся ещё раз дёрнуть `STATUS` — и сервис в ответе выплёвывает флаг.
Вся цепочка:
| # | Команда | Что происходит |
|---|---|---|
| 1 | `STATUS` | Утечка адреса `win()` |
| 2 | `PARSE` | Проверка, что соединение живое |
| 3 | `EXEC` | Переполнение `name[64]`, перезапись `validate` |
| 4 | `PARSE` | Триггер `win()` — флаг читается в буфер |
| 5 | `STATUS` | Читаем `flag=...` в ответе |
## Флаг
`caplag{g0r0ut1n3_h1j4ck_cg0_pwn3d}`