22 KiB
22 KiB
Заметки по разработке burterm
Чистое описание архитектуры — в ARCHITECTURE.ru.md.
Этот файл — то, что не годится в ARCHITECTURE, потому что описывает не «как устроено», а «почему так вышло» и «на что обратить внимание». Весь код писался в среде без сети и без установленного Go — ни разу не проходил через компилятор.
Реальные баги, найденные и исправленные по ходу разработки
Не гипотетические — конкретные, с объяснением, как нашлись.
- UTF-8 в
§маркерах§. Маркер§(U+00A7) — два байта в UTF-8 (0xC2 0xA7). Самая ранняя версияParseMarkersсравнивала байты напрямую и детектила маркер только по второму байту, оставляя первый (0xC2) мусором в очищенной строке. Баг жил с первого черновика, найден при разработкеDetectParamName(умные заготовки payload'ов). Исправлено: сканирование черезrangeпо рунам. - O(N²) в Results.
resultsModelна каждый входящий результат атаки заново парсил текст всех полей фильтра и пересобирал весь список отфильтрованных строк черезstrings.Join. На словаре в миллион строк это останавливало TUI намертво. Исправлено тремя механизмами: кэшированные предикаты (парсятся один раз при изменении текста поля, а не на каждый результат), инкрементальныйstrings.Builderвместо пересборки списка, дросселированный рендер воviewportпо таймеру раз в 300мс — и только для той атаки, что видна на экране. MaxIdleConnsPerHost— баг с самого начала, не только для больших словарей. Голыйhttp.Transportдаёт дефолт 2 соединения на хост. ПриConcurrency > 2(а дефолт формы — 10) большинство воркеров закрывало и переоткрывало TCP/TLS-соединение на каждый запрос вместо переиспользования из пула. Исправлено вengine.Newи вspider.New(там клиент был вообще голый — Transport==nil, то есть неявно общий пакетныйhttp.DefaultTransport).- Прокси не читал тело ответа. Изначальная версия сохраняла только
заголовки/статус — без тела полнотекстовый поиск по Project был бы
пустым обещанием. Исправлено:
io.ReadAllполного тела, капированная копия (8КБ) для хранения,resp.Bodyвосстанавливается черезio.NopCloserпередresp.Write()(тело можно прочитать только один раз). - Утечка горутин при параллельных атаках. Изначально
Modelхранила одинresultsCh/cancelна всё приложение — вторая атака без дожидания первой тихо переключала отслеживание, а горутины первой оставались писать в канал, который больше никто не читает (небуферизованный канал — блокировка навечно). Исправлено переходом на[]*attackRun(независимые атаки) и наselectсctx.Done()при записи вoutвнутриengine.send(). proxyModel.SetSizeвообще не резервировал место под собственные заголовок/футер.View()всегда рисует "Перехвачено: N" + пустую строку сверху и пустую строку + строку подсказки снизу — 4 строки сверхviewport, аSetSizeотдавалviewport.Heightвсю переданную высоту без остатка. Из-за этого вкладка Proxy стабильно вылезала за экран независимо от того, сколько высоты ей давалmodel.go— сам бюджет внутри файла никогда не учитывал собственный хром. Найдено построчным разборомView(), а не подбором чисел снаружи. Исправлено вычитанием фиксированных 4 строк внутриSetSize.comparerModel:SetSizeсчитал раскладку A/B side-by-side (делит ширину пополам черезhalfWidth), аView()рисовал их друг под другом. Из-за этого реальная высота была примерно вдвое больше того, что закладывал бюджетdiff.Height— вкладка Comparer вылезала за экран независимо от общего бюджета высоты. Исправлено не подгонкой чисел, а починкой самого рассинхрона:View()теперь реально кладёт A и B рядом черезlipgloss.JoinHorizontal, как и предполагала раскладка ширины с самого начала.- Футер Dashboard врал про
pgup/pgdn. Подсказка гласила "pgup/pgdn — прокрутка", хотя после того как эти клавиши стали перехватываться глобально под переключение вкладок (см. раздел решений ниже), доviewportони больше не доходят и ничего не прокручивают. Текст поправлен на "↑/↓ — прокрутка". - Бюджет высоты был неверен на 10 из 12 вкладок одновременно — и по
двум независимым причинам. Первая: строки-подсказки в футере
доходили до 200+ символов (
targetview.go— 425 символов исходно с учётом экранирования,repeater.go— 219) — терминал переносил их сам, а высота этого переноса нигде не учитывалась. Исправлено разбиением каждого длинного футера на гарантированные 2 (иногда 3) строки: специфичные подсказки вкладки отдельно, общая навигацияf1-f12, pgup — вкладкиотдельно — так поместится в одну физическую строку почти на любом терминале, а не будет угадывания, перенесётся или нет. Вторая причина — независимая от первой: у нескольких вкладок константа вычитания была просто арифметически неверна с самого начала, до всей этой истории с футерами (Fuzzer:-12вместо нужных-20, Repeater:-2вместо-8, Decoder/Sequencer:-4вместо-8/-6, Spider/Project:-6/-8вместо-11). Обе причины нашлись только построчным подсчётом каждогоView()— при этом подсчёте я сам дважды ошибся на лету (Decoder сначала посчитал 5 вместо верных 8) и пересчитывал заново. Итоговые числа для всех 12 файлов сведены в одну таблицу для сверки:
Request Builder сюда не входит — он не считает высотуcomparerview.go: diff.Height = height - inputHeight - 6 dashboardview.go: vh = height - 3 decoderview.go: output.Height = height - inHeight - 8 fuzzerview.go: vh = height - 20 (минимум для "чистого" состояния) projectview.go: vh = height - 11 proxyview.go: vh = height - 6 (плюс ещё -5 снаружи, в model.go, под "Прокси:...CA:...") repeater.go: response.Height = height - reqHeight - 8 results.go: vh = height - 11 (плюс ещё -6 снаружи, в model.go, под заголовок атаки) scannerview.go: vh = height - taHeight - 15 - len(scanner.AllCheckNames()) sequencerview.go: output.Height = height - inHeight - 6 spiderview.go: vh = height - 11 targetview.go: vh = height - 13viewportпо фиксированному бюджету (сайзинг пропорциональный,height/3), так что этот класс бага его не касается, хотя длинный футер разбил и там для единообразия. - Comparer вылезал за экран ещё дважды после починки JoinHorizontal
(сначала
-5, потом-6— оба раза построчный подсчёт по коду казался верным, но на практике не сходилось). Рабочая гипотеза:textarea.View()(компонент bubbles), возможно, рендерит не ровно столько строк, сколько задано черезSetHeight— то есть у неё есть какой-то собственный оверхед (рамка/индикатор/что-то ещё), которого нет в исходниках, доступных мне без реального терминала. Это касается не только Comparer —Decoder,Repeater,Scanner,Request Builderтоже используютtextareaи МОГЛИ пострадать той же проблемой, просто не в такой явной форме (или ещё не протестированы так тщательно). Для Comparer вместо третьей попытки точного подсчёта поставлен солидный запас (-12вместо расчётных6,inputHeightурезан доheight/4) — если и остальные текстовые вкладки продолжат вылезать за экран после всех починок из записи выше, это будет подтверждением гипотезы, и той же логике (запас вместо точного числа) нужно последовать и там.
Непроверенные места (порядок — от вероятных проблем к маловероятным)
go.mod:modernc.org/sqlite v1.34.4— версия по памяти,go mod tidyможет подтянуть другую,go.sumпересоберётся с нуля.- bubbles/bubbletea API —
Focus()уtextinput/textareaв некоторых версиях возвращаетtea.Cmd, в некоторых — нет; сигнатурыspinner.Tickмогли измениться между версиями. exitErr.Sys().(syscall.WaitStatus)вinternal/fuzzer/fuzzer.go— platform-specific type assertion, работает только на Unix.- Гонки данных, не проверенные race detector'ом. Весь проект
писался с оглядкой на конкурентный доступ (
sync.RWMutexвproxy.Server, независимые снапшотыsitemap.Scope,atomicв счётчиках фаззера), но ни разу не прогонялся подgo test -race. pgup(вкладка Fuzzer, единственная за пределами F1-F12 после выделения Steganography/аудио в отдельный проект stegterm) — четвёртая попытка навигации на вкладку, не влезающую в F1-F12 (история первых трёх — в разделе решений ниже). PageUp кодируется терминалом так же атомарно, как F-клавиши (в отличие от Alt+буква, которая на практике расщепляется на ESC+символ) — по конструкции должен быть надёжнее, но конкретно в связке с Zellij ещё не подтверждён.
Сознательно отложенные и оставленные как есть решения
- Навигация на Fuzzer — четыре попытки подряд, есть разобранная
причина третьего провала. У физической клавиатуры нет F13/F14, а
13 вкладок в F1-F12 не помещаются (изначально этой проблеме были
подвержены сразу две вкладки — Fuzzer и Steganography, — но
Steganography с тех пор выделена в отдельный проект stegterm, и
сейчас в лишней клавише нуждается только Fuzzer). История:
f13/f14— не существуют физически, отпало сразу.shift+f1— по историческому терминальному соглашению это и есть F13 (kf13в terminfo), но подтверждённо не сработало в связке с Zellij.alt+f— подтверждённо не сработало, и в этот раз причина найдена: терминал расщепляетAlt+буквана два отдельных байта (ESC, затем сама буква) вместо единой комбинации. Если между ними случается задержка (обычное дело через мультиплексор/SSH), bubbletea не успевает склеить их обратно и видит одинокую букву — а раз в фокусе текстовое поле (что почти всегда так по умолчанию), эта буква просто печатается как обычный ввод, а не распознаётся как комбинация. F-клавиши от этой болезни не страдают: кодируются терминалом атомарно, не могут расщепиться в печатный символ.pgup— та же атомарная категория кодирования, что и у F-клавиш, поэтому по конструкции не должен страдать той же болезнью, что Alt-комбинации. Компромисс:viewport(Results, Proxy, Scanner и т.д.) по умолчанию тоже вешает постраничную прокрутку на PageUp — раз эта клавиша перехватывается глобально на уровне корневой модели раньше, чем дойдёт до конкретной вкладки, постраничная прокрутка пропадает везде, остаются только стрелки (построчно). Осознанный выбор: надёжный доступ к вкладке важнее удобства постраничной прокрутки. Не проверено на практике конкретно в связке с Zellij.
- Единая рамка вокруг тела каждой вкладки — откачена. Была попытка
обернуть body любой вкладки (кроме Dashboard) в
panel()для визуальной консистентности со всем приложением. Дважды подряд не смог вслепую подобрать для неё правильный бюджет высоты: сначала меню обрезалось снизу на нескольких вкладках, потом (после первой попытки исправить) сама рамка вылезала за край экрана на вкладках с длинным содержимым (Comparer, Scanner). Без реального терминала под рукой это угадывание, а не расчёт — после второго провала откатил саму обёртку, а не в третий раз стал подбирать числа. Остались только подтверждённо рабочие вещи: Dashboard со своими собственными панелями и стилизованный таб-бар. Если понадобится вернуться к идее единой рамки на каждой вкладке — делать это нужно с реальным терминалом под рукой для итеративной проверки, не вслепую. - Плагины. Обсуждалась возможность подключения внешних модулей.
Решение: не строить сейчас, направление на будущее — подпроцессы поверх
JSON Lines по stdin/stdout (не Go
plugin-пакет — хрупкий, требует точного совпадения версии Go; не встроенный Lua — новая непроверяемая зависимость с тем же риском, что и sqlite). Подпроцесс живёт весь прогон (атаку/скан), не спавнится на каждый вызов. Три типа плагинов на будущее:payload-generator,scanner-check,decoder. В коде не реализовано ничего. - Побочный эффект рекурсии Intruder. У
Modelизначально было заложено поведение «после старта атаки переключиться на Results» — рекурсия тоже шлётattackStartedMsgна каждую находку, значит при активной рекурсии с несколькими совпадениями пользователя может несколько раз подряд дёрнуть на вкладку Results. Решение неочевидно (нужно различать «юзер нажал ctrl+r» от «рекурсия сама что-то нашла»), оставлено как есть. drainGeneratorдля Pitchfork/ClusterBomb вычитывает генератор целиком в память перед стартом. Для словарей на миллионы строк — разовая пауза и всплеск памяти перед первым запросом. Не исправлено: риск сломать корректность одометра выше выигрыша.- Только WAV, не MP3/FLAC. Сознательный выбор, не недоделка: сжатие с потерями убивает мелкую структуру спектра, в которую в CTF прячут сообщения — задачи почти всегда именно в PCM WAV.
- Поля "второе изображение"/"длина LSB" видны и в аудио-режиме
вкладки Steganography, хотя там ничего не делают. Условный layout
ради их сокрытия усложнил бы
View()заметнее, чем стоит того. - ASCII-логотип не в блочном стиле (как у референсных TUI-инструментов вроде Burp) — сознательно: точная посимвольная геометрия блочных букв без реального терминала для проверки выравнивания легко разъезжается на один символ. Вместо этого — разрядка текста акцентным цветом.
- Заголовки панелей — не врезаны в саму рамку (
┌─ TITLE ─┐), а первая строка жирным текстом внутри неё — по той же причине: точная ширина без права на ошибку.
Порядок отладки при первой сборке
go mod tidy— разберётся с зависимостями иgo.sum.go build ./...— почти наверняка всплывут точечные несовпадения сигнатур bubbles/bubbletea. Правятся по месту, по тексту ошибки.go vet ./...— статический анализ.go run -race ./cmd/burterm— особенно при активном Proxy + одновременной смене scope-правил.- Дальше — по вкладкам, от простых к сложным: Decoder/Comparer/Sequencer (чистая логика, без сети) → Repeater (один запрос) → Intruder на маленьком словаре → Proxy → Scanner/Fuzzer последними — там наибольшая оставшаяся неопределённость.