Files
2026-04-22 10:58:32 +03:00

3.8 KiB
Raw Permalink Blame History

Навигация

PWN 946 pts

Сервис на Go, но парсер зовётся через CGo — и именно в этой части расположено классическое переполнение буфера. В глобальной структуре Parser лежит 64-байтное поле имени и три указателя на функции:

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 заголовок:

magic  = 0xDEADBEEF   (4 байта)
type                 (4 байта)
length               (4 байта)
id                   (4 байта)

Команда STATUS выдаёт диагностику с адресами всех интересных функций, включая сам win() — ASLR снимается одним запросом:

STATUS worker=... cache=0x... parser=0x... win=0x...

Дальше отправляем EXEC с пейлоадом:

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 перепишет только значимую часть, старшие нули оставит как есть — и указатель получается корректный.

Триггер — обычный 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}