The architecture of it all
A deep dive into the internal design of Prelude Operator
In 2012, I was working at John Deere, based out of a contractor office in Coralville, Iowa. I was leading a development team on a project code named Stellar, which was the support system for John Deere dealers. You know when driving through rural United States how you’ll see businesses selling John Deere tractors? Odds are, they were a dealer that used the system my team built.
While at Deere, I worked in a strictly best-practice software engineering environment, building a large Java Spring application which included a website front-end, backend pub/sub and an extensive database layer.
Through my time on the project, the software engineering teams supporting the work had to work together as a cohesive unit of 30+ engineers and quality assurance (QA) testers.
It was important that we followed best practice engineering principles, such as the Principle of Least Privilege (give users only the access they require) and the Single Responsibility Principle (ensure each code block does exactly one thing). One of the most important principles was the Open/Closed Principle, which was coined by Bertrand Meyer in 1988 in his book Object-Oriented Software Construction. Meyer defined the principle in this way:
“Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.”
Later on, Robert C. Martin (aka, Uncle Bob) later said this is “the most important principle of object-oriented design”.
Building software systems that are meant to last means building them in modular ways which follow the Open/Closed Principle. At John Deere, we focused heavily on this principle as a way to design systems which were compartmentalized, with each component having its own specialty and an input/output feature that allowed programmers to interact with it.
Today, the Open/Closed Principle is often seen in the form of micro-service architecture, in which an application is a loose collection of coupled services.
How Operator is designed
With the concept of modular software systems in mind, let's take a look at the high-level architecture Operator implements.
The main components are:
Operator desktop application
Operator is built around the concept of the Open/Closed principle, both at the high-level and down in the weeds. From the top, you can see that we've broken the system into multiple components, each with a standard input/output which ties it to the others. This design allows us to upgrade specific areas over time, without disrupting the rest of the stack.
In the weeds, much of the software Operator implements is designed in a similar way. While far too detailed for this post, in the future we'll break down various pieces of the internal software as we describe it and you should see how the design enables our development team to move quickly without (too much!) fear of breaking unrelated areas of the application.
As we go through this post, keep in mind our goal is to put you in the mind of our development team so you can get a glimpse at how we designed the Operator stack and how the components work together.
The first - and most central - piece of the architecture is the desktop application. Operator is the command-and-control center (C2) and is where an operator, on either offense or defense, is able to launch and monitor red team security assessments.
Operator is written in NodeJS, using the ElectronJS framework. The internals of the desktop application are currently closed-source but the code is not secretive, just not yet ready for public release.
The internal design of the desktop application is split along two approaches, as we've been growing the app and iterating:
The React framework
Within these two approaches are two building blocks:
Built-in sections. These sections include the home page, documentation, training, reports, emulation, editor, plugins and settings. These are core to the platform and therefore have dedicated space.
When Operator starts it runs a series of actions:
Checks the user’s license
Automatically pulls down any new procedures merged into the Community repository (and Professional, if you have that license).
Starts agent listening posts on UDP, TCP, gRPC and HTTP ports
Verifies connectivity to the GateKeeper API
Loads all procedures, adversary profiles, plugins, previously connected agents and facts/results
Applies any local settings associated with the installed app
Operator only requires a connection to the next piece, the GateKeeper API. It does not require access to any other internet resources.
There are some resources you may want to use the internet for - such as importing TTPs from GitHub.com or accepting beacons over the internet - but the general internet is not required for their usage.
GateKeeper is the code name for the API that Operator uses for authentication and authorization. This API, written in Python 3 and using the asyncio framework, is designed to live offsite from the desktop app(s) and acts as a central “team” server.
Did you know you can go to https://login.prelude.org, log in and view your account settings? This website is the front-end component of GateKeeper and will be leveraged in the future to give you web access to many Operator features.
Besides verifying the identity of each user, GateKeeper also serves the following purposes:
Loads all training programs on demand, as you start interacting with them.
Allows a user to flip “cloud persistence on”, which backs up all emulation data (links and results) in GateKeeper.
Analyzes emulation data in order to form security recommendations.
Loads all TTPs from the Community repository, along with any other closed-source licensable TTP plugins the member has access to, so they are on-demand when an Operator instance checks in. GateKeeper does not have access, or even knowledge of, any custom TTPs and adversary profiles you build within Operator.
Government licensed versions are available, if you require a fully offline installation of this system.
Additionally, GateKeeper tracks all open (and closed) source repositories Operator relies on. These include:
Operator-support: the general Prelude repository for tracking issues with the platform. This repository is also home to our open-source plugins.
Community: our collection of open-source procedures.
Pneuma: our base open-source agent, which while fully operational, acts as an all-purpose example for your own agents. More on this in a minute.
GateKeeper contains a configuration file called “autonomous”, which is reloaded every time Operator is opened. Autonomous contains a number of configuration objects Operator uses to feed the internal decision-making process (called the brain).
The most dynamic of the objects are parsers. As you conduct security assessments through Operator, all results are automatically piped through the parsers, which hunt for important data - such as IP addresses, SSH commands, files and directories - through a series of regex statements. Each parser is designed to locate a specific “fact”, or identifiable piece of information, which Operator can use to unlock future capabilities.
When using Operator, you'll first deploy agents into your network. This process is known as "post-compromise" testing, where you assume you've been breached and you are testing from that moment forward. Operator does not currently support initial access security testing, which is the process of attempting to "drop" an agent onto your network.
Testing initial access is a time-consuming process. It requires either continual probing of your perimeter defenses, such as port-scanning, or an external analysis of the software an adversary can reach to match up to known vulnerabilities to exploit. Initial access is often the first "white card" thrown in a manual red team assessment. A white card indicates the step will be skipped due to the feasibility of executing it (usually due to a time constraint).
Operator includes a base agent, called Pneuma, which you can use out of the box.
Pneuma is a separate repository, fully open-source, and is available for modification. This separate component connects only to the desktop application and forms either a persistent or continual (pinging) network connection, depending on which protocol you use.
Pneuma is written in GoLang, in order to have the most ability for cross-compiling on a variety of operating systems.
The code has four core protocols (TCP, UDP, gRPC and HTTP) to select from. When starting Pneuma, pick one and ensure it has outbound firewall access to your Operator instance. Once a connection is established, your agent is operational.
If you try to connect an agent to Operator over the internet, you will need to deploy a redirector. The cloud plugin allows you to deploy redirectors in a cloud provider (AWS and GCP currently supported). A redirector acts as a proxy, accepting agent beacons from anywhere and forwarding them safely to your desktop Operator.
Pneuma only communicates with the desktop app using encrypted data, regardless of which protocol it's connected to. In addition, the agent can be dynamically compiled anytime - changing its file hash, to avoid file-based detection.
Why doesn't Pneuma use SSL by default? You may want to embed an SSL certificate in your agent and use that to encrypt your traffic. You can do this by applying the SSL termination in your redirector. But before you do, consider this. Many times real-world adversaries will not use (paid) certificates because they can be attributable. And self-signed certificates offer little additional security over encrypting the data in transit. Because of this, our default behavior is to encrypt the data in transit using AES-256 protected by a 32-character key. We always lean toward defaults which are realistic toward the behavior of a real-world adversary.
Modeled after the traditional Open/Closed Principle, Operator aims to be a scalable system that will prove different than most cyber security solutions available today.
Using a modular design, the Prelude development team is able to evaluate and upgrade various pieces independent of the rest of the system. This will allow for selecting the right technology for each component versus needing to force an application into a technology that isn’t best fitting.
As we grow the platform, extending various components following our public roadmap, we hope you connect with us and let us know how Operator is helping you, which components are working well and which could be rethought.