Оглавление


В процессе реагирования на инциденты к нам на анализ приходят довольно разнообразные сэмплы, в том числе и различные вариации beacon’ов CobaltStrike. Наше внимание привлек CrossC2 — генератор полезных нагрузок под Unix-системы. По какой-то причине на просторах интернета встречается лишь одна техническая публикация, посвященная данному фреймворку, - от китайской ИБ-компании Qi An Xin.

В данной статье мы попробуем окунуться в процедуру генерации beacon’ов для версии 3.3, а также опишем процесс извлечения из них конфигов статически для получения из них полезных индикаторов.



Что такое CrossC2?

Этот фреймворк позволяет генерировать полезные нагрузки Cobalt Strike под Unix-платформы. Вместе с самим генератором (genCrossC2) предоставляются Agressive-скрипты для взаимодействия с нагрузкой. Данный генератор также является кросс-платформенным, так как написан на языке Golang.

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

В настоящий момент злоумышленники продолжают использовать данный фреймворк в различных вредоносных кампаниях. Упоминания о нем встречаются в отчетах от Moonlock Lab (июль 2023), Recorded Future (июль 2024). Последний раз семплы, сгенерированные с помощью данного фреймворка, встречались нам в августе 2024 года.

Доступные в генераторе опции
Доступные в генераторе опции

Среди доступных опций есть несколько платформ. Есть возможность запаковать сгенерированный бинарный файл с помощью UPX. Приведенный на скриншоте генератор поддерживает версии CobaltStrike 4.x. Версии в репозитории распределяются следующим образом.

Ветка

CS3.14

CS4.0

CS4.X (4.1~4.8)

Master

+

 

 

cs4.0

 

+

 

cs4.1

 

 

+

В расширенном ReadME приводится информация по поддерживаемым платформам.

 

Windows

Linux

MacOS

iOS

Android

Embedded

Run Env (x86)

 

+

 

 

 

 

Run Env (x64)

+

+

+

 

 

 

gen beacon (x86)

 

+

 

 

+

 

gen beacon (x64)

 

+

+

 

 

 

gen beacon (armv7)

 

 

 

in progress

+

 

gen beacon (arm64)

 

 

 

+

+

 

gen beacon (mips[el])

 

 

 

 

 

+



Попытка 1. Погружаемся в дебри beacon’a

Рассмотрим подробнее сэмпл, сгенерированный с использованием фреймворка CrossC2, который встретился нам ITW.

MD5

b103b00239a6819abd9d8c69dc9eaec7

SHA1

3850f7cccbdb3b08d05df6bde09e4396569628f5

SHA256

1a8c9b9a953012396ee8140b04b4d2a9a383d68708dea670d0b1396a96e5e2fa

File Type

ELF

File size

1393 kB

File Name

t_cc2

Бинарный файл запакован с помощью UPX, однако расшифровка с помощью upx -d не удается.

Вывод команды upx -d
Вывод команды upx -d

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

Overlay в конце файла
Overlay в конце файла

Если убрать из файла данную структуру, то UPX сможет успешно ее распаковать.

MD5

e7347a3cb7cef52e728112b372c3d09a

SHA1

364d185fe2cfb55346d1718d2c76d5d955ddaa71

SHA256

3d1846f254f33e7d03d36491ba5980c715006053dd17a3a67348cc5fa4377739

File Type

ELF

File size

3713 kB

В распакованном файле встречается обфускация LLVM, которая включает шифрование строк. Расшифровка строк происходит побайтово с помощью XOR. Ключ хранится в виде imm8-значений в коде.

Обфускация строк
Обфускация строк

Среди расшифрованных строк встречается скрипт на Python.

	
import imp
import platform
cc2_g_modules = {}
if (int(platform.python_version()[0]) == 2):
	class StringImporter(object):
		def __init__(self, modules):
			self._modules = dict(modules)
		def find_module(self, fullname, path):
			if fullname in self._modules.keys():
				return self
			return None
		def load_module(self, fullname):
			if not fullname in self._modules.keys():
				raise ImportError(fullname)
			new_module = imp.new_module(fullname)
			code = compile(self._modules[fullname], "", "exec")
			exec(code,new_module.__dict__)
			return new_module
	def StringImport(data):
		sys.meta_path.append(StringImporter(data))
elif (int(platform.python_version()[0]) == 3):
	import importlib
	import types
	class StringLoader(importlib.abc.Loader):
		def __init__(self, modules):
			self._modules = modules
		def has_module(self, fullname):
			return (fullname in self._modules)
		def create_module(self, spec):
			if self.has_module(spec.name):
				module = types.ModuleType(spec.name)
				exec(self._modules[spec.name], module.__dict__)
				return module
		def exec_module(self, module):
			pass
	class StringFinder(importlib.abc.MetaPathFinder):
		def __init__(self, loader):
			self._loader = loader
		def find_spec(self, fullname, path, target=None):
			if self._loader.has_module(fullname):
				return importlib.machinery.ModuleSpec(fullname, self._loader)
	def StringImport(data):
		sys.meta_path.append(StringFinder(StringLoader(data)))
			 
		 

Из-за обфускации дальнейший анализ затруднен, поэтому попробуем найти обходные пути.



Попытка 2. Заглядываем в genCrossC2

Мы заглянули в сам генератор beacon’ов в надежде найти что-то интересное. В репозитории CrossC2 можно скачать бинарь genCrossC2 для Windows, Linux, MacOS. Для простоты возьмем версию под Windows.

MD5

186eacd22fe98b0ccb7b07994b3f9933

SHA1

a1d1fcf6cef691bab0678ae55df1dd0e2172a24e

SHA256

315a7a92dc639983ee24bf711986fca8eb2c880915a188f1d897029d22f2fc51

File Type

PE32+

File size

3713 kB

File Name

genCrossC2.exe

Это запакованный с помощью UPX файл, написанный на языке Golang. Символы не обфусцированы, однако в генераторе также частично встречаются зашифрованные строки и запутывание CFG.

Хорошо забытая классика
Хорошо забытая классика

В файле можно встретить очень длинную закодированную base64 строку, которая при декодировании оказывается .jar файлом. Данный файл вшит в сэмпл genCrossC2 и предназначен для десериализации публичного и приватного ключей (.cobaltstrike.beacon_keys), которые сгенерировал TeamServer. Публичный ключ затем кодируется в base64 и записывается в файл .sub_bpKey.txt

Интересное решение
Интересное решение

Похоже, разработчик genCrossC2 не захотел бороться с десериализацией объектов Java, используя Go, и решил написать вспомогательную программу на языке Java. Получился своего рода “костыль”.

Так как статический анализ основного бинарного файла также сильно затруднен, посмотрим работу программы в динамике. Будем использовать API Monitor. Попробуем сгенерировать под ним beacon CobaltStrike 4.8 для Linux, запакованный с помощью upx

> genCrossC2.exe 192.168.17.128 443 .cobaltstrike.beacon_keys null Linux x64 output_upx upx 4.8

В трейсах заметим инициализацию строк с последующим вызовом функций стандартной библиотеки, среди которых увидим следующее:

Вывод API Monitor
Вывод API Monitor

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

Странная строка
Странная строка

Данный вызов похож на инициализацию ключа или каких-то вспомогательных данных, связанных с шифрованием. Благо API Monitor предоставляет информацию о стеке вызовов. Нас интересует функция по смещению 0xda39.

Стек вызовов
Стек вызовов

При дальнейшей отладке выясняется, что для шифрования используется алгоритм AES-128 CBC, а значения aaaabbbbccccdddd и abcdefghijklmnop соответственно представляют собой ключ и вектор IV.

Ключ шифрования
Ключ шифрования

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



Собираем всё воедино

В результате динамического анализа мы узнали ключ шифрования и вектор IV для конфига. Попробуем расшифровать overlay, который был присоединен к запакованной нагрузке. Для начала опишем структуру зашифрованного конфига.

	
struct config_encrypted {
	DWORD magic; // HOOK
	DWORD firstPartDataSize
	BYTE firstPartData[firstPartDataSize];
	DWORD secondPartMagic; // 08 FB 80 8F
	DWORD secondPartDataSize;
	BYTE secondPartData[secondPartDataSize];
};
			 
		 

Она начинается с сигнатуры HOOK. За сигнатурой следует размер данных первой части конфига, за которым следует сигнатура второй части с ее размером и данными. Что за первая и вторая часть? В первой части конфига хранится основная информация о C2.

	
struct config_decrypted {
	DWORD dataSize; // длина последующих данных без учета этого поля
	DWORD ip; // IP в виде байтов, напр. C0 A8 11 80 - 192.168.17.128
	DWORD port; // то же самое, но с портом
	DWORD ipStrSize; // длина строкового представления IP
	BYTE ipStr[ipStrSize]; // строковое представление IP
	DWORD beaconConfigurationSize; // config.ini
	BYTE beaconConfiguration[beaconConfigurationSize]; // config.ini
	DWORD publicKeyStrLen; // размер публичного ключа
	STR publicKey[publicKeyStrLen];
}		
			 
		 

beaconConfiguration представляет собой глобальную конфигурацию полезной нагрузки, которая настраивается с помощью .ini файла. Пример такой конфигурации приведен на вики-странице репозитория CrossC2:

	
# Configure the function symbol and global configuration of the beacon itself hook
[c2_config]
# hook function
# initialization stage: void cc2_init() {}
cc2_init = aa1
# Error reconnection stage, incoming reconnection times: void cc2_retryConnect(int retryCount) {}
cc2_retryConnect = bb
				 
# Function name customization for protocol rebinding library
cc2_rebind_get_protocol = cc
cc2_rebind_post_protocol = dd
cc2_rebind_http_get_send = ee3
cc2_rebind_http_get_recv = ff
cc2_rebind_http_post_send = gg
cc2_rebind_http_post_recv = hh
				 
# global configuration
# automatically delete after running
cc2_auto_delete = false
# background process
cc2_daemon = false
# sleep time (10sec)
sleeptime = 10
# heartbeat jitter time
jitter = 37
# data submission jitter time
data_jitter = 100
# create the pipe name of the task(default joblist)
job_pipe_name = joblist
# the name of the pipe to run the task on(default .syspipe)
process_pipe_name = sys_pipe
# requested dns service
dns_server = 8.8.8.8		
			 
		 

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

Первая часть конфига для тестового файла
Первая часть конфига для тестового файла

Вторая часть является опциональной и добавляется при переопределении Malleable Profile. Концепция Malleable Command and Control используется в Cobalt Strike для контроля индикаторов HTTP, в число которых входит URI, различные HTTP заголовки и другое. На вики-странице CrossC2 приведен следующий пример профайла:

	
http-get {
		 
	set uri "/aaaaaaaaa";
	set verb "GET";
				 
	client {
				 
		header "Accept" "accccccc";
		header "Host" "www.google.com";
		header "Referer" "http://www.google.com/";
		header "Accept-Encoding" "gzip, deflate";
				 
		metadata {
			base64url;
			prepend "SESSION=";
			header "Cookie";
		}
	}
				 
	server {
				 
		header "Server" "nginx";
		header "Cache-Control" "max-age=0, no-cache";
		header "Pragma" "no-cache";
		header "Connection" "keep-alive";
		header "Content-Type" "charset=utf-8";
				 
		output {
			base64;
			prepend "ffffffff1";
			append "eeeeeeee2";
			print;
		}
	}
}
				 
				 
http-post {
				 
	set uri "/bbbbbbbbb";
	set verb "POST";
				 
	client {
				 
		header "Accept" "accccccc";
		header "Host" "www.google.com";
		header "Referer" "http://www.google.com/";
		header "Accept-Encoding" "gzip, deflate";
				 
		id {
			base64;
			parameter "SESSION";
		}
				 
		output {
			base64url;
			print;
		}
	}
				 
	server {
				 
		header "Server" "nginx";
		header "Cache-Control" "max-age=0, no-cache";
		header "Pragma" "no-cache";
		header "Connection" "keep-alive";
		header "Content-Type" "charset=utf-8";
				 
		output {
			mask;
			base64url;
			prepend "ffffffff1";
			append "eeeeeeee2";
			print;
		}
	}
}		
			 
		 

Расшифрованный вариант профайла выше приведен на следующем рисунке.

Вторая часть конфига для тестового файла
Вторая часть конфига для тестового файла

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

Для ITW-сэмпла конфиг в расшифрованном виде выглядит следующим образом:

Первая часть конфига ITW
Первая часть конфига ITW
Malleable Profile ITW
Malleable Profile ITW

Где 45.153.231[.]244 - адрес C2. Данный IP принадлежит “любимому” хостеру злоумышленников Stark Industries. Если обратиться к Shodan, то можно найти еще один командный сервер тех же злоумышленников - 94.232.247[.]97, так как они забыли поменять заголовок Host в Malleable Profile, который остался равен адресу предыдущего сервера.



Вывод

CrossC2 создает грозное впечатление за счет своей обфускации и сложной структуры, что затрудняет анализ. Однако, как показывает наш случай, даже в таких, казалось бы, безнадежных ситуациях существуют методы, позволяющие извлечь критическую информацию из семплов. С помощью динамического анализа нам удалось изучить механизм шифрования конфига и расшифровать конфигурационные данные и Malleable Profile, что открыло доступ к информации о C2-серверах. Этот пример подчеркивает важность комплексного подхода к анализу вредоносного ПО и демонстрирует, что даже при высоком уровне обфускации анализ семплов не безнадежен.



IOCs


C2

45.153.231[.]244
94.232.247[.]97


File Hashes

MD5

b103b00239a6819abd9d8c69dc9eaec7

Распакованные файлы

e7347a3cb7cef52e728112b372c3d09a
0b32ee72f3c1c2a64ff69158a4346732
8ba0a4627d51cbd97c38a2f74897a873
65f4a19162d8de65616e855a5d0f488d
ce926ee1f669a975c22bf8be0bedfcc7
f02d85e80ca322df19bf9b8efae65f60
d784d9fc0fda2c75355ed7785ac8ea0c
623452111f8e6e745452b7f8496e4ee4

SHA1

3850f7cccbdb3b08d05df6bde09e4396569628f5

Распакованные файлы

364d185fe2cfb55346d1718d2c76d5d955ddaa71
1cd2c10b53cf59fb1c00cf341e3e00adda5fdc89
6399729d00fae2ae1ee98a860cebc6b56f74a1a9
3fc8a341a52659d1bf7eb4db6b33139ab6dc3fac
5bfcdb326f9ca5d18704ffd58a8137f38adb10ba
e1232f0e6fd45a7f861fa3e248ec49c50473d1f1
fd1be901feea0b7f59554bae305e354cd29aaa90
d21c995dfe3dd900dbe8ea219b96d40ba0051920

SHA256

1a8c9b9a953012396ee8140b04b4d2a9a383d68708dea670d0b1396a96e5e2fa

Распакованные файлы

3d1846f254f33e7d03d36491ba5980c715006053dd17a3a67348cc5fa4377739
fdae151fc523334d7b443e14c0fd6dbac59cf0e487598e529c87b33e2d140e4a
09f37abffeb1eced45cdc97165eb873db7a00aec00b5d1a62ea09cb22d791a2f
122755f45ad2860509a402641d9db8555ceee049cc26dd1af747b39e230c8fd5
5769fa8d6da03c0c895186ce896045523ee3acfc7dec8fd49e2955006d5bb81a
7e3f714fc750a6ef8e9f8b7c47ad6beed0d49e3a512b2a367552db21378b8a4b
51f2eb0e8b846d333d6c2168b376f8c3e973ce83c485606d319acf0e9ef799d0
3bd2682928099327f8e186a6d41caddb7a9a4ed00f0eaffe25a859ac28206a38



Приложение 1. Расширенная информация по файловым IOCs

MD5

SHA1

SHA256

Комментарий

e7347a3cb7cef52e728112b372c3d09a

364d185fe2cfb55346d1718d2c76d5d955ddaa71

3d1846f254f33e7d03d36491ba5980c715006053dd17a3a67348cc5fa4377739

Linux-x64 (+ITW)

0b32ee72f3c1c2a64ff69158a4346732

1cd2c10b53cf59fb1c00cf341e3e00adda5fdc89

fdae151fc523334d7b443e14c0fd6dbac59cf0e487598e529c87b33e2d140e4a

MacOS

8ba0a4627d51cbd97c38a2f74897a873

6399729d00fae2ae1ee98a860cebc6b56f74a1a9

09f37abffeb1eced45cdc97165eb873db7a00aec00b5d1a62ea09cb22d791a2f

Linux-x86

65f4a19162d8de65616e855a5d0f488d

3fc8a341a52659d1bf7eb4db6b33139ab6dc3fac

122755f45ad2860509a402641d9db8555ceee049cc26dd1af747b39e230c8fd5

Linux-bind-x86

ce926ee1f669a975c22bf8be0bedfcc7

5bfcdb326f9ca5d18704ffd58a8137f38adb10ba

5769fa8d6da03c0c895186ce896045523ee3acfc7dec8fd49e2955006d5bb81a

Linux-bind-x64

f02d85e80ca322df19bf9b8efae65f60

e1232f0e6fd45a7f861fa3e248ec49c50473d1f1

7e3f714fc750a6ef8e9f8b7c47ad6beed0d49e3a512b2a367552db21378b8a4b

Linux-lib-x64

d784d9fc0fda2c75355ed7785ac8ea0c

fd1be901feea0b7f59554bae305e354cd29aaa90

51f2eb0e8b846d333d6c2168b376f8c3e973ce83c485606d319acf0e9ef799d0

Linux-lib-x86

623452111f8e6e745452b7f8496e4ee4

d21c995dfe3dd900dbe8ea219b96d40ba0051920

3bd2682928099327f8e186a6d41caddb7a9a4ed00f0eaffe25a859ac28206a38

MacOS-lib

b103b00239a6819abd9d8c69dc9eaec7

3850f7cccbdb3b08d05df6bde09e4396569628f5

1a8c9b9a953012396ee8140b04b4d2a9a383d68708dea670d0b1396a96e5e2fa

ITW семпл t_cc2