Русский
Русский
English
Статистика
Реклама

Графы и визуализация

Перевод Руководство по анализу Sysmon-угроз, часть 2. Использование данных из Sysmon-событий для выявления угроз

08.07.2020 16:06:56 | Автор: admin


Эта статья является первой частью серии по анализу Sysmon-угроз. Все остальные части серии:
Часть 1. Знакомство с анализом логов Sysmon
Часть 2. Использование данных из Sysmon-событий для выявления угроз (мы тут)
Часть 3. Углубленный анализ Sysmon-угроз с помощью графов

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

  1. Использование PowerShell для прямого доступа к гранулированной информации о процессах;
  2. Построение и визуализация иерархии процессов первый важный шаг в поиске угроз;
  3. Использование метаданных Sysmon для формирования важных метрик, полезных при углублённом расследовании угроз, таких как подсчёт частоты, с которой запускаются конкретные процессы.

Использование Get-Sysmonlogs


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

$events = Get-WinEvent  -LogName "Microsoft-Windows-Sysmon/Operational" | where { $_.id -eq 1 } foreach ($event in $events)  {    $ev = $event.Message -split "`r`n"    $jsons="{ "    foreach ($line in $ev) {        $line=$line -replace "\\","\\" `               -replace "\{"," " `               -replace "\}"," " `               -replace '"','\"' `               -replace "`n"," "         $line=$line -replace '(\s*[\w\s]+):\s*(.*)', '"$1":"$2",'        $jsons = $jsons + $line }         $jsons =$jsons + '"blah" : "blah" }'         ConvertFrom-Json -InputObject $jsons     }}

Весь код сейчас выложен на GitHub и вы можете его скачать и импортировать как Sysmon-модуль для собственного проекта. Единственная нестабильность связана с удалением нескольких неприятных символов скобки, бекслэши, символы конца строки, кавычки чтобы сделать вывод приближённым к JSON.
Итак, классическим сигналом нарушителя, копошащимся вокруг системы, является использование команды whoami, и зачастую следующей после hostname. Хакер (или, возможно, инсайдер), заполучивший чью-то учётную запись, хочет убедиться, что имперсонализация работает, поэтому он часто набирает вышеуказанные команды, как только оказывается на сервере жертвы. Для остальных же whoami и hostname это не те слова, которые они будут вводить в консоли собственной системы, даже если они когда-либо пользуются командной строкой.

С помощью моей аккуратной команды, дающей доступ ко всем записям лога Sysmon, мы легко можем состряпать цепочку, фильтрующую имя процесса (как мы это сделали в первой части). При этом с Sysmon мы можем подойти к вопросу ещё более гранулярно, заглянув в командную строку родительского процесса.

Обычно, когда хакер проникает в сеть и получает доступ к командой строке, она представляет из себя устаревшую cmd кстати, именно так и происходит в случае взлома при помощи psexec или smbexec. Используя вывод get-symonlogs, можно отловить процессы whoami, которые были порождены этими устаревшими шеллами, и это будет хорошим доказательством угрозы.

Внимание: Whoami запустился через устаревший шелл cmd


Внимание: Whoami запустился через устаревший шелл cmd


С практической же точки зрения поиск по сырым логам журнала событий Windows и сопоставления процессов просто невозможно. Как мы только что видели, записи в Sysmon открывают множество возможностей для анализа угроз. Поэтому давайте продолжим наше исследование посредством сопоставления Sysmom-данных в более сложные структуры.

Азы структур данных: списки и графы


Логи Sysmon не только предоставляют нам командную строку родительского процесса, но и идентификатор этого процесса!

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

Сначала я думал, что мне придётся сдувать пыль с моей копии Структуры данных для поэтов и су-шефов, но тут меня выручили интернеты. Я наткнулся на шикарную коллекцию базовых алгоритмов Дага Финке (Doug Finke) на Gihub, написанную на PowerShell. Спасибо тебе, Даг!
После преодоления некоторой кривой обучения, я смог использовать его алгоритмы для структуризации моих событий Sysmon. Я построил структуры данных в виде списка и графа, а затем, с использованием API, написал PowerShell-функцию поиска команды и вывода иерархии процесса. Круто.

Я назвал её show-threat-path. Она осуществляет поиск в глубину по иерархии процесса и выводит имена приложений и ассоциированные с ними команды для корневого приложения, указанного в качестве входного параметра. В качестве моего первого теста я поискал по whoami.exe. И вот что увидел:

Иерархия процессов: процесс 2452 выглядит подозрительным!


Иерархия процессов: процесс 2452 выглядит подозрительным!


Дополнительный бонус тому, кто заметил на выводе выше, что whoami, ассоциированный с процессом 2452, был вызван через устаревший шелл cmd, который уже в свою очередь был запущен exe-файлом со странным именем в папке Windows.

Хммм. Если вы знакомы с механиками удалённых вызовов psexec, описанными здесь , то мысленно должны уже бить в колокола. Но я расскажу вам маленький секрет: играя роль хакера, я предварительно запустил данный whoami с удалённого сервера Linux с помощью python-скриптов Impacket.

Целью является демонстрация того, что с помощью обогащённых Sysmon логов и небольшой порции PowerShell, можно приготовить вполне практичную утилиту по выявлению уязвимостей, как я это только что проделал с show-threat-path.

Охота на угрозы с помощью направленных графов


Пришло время заняться более странными вещами. Обладая всей этой информацией о процессах, полученной из Sysmon, можно взглянуть на связи в более общем виде. Другими словами, я хочу рассматривать запущенные приложения PowerShell.exe, Explorer.exe, и т.д. как вершины графа и связать их с тем приложением, которое в свою очередь их запустило. В результате получится схема, показывающая, каким образом приложения взаимодействуют друг с другом (вместо создания отдельной вершины на каждый инстанс процесса).

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

На данном этапе было бы неплохо взглянуть на визуализацию того, о чём я веду речь. К счастью, существует великолепная утилита визуализации PowerShell графов под названием GraphViz, у которой есть чрезвычайно простые оболочки, доступные через PSQuickGraph. Тогда с помощью небольшого куска кода

#Let's graph it!!!$gv = New-Graph -Type BiDirectionalGraph # PSQuickGraphforeach ($e in $g.getAllEdges() )  { $g from Doug Fink's functions    $vs= $e.startvertex   $ve= $e.endvertex    PSQuickGraph\Add-Edge -From $vs.value.Key -To $ve.value.Key -Graph $gv |Out-Null}Show-GraphLayout -Graph $gv

можно визуализировать сложные взаимодействия между приложениями через интерфейс GraphViz:

GraphViz:Библиотека PowerShell для визуализации иерархий процессов


GraphViz: Библиотека PowerShell для визуализации иерархий процессов


Что это даёт? По сути это графический способ выявлять угрозы. Вместо того, чтобы искать определённую сигнатуру текста, как мы это делали раньше с командой show-threat-path, теперь мы можем попытаться найти аномалии на графе.

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

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

В третьей части нашего обзора мы углубимся в анализ и применение алгоритмов и методов для поиска уязвимостей. Оставайтесь с нами!
Подробнее..

Баланс в настольном геймдизайне строим графы с помощью Google App Script и Gephi

11.12.2020 16:14:18 | Автор: admin

Всем привет! Меня зовут Никита, и я хотел бы поделиться с вами некоторыми практическими аспектами разработки моей настольной игры Письма призрака (в этом месяце будет выпущена издательством "Экономикус"). Мы старались подходить к процессу разработки максимально системно, так что наш опыт может оказаться для кого-то интересным.

"Письма призрака" детективная настольная игра с тайными ролями на дедукцию, блеф и ассоциативное мышление. Если вы любите играть в "Мафию" или "Имаджинариум" уверен, вам она тоже понравится. Из современных настолок по жанру она ближе всего к "Мистериуму" и "Криминалисту".

Поставленные задачи

Основная механика Писем призрака строится на ассоциациях между картами с изображениями различных предметов (картами улик). И на одном из ранних этапов разработки мы задались вопросом: А можно ли просчитать и выстроить баланс в игре на ассоциации?. Собственно, почему бы не попробовать.

Задача балансировки ассоциаций состояла примерно в следующем:

  • Минимизировать количество сильных однозначных ассоциаций. Каждая карта в идеале должна примерно с равной силой ассоциироваться с несколькими другими.

  • Избежать обособленных групп ассоциаций. Не должно быть групп карт, которые ассоциируются только друг с другом и не ассоциируются ни с какими другими картами.

  • Добиться максимальной равномерности и хаотичности распределения ассоциаций по всему множеству карт.

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

Инструменты

Google Docs

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

  • Автоматизация с помощью Google App Script. При наличии минимальных навыков программирования на JS даёт практически безграничные возможности для анализа и формирования данных, а также интеграции с другими онлайн-сервисами.

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

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

Gephi

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

Позволяет строить даже вот такие масштабные штуки:

Подготовка данных

Для построения графа надо подготовить информацию обо всех вершинах и связях между ними. Для этого мы использовали таблицу в Google Sheets, где каждая строка и колонка соответствуют конкретным картам, а на их пересечении указывается сила ассоциации между ними.

Мы условно разделили ассоциации на 4 уровня по силе:

0 полное отсутствие ассоциаций
1 слабая ассоциация либо сходство по форме, цвету или материалу
2 достаточно крепкая ассоциация
3 однозначная ассоциация

Естественно, ассоциации явление достаточно субъективное, но мы постарались учесть самые очевидные и часто возникающие.

Чтобы прописать связи между всеми уликами, надо заполнить половину таблицы 150 на 150 ячеек (вторая половина получается зеркальной, так как ассоциации между двумя картами симметричные). Достаточно трудоёмкий процесс, который крайне трудно автоматизировать. Разработка нейросети, анализирующей ассоциации между изображениями, в теории возможна, но явно выходит за рамки нашего проекта.

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

Фрагмент получившейся таблицы:

Для этой автоматизации как раз потребовалось использовать скрипты. По координатам выделенной ячейки определяются id карт, которые надо вывести в углу таблицы. Изображения карт берутся из другой таблицы, куда загружены все картинки с соответствующими id.

// Обновление изображений в углу таблицыfunction RefreshPictures() {  var ss = SpreadsheetApp.getActiveSpreadsheet();  // Таблица ассоциаций  var sheet_a = ss.getSheetByName("Ассоциации");  var range_a = sheet_a.getDataRange();  // Таблица с картинками  var sheet_p = ss.getSheetByName("Картинки");  var range_p = sheet_p.getDataRange();    // Получаем координаты выделенной ячейки  var row = sheet_a.getActiveCell().getRow();  var col = sheet_a.getActiveCell().getColumn();    // Получаем id карт  var id1 = range_a.getCell(row, 1).getDisplayValue().toString();  var id2 = range_a.getCell(1, col).getDisplayValue().toString();    // Ищем в таблице картинок нужную строчку по id  var pos_pic1 = RowOfId(id1, range_p);  var pos_pic2 = RowOfId(id2, range_p);    // Проверяем, удалось ли найти картинки с нужными id  if (pos_pic1 != -1) {    // Картинки подгружены в таблицу в виде формул,    // содержащих ссылки на файлы в облаке    var pic1_f = range_p.getCell(pos_pic1, 2).getFormula();    range_a.getCell(2, 1).setFormula(pic1_f);  }  else  {    range_a.getCell(2, 1).setValue("X");  }    if (pos_pic2 != -1) {    var pic2_f = range_p.getCell(pos_pic2, 2).getFormula();    range_a.getCell(2, 2).setFormula(pic2_f);  }  else  {    range_a.getCell(2, 2).setValue("X");  }}// Поиск в таблице картинок нужной строчки по idfunction RowOfId(id, rng) {    var height = rng.getHeight();  var data = rng.getValues();    for (var i = 1; i < height; i++) {        if (data[i][0].toString() == id) {      return i + 1;    }  }    return -1;}

С загрузкой картинок в таблицу тоже пришлось повозиться. Загружать 150 картинок по одной совсем не хотелось, а закинуть сразу все функционал Google Sheets пока не позволяет (сама возможность вставлять картинки в ячейки таблицы появилась сравнительно недавно). Я написал отдельный скрипт для загрузки всех изображений из папки на гугл-диске, через Google App Script довольно удобно работать с другими их сервисами.

// Загрузка картинок в таблицу из папки в Google Drivefunction LoadPicturesFromDrive() {  var ss = SpreadsheetApp.getActiveSpreadsheet();  var sheet_p = ss.getSheetByName("Картинки");  var range_p = sheet_p.getDataRange();    var art_folder = DriveApp.getRootFolder().getFoldersByName("Папка с картинками").next()  var files = art_folder.getFiles();    // Перебираем все файлы в папке  var i = 1;  while (files.hasNext()) {    var file = files.next();        var file_name = file.getName();    // Вытаскиваем из имени файла id карты    var id = file_name.slice(0, file_name.indexOf("."));        // Записываем id в таблицу    sheet_p.getRange(i + 1, 1).setValue(id);        // Устанавливаем права доступа к файлу    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);    var file_id = file.getId();        // Вставляем картинку в ячейку с помощью формулы IMAGE    sheet_p.getRange(i + 1, 2).setFormula("=IMAGE(\"" + "https://drive.google.com/uc?export=download&id=" + file_id + "\")");        i = i + 1;  }}

Но тут выяснилось, что разработчики из гугла не сумели полноценно подружить Google Sheets и Google Drive, из-за чего примерно 10% всех изображений просто не отображались. Пошарив по форумам, я выяснил, что далеко не первый столкнулся с этой проблемой, и этот баг пока не исправлен. Пришлось дополнительно разбираться с API Dropbox, чтобы сделать загрузку изображений уже оттуда. Поскольку Dropbox не является частью гугловской экосистемы, это потребовало гораздо больше манипуляций, зато в итоге всё заработало безотказно.

// Загрузка картинок в таблицу из папки в Dropboxfunction LoadPicturesFromDropbox() {  var ss = SpreadsheetApp.getActiveSpreadsheet();  var sheet_p = ss.getSheetByName("Картинки");  var range_p = sheet_p.getDataRange();    // Задаём параметры для POST-запроса  var data = {    "path": "",    "recursive": false,    "include_media_info": false,    "include_deleted": false,    "include_has_explicit_shared_members": false,    "include_mounted_folders": true,    "include_non_downloadable_files": true  };  var payload = JSON.stringify(data);    var options = {    "method" : "POST",    "contentType" : "application/json",    "headers" : {       "Authorization" : "Bearer [код авторизации]"    },    "payload" : payload,    muteHttpExceptions : true  };    // Отправляем POST-запрос для получения списка файлов в папке  var url = "https://api.dropboxapi.com/2/files/list_folder";  var response = UrlFetchApp.fetch(url, options);  var json = JSON.parse(response.getContentText());    // Обрабатываем полученный список файлов  for (var i = 0; i < json.entries.length; i++) {    var name = json.entries[i].name;    // Создаём публичную ссылку на файл    CreateSharedLink(name);    var sh_link = GetSharedLink(name);        // Вытаскиваем из имени файла id карты    id = name.slice(0, name.indexOf("."))      // Вставляем картинку в ячейку с помощью формулы IMAGE    sheet_p.getRange(i + 2, 1).setValue(id);    sheet_p.getRange(i + 2, 2).setFormula("=IMAGE(\"" + sh_link+"\")");  }}// Создание публичной ссылки на файл function CreateSharedLink(name) {  // Задаём параметры для POST-запроса  var data = {    "path": ("/" + name),    "settings": {        "requested_visibility": "public",        "audience": "public",        "access": "viewer"    }  };  var payload = JSON.stringify(data);    var options = {    "method" : "POST",    "contentType" : "application/json",    "headers" : {       "Authorization" : "Bearer [код авторизации]"    },    "payload" : payload,    muteHttpExceptions : true  };    // Отправляем POST-запрос для создания публичной ссылки на файл  var url = "https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings";  var response = UrlFetchApp.fetch(url, options);}// Получение публичной ссылки на файлfunction GetSharedLink(name) {  // Задаём параметры для POST-запроса  var data = {    "path": ("/" + name)  };  var payload = JSON.stringify(data);    var options = {    "method" : "POST",    "contentType" : "application/json",    "headers" : {       "Authorization" : "Bearer [код авторизации]"    },    "payload" : payload,    muteHttpExceptions : true  };    // Отправляем POST-запрос для получения публичной ссылки на файл  var url = "https://api.dropboxapi.com/2/sharing/list_shared_links";  var response = UrlFetchApp.fetch(url, options);  var json = JSON.parse(response.getContentText());    // Вытаскиваем ссылку на файл из полученного ответа  var urlForDownload = json.links[0].url.slice(0, -1) + '1';    return urlForDownload;}

Получившаяся таблица ассоциаций была преобразована в специальный вид для загрузки в Gephi (опять таки с помощью скриптов) и экспортирована в формате CSV. Нужны две таблицы: таблица меток вершин (столбцы: id, label) и таблица связей между вершинами (столбцы: source, target, weight).

// Преобразование получившейся матрицы в таблицу меток и таблицу связейfunction CreateGraph() {  var ss = SpreadsheetApp.getActiveSpreadsheet();  var sheet_a = ss.getSheetByName("Ассоциации");  var range_a = sheet_a.getDataRange();  var data = range_a.getValues();  var height = range_a.getHeight();    // Таблица меток вершин  var sheet_lbl = ss.getSheetByName("Graph Labels");  // Таблица связей между вершинами  var sheet_edg = ss.getSheetByName("Graph Edges");    // Массив допустимых весов для связей  var weights = new Array("1", "2", "3");  var edg_num = 0;    // Заголовок таблицы меток вершин  var lbl_header = ["Id", "Label"];  // Заголовок таблицы связей между вершинами  var edg_header = ["Source", "Target", "Weight"];    // Очищаем таблицы  sheet_lbl.clear();  sheet_edg.clear();    // Добавляем заголовки в таблицы  sheet_lbl.appendRow(lbl_header);  sheet_edg.appendRow(edg_header);    // Список, в котором мы будем накапливать связи между вершинами  var tmp_arr = [];  var tmp_arr_len = 0;    // Перебираем все ячейки матрицы (без учёта зеркальных)  for (var i = 2; i < height; i++) {    var id1 = data[i][0];    var name1 = data[i][1];        // Добавляем запись в таблицу меток вершин    var lbl_row = [id1, name1];    sheet_lbl.appendRow(lbl_row);        for (var j = i + 1; j < height; j++) {      var wt = data[i][j].toString();            if (weights.includes(wt)) {        var id2 = data[0][j];        edg_num += 1;                var edg_row = [id1, id2, wt];                tmp_arr.push(edg_row);        tmp_arr_len += 1;                // Как только накопим в списке 100 записей, добавляем их в таблицу связей.        // Если добавлять записи по одной сразу в таблицу, программа выходит за        // максимальное допустимое время выполнения Google App Script        if (tmp_arr_len >= 100) {          sheet_edg.getRange(sheet_edg.getLastRow() + 1, 1, tmp_arr_len, 3).setValues(tmp_arr);          tmp_arr = [];          tmp_arr_len = 0;        }      }    }  }    // Добавляем оставшиеся в списке записи в таблицу связей  if (tmp_arr_len > 0) {    sheet_edg.getRange(sheet_edg.getLastRow() + 1, 1, tmp_arr_len, 3).setValues(tmp_arr);    tmp_arr = [];    tmp_arr_len = 0;  }}

Построение графа

После загрузки данных в Gephi нужно настроить внешний вид графа и более наглядно разместить его вершины (сначала они расположены абсолютно хаотично). Для размещения вершин используются встроенные алгоритмы укладки, но только их недостаточно, зачастую до финального вида граф приходится доводить, перетаскивая вершины вручную. Всё это подробно описано в упомянутой ранее статье.

Нагляднее всего получается, если настроить отображение так, чтобы толщина ребра соответствовала силе связи между вершинами. Сразу стало заметно, что связи с силой 1 визуально перегружают граф, так как их слишком много. Поэтому я решил отображать только связи со значениями 2 и 3. Размер вершины соответствует количеству ведущих от неё связей.

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

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

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


Если вам интересно следить за развитием проекта, подписывайтесь на группу игры ВКонтакте и Instagram. Там я выкладываю заметки о разработке, сюжетные фрагменты и многое другое.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru