Overriding encryption

How to use Operator's encryption plugin to secure your beacons

Encryption is king. Without it, the danger of the internet wouldn't just be palpable, it would be insurmountable. Encryption is a critical component to internet safety and privacy.

You can break encryption into two camps:

  • Transit: Data en-route from one computer to another is considered in transit. In transit data is open to "sniffing" (people reading it). Consider the website you're reading this post on. When you clicked into this site, your computer sent the request to your router, which piped it through your modem, through your Internet Service Provider (ISP) who routed it through a number of physical locations, before it was received by Substack, who was able to route you to the correct web page. This is obviously a simplified example, as those who know the TCP/IP model will point out.

  • Rest: Data being stored on disk is referred to as data at rest. This data isn't moving (in transit) but it may be read by a database operation or file read, for example. Data at rest is open to theft, as anyone who can access the physical location of the hard drive can easily take off with it.

Operator beacons

When agents connect to Operator, they do so through in transit beacons. A beacon is simply a JSON string containing specific properties. Here is an example beacon.

"Name": "test",
"Location": "/tmp/me.go"
"Platform": "darwin",
"Executors": ["sh"],
"Range": "red",
"Pwd": "/tmp",
"Links": []
view raw beacon1.go hosted with ❤ by GitHub

When an agent starts, it creates a beacon then encrypts a string version of it before sending it to Operator for processing. A beacon serves two purposes:

  • Informational (let Operator know about the agent)

  • Greedy (ask Operator for instructions)

Operator expects beacons to be encrypted upon receipt, otherwise it will display a decryption error. The beacon's encryption must match the implementation used by Operator, which decrypts beacons (text variable in the example below) by default using hex / UTF-8 → AES-256-CBC, alongside your specific 32-character encryption key.

const ALGORITHM = 'aes-256-cbc'
const BLOCK_SIZE = 16
const contents = Buffer.from(text, 'hex')
const iv = contents.slice(0, BLOCK_SIZE)
const textBytes = contents.slice(BLOCK_SIZE)
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
let decrypted = decipher.update(textBytes, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted.trim()
view raw decryption.js hosted with ❤ by GitHub

In a similar way, Operator encrypts the beacon before sending it back to the agent.

const iv = crypto.randomBytes(BLOCK_SIZE)
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
let cipherText
try {
cipherText = cipher.update(text, 'utf8', 'hex')
cipherText += cipher.final('hex')
cipherText = iv.toString('hex') + cipherText
} catch (e) {
cipherText = null
return cipherText
view raw encrypt1.js hosted with ❤ by GitHub

Operator's default encryption is great at protecting data in transit but it may not be suitable for your specific needs. You may have an existing agent that uses a different encryption implementation. You may have difficulty writing code around AES-256-CBC. Or you just may be an encryption junkie and want to do something super custom! Whatever it is, you can override the encryption functionality within Operator and inject your own.

Another reason to use your own encryption is that your defenses may be good at spotting common encryption (or obfuscation) techniques. If they start picking up on a pattern in your communications, they may catch you quicker. By swapping your implementation, you may stay under the radar longer and not set off automated analytics hunting for patterns.

Using your own encryption

To use your own encryption, you'll need to have a Professional license to Operator. Ready? Ok let's get started.

From Operator, head to the Plugins section and click on the Encryption plugin. After installing, you'll see the Operator encrypt/decrypt functions.

Simply change these functions to the behavior you want. I'm going to override the encryption with a simple base64 encoding/decoding functionality, bypassing encryption altogether (ignoring the encryption key).

We wouldn't advise you do this for sensitive, over-the-internet beacons but for internal security assessments or for just building an agent quickly, it may work well for you.

Click Validate to ensure your encrypt/decrypt functions work as expected, if successful, go ahead and click Save. Now, every time your Operator starts, it will use these functions to decrypt all agent beacons and encrypt beacon responses before sending them back.

Update your agent

Now that you have new encrypt/decrypt functions, you'll need to adjust your agent, otherwise you'll get the dreaded encryption failed notifications.

In Pneuma, you can do this easily by changing the references in cryptic.go which handle the encryption. By default, they use AES-256-CBC, like this:

//Encrypt the results
func Encrypt(bites []byte) []byte {
plainText, err := pad(bites, aes.BlockSize)
if err != nil {
return make([]byte, 0)
block, _ := aes.NewCipher(encryptionKey)
cipherText := make([]byte, aes.BlockSize+len(plainText))
iv := cipherText[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return make([]byte, 0)
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText[aes.BlockSize:], plainText)
return []byte(fmt.Sprintf("%x", cipherText))
//Decrypt a command
func Decrypt(text string) string {
cipherText, _ := hex.DecodeString(text)
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return ""
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherText, cipherText)
cipherText, _ = unpad(cipherText, aes.BlockSize)
return fmt.Sprintf("%s", cipherText)
view raw golang-encryption.go hosted with ❤ by GitHub

When you swap them to base64 encode/decode, they may look something like this instead:

func Encrypt(bites []byte) []byte {
return []byte(b64.StdEncoding.EncodeToString(bites))
func Decrypt(text string) string {
beacon, _ := b64.StdEncoding.DecodeString(text)
return beacon
view raw base64.go hosted with ❤ by GitHub

Encryption is an important part of your overall security posture and this is especially true for conducting realistic adversary emulation exercises in your environment. Operator is designed for extensibility and you should experiment with examples like this one to not only learn but to devise solutions that help secure your organization. Happy hiding.