Пост охраны (io_hrana) — Writeup
Автор: Ксения @g1inko, SPbCTF
Описание задания:
Дан файл io_hrana.elf — «сторожевой шершень», который аттестует систему перед тем, как впустить в улей. Нужно пройти аттестацию и доказать, что «ты не робот, не верблюд и не медоед».
Разведка
$ file io_hrana.elf io_hrana.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[...]=..., for GNU/Linux 3.2.0, stripped
Статически слинкованный, stripped, ~2 МБ. Первый запуск:
$ ./io_hrana.elf
--=[ I WILL ONLY REVEAL MY SECRETS TO THE WORTHY ]=--
[*] Initializing.
system name = Linux
node name = vm
...
[+] I am now ready to test your system
[*] Starting attestation
5
4
3
2
1
0
CHECK [1/4]
[+] init ok
CHECK [2/4]
...
Дальше идут четыре последовательные проверки. Обычный strings по бинарю не находит ни одной из выводимых фраз ("attestation", "CHECK", "dedicated user" и т.д.) — то есть все текстовые константы намеренно спрятаны от статического анализа.
Первая попытка воспользоваться strace, чтобы посмотреть, какие файлы/сокеты трогает программа на каждой проверке, тоже почти ничего не даёт: в трассе видно буквально пару системных вызовов (uname, readlink /proc/self/exe, один openat("/proc/1/comm")) — а весь остальной ввод-вывод, включая все write в stdout, в strace просто отсутствует, хотя текст реально печатается на экран.
Причина: io_uring + SQPOLL как анти-трассировочный приём
В трассе, однако, виден характерный вызов:
io_uring_setup(16, {flags=IORING_SETUP_SQPOLL|IORING_SETUP_R_DISABLED, ...}) = 3
io_uring_register(3, IORING_REGISTER_ENABLE_RINGS, NULL, 0) = 0IORING_SETUP_SQPOLL поднимает отдельный поток ядра, который сам вычитывает Submission Queue из shared-memory кольца и выполняет операции (open, read, write, connect...) — приложению для этого не нужно делать системный вызов io_uring_enter вообще. Именно поэтому strace/ltrace, работающие через ptrace на пользовательском потоке, не видят ни файловых операций проверок, ни даже обычных write() в терминал: с точки зрения трассировщика поток программы почти ничего не делает, всю грязную работу выполняет невидимый kernel-thread. Это и есть «сторожевой шершень» из легенды задания.
Дополнительно все строковые константы в бинаре не лежат в .rodata, а побайтово записываются в data-сегмент прямо в коде main — тысячи инструкций вида:
movb $0x13, 0x5e3520(%rip) movb $0x22, 0x5e3521(%rip) movb $0xf3, 0x5e3522(%rip) ...
Это классический анти-strings приём: раз строка никогда не лежит в бинаре одним куском, strings/grep по файлу её не находят.
Стратегия: раз статика и обычная трассировка бессильны — смотрим живую память
Раз строки всё равно оказываются расшифрованными (или просто дозаписанными) в адресном пространстве процесса к моменту вывода/сравнения, самый быстрый путь — не реверсить вручную тысячи movb, а поймать процесс в нужный момент через gdb и сдампить память:
import gdb
gdb.execute("catch syscall exit_group")
gdb.execute("run < /tmp/enters.txt")
gdb.execute("dump memory seg.bin 0x5e4000 0x60b000")Как только процесс доходит до exit_group() (падение очередной проверки), дампим все rw--регионы и прогоняем через strings — там уже лежит расшифрованный текст, который не найти в самом файле.
Проверка за проверкой
CHECK 1/4 — инициализация
Ничего интересного, просто uname().
CHECK 2/4 — дистрибутив
Дамп памяти в момент ошибки содержит:
/etc/os-release PROpolisOS
Программа читает /etc/os-release и требует ID=PROpolisOS (POPolis — привет прополису от пчёл). Патчим файл:
sed -i 's/^ID=.*/ID=PROpolisOS/' /usr/lib/os-release
→ [+] release ok
CHECK 3/4 — «выделенный пользователь»
В следующем дампе находится:
/home/apis_mellifera [-] The dedicated user not found, cannot proceed
apis_mellifera — латинское название пчелы медоносной. Создаём каталог:
mkdir -p /home/apis_mellifera
Проверка идёт дальше, но теперь ругается на время:
[i] user creation time: 13:9 MSK [-] A special user wasn't created at 13:37 MSK. Not cool!!
Программа берёт время создания каталога (ctime/birth time инода) и сравнивает его с 13:37 по MSK (UTC+3) — «leet time». ctime нельзя выставить через touch (он всегда «сейчас» при любом изменении метаданных inode), поэтому единственный надёжный способ — временно перевести системные часы и создать каталог именно в этот момент:
date -s "2026-07-18 10:37:00 UTC" # 10:37 UTC = 13:37 MSK rm -rf /home/apis_mellifera && mkdir -p /home/apis_mellifera
→ [i] user creation time: 13:37 MSK → [+] user ok
CHECK 4/4 — окружение
Проходит автоматически (там же в дампе виден systemd — похоже на проверку /proc/1/comm, то есть что init-процесс контейнера называется systemd, а не что-то подозрительное).
После всех четырёх проверок:
[+] All good! Grab your prize! https://prizering-gaxqsak4.avitoctf.ru/ [*] Press ENTER...
Финальный этап: приватный сокет
После нажатия Enter:
Well... Now as for those secrets I claimed to have... Since you've come this far, I'm sure you know how to find a special private socket that you need to set up, so that I can share a passcode for you to get into. [*] Press ENTER... [-] Failed to talk to a special confidential socket, cannot hand my secret to you...
Снова дампим память в момент этой ошибки и находим строку:
h0n3ycOmb
(с нулевыми байтами вокруг — характерно для sockaddr_un в abstract namespace, т.е. путь вида \0h0n3ycOmb, не привязанный к файловой системе). Поднимаем такой сокет заранее и слушаем его:
import socket
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind("\0h0n3ycOmb") # abstract socket, начинается с NUL
s.listen(1)
conn, _ = s.accept()
print(conn.recv(4096))Запускаем сервер, затем сам io_hrana.elf (с уже пропатченными os-release и временем создания /home/apis_mellifera) — бинарь коннектится к абстрактному сокету h0n3ycOmb и присылает в него флаг открытым текстом.
Итог: что проверялось задачей
Узнавание io_uring/SQPOLL как техники, из-за которой strace/ltrace оказываются бесполезны (I/O выполняется kernel-thread'ом без «человеческих» системных вызовов на стороне трассируемого процесса).
Приём анти-strings через побайтовую запись строк в память в рантайме — и обход через дамп живой памяти вместо ручной дизассемблировки каждого movb.
Внимательность к деталям окружения: /etc/os-release, время создания файла (ctime vs mtime, и что ctime подделать можно только через системные часы), и abstract Unix-сокеты как канал передачи секрета.