Это вторая часть блога, демонстрирующая элементарный анализ данных для сетевой безопасности на синтетическом наборе данных из Wildcard 400–2019 Trendmicro CTF. Первую часть вы можете найти здесь.
Вопрос 4.Частный канал управления и контроля
У нас всегда низкосортная инфекция; на некоторых внутренних машинах всегда будет какое-то вредоносное ПО. Некоторые из этих зараженных хостов звонят домой в C&C по частному каналу. Какой уникальный порт используется внешним вредоносным ПО C&C для маршалинга своих ботов?
Ответ 4:
Нам нужно найти порт, который используется внешним C&C-сервером для связи с несколькими внутренними машинами (ботами). Такой порт не будет широко известен и будет использоваться только (одним или несколькими) внешними C&C-серверами.
Мы вычисляем количество внешних IP-адресов, которые взаимодействуют с внутренним хостом, используя каждый порт.
src_count_by_port = combine(groupby(df_ext_int, :port), :src => (x -> size(unique(x))[1]) => :src_count) sort!(src_count_by_port, :src_count)
В коде используется groupby
для группировки потоков в df_ext_int по портам и combine
для вычисления количества различных IP-адресов внешнего источника для каждой группы. Выражение: :src => (x -> size(unique(x))[1]) => :src_count
вычисляет количество различных src
. Результат сохраняется в столбце src_count
.
Следующая строка сортирует src_count_by_port по src_count. На рис. 1 показаны несколько записей из src_count_by_port.
Порт 113 отличается от любого другого порта. Он используется ровно одним внешним IP-адресом для связи с внутренними машинами. Скорее всего, это порт, используемый внешним вредоносным ПО C&C для маршалинга своих ботов. На рис. 2 показаны несколько записей из df_ext_int, где указан порт 113.
df_ext_int[df_ext_int.port .== 113, :]
Внешний IP-адрес C&C — 15.104.76.58.
Вопрос 5: Внутренний P2P
Иногда наша вялотекущая инфекция видна по-другому. Один конкретный вирус распространился по нескольким машинам, которые теперь используются для передачи команд друг другу. Вредоносная программа создала внутреннюю P2P-сеть. Какой уникальный порт используется самой большой внутренней кликой из всех хостов, общающихся друг с другом?
Ответ 5:
Чтобы ответить на этот вопрос, нам нужно вычислить клики из сети с помощью пакета Julia Graphs.
using Graphs function max_clique_size(src, dst) x = union(Set(src), Set(dst)) n = length(x) g = simple_graph(n, is_directed=false) broadcast((x, y) -> add_edge!(g, x, y), src, dst) result = 0 for x in maximal_cliques(g) if length(x) > result result = length(x) end end return result end
Функция max_clique_size строит график для данных исходного и целевого массивов в качестве входных данных. Функция ожидает, что источник и место назначения будут массивами целых чисел, то есть каждый источник и место назначения идентифицируются с помощью целого числа. Функция использует функцию maximal_cliques из пакета Graphs для вычисления клик. Наконец, он вычисляет размер наибольшей клики, найденной в графе.
Затем мы кодируем IP-адреса источника и получателя в dst_int_int как целые числа.
unique_ips = union(Set(df_int_int.src), Set(df_int_int.dst)) lookup = Dict() for (i, ip) in enumerate(unique_ips) push!(lookup, (ip => i)) end
unique_ips — это набор хостов-источников и хостов-получателей, а поиск — это словарь с ключом в виде IP-адреса хоста и значением в виде закодированного целого числа. Мы используем словарь поиска для кодирования src и dst в df_int_int и уникальную функцию для дедупликации результата.
df_int_int_encoded = transform(df_int_int, [:src, :dst] .=> ByRow(x -> lookup[x]) .=> [:src, :dst])[!, [:src,:dst,:port]] unique!(df_int_int_encoded)
Поскольку нам нужно найти самую большую клику по портам, мы группируем df_int_int_encoded по портам и используем функцию max_clique_size для вычисления максимального размера клики для каждого порта.
df_int_int_encoded_by_port = groupby(df_int_int_encoded, :port) clique_sizes_df = combine(df_int_int_encoded_by_port, [:src, :dst] => ((src, dst) -> max_clique_size(src, dst)) => :max_clique_size)
Самая большая клика в clique_sizes_df относится к порту 83, что является ответом на этот вопрос.
На рис. 2.5 показаны несколько записей из df_int_int, использующих порт 83.
Вопрос 6: Контроллер вредоносных программ
Мы просто попали в черный список службы репутации IP-адресов, потому что какой-то хост в нашей сети ведет себя плохо. Один хост является ботоводом, получающим обратные вызовы C&C от своего ботнета, у которого мало других причин для связи с хостами на предприятии. Какой у него IP?
Ответ 6:
Нам нужно найти один внутренний хост, который получает C&C обратные вызовы от ботнета, то есть несколько внешних IP-адресов.
Сначала мы вычисляем количество различных внутренних хостов, к которым обращается каждый внешний IP-адрес. Результат сортируется по количеству различных внутренних хостов.
dst_count_by_src = combine(groupby(df_ext_int, :src), :dst => (x -> size(unique(x))[1]) => :dst_count) sort!(dst_count_by_src, [order(:dst_count, rev=false)])
На рис. 3 показаны несколько записей из dst_count_by_src.
Фрейм данных dst_count_by_src содержит несколько внешних IP-адресов, которые взаимодействуют ровно с одним внутренним хостом. Затем нам нужно найти внутренние хосты, с которыми взаимодействуют эти внешние IP-адреса.
Во-первых, мы извлекаем список внешних IP-адресов из кадра данных dst_count_by_src, где количество различных внутренних хостов равно 1.
dst_ips_list = dst_count_by_src[dst_count_by_src.dst_count .== 1, :].src
Далее мы запрашиваем записи в df_ext_int с src в списке dst_ips_list.
df_ext_int_botnet = filter(row -> row.src ∈ dst_ips_list, df_ext_int)
Обратите внимание на символ ∈ в приведенном выше коде. Джулия позволяет использовать символы Юникода в качестве операторов. В частности, ∈ — это оператор in для набора, который возвращает true, если элемент находится в наборе.
На рис. 4 показаны несколько записей из df_ext_int_botnet.
Все внешние IP-адреса взаимодействуют с внутренним хостом 14.45.67.46. Мы подтверждаем, что 14.45.67.46 является единственным внутренним хостом в df_ext_int_botnet.
unique(df_ext_int_botnet.dst)
Результат приведенного выше оператора: 14.45.67.46.
Вопрос 7: Зараженный хост
Один хост является частью ботнета из вопроса 6, какой у него IP?
Ответ 7:
Нам нужно найти внутренние хосты, которые взаимодействуют с хостом 14.45.67.46 и портом 27, которые мы нашли в ответе 6.
df_int_int_botnet = df_int_int[(df_int_int.dst .== "14.45.67.46") .& (df_int_int.port .== 27), :]
На рис. 5 показаны все записи из df_int_int_botnet.
В состав ботнета входят два хоста: 14.51.84.50 и 13.42.70.40.
Вопрос 8: Ботнет внутри
В сети есть более незаметный ботнет, использующий низкочастотные периодические обратные вызовы на внешний C&C со встроенными высокочастотными вызовами. Какой порт он использует?
Ответ 8:
Чтобы ответить на этот вопрос, предположим, что байты, отправляемые хостами ботнета на внешние C&C-серверы, будут иметь низкую изменчивость.
Мы вычисляем стандартное отклонение байтов, отправляемых на внешние IP-адреса, и сортируем результат по стандартному отклонению.
df_int_ext_stdbytes_by_dst = combine(groupby(df_int_ext, :dst), :bytes => std => :bytes_std) sort!(df_int_ext_stdbytes_by_dst, [order(:bytes_std)])
На рис. 6 показаны несколько записей из df_int_ext_stdbytes_by_dst.
Первые 6 внешних IP-адресов имеют стандартное отклонение 0. Скорее всего, это внешние IP-адреса, которые являются частью ботнета.
Затем мы запрашиваем внутренние хосты, которые взаимодействуют с 6 внешними IP-адресами.
external_ips_list = bydst_bytes[bydst_bytes.bytes_std .== 0, :dst] df_int_ext_botnet = filter(row -> row.dst ∈ r, df_int_ext)
На рис. 7 показаны несколько записей из df_int_ext_botnet.
Все указанные выше записи используют порт 51. Мы подтверждаем это, запрашивая уникальные порты из df_int_ext_botnet.
unique(df_int_ext_botnet.port)
Вопрос 9: Латеральный брут
После того, как машина взломана, она часто используется для изучения того, к чему еще можно добраться. Один хост используется для громкого зондирования всего предприятия, пытаясь найти пути к каждому другому хосту на предприятии. Какой у него IP?
Ответ 9:
Мы подсчитываем количество хостов назначения, к которым обращается каждый из исходных хостов, и сортируем результат по количеству хостов назначения в обратном порядке.
df_int_int_bysrc_dstcount = combine(groupby(df_int_int, :src), :dst => (x -> size(unique(x))[1]) => :dst_count) sort!(df_int_int_bysrc_dstcount, order(:dst_count, rev=true))
На рис. 8 показаны несколько записей из df_int_int_bysrc_dstcount.
Понятно, что хост 13.42.70.40 сканирует значительное количество внутренних хостов.
Вопрос 10: Боковой шпион
Один хост пытается найти путь к каждому другому хосту более тихо. Какой у него IP?
Ответ 10:
Хост, который пытается найти путь к цели, попытается подключиться к цели через несколько портов. Некоторые из этих портов будут необычными и не будут использоваться другими источниками для подключения к цели. Чтобы ответить на этот вопрос, мы ищем такие необычные (порт, цель), а затем опрашиваем хосты-источники, которые пытаются подключиться к этим необычным парам (порт, цель).
Сначала мы отфильтровываем хосты из предыдущих ответов из df_int_int, так как знаем, что ответ на этот вопрос другой.
prev_answers = ["13.37.84.125", "12.55.77.96", "12.30.96.87", "13.42.70.40", "14.51.84.50"] filtered_df_int_int = filter(row -> row.src ∉ prev_answers, df_int_int)
Затем мы сначала подсчитываем количество отдельных исходных хостов, которые подключаются к паре (порт, цель).
df_int_int_bydstport_srcount = combine(groupby(filtered_df_int_int, [:port, :dst]), :src => (x -> size(unique(x))[1]) => :src_count)
Затем мы ищем пары (порт, цель), где src_count равен 1, то есть где один источник подключен к паре (порт, цель).
df_int_int_bydstport_srcount_1 = df_int_int_bydstport_srcount[ df_int_int_bydstport_srcount.src_count .== 1, [:dst, :port]]
На рис. 9 показаны несколько записей из df_int_int_bydstport_srcount_1. Это показывает, что один источник подключен к 14.36.98.25 через несколько разных портов.
Затем мы соединяем filtered_df_int_in и tdf_int_int_bydstport_srcount_1, чтобы найти источник.
result = innerjoin(filtered_df_int_in, tdf_int_int_bydstport_srcount_1, on = [:port => :port, :dst => :dst])
На рис. 10 показан результирующий кадр данных.
Наконец, мы находим уникальный хост-источник.
unique(result.src)
Боковой шпионский хост: 12.49.123.62.
В заключение, этот блог продемонстрировал, как можно анализировать данные о сетевой активности для выявления угроз кибербезопасности. Также показано использование языка Julia для реализации анализа.