Это вторая часть блога, демонстрирующая элементарный анализ данных для сетевой безопасности на синтетическом наборе данных из 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 для реализации анализа.