InquirerSara Duterte says to avail all remedies amid grave threats rapsPunchWhy AI cannot replace actors — FilmmakerDaily MaverickESCAPE: How a humble Soweto parkrun cultivates joy and community in a neglected parkThe Jerusalem PostIsrael warned of potential Hamas kidnapping operation years before Oct. 7, IDF col. claims - reportCNN TürkOpenAI ve Samsung arasındaki yakınlaşmaRTP DesportoJames Rodríguez regressa ao futebol colombianoBollywood HungamaBipasha Basu seeks Tukaram Mundhe’s help after she finds worms in protein powder: “You are our only hope”Inquirer EntertainmentAi-Ai delas Alas says ‘not affected’ by ex Gerald Sibayan’s new marriageוואלהצה"ל ושב"כ: חיסלנו את מפקד חטיבת חאן יונס בזרוע הצבאית של חמאסWirtualna PolskaWojna o Trybunał. Czarzasty odmawia Nawrockiemu i żąda ślubowania BerkaDaily MailMy husband will leave our home to his two children, can I carry on living here if he dies first?ColliderOnly 10 Anime Series From the 1990s Are True Masterpieces
The Daily Newsstand · Free, Always
Friday, September 11, 2026

Ищем lateral movement нейросетью, обученной на синтетических данных

Translate

Можно ли научить детектор атак, ни разу не показав ему настоящую атаку?

Звучит как противоречие. Если хочешь, чтобы нейросеть находила боковое движение, ей вроде бы надо показать боковое движение. Я сделал наоборот: сгенерировал целую корпоративную сеть с её историей входов, устроил в этом выдуманном мире нападение и обучил на нём сети. Ни одной настоящей строки в обучении. Весь мир описан конфигом на 135 строк, каждая сеть весит четыре тысячи параметров и учится за секунды на ноутбуке; лучший результат дали шесть таких сетей, обученных на шести разных выдуманных мирах.

Потом я выпустил их на настоящие данные: журналы аутентификации Лос-Аламосской лаборатории, 1.65 миллиарда событий, с размеченными учениями красной команды.

И это сработало. Сети выстраивают 3.6 миллиона окон по подозрительности, и в верхних двадцати трёх строках списка стоят шестнадцать настоящих атак и семь ложных тревог: аналитику остаётся открыть эти строки. Пороговому счётчику, чтобы добраться до шестнадцатой атаки на тех же данных, нужно сто шестьдесят одна тысяча ложных тревог. По AUC синтетика вошла в диапазон исследовательских работ, обученных на настоящих размеченных данных, хотя сравнивать их в лоб нельзя, и я объясню почему.

Чуда всё равно не случилось. Зато случилось другое: я дважды написал красивый вывод и дважды забрал его назад, когда эксперимент его опроверг. И нашёл петлю, ради которой вообще стоит писать генератор: ошибка детектора показывает конкретную машину, ты понимаешь, какого явления нет в твоём выдуманном мире, дописываешь две строки конфига, и ошибка исчезает.

Дальше по порядку: кто я и зачем мне это, что такое боковое движение, как устроен выдуманный мир, что показал экзамен и сколько из этого честно. Кому теория не нужна, можно перескочить к разделу “Первый результат на настоящих данных”.

Я пишу открытый конструктор тестовых данных TDCV2 и хотел понять, на что он годен на самом деле. Одно дело насыпать в таблицу фейковых Ивановых, другое - породить данные, на которых обучится что-то работающее. Проверять такое надо не на игрушке, поэтому я взял настоящую задачу информационной безопасности, обучил классификатор только на выдуманных данных и выпустил его на журналы реальной сети Лос-Аламосской национальной лаборатории.

Три оговорки, чтобы дальше читалось правильно.

Я не специалист по машинному обучению. Всё, что ниже, - работа человека, который пишет генератор данных и решил проверить его на серьёзной задаче. Если вам покажется, что я где-то изобретаю велосипед или беру не тот инструмент - скорее всего, так и есть.

Я ни с кем не соревнуюсь. Результаты исследовательских групп дальше приведены как система координат, чтобы читатель понимал масштаб, а не потому, что я претендую на место в этом ряду.

Я не рассчитывал на результат. Честно говоря, ждал, что синтетика покажет себя хуже! Получилось иначе, и вот об этом интересно рассказать. Чуда всё равно не случилось, зато случились две вещи поинтереснее чуда: измеренная граница целого класса методов и предел обучения на порождённых данных, который я потом сам же и отозвал.

Что такое боковое движение

Раз уж заголовок начинается с английского термина, давайте я сначала объясню, что это за зверь. Тем более что явление устроено неочевидно и половина интуиции про “взлом” тут не работает.

Есть распространённое представление, что злоумышленник ломает пароль и на этом всё. На самом деле подобрать вход куда-нибудь - только начало, и попадает он почти никогда не туда, куда хотел. Первой падает чья-нибудь рабочая станция: кто-то открыл вложение, или наружу торчал сервис со старой дырой. На этой машине нет ничего ценного. Ценное - в базе, на файловом сервере, в контроллере домена, и до них ещё надо дойти.

Вот это “дойти” и называется боковым движением - lateral movement. В классификации MITRE ATT&CK это отдельная тактика TA0008, а самая неприятная её техника называется T1078 Valid Accounts, то есть “действующие учётные записи”.

Как оно расходится

Механизм простой. На захваченной машине почти всегда лежат чужие учётные данные: кэш доменного входа, токен в памяти, пароль в служебном скрипте, сохранённое подключение. Злоумышленник забирает их и заходит с ними на соседнюю машину - обычным входом, тем же способом, каким туда ходит настоящий сотрудник. Там он забирает уже её учётки. И так по кругу.

Дальше работает арифметика: с каждым шагом растёт и число машин, и число доступных учёток, а каждая новая учётка открывает новые машины. Из одной станции получается десяток, из десятка сотня.

Схема того, как это расходится. В наших данных всё было ровно так: 94% помеченных событий шли с одного-единственного плацдарма, а всего учения дошли до 301 машины

Схема того, как это расходится. В наших данных всё было ровно так: 94% помеченных событий шли с одного-единственного плацдарма, а всего учения дошли до 301 машины

Скорость тут не как в кино. Каждый шаг занимает часы, вся картина разворачивается днями и неделями. Это, в общем, единственная хорошая новость - время заметить есть.

Кого это касается, а кого нет

Боковому движению нужно, собственно, куда-то двигаться. Оно живёт в больших корпоративных сетях: сотни и тысячи машин, общий домен, служебные учётки с доступом сразу ко многим местам, администраторы, которые ходят повсюду законно.

Маленькому бизнесу это явление само по себе не угрожает - не потому, что там безопаснее, а потому, что “двигаться” некуда. Если в компании пять человек, три ноутбука и вся работа в облачных сервисах, то злоумышленник, забравший ноутбук бухгалтера, уже находится там, где деньги. Ему не нужна никакая “тактика”, у него всё под рукой. Проблемы у такой компании будут другие, и ловятся они по-другому.

Так что всё дальнейшее - про сеть, в которой есть куда идти.

Почему это трудно поймать

Неудобное свойство этого явления. Каждое отдельное событие бокового движения безупречно: пароль верный, ответ “успех”, учётная запись настоящая, домен свой, время рабочее. Ничего аномального в нём нет, потому что это и правда обычный вход - просто сделал его не тот человек.

Для сигнатурного детектора и для любого порогового счётчика это неотличимо от работы сотрудника.

Слева обычный вход, справа боковое движение. Все поля валидны в обоих случаях, отличить их по одному событию нельзя

Слева обычный вход, справа боковое движение. Все поля валидны в обоих случаях, отличить их по одному событию нельзя

Зачем мне понадобилась именно эта задача

Теперь можно вернуться к тому, с чего я начал, - к генератору.

Проверять конструктор данных на задаче, где ответ известен заранее, не интересно. Нужна такая, где выдуманные данные либо сработают, либо честно провалятся, и где итог можно измерить, а не оценить на глаз. Боковое движение подошло по трём причинам, и каждая важна.

  1. Размеченных примеров нет ни у кого. Чтобы обучить детектор обычным способом, нужны примеры атаки в вашей собственной сети. А чтобы их получить, надо сначала подвергнуться компрометации, заметить её и разметить. Синтетика здесь не срезание угла, а единственная дорога - если она вообще работает.

  2. У явления есть вычислимое определение. “Учётная запись входит туда, где её не было, с машины, на которой её не было” - это правило, а не картинка. А правило можно записать конфигом, не имея перед глазами ни одного настоящего примера.

  3. Есть открытые данные настоящей сети, и в них есть размеченная правда. То есть экзамен можно сдать по-честному: обучиться на выдуманном, а проверить на живом, и не самому себе поставить оценку.

План из этого следовал простой. Не показать сети ни одной настоящей строки - ни в обучении, ни в проверке, ни в подборе порогов. Весь обучающий мир написать конфигом генератора. А потом выпустить её на реальные журналы и посмотреть, что она там увидит.

Данные для экзамена тут - открытый набор Лос-Аламоса Comprehensive, Multi-Source Cyber-Security Events: 58 суток, 17 684 машины, 12 425 пользователей, 1.65 миллиарда событий. Меня интересует журнал аутентификации.

Размеченная правда там - записи учений красной команды. И выглядит она так:

749 событий учений: 104 скомпрометированные учётные записи, 301 машина назначения и всего 4 источника. 94% событий идут с одного плацдарма.

Одна машина. Сто чужих логинов. Триста мест.

Активность учений распределена по суткам крайне неравномерно, и дальше я постоянно ссылаюсь на три набора. Считать всё дальше буду в окнах: окно - это одна машина за один час, всё, что она за этот час сделала. Размеченное окно - то, в которое попало хотя бы одно событие учений. Чтобы не путаться, договоримся об именах сразу:

набор

что это

окон

размечено

восьмые сутки

самая плотная активность, на них велась отладка

231 787

15

двенадцатые сутки

вторые по плотности, тоже рабочие

223 987

12

отложенный набор

шестнадцать прочих суток, к ним не прикасались

3 600 398

64

Восьмые оказались лёгкими - их решает почти любая модель. Двенадцатые трудными: на них модели наконец различаются. Отложенный набор - единственная честная мера, и дальше будет видно почему.

Правило, которое спасло проект: сначала экзамен

Первым порывом было сесть писать конфиг генератора. Хорошо, что не сел.

Я потратил полдня на то, чтобы вскрыть и измерить реальные данные до первой строки конфига. И оказалось, что задача не та, которую я себе представлял: я думал про перебор паролей, а размечено совсем другое явление. Всё, что я написал бы заранее, пошло бы в мусор целиком.

Разведка дала три факта, которые определили вообще всё дальнейшее.

Имя учётки не несёт сигнала

Главная “жертва” учений - учётка, через которую прошло больше всего помеченных событий. В чистом четырёхчасовом срезе она встречается 30 497 раз. Это активнейший штатный администратор, и ловить его как аномалию бессмысленно.

Значит, признак должен быть не о том, чья учётка, а о том, откуда она пошла. Не про свойство, а про отношение.

Наивный счётчик обманут устройством сети

Гипотеза “много разных учёток с одной машины - подозрительно” разбивается о топологию:

2996  2752  2686  2590   контроллеры домена
 252   118   110          файловые серверы
   1     2     1          рабочие станции

Плацдарм со своими сотней учёток попадает между контроллерами и станциями и неотличим от мелкого сервера. Со счётчиком по машинам назначения ровно то же самое.

Порог, который поймает плацдарм, накрывает половину серверов. Порог, который не трогает серверы, пропускает плацдарм

Порог, который поймает плацдарм, накрывает половину серверов. Порог, который не трогает серверы, пропускает плацдарм

Настоящий сигнал - новизна связи

Боковое движение порождает новые рёбра в графе входов: пару “учётка - машина, с которой она раньше не входила” или “машина - место, куда она раньше не обращалась”. И вот замер, ради которого стоило делать разведку. Доля новых рёбер у нормальной активности:

доля новых рёбер у нормы

значение

медиана

0.0000

90-й процентиль

0.0000

99-й процентиль

0.5000

Для тех, кто не в теме. Процентиль - это “у скольких процентов окон значение не больше этого”. 90-й процентиль равен нулю - значит, у девяти окон из десяти новых рёбер нет вовсе; и только у одного окна из ста доля новых доходит до половины.

Люди ходят по накатанным маршрутам. Фон почти идеально чист, и именно это делает задачу решаемой в принципе.

Обычный день добавляет к графу входов ноль новых рёбер. Боковое движение состоит из них почти целиком

Обычный день добавляет к графу входов ноль новых рёбер. Боковое движение состоит из них почти целиком

Мир из ста тридцати строк

Только теперь можно писать генератор - уже понимая, что именно порождать.

Ключевая мысль: не имитировать боковое движение, а породить его определение. У явления есть вычислимая формулировка - “учётка входит туда, где её не было, с машины, на которой её не было”. Раз определение алгоритмическое, оно синтезируемо без потери сути.

Сердце конфига выглядит так:

<pool name="Work" count="290">
  <sequence name="hid"><gen type="increment" value="1"/></sequence>
  <sequence name="role"><gen type="text" value="0,2,3,4"
                             percent="78.8,0.6,20,0.6"/></sequence>

  <sequence name="userBase"><gen type="formula" expr="hid * 30"/></sequence>
  <sequence name="userSpan"><gen type="number" value="1..4"/></sequence>

  <sequence name="fgBase"><gen type="number" value="1..290"/></sequence>
  <sequence name="fgSpan"><gen type="number" value="2..60"/></sequence>
  ...
</pool>

Машины лежат в справочнике, у каждой свой номер, своя роль и свой круг учёток: машина с номером hid владеет аккаунтами от hid*30 до hid*30+span. Отдельно у неё есть круг компрометации - номера других машин, чьи учётки через неё ходят. Ширина этого круга от 2 до 60 - это не лень, а сознательный разброс: сеть должна выучить класс, а не конкретный пресет.

Дальше номер учётки для события собирается формулой:

<sequence name="User">
  <gen type="formula" expr="Foreign == 1
     ? ((W.fgBase + floor(hash(N, 1) * W.fgSpan)) % 290) * 30
       + floor(hash(N, 18) * 2)
     : UserBase + floor(hash(N, 1) * UserSpan)"/>
</sequence>

Формула здесь укорочена: ветки служебных учёток и текучести убраны, полная в спойлере ниже. Здесь hash(N, соль) - детерминированная функция от номера строки и соли, дающая воспроизводимое число от нуля до единицы. Весь набор детерминирован по зерну и пересобирается побайтово.

Для тех, кто не в теме. Зерно - число, от которого отсчитывается вся “случайность” генератора. Одно и то же зерно даёт один и тот же мир байт в байт, другое зерно - другой мир по тем же правилам. Дальше слово встретится ещё в одном смысле: зерно обучения - такое же число, но для случайного начального состояния сети. Разные зёрна обучения - это одна и та же сеть, обученная несколько раз с разного старта.

Полный мир - сто тридцать строк и 400 тысяч событий за 9 секунд. Машины в нём бывают пяти видов, и каждый нужен по делу:

  • рабочие станции - одна-четыре своих учётки, основная масса;

  • серверы - сотни учёток законно. Без них сеть выучит, что “много учёток равно аномалия”, и утонет в ложных тревогах;

  • новые машины - истории нет, у них всё ново законно;

  • скомпрометированные машины;

  • машины одновременно новые и скомпрометированные - иначе сеть, обученная не трогать законную новизну, не научится их различать.

Ещё два механизма живут не на машине, а на событии, и потому в этот список не входят. Служебные учётки ходят по многим машинам, но на каждой постоянно, поэтому новизны не создают: такую учётку может использовать машина любого вида. И текучесть - новый сотрудник, переезд человека, переставленный сервер. Без неё у нормы новизна не встречается никогда, и сеть делает простой вывод “любое новое событие - компрометация”.

Весь конфиг базового мира целиком, 135 строк

Это рабочий файл целиком, без сокращений (в репозитории русская копия лежит в ru/gen/, рабочая в gen/ с теми же строками и английскими комментариями), вместе с комментариями, которые я писал себе по ходу дела. Комментарии тут важнее кода: почти каждый отвечает на вопрос “зачем этот класс вообще нужен”, а не “что тут написано”. Именно эти ответы и были настоящей работой - синтаксис занял минуты.

<tdc version="0.1">
  <!-- Синтетический мир: сеть из машин, у каждой своя история входов.
       Два пула, потому что нагрузка в сети распределена крайне неравномерно:
       горстка серверов видит огромный поток и сотни учёток ЗАКОННО, а рабочих
       станций много и каждая тиха.

       Роли рабочих машин: 0 - станция (свои 1..4 учётки), 2 - НОВАЯ машина
       (истории нет: у неё всё ново законно), 3 - станция, чьи учётки во второй
       период становятся чужими, 4 - машина ОДНОВРЕМЕННО новая и работающая под
       чужими учётками. Роль 4 существует затем, что сеть, обученную молчать на
       законной новизне, надо отдельно научить различать её от новизны чужой:
       у честной новой машины учётки свежие, у этой - переехавшие с других
       машин, то есть в сети давно известные.

       Период 0 - история, период 1 - наблюдение. Ручки развёрнуты внутри
       класса намеренно широко: сеть должна учить класс, а не пресет.

       Состав мира (доля серверов, редкость новых машин, плотность потока)
       откалиброван по ГРУБОЙ статистике реальной сети. Признаки самого
       наблюдаемого явления не калибровались ни по чему. -->

  <env count="400000" seed="world-train-1">

    <pool name="Work" count="290">
      <sequence name="hid"><gen type="increment" value="1"/></sequence>
      <sequence name="role"><gen type="text" value="0,2,3,4" percent="78.8,0.6,20,0.6"/></sequence>
      <sequence name="userBase"><gen type="formula" expr="hid * 30"/></sequence>
      <sequence name="userSpan"><gen type="number" value="1..4"/></sequence>
      <sequence name="dstBase"><gen type="number" value="1..4500"/></sequence>
      <sequence name="dstSpan"><gen type="number" value="2..9"/></sequence>
      <!-- разброс по часам: чем шире, тем реже события в окне; часть событий
           уходит за конец наблюдения - так рождаются и плотные окна, и тихие -->
      <sequence name="spread"><gen type="number" value="1..40"/></sequence>
      <!-- у одних машин состав не меняется годами, у других течёт постоянно -->
      <sequence name="churnMil"><gen type="number" value="0..8"/></sequence>
      <!-- у чужой активности свой ограниченный круг учёток и мест: ось
           развёрнута ШИРОКО (2..60), чтобы сеть не заучила один размер -->
      <!-- ДОЛЯ чужих событий среди всех событий машины: от 2% до 100%.
           В реальном журнале чужое событие бывает одно на полсотни обычных,
           и признак-доля его топит. Сеть обязана увидеть разбавленный случай,
           поэтому ось развёрнута до самых редких значений. -->
      <sequence name="fgRateMil"><gen type="number" value="1000..1000"/></sequence>
      <!-- круг чужих учёток: НОМЕРА ДРУГИХ МАШИН, чьи аккаунты используются -->
      <sequence name="fgBase"><gen type="number" value="1..290"/></sequence>
      <sequence name="fgSpan"><gen type="number" value="2..60"/></sequence>
      <sequence name="fgDstBase"><gen type="number" value="1..4900"/></sequence>
      <sequence name="fgDstSpan"><gen type="number" value="2..70"/></sequence>
      <sequence name="failMil"><gen type="number" value="20..700"/></sequence>
      <sequence name="baseMil"><gen type="number" value="1..30"/></sequence>
    </pool>

    <pool name="Serv" count="10">
      <sequence name="sid"><gen type="increment" value="1"/></sequence>
      <sequence name="userBase"><gen type="number" value="1..7000"/></sequence>
      <sequence name="userSpan"><gen type="number" value="60..280"/></sequence>
      <sequence name="dstBase"><gen type="number" value="1..3000"/></sequence>
      <sequence name="dstSpan"><gen type="number" value="40..400"/></sequence>
      <sequence name="spread"><gen type="number" value="15..22"/></sequence>
      <sequence name="baseMil"><gen type="number" value="1..30"/></sequence>
    </pool>

    <sequence name="W"><gen type="pool" value="Work"/></sequence>
    <sequence name="S"><gen type="pool" value="Serv"/></sequence>
    <sequence name="N"><gen type="increment" value="1"/></sequence>

    <!-- треть потока идёт на серверы: их мало, поток на каждый огромен -->
    <sequence name="IsServ"><gen type="formula" expr="hash(N, 10) < 0.35 ? 1 : 0"/></sequence>

    <!-- Служебные и машинные учётки: в настоящей сети они входят повсюду, и
         охват такой учётки - тысячи машин. Без них мир состоит из одних
         "привязанных к рабочему месту" аккаунтов, каких в жизни не бывает. -->
    <!-- Каждая машина пользуется одними и теми же служебными учётками (потому
         новизны они не создают), а каждая такая учётка обслуживает несколько
         машин - отсюда широкий охват. Малая часть (тир 9500+) ходит везде. -->
    <sequence name="IsSvc"><gen type="formula" expr="hash(N, 12) < 0.30 ? 1 : 0"/></sequence>

    <!-- имя машины: серверы и рабочие машины в разных диапазонах номеров -->
    <sequence name="Host"><gen type="formula" expr="IsServ == 1 ? 9000 + S.sid : W.hid"/></sequence>
    <sequence name="Role"><gen type="formula" expr="IsServ == 1 ? 1 : W.role"/></sequence>
    <sequence name="Spread"><gen type="formula" expr="IsServ == 1 ? S.spread : W.spread"/></sequence>

    <!-- новая машина живёт только во втором периоде -->
    <sequence name="Period">
      <gen type="formula" expr="Role == 2 || Role == 4 ? 1 : (hash(N, 6) < 0.60 ? 0 : 1)"/>
    </sequence>

    <sequence name="Time">
      <gen type="formula"
           expr="Period == 0 ? floor(hash(N, 5) * 28800) : 28800 + floor(hash(N, 3) * Spread) * 3600 + floor(hash(N, 4) * 3600)"/>
    </sequence>

    <!-- на этой машине во втором периоде учётки чужие -->
    <sequence name="Foreign">
      <gen type="formula" expr="(Role == 3 || Role == 4) && Period == 1 && hash(N, 11) < W.fgRateMil / 1000 ? 1 : 0"/>
    </sequence>

    <sequence name="UserBase"><gen type="formula" expr="IsServ == 1 ? S.userBase : W.userBase"/></sequence>
    <sequence name="UserSpan"><gen type="formula" expr="IsServ == 1 ? S.userSpan : W.userSpan"/></sequence>
    <sequence name="DstBase"><gen type="formula" expr="IsServ == 1 ? S.dstBase : W.dstBase"/></sequence>
    <sequence name="DstSpan"><gen type="formula" expr="IsServ == 1 ? S.dstSpan : W.dstSpan"/></sequence>

    <!-- Законное изменение бывает двух родов, и они выглядят по-разному:
         новый сотрудник (учётки не было в сети вообще, диапазон 20000+) и
         переход существующего человека на другую машину. Без обоих сеть не
         научится отличать законную новизну от чужих учётных данных. -->
    <sequence name="Churn">
      <gen type="formula" expr="Foreign == 0 && IsSvc == 0 && hash(N, 14) < W.churnMil / 10000 ? 1 : 0"/>
    </sequence>

    <sequence name="User">
      <gen type="formula"
           expr="Foreign == 1 ? ((W.fgBase + floor(hash(N, 1) * W.fgSpan)) % 290) * 30 + floor(hash(N, 18) * 2) : (IsSvc == 1 ? (hash(N, 13) < 0.25 ? 9500 + floor(hash(N, 1) * 20) : 9000 + (Host * 7 + floor(hash(N, 1) * 6)) % 400) : (Churn == 1 ? (hash(N, 17) < 0.5 ? 20000 + floor(hash(N, 15) * 900) : (floor(hash(N, 15) * 290)) * 30 + floor(hash(N, 19) * 4)) : UserBase + floor(hash(N, 1) * UserSpan)))"/>
    </sequence>

    <sequence name="Dst">
      <gen type="formula"
           expr="Foreign == 1 ? 1 + (W.fgDstBase + floor(hash(N, 2) * W.fgDstSpan)) % 4900 : (Churn == 1 ? DstBase + DstSpan + floor(hash(N, 16) * 50) : DstBase + floor(hash(N, 2) * DstSpan))"/>
    </sequence>

    <sequence name="FailCut">
      <gen type="formula"
           expr="Foreign == 1 ? W.failMil / 1000 : (IsServ == 1 ? S.baseMil : W.baseMil) / 1000"/>
    </sequence>

    <sequence name="Ok">
      <gen if="hash(N, 8) < FailCut" type="text" value="Fail"/>
      <gen type="text" value="Success"/>
    </sequence>

  </env>

  <block>
    <line><data>${{Time}},U${{User}},C${{Host}},D${{Dst}},${{Ok}},${{Foreign}}</data></line>
  </block>
</tdc>

На что стоит посмотреть, если читать бегло. Два пула вместо одного - потому что нагрузка в сети распределена дико неравномерно, и горстка серверов законно видит сотни учёток. Роль 4 - машина одновременно новая и работающая под чужими учётками; она существует ровно затем, чтобы сеть, обученную молчать на законной новизне, отдельно научить отличать новизну честную от краденой. Развёртка ручек - 2..60, 1..40, 20..700: сеть должна выучить класс, а не конкретный набор чисел. И Churn - законная текучесть двух родов, без которой у нормы новизна не встречается никогда и сеть делает вывод “любое новое событие - компрометация”.

Строка вывода в самом низу - весь формат журнала: время, учётка, машина, назначение, успех, метка. Последнее поле в обучении не участвует, оно нужно измерителю, чтобы знать правду.

Две формулы в файле длинные и уезжают вбок - User и Dst. Разбирать их скроллом не надо, они устроены одинаково: это лесенка вложенных условий “если чужая - взять из круга компрометации, иначе если служебная - из служебного диапазона, иначе если текучесть - свежую или переехавшую, иначе свою”. Одна строка вместо четырёх ветвлений снаружи; читается плохо, зато правится в одном месте.

Ошибка, которая стоила больше всех настроек

Долго мои “скомпрометированные” учётки были несуществующими: генератор разыгрывал номер из диапазона, и такого аккаунта не было в сети нигде. Сверка распределений с реальностью вскрыла это моментально:

у размеченных окон

реальность

синтетика до правки

учётка известна сети, но на этой машине впервые

26

2

учётки не было в сети вообще

0

11

В жизни боковое движение идёт существующими учётками - в этом весь смысл кражи. Я же порождал не кражу, а выдумку, и сеть прилежно училась ловить артефакт, которого в живых данных не бывает.

Слева то, что я порождал сначала: учётки нет нигде. Справа то, что происходит на самом деле: учётка настоящая, работает в другом месте

Слева то, что я порождал сначала: учётки нет нигде. Справа то, что происходит на самом деле: учётка настоящая, работает в другом месте

Одна эта правка сократила ложные срабатывания вчетверо при той же полноте. Больше, чем дали все настройки сети вместе взятые.

Синтетика должна быть верна по смыслу, а не похожа по статистике.

Сеть

Первая сеть - полносвязная, самая обычная: восемнадцать признаков на входе, восемь слоёв по двенадцать нейронов, 1333 веса. Обучение - секунды на ноутбуке, без видеокарты. В таблице ниже она значится как “глубокая [12]x8”; дальше в статье появится и вторая, рекуррентная, - её черёд придёт после замера ёмкости.

Единица наблюдения - окно “машина за час”. Признаки трёх сортов: объём и разнообразие (сколько событий, учёток, мест назначения), доли новизны и контекст машины (сервер она или тихая станция). Два признака стоит назвать отдельно, потому что именно они кодируют смысл украденных данных:

  • переехавшие учётки - число событий, где учётка известна сети, но на этой машине впервые. Подпись кражи: аккаунт существует и работает в другом месте;

  • свежие учётки - число событий, где учётки не было в сети вообще. Подпись нового сотрудника.

Без этого различия законный выход нового человека на работу неотличим от компрометации.

Признаки-счётчики берутся под логарифмом, и это не косметика. Синтетический мир - 290 машин, реальный - 17 684. Линейный признак увёл бы активации далеко за пределы обучающего диапазона, где сеть ведёт себя непредсказуемо; логарифм превращает разницу масштабов в сдвиг.

Один и тот же код считает признаки для синтетики и для реальности. Это не декларация: восьмые сутки пересчитаны двумя независимыми реализациями измерителя и сличён построчно, 231 787 строк совпали точно.

На синтетике с другим зерном - то есть на другом экземпляре того же мира - сеть даёт AUC 0.99996. Это означает ровно одно, и я записал вывод до экзамена: сеть выучила мой генератор досконально. И только его.

Для тех, кто не в теме. AUC - area under the ROC curve, площадь под ROC-кривой. Проще всего понимать её так: берём наугад одно размеченное окно и одно обычное; AUC - вероятность того, что модель поставит размеченное выше. 1.0 - порядок идеальный, 0.5 - монетка. Дальше в статье будет отдельный раздел о том, почему на этой задаче AUC врёт, но пока пусть будет “доля правильно упорядоченных пар”.

Сколько весов нужно на самом деле

Архитектуру я сначала взял наугад - типичная ошибка. Потом померил: тридцать одна конфигурация, по три зерна обучения на каждую (три обучения с разного старта, чтобы видеть разброс), девяносто три обученные сети за 170 секунд на процессоре. Характерные строки:

архитектура

весов

лёгкий день

трудный день

плоская [4]

81

0.99868

0.94208

плоская [8]

161

0.99998

0.94151

плоская [256]

5121

0.99997

0.95646

две ступени [128,64]

10753

0.99995

0.96318

глубокая [12]x8

1333

0.99997

0.97518

глубокая [12]x12

1957

0.99940

0.94923

Отсюда три вывода.

  1. Лёгкий день насыщен: от 161 веса до 10 753 всё лежит в диапазоне 0.99995-0.99998, ёмкость там просто не является ограничением.

  2. Глубина бьёт ширину: 1333 веса дают больше, чем 10 753.

  3. И слишком глубоко тоже плохо: двенадцать слоёв без остаточных связей падают до 0.949 и портят даже лёгкий день.

Позже я прогнал девять последовательностных архитектур, включая саму LSTM, - от расширяющихся свёрток до внешней памяти в духе нейронной машины Тьюринга. Половину названий узнал в тот же вечер, так что смотрите на это как на любопытство, а не на обзор. Лучшей осталась простая рекуррентная сеть на 4249 параметров - LSTM, сеть, которая читает часы одной машины по порядку и помнит, что было в предыдущих. На трудных сутках она даёт 0.990 против 0.975 у лучшей полносвязной, и дальше в статье “сеть” - это она. Худшим оказался трансформер: самовнимание жадно до данных и хочет связывать далёкие элементы, а у нас последовательности по 24 шага, и связывать в них нечего. Полные таблицы - в ARCHITECTURES в репозитории.

Первый результат на настоящих данных

Сутки с самой плотной активностью учений: 231 787 окон, из них 15 размечено.

мера

значение

найдено размеченных окон

15 из 15

ложных тревог при этом

11

места размеченных окон в общем списке

все в первых 32

А вот цена полноты - сколько ложных тревог надо просмотреть, чтобы дойти до последнего настоящего окна:

метод

ложных

счётчик “учёток с машины”

175 904

счётчик “мест назначения”

206 651

правило из четырёх порогов, подобранное по ответам

не доходит никогда

сеть на синтетике

10

Три числа про один и тот же день выглядят противоречиво, поэтому поясню, это три разные меры. 11 - ложные при фиксированном пороге 0.99, то есть если объявлять тревогой всё, что модель оценила выше этого значения. 32 - худшее место размеченного окна в общем списке из 231 787: даже самое неудачное попало в первые три десятка. 10 - цена полноты, то есть сколько ложных стоит между началом списка и последним настоящим окном. Меры разные, данные одни, и путать их не надо.

Про строку с порогами стоит сказать отдельно, потому что она объясняет, зачем вообще нужен отложенный набор. Я честно попробовал подогнать правило, подглядывая в ответы: четыре порога, настроенные по размеченному дню. Оно даёт 13 окон из 15 при нуле ложных - и не доходит до полноты никогда, потому что два тихих окна на три и шесть событий не проходят пороги по объёму. Оптимизация на тесте дала локально красивую и глобально бесполезную модель.

Числа хорошие, и радоваться им рано: они сняты на тех самых сутках, на которых я всё и настраивал. Настоящая проверка была впереди.

Отложенный набор четыре раза сказал “нет”

Я отложил всё, кроме двух рабочих суток: шестнадцать дней, 3 600 398 окон, 64 размеченных. Не смотрел, не крутил.

А потом начал улучшать - и каждый раз получал прирост на двенадцатых сутках:

улучшение

двенадцатые сутки

отложенный набор

новый класс машин “администратор на обходе” (законно заходит на многие машины)

лучше

хуже

ансамбль из трёх сетей, обученных на одном мире

лучше

хуже

надстройка над слабыми учениками (обучаемый судья над несколькими нарочно слабыми сетями)

лотерея

лотерея

богатый мир: 8 млн событий, стадии нападения

+1.26 пункта

без изменений

Для тех, кто не в теме. “Богатый мир” - попытка сделать нападение процессом с четырьмя стадиями: тихое закрепление, сбор учёток, распространение, выход на серверы. Ранние стадии - единичные события, и сеть на них научилась срабатывать на любое тихое окно: на отложенном наборе цена поиска у богатого мира оказалась в сто раз хуже. Дальше это слово будет встречаться как имя ошибки, а “чистый мир” - как мир без этих стадий.

Четыре раза подряд. В последнем случае прирост превышал разброс по зёрнам обучения в три с половиной раза - и всё равно не перенёсся.

Причину я понял не сразу. Я мерил разброс по зёрнам обучения и считал, что этого довольно. Но есть второй источник шума, невидимый пересевом: какие именно двенадцать окон оказались размечены на двенадцатых сутках. Это разброс по выборке размеченной правды, и оценить его пересевом модели невозможно - нужен другой день.

Двенадцать положительных примеров не образуют меры ни при каком разбросе зёрен.

Половина моих вечерних сравнений была за пределами разрешающей способности данных. Я сравнивал шум с шумом и делал выводы. Ровно для этого и нужен отложенный набор: он четырежды сказал “нет” там, где двенадцатые сутки говорили “да”.

Забегая вперёд: одну строку из этой таблицы я потом всё-таки отыграл назад. Ансамбль переносится - но не тот, который я пробовал тогда. Про это будет отдельный раздел, и вывод там окажется противоположным.

Почему AUC врёт на такой задаче

А здесь я едва не выбрал не ту модель.

Когда я добрался до градиентного бустинга - это ансамбль решающих деревьев, XGBoost, стандартный сильный метод для табличных данных, - он выиграл по AUC у всего остального: 0.918 против 0.909 у рекуррентной сети и 0.862 у полносвязной. Казалось бы, вопрос закрыт. И тут я чуть не сделал неверный вывод.

AUC - не то, что нужно аналитику. Ему нужен чистый верх списка: он открывает первые двадцать окон и смотрит, сколько из них настоящие. Правильная мера - цена поиска: сколько ложных тревог придётся просмотреть на пути к N-му найденному окну. Отложенный набор, 3.6 миллиона окон:

поймано окон

полносвязная

бустинг

рекуррентная

1

17

7

3

8

55

8 053

102

16

245

23 781

188

48

185 613

304 954

137 014

Бустинг лучше только в хвосте - на последних окнах, которые никто никогда не откроет. В рабочей области он хуже в сто раз.

Механизм простой. Положительных примеров 64, отрицательных 3.6 миллиона. AUC - это доля пар, где положительный стоит выше отрицательного, усреднённая по всем парам. Модель набирает высокий балл, аккуратно рассортировав безнадёжный хвост, при совершенно грязном верхе списка. Это как хвалить поисковик за правильный порядок результатов с пятидесятитысячного по шестидесятитысячный.

Самый выразительный случай - эхо-сеть. Это забавная конструкция: внутри резервуар из пяти тысяч случайных весов, которые никогда не обучаются, а учится только линейный считыватель на 65 параметров.

Две сотых разницы в AUC - и разница в три с половиной тысячи раз в том, что аналитик увидит на самом деле

Две сотых разницы в AUC - и разница в три с половиной тысячи раз в том, что аналитик увидит на самом деле

При вполне приличном AUC 0.969 на двенадцатых сутках она требует 3406 ложных тревог до первого найденного окна - против одной у рекуррентной сети. Разница в три с половиной тысячи раз при разнице в AUC в две сотых. И это не курьёз: в таблице из пяти архитектур порядок по AUC вообще не совпадает с порядком по цене поиска.

Если вы делаете поиск редких событий и меряете качество по AUC - почти наверняка вы меряете не то.

Находка первая: ножницы опоры

Признак определён как новизна относительно собственной истории машины: списка учёток и мест, которые на ней уже видели. Эту накопленную историю я дальше называю опорой. И тут обнаруживается ловушка, в которую попадает любой детектор, строящий профиль поведения.

Опора травится тем самым явлением, которое ловит

К двенадцатым суткам плацдарм “знает” 84 учётки. Из них 49 - те самые скомпрометированные, которыми через него же и ходили раньше.

58.3% “нормальной истории” машины составляет само наблюдаемое вторжение.

Механизм: история копится по факту наблюдения, поэтому любая достаточно длительная активность входит в норму и перестаёт быть новой. Сигнал слабеет ровно потому, что атака продолжается. Медиана новизны у размеченных окон упала с 1.00 на восьмые сутки до 0.57 на двенадцатые.

Лечение очевидное: заморозить опору на раннем периоде. Помогает сразу - найдено 10 окон из 12 вместо 5, и все размеченные окна поднимаются в первые двадцать семь строк списка вместо разброса до восьмисот.

Но замороженная опора устаревает

Два способа испортить опору, и оба работают. Рабочая точка узкая: не моложе недели и не старше двух-трёх

Два способа испортить опору, и оба работают. Рабочая точка узкая: не моложе недели и не старше двух-трёх

сутки

9

15

29

машин, неизвестных опоре

1.1%

3.3%

6.1%

Дрейф примерно 0.25% машин в сутки. К концу наблюдения каждая шестнадцатая машина незнакома опоре и потому выглядит аномальной: 39% всех ложных тревог на отложенном наборе приходится именно на такие машины. На восьмых сутках их было ноль.

Обновлять опору часто - травится наблюдаемым. Заморозить - устаревает со скоростью четверти процента машин в сутки. Рабочая точка: опора не моложе недели и не старше двух-трёх.

Это свойство не моей модели, а любого метода, который строит профиль по наблюдаемой истории без внешней проверки учётных данных.

Находка вторая: тут я был неправ

Тут я сделал вывод, который меня сначала обрадовал: он аккуратно объяснял всё разом. А потом оказался неверным.

Назовём совпадением нормальное окно, которое несёт полную подпись компрометации: и высокую новизну, и высокую долю отказов одновременно. В реальности такие есть - 51 окно из 231 772, две сотых процента. В моих мирах их было ноль. Ни одного, ни в базовом, ни в богатом на восемь миллионов событий. При этом по отдельности оба признака воспроизводились нормально:

высокая новизна

много отказов

и то и другое

реальность

0.644%

0.669%

0.022%

мой богатый мир

0.887%

6.933%

0.000%

Каждый признак по отдельности лёг в реальность. Их пересечение оставалось пустым - и я решил, что это предел

Каждый признак по отдельности лёг в реальность. Их пересечение оставалось пустым - и я решил, что это предел

Отсюда я и сделал вывод, который казался красивым и глубоким: генератор порождает закономерности, а реальность состоит из закономерностей плюс совпадений. Совпадение - это по определению отсутствие механизма, незапланированное пересечение независимых причин. Чтобы получить их, надо смоделировать все механизмы реального мира вместе с их связями, то есть смоделировать реальность целиком. Красиво, безнадёжно и, как выяснилось, неверно.

Контрольный прогон, который стоил одного запуска

Я взялся порождать совпадения нарочно: сделал мир, где у машины законно сходятся новые учётки и отказы. И заодно - контроль: те же миры с этим механизмом, выключенным в ноль.

Контрольные миры дали совпадения не хуже.

То есть механизм был ни при чём. Совпадения родились от того, что мир стал меньше - полтора миллиона событий на те же машины, - и окна поредели. В редких окнах доли начинают плавать: в окне из двух событий “доля новых учёток” принимает значения 0, 0.5 и 1, и половина из них выглядит как подпись атаки. Ровно так совпадения устроены и в реальности.

Без контроля я бы приписал эффект механизму и написал бы в статью неправду. Контроль стоил одного запуска.

Разреженный мир

Дальше уже понятно, что строить: мир, где события размазаны по времени, как в живой сети. Беру мир без стадий, поднимаю его до четырёх миллионов событий и добавляю тихие машины. Восемнадцать процентов “тихих” машин, чьи события растянуты на 80-260 часов. Форма окон впервые села на реальность:

событий в окне

1-й процентиль

медиана

90-й процентиль

реальность LANL

2

22

49

разреженный мир

1

19

57

прежний (базовый) мир

5

16

53

Совпадения появились сами, и притом правильного размера: в настоящих совпадениях медиана два события, у меня четыре, а раньше было двадцать четыре. Никакого механизма “совпадений” в конфиге нет до сих пор. Есть разреженность, из которой они следуют.

Вот всё, что делает разреженность, дословно из world-sparse: две последовательности в справочнике машин и одна формула времени.

<!-- 18% машин - "тихие": их события размазаны по 80-260 часам вместо 1-40 -->
<sequence name="isQuiet"><gen type="text" value="1,0" percent="18,82"/></sequence>
<sequence name="spread">
  <gen if="isQuiet == 1" type="number" value="80..260"/>
  <gen type="number" value="1..40"/>
</sequence>

<!-- час события: разброс машины задаёт, на сколько часов лягут её события -->
<sequence name="Time">
  <gen type="formula"
       expr="... 28800 + floor(hash(N, 3) * Spread) * 3600 + floor(hash(N, 4) * 3600)"/>
</sequence>

Здесь три штатных средства TDCV2. percent задаёт долю точно, а не вероятностью: тихих машин будет ровно 18%. Атрибут if выбирает генератор по условию: тихой машине достаётся разброс 80-260 часов, остальным 1-40. А формула времени раскладывает события машины по Spread часам. Событий у тихой машины примерно столько же, сколько у любой другой, только ложатся они по одному-два в час - и “доля новых учёток” в таком окне начинает принимать значения 0, 1/2 и 1, а половина из них выглядит как подпись атаки. Никакого механизма совпадений тут нет: есть тихие машины, а совпадения - их арифметическое следствие.

Совпадение - не механизм, который надо смоделировать. Это то, что получается само, если форма данных правильная.

Шесть миров, шесть сетей

Второй вывод, который пришлось забрать назад, - про ансамбли.

Я честно измерил и записал: объединять сети бесполезно, потому что разброс их предсказаний 0.0002, они ошибаются в одних и тех же местах, и усреднять нечего. Это была правда - но правда про сети, обученные на одном мире.

А если обучить шесть сетей на шести разных мирах и усреднить их ранги - не оценки, а места: каждая сеть выстраивает все 3.6 миллиона окон в свой список, и окну приписывается среднее место по шести спискам, - получается вот что. Миры здесь - чистые: без стадий нападения, которые в богатом мире учили сеть срабатывать на тихие окна, и ещё без разреженности - она добавится следующим шагом. В таблице - сколько ложных тревог просматривает аналитик на пути к N-й найденной атаке, отложенный набор, 3.6 миллиона окон:

что именно проверяем

до 1-й

до 8-й

до 16-й

до 24-й

базовый мир, одна сеть - с чего начинали

3

102

188

412

базовый мир, шесть зёрен, ранги (что даёт само усреднение)

11

80

117

504

один чистый мир, одна сеть (что даёт сам чистый мир)

0

2

30

222

шесть разных чистых миров, шесть сетей, ранги

0

3

7

19

те же шесть миров, слитых в один набор, одна сеть

814

6 992

26 297

59 973

Сто восемьдесят восемь ложных тревог превратились в семь. И ни один множитель по отдельности этого не даёт: усреднение само по себе - 117, чистый мир сам по себе - 30, а вместе - 7.

А теперь последняя строка, ради которой и ставился контроль. Те же шесть миров, слитые в один обучающий набор для одной сети, дают 26 297 ложных вместо 188. Хуже базового в сто сорок раз.

Разнообразие, слитое в один набор, становится противоречием. Разложенное по отдельным моделям - становится силой.

И к этому прилагается измеритель, которым можно пользоваться до экзамена. Разброс мнений между сетями - среднее отклонение оценок по окнам:

сети обучены

разброс мнений

выигрыш

на одном наборе

0.0002

никакого

на одном мире, разные зёрна

0.004

в 1.6 раза

на разных мирах

0.014

в двадцать семь раз

Это удобно тем, что разброс снимается на самих сетях, не трогая отложенный набор. Если сети единодушны - объединять их незачем, и можно не тратить экзамен на проверку.

Микропетля: реальность показывает машину, конфиг получает строку

Дальше началось то, ради чего вообще стоит писать генератор.

Семёрку я не вскрывал. А вот ансамбль шести разреженных миров дал лучшую AUC за проект, 0.944, и при этом шесть ложных тревог выше первой атаки. Вот на них я впервые и посмотрел, что это за окна. Пять из шести оказались одним явлением: шторма отказов. Машины с 390-3391 событиями в час и 90-99% неуспешных входов. По меткам это норма, по смыслу - сломанная служба или чей-то перебор. Для аналитика находка, для метрики ошибка.

Посмотрел, сколько таких в данных: в LANL от 3 до 13 в сутки, 249 на всём отложенном наборе. А в моих обучающих мирах - ноль. Роль “служба со сломанным паролем” в конфиге была, но давала 30-80% отказов при обычном объёме событий. Не тот шторм.

И тут выяснилась важная вещь. К этому моменту у меня было три семьи миров - шесть чистых, шесть разреженных и, забегая вперёд, шесть со штормами; семья - это один конфиг, размноженный шестью зёрнами с разной долей совпадений. Так вот, все восемнадцать сетей трёх семей ставили штормам максимальный ранг. Ансамблю нечего было исправлять.

Ансамбль лечит разногласия, а не общее заблуждение. Общее заблуждение рождается из общей дыры в мирах, и закрыть его может только мир.

Дальше пошла петля, которая и есть настоящая работа с генератором: смотрю на верхние ложные срабатывания, нахожу конкретную машину, понимаю, чего про неё не знает мой мир, дописываю одну-две строки конфига. Первая строка таблицы - базовый мир, вторая - разреженный, с которого петля началась, дальше три круга подряд. AUC здесь - на трудных двенадцатых сутках, а “ложных до 6-й” - сколько ложных тревог стоит до шестой из двенадцати размеченных там атак:

что добавлено в мир

по какой машине найдено

AUC, трудные сутки

ложных до 6-й

базовый мир, с чего начинали

-

0.990

-

разреженный мир: хвост тихих машин

форма окон LANL

0.99327

0

шторма отказов: 1.5% машин, 90-100% отказов, 1-4 часа

верхушка отложенного набора

0.99379

3

переезд человека на другую машину

C3699 занимала 22 из 40 верхних

0.99636

0

новых машин 0.6% -> 3%

первые часы новых машин

0.99743

0

Про переезд стоит рассказать отдельно, потому что там я ошибся с первого раза - и ошибка оказалась содержательной.

На трудных сутках одна машина, C3699, занимала двадцать две строки из сорока в верхушке ложных. Разбор: на неё пересел человек с другой машины. Я дописал роль “переезд”: с какого-то часа половина событий машины идёт от пользователя-чужака. Не подействовало совсем.

Сравнил признаки настоящего C3699 и своего синтетического - и разница оказалась в одном: у настоящего переезда 27 событий из 27 дают новую тройку “учётка, источник, назначение”, у моего - ноль. Причина простая, стоило подумать головой: человек, пересевший на другую машину, ходит на свои привычные серверы, а для новой машины они все новые. Я же заставлял его ходить по назначениям хозяина машины.

Правка в одну строку. C3699 исчезла из сорока верхних целиком. После третьего круга, с новыми машинами, она частично вернулась - восемь окон, начиная с пятнадцатого места, - но верхушку уже не занимала.

Реальность показывает конкретную машину, конфиг получает одну-две строки. Три круга - три ошибки, ушедшие из верхушки

Реальность показывает конкретную машину, конфиг получает одну-две строки. Три круга - три ошибки, ушедшие из верхушки

Как эти три правки выглядят в конфиге

Чтобы “две строки” не звучало фигурой речи - вот они дословно, из настоящих файлов.

Шторма (world-storm): одна роль и три переопределения там, где у обычной машины были свои значения (в файле они стоят в трёх разных последовательностях, здесь собраны вместе).

<!-- ШТОРМ. Служба со сломанным паролем, которая долбит без остановки:
     один-два пользователя, несколько назначений, 90-100% отказов,
     сотни событий в час. В LANL таких 3-13 окон в сутки, метка норма.
     Прежние миры их не содержали, и сети принимали шторм за нападение. -->
<sequence name="isStorm"><gen type="text" value="1,0" percent="1.5,98.5"/></sequence>

<!-- и три переопределения там, где у обычной машины свои значения -->
<gen if="isStorm == 1" type="number" value="1..2"/>       <!-- учёток -->
<gen if="isStorm == 1" type="number" value="1..4"/>       <!-- часов: весь шторм в 1-4 часа -->
<gen if="isStorm == 1" type="number" value="900..1000"/>  <!-- отказов, из тысячи -->

Переезд (world-move) вышел длиннее, потому что пришедшего надо не просто впустить, а отправить ходить по своим местам:

<!-- ПЕРЕЕЗД и НОВИЧОК. С некоторого часа половина событий машины идёт от
     пользователя, которого она не видела, и так до конца дня, без отказов.
     Переезд - пользователь другой машины (в сети он известен), новичок -
     свежая учётка (не известна нигде).
     Пришедший ходит на СВОИ назначения, новые и для машины, и для него:
     у C3699 все тройки новы - вот в этом и была разница с первой версией. -->
<sequence name="isMove"><gen type="text" value="1,0" percent="2,98"/></sequence>
<sequence name="isFresh"><gen type="text" value="1,0" percent="1,99"/></sequence>
<sequence name="moveHour"><gen type="number" value="8..20"/></sequence>

<sequence name="Newcomer">
  <gen type="formula"
       expr="Foreign == 0 && IsSvc == 0 && IsServ == 0 && Period == 1
             && (W.isMove == 1 || W.isFresh == 1)
             && floor(Time / 3600) >= W.moveHour && hash(N, 21) < 0.5 ? 1 : 0"/>
</sequence>
<sequence name="NewUser">
  <gen type="formula" expr="W.isMove == 1 ? ((W.hid * 37 + 11) % 2000) * 30 : 90000 + W.hid"/>
</sequence>

Разница между первой версией переезда и второй - не в этих строках, они в обеих версиях одинаковы. Она в формуле назначений, которой выше нет. В первой версии пришедший ходил по назначениям хозяина машины, и признак “новая тройка” не срабатывал: назначения-то машине знакомы. Во второй у него свой диапазон, новый и для машины тоже, - одна ветка в начале формулы Dst:

expr="Newcomer == 1 ? 5000 + (W.hid * 13) % 900 + floor(hash(N, 2) * 4) : (...)"

Именно это и отличало настоящую C3699 от моей выдуманной.

Третья правка, “новых машин 3%”, - вообще одно число: доля роли “новая машина” в справочнике меняется с 0.6 на 3. Базовый мир занимал 135 строк, финальный - 210. Всё, что между ними, добыто вот такими кругами.

Генератор во всём этом обязан ровно одному: не мешать. Он не знает про сети, штормы и переезды и знать не должен - его дело в том, чтобы описать их заняло две минуты, когда ты уже понял, чего не хватает. Понимание - работа человека, и она никуда не девается.

Сколько из этого честно

Тут нужна отдельная бухгалтерия, иначе получится жульничество.

Отложенный набор ценен ровно до первого взгляда на него. Семёрка выше получена до того, как я впервые открыл верхушку ложных срабатываний, - это честный перенос. А шторма я нашёл, уже посмотрев туда, и всё, что построено поверх, унаследовало это знание. Разница принципиальная, и смешивать нельзя:

результат

AUC

ложных до 16-й

как получено

одна сеть, базовый мир - первая половина статьи

0.909

188

честно

шесть чистых миров, шесть сетей

0.938

7

честно

шесть разреженных миров

0.944

10

честно

восемнадцать сетей трёх семей: чистые, со штормами, финальные

0.932

1

после разбора ошибок

Честная цифра для сравнения с той, с которой начинали, - семь вместо ста восьмидесяти восьми. Единица тоже настоящая, но она уже с подглядыванием, и я её привожу только с этой пометкой.

И ещё одна строка в ту же бухгалтерию: обращений к отложенному набору за проект набралось не меньше двадцати трёх. Каждое понемногу снижает его ценность. По-настоящему чистая проверка - свежие сутки Лос-Аламоса, которых мы не трогали вовсе, одним обращением. Она не сделана, и это честнее сказать, чем умолчать.

Заодно выяснилось, сколько сетей имеет смысл держать. Кривая насыщения: одна сеть даёт 43 ложных до шестнадцатой атаки, две - 10, четыре - 4, шесть - 3, двенадцать - 1, а дальше ничего не меняется. Насыщение примерно на двенадцати сетях и трёх семьях миров с разными явлениями. Четвёртая семья не добавила ничего.

Для ориентира: что делают специалисты

Это не турнирная таблица, а система координат. Мне самому было непонятно, много это или мало - 0.909, - пока я не посмотрел, что получается у людей, которые занимаются задачей профессионально.

решение

AUC

на чём обучено

LMTracker

~0.95

на размеченных данных Лос-Аламоса

шесть разреженных миров, шесть сетей

0.944

только синтетика

шесть чистых миров, шесть сетей

0.938

только синтетика

UGEA-LMD

0.9254

на размеченных данных Лос-Аламоса

мой бустинг

0.918

только синтетика

одна сеть, базовый мир

0.909

только синтетика

полносвязная, 1333 параметра

0.862

только синтетика

счётчик “учёток с машины”

0.618

-

счётчик “мест назначения”

0.535

-

Оговорка про верхнюю строку: часть работ публикует не само значение, а только первенство в своём сравнении, поэтому диапазон 0.92-0.95 я беру по тем, кто число приводит. И сравнивать AUC разных протоколов оценки в лоб некорректно - сплиты разные, - так что это именно ориентир, а не сопоставление.

Три честных вывода.

Первый. Пороговые правила отстают сильно. На отложенном наборе они работают немногим лучше монетки. По цене поиска разница на три порядка: чтобы найти шестнадцать окон из 64, счётчику нужно около 161 тысячи ложных тревог, а рекуррентной сети - 188. Вот этого я не ожидал вовсе.

Второй. В диапазон исследовательских работ синтетика вошла, и это приятнее, чем я рассчитывал. Выше UGEA-LMD, ниже LMTracker, до верхней границы шесть тысячных. Оба числа - честный перенос, без подглядывания в отложенный набор. Радоваться тут особо нечему по причине из предыдущего абзаца: протоколы разные, лобовое сравнение некорректно. Но раньше я был ниже всех, кто публикует AUC, а теперь внутри.

Строку с бустингом привожу нарочно, хотя выше объяснил, что он негоден: по AUC он формально держится в том же ряду, а по цене поиска хуже в сотни раз. Лучшая иллюстрация того, почему сравнивать методы по одному AUC бессмысленно - включая сравнение с этой самой таблицей. И цены поиска, кстати, не публикует никто из чужих работ, так что по главной для аналитика мере ориентиров просто нет.

Третий, и главный: постановка у нас разная. Всем перечисленным нужны размеченные примеры атаки в вашей сети. А их у обычной организации нет: чтобы получить разметку, надо сначала подвергнуться компрометации, обнаружить её и разметить. Промышленные системы обходят это профилированием без учителя, но требуют недель наблюдений.

Работ, где детектор бокового движения обучен только на синтетике и проверен на LANL, я не нашёл. Но заявлять “я первый” не буду: отсутствие в результатах поиска доказательством не является.

Можно ли это внедрять

Как готовый продукт - нет. Как слой первичного отбора - да.

Что работает: четыре тысячи параметров, признаки считаются одним потоковым проходом без загрузки данных в память, не нужно ни одного размеченного инцидента в вашей сети, а на выходе не приговор, а очередь - “вот двадцать окон, посмотри их первыми”.

Три условия, каждое измерено.

  1. История сети не меньше недели. На первые сутки, где у машины 464 события истории против 3500 на восьмые, метод проваливается.

  2. Опору обновлять раз в две-три недели - из-за ножниц выше.

  3. Это фильтр, а не автоблокировка. Чтобы найти шестнадцать настоящих окон, аналитик просматривает двадцать три окна на 3.6 миллиона - шестнадцать настоящих и семь ложных. Для очереди разбора приемлемо, для автоматического реагирования всё равно нет: половина размеченных окон лежит в хвосте, на местах от трёх тысяч до двух миллионов, и по признакам часа они неотличимы от шума - одно событие, один новый пользователь, ни одного отказа. Достать их можно только другими признаками, связями между машинами, а этого в работе нет.

Отдельно про антипаттерн. Белые списки вида “машина прошла проверку - исключаем её из анализа” - это ускоренная версия отравления опоры. Помните про 58.3%.

Что не сработало

Отрицательных результатов набралось больше, чем положительных, и каждый с понятым механизмом - это дороже самого результата.

Обучение на разбавленных окнах. Звучало логично: научим сеть видеть одно чужое событие среди тридцати семи. Получил 1428 ложных вместо 27. Механизм посчитан: разбавление учит, что любое событие с тройной новизной означает компрометацию, а таких хватает у 1330 нормальных окон.

Накопление подозрения по соседним часам. Вторжение длится часами, казалось - надо копить. От нейтрального до разрушительного, AUC 0.504. Разбор: тот же механизм самоотравления повторился на масштабе часов, уже со второго часа активность попадает в собственную опору.

Ансамбли из сетей одного мира. Надстройка над консилиумом проигрывает простому усреднению во всех составах, причина - тот самый разброс 0.0002. Слабые ученики дали надстройке расхождение мнений, и она впервые обогнала усреднение, но абсолютный уровень остался ниже одиночной сети. Оговорка “одного мира” тут существенная: как только миры стали разными, ансамбль заработал - об этом было выше, и это ровно тот случай, когда отрицательный результат оказался верным только наполовину.

И мои собственные ошибки. Дважды на вход модели уезжало двенадцать признаков из шестнадцати, недостающие приходили пустыми, и сеть выдавала константу. Ловил по характерной подписи - AUC ровно 0.50000. Ещё раз поиск порога оказался квадратичным и на 3.6 миллиона значений просто не досчитывался. И один раз я нарушил собственное правило “сверять синтетику до обучения” - результат упал вдвое.

Что где лежит и как это проверить

Всё, о чём выше, - настоящие файлы, а не пересказ. Лежит это в репозитории tdc-guard, в README пошаговая инструкция с ожидаемыми числами на каждом шаге (английская версия - README по-английски). Раскладка, чтобы можно было посмотреть глазами или запустить у себя. Отложенный набор в именах файлов репозитория называется sealed: exam/sealed.mjs, results/sealed-windows.csv.

Миры

Конфиги лежат в репозитории в двух экземплярах: в gen/ с английскими комментариями и рядом - русские копии, как они процитированы в этой статье. Строки кода в обоих одинаковые.

файл

что в нём

world

базовый мир, 135 строк - тот, что целиком лежит в спойлере выше

world-nostage

чистый мир: богатый мир без стадий нападения, семья ns - шесть таких дали честную семёрку

world-sparse

разреженный: тихие машины, из которых сами родились совпадения

world-storm

плюс шторма отказов

world-move

плюс переезд человека на другую машину

world-newhost

финальный, 210 строк

world-coin

make-control

мир с явным механизмом совпадений и тот самый контроль, который его опроверг

скрипты сборки миров

скрипты make-*-worlds.sh: шестёрки миров с разными зёрнами, по одному скрипту на семью

Измеритель и экзамен

файл

что в нём

windows

тот самый единственный измеритель признаков, общий для синтетики и для реальности

rows

загрузчик, который падает, если пришли не все признаки - после двух моих AUC ровно 0.50000

прогоны экзамена

прогоны *.mjs по настоящим журналам Лос-Аламоса

Сети и опыты

файл

что в нём

model

data

evaluate

модель, кодирование признаков, AUC рангами и кривая цены за один проход

sweep_big

развёртка по 31 полносвязной архитектуре

recurrent

exotic

рекуррентные и девять экзотических

heldout_ensemble

шесть сетей на шести мирах и отложенный набор - главный результат

heldout_sameworld

контроль: шесть зёрен одного мира, ради строки в лестнице атрибуции

top_false

сорок верхних ложных срабатываний с признаками - инструмент микропетли

saturation

кривая насыщения: сколько сетей имеет смысл держать

Летопись

DIARY - записи по датам, каждый прогон по порядку, включая неудачные и грабли. FACTS - свод чисел с пометками, что получено честно, а что после разбора ошибок. Второй документ полезнее для проверки, первый честнее: там видно, сколько раз я ошибался по дороге. Основной язык репозитория - английский (DIARY по-английски, FACTS по-английски, README по-английски), русские версии лежат рядом с суффиксом .ru.

Данные открыты и качаются свободно с портала Лос-Аламоса. Миры детерминированы по зерну и пересобираются побайтово, так что числа выше воспроизводятся у вас без обращения ко мне.

Что из этого следует

Можно получить работающий детектор без единого размеченного инцидента в вашей сети. Двадцать пять тысяч параметров, шесть маленьких сетей, обучение секундами на ноутбуке. Семь ложных тревог на пути к шестнадцатой атаке из трёх с половиной миллионов окон - против ста шестидесяти одной тысячи у порогового счётчика, который при этом работает прямо по вашим настоящим данным. Вся работа - два дня: один на разведку данных, генератор и первую сеть, второй на опыты с мирами, ансамблями и микропетлёй.

Нельзя - и вот тут мне пришлось дважды забрать свои слова назад.

Сначала я написал, что упёрся в принципиальный предел: генератор порождает закономерности, а реальность состоит из закономерностей плюс совпадений, и совпадения не синтезируются. Звучало солидно. Оказалось, что совпадения получаются сами, если у окон правильная форма, и всё, чего не хватало, - это разреженности.

Потом я написал, что ансамбли на этой задаче бесполезны, потому что сети ошибаются в одних и тех же местах. Тоже правда - ровно до тех пор, пока сети учатся на одном мире.

Оба раза потолком оказывался не метод, а то, чего я не додумал про свой собственный мир. Настоящие границы, которые я готов защищать, лежат в другом месте: половина размеченных окон по признакам часа неотличима от шума, и достать её нечем; отложенный набор я трогал не меньше двадцати трёх раз, и по-честному его давно пора сменить на свежие сутки.

Пять правил, которые я вынес

  • Синтетика должна быть верна по смыслу, а не похожа по статистике. Учётка обязана быть настоящей и чужой; выдуманная ломает всё, и никакие настройки сети этого не чинят.

  • Мир и архитектура - одно решение, а не два. Пять раз за проект смена мира меняла, какая архитектура выигрывает. Сравнивать сети на одном мире и переносить вывод на другой нельзя.

  • AUC и цена поиска расходятся. Не дважды и не случайно, а систематически: порядок моделей по одной мере не совпадает с порядком по другой.

  • Ансамбль лечит разногласия, а не общее заблуждение. Если все сети ошибаются одинаково, объединять их нечем - дыру закрывает только мир.

  • Разнообразие, слитое в один набор, становится противоречием; разложенное по моделям - силой. Те же шесть миров: по сети на мир - семь ложных, слитые в один набор - двадцать шесть тысяч.

Про сам генератор вывод скучный, и это лучшее, что можно про него сказать. Он пережил больше десятка версий мира и ни разу не стал узким местом. Узкое место всё это время было в другом: понять, что именно порождать. Разведка данных, разбор конкретной машины в верхушке ложных срабатываний, догадка, чего про неё не знает мой мир, - и только потом одна строка конфига.

В задачах на синтетике узкое место никогда не в модели.

Больше всего мне в этой работе понравилась петля из предпоследних разделов. Ошибка детектора показывает конкретную машину; смотришь на неё и понимаешь, какого явления нет в твоём выдуманном мире; дописываешь две строки; ошибка исчезает. Заметьте, что интересная часть тут целиком человеческая - и это, пожалуй, хорошая новость для тех, кто боится, что данные скоро начнут порождаться сами.

Инструмент называется TDCV2, лежит под MIT.

Если вы занимаетесь этой областью профессионально и видите, где я ошибся, - напишите в комментариях, мне правда интересно. Особенно про ножницы опоры: я описал их как измеренное свойство, но не знаю, как с этим живут промышленные системы, и утверждать не берусь.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.