Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@

escort

plugin/bar

plugin/baz

plugin/foo
3 changes: 3 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CoreDNS Plugins

* https://github.com/coredns/coredns/blob/master/plugin.md
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@

![](./escort.jpg)

`escort` is an experiment at using DNS TXT records for transmitting malicious payloads to bypass Anti Virus detection. It currently only supports PowerShell payloads (ie reverse shells with powershell) however ideally I will expand this to other potential payload systems. It consists of taking your payload, compressing with DEFLATE and base64 encoding it.
`escort` is a framework for using DNS a means of smuggling information across network boundaries while evading detection. It provides a loader system for Windows based OS' using PowerShell, as well as a CLI, and Golang library for assisting with the creation of escort'd DNS records. In addition to this a CoreDNS plugin is provided which makes it easy to run a DNS server with built-in escort capabilities.

Because not all DNS servers are equal, some servers may return DNS record values not in the order they are declared in. For example CoreDNS using the BIND zone file format will serve results in the order they are declared in, but AWS Route53 may or may not do this. As such we mark the beginning of the base64 encoded with a segment identifier.
`escort` relies on a few different factors to accomplish its objects:
* DNS is used on practically every single compute network out there, regardless of whether or not the network is internet connected
* A number of different record types exist, allowing for multiple different methods of disguising data
* If a particular domain name becomes identified as associated with `escort` traffi and blacklisted, get a new domain name and problem solved
* Lets encrypt makes it easy to create valid certificates

We use the `|` character which is not a valid base64 encoded character to mark the end of the segment identifier and the beginning of the base64 encoded segment. Escort uses this information to recombine the bsae64 segments before decoding them. After decoding we decompress the output and execute it with `Invoke-Expression` cmdlet to avoid writing the script to disk
In addition to this, when parsing data to be served by `escort` a process known as `trick`'ing, an extremely simple single-byte XOR filter can be used, which helps to slow down the process of signature analysis of escort related traffic.

# Usage
At the moment the `escort` workflow consists of the following:

* XOR the data to be tricked if needed
* DEFLATE compress the data
* base64 encode the data
* segment data
* create TXT records

`escort` is alpha software intended for research purposes only.

# Usage (out of date)

To showcase usage an example payload is included in `payload.txt`

Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ go 1.15

require (
github.com/andybalholm/brotli v1.0.1
github.com/coredns/caddy v1.1.1
github.com/coredns/coredns v1.9.0
github.com/miekg/dns v1.1.46
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
github.com/v2fly/v2ray-core/v4 v4.44.0
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
)
1,173 changes: 1,170 additions & 3 deletions go.sum

Large diffs are not rendered by default.

29 changes: 11 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package main

import (
"bytes"
"compress/flate"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"

"github.com/bonedaddy/escort/pkg"
"github.com/urfave/cli/v2"
)

Expand All @@ -20,33 +19,27 @@ func main() {
Name: "compress",
Usage: "compress data using DEFLATE and base64 encode it",
Action: func(c *cli.Context) error {
var data string
var (
data []byte
err error
)
if c.String("input") != "" {
data = c.String("input")
data = []byte(c.String("input"))
} else if c.String("input.file") != "" {
dBytes, err := ioutil.ReadFile(c.String("input.file"))
data, err = ioutil.ReadFile(c.String("input.file"))
if err != nil {
return err
}
data = string(dBytes)
} else {
return errors.New("input.file and input are nil")
}
buffer := new(bytes.Buffer)
writer, err := flate.NewWriter(buffer, flate.BestCompression)
core := pkg.NewCore(nil, "1", 250)
parts, err := core.Trick(data)
if err != nil {
return err
}
if _, err := writer.Write([]byte(data)); err != nil {
return err
}

if err := writer.Close(); err != nil {
return err
}
parts := Chunks(base64.StdEncoding.EncodeToString(buffer.Bytes()), 250)
for i, part := range parts {
fmt.Printf("%v|%s\n", i, part)
for _, part := range parts {
fmt.Println(part)
}
return nil
},
Expand Down
158 changes: 158 additions & 0 deletions pkg/pkg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package pkg

import (
"bytes"
"compress/flate"
"encoding/base64"
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"
)

// Core implementation of escort
type Core struct {
xorKey *byte
segmentIdentifier string
segmentSize int
}

func NewCore(
xorKey *byte,
segmentIdentifier string,
segmentSize int,
) *Core {
return &Core{
xorKey: xorKey,
segmentIdentifier: segmentIdentifier,
segmentSize: segmentSize,
}
}

// Trick optionally XOR's the binary data before performing
// DEFLATE compressiong, followed by base64 encoding, and segmenting
// the encoded data.
func (c *Core) Trick(
data []byte,
) ([]string, error) {
if c.xorKey != nil {
data = c.xorData(data)
}
// if there is a key, perform xor encryption on the data first
buffer := new(bytes.Buffer)
writer, err := flate.NewWriter(buffer, flate.BestCompression)
if err != nil {
return nil, err
}
if _, err := writer.Write(data); err != nil {
return nil, err
}

if err := writer.Close(); err != nil {
return nil, err
}
// 250 is the maximum size of data in byteswe will return per record
parts := Chunks(base64.StdEncoding.EncodeToString(buffer.Bytes()), c.segmentSize)
segmentedData := make([]string, 0, len(parts))
for i, part := range parts {
segmentedData = append(segmentedData, fmt.Sprintf("%v%s%s\n", i, c.segmentIdentifier, part))
}
return segmentedData, nil
}

// Trick parses the compressed, encoded data segments (tricked data)
// into it's original form
func (c *Core) TurnOut(
dataSegments []string,
) ([]byte, error) {
type segment struct {
// in a segment, this is the data portion minus the segment identifier
data []byte
// in a segment, this is the index portion
index int64
}
var (
// unsorted
segments = make([]segment, 0, len(dataSegments))
totalSegmentLen int
)
for _, dataSegment := range dataSegments {
totalSegmentLen += len(dataSegment)
parts := strings.Split(dataSegment, c.segmentIdentifier)
if len(parts) < 2 {
return nil, fmt.Errorf("encountered invalid %s", dataSegment)
}
segmentIndex, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse segment identifier %s", err)
}
segments = append(segments, segment{
data: []byte(parts[1]),
index: segmentIndex,
})
totalSegmentLen += len(parts[1])
}
// sort segments
sort.Slice(segments, func(i, j int) bool {
return segments[i].index < segments[j].index
})
var (
buf bytes.Buffer
)
buf.Grow(totalSegmentLen)
for _, segment := range segments {
// todo(bonedaddy): should we care about the amount of data written
_, err := buf.Write(segment.data)
if err != nil {
return nil, fmt.Errorf("failed to write to buffer %s", err)
}
}
compressedData, err := base64.StdEncoding.DecodeString(buf.String())
if err != nil {
return nil, fmt.Errorf("failed to decode data %s", err)
}
// decompress the data
// todo(bonedaddy): not very memory efficient
decompressedData, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(compressedData)))
if err != nil {
return nil, fmt.Errorf("failed to decompress data %s", err)
}
buf.Reset()
// check to see if we need to xor it first
if c.xorKey != nil {
decompressedData = c.xorData(decompressedData)
}
return decompressedData, nil
}

// Chunks is used to split a string into segments of chunkSize
// https://stackoverflow.com/questions/25686109/split-string-by-length-in-golang
func Chunks(s string, chunkSize int) []string {
if chunkSize >= len(s) {
return []string{s}
}
var chunks []string
chunk := make([]rune, chunkSize)
len := 0
for _, r := range s {
chunk[len] = r
len++
if len == chunkSize {
chunks = append(chunks, string(chunk))
len = 0
}
}
if len > 0 {
chunks = append(chunks, string(chunk[:len]))
}
return chunks
}

func (c *Core) xorData(data []byte) []byte {
xorKey := *c.xorKey
for idx, b := range data {
data[idx] = b ^ xorKey
}
return data
}
49 changes: 49 additions & 0 deletions pkg/pkg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package pkg

import (
"crypto/sha256"
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestCompressNoXor(t *testing.T) {
dataToCompress := "hello world 420 blaze it putin can ligma deez nuts"
for i := 0; i < 10; i++ {
dataToCompress = dataToCompress + dataToCompress
}
dataToCompressOrigChecksum := sha256.Sum256([]byte(dataToCompress))
core := NewCore(nil, "|", 250)
segmentedData, err := core.Trick([]byte(dataToCompress))
require.NoError(t, err)
require.Len(t, segmentedData, 2)
require.Equal(t, "0|7MvRCcIwGAbAVb4RRFyo2qCB31Q0Rej0DuHrDXCPVrXlu71rzeV8yrWWo6XPvPbZR27LSPX7c8na2pGxzw9BEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEA\n", segmentedData[0])
require.Equal(t, "1|RBEARBEARBEARBEARBEARBEARBEARB/C9+AQAA//8=\n", segmentedData[1])
trickedData, err := core.TurnOut(segmentedData)
require.NoError(t, err)
trickedDataChecksum := sha256.Sum256(trickedData)
require.Equal(t, dataToCompressOrigChecksum, trickedDataChecksum)
}

func TestCompressXor(t *testing.T) {
var letters = "abcdefghijklmnopqrstuvwxyz"
dataToCompress := "hello world 420 blaze it putin can ligma deez nuts"
for i := 0; i < 10; i++ {
dataToCompress = dataToCompress + dataToCompress
}
dataToCompressOrigChecksum := sha256.Sum256([]byte(dataToCompress))
for _, letter := range letters {
fmt.Println("using xorkey ", string(letter))
xorKey := byte(letter)
core := NewCore(&xorKey, "|", 250)
segmentedData, err := core.Trick([]byte(dataToCompress))
require.NoError(t, err)
require.Len(t, segmentedData, 2)
trickedData, err := core.TurnOut(segmentedData)
require.NoError(t, err)
trickedDataChecksum := sha256.Sum256(trickedData)
require.Equal(t, dataToCompressOrigChecksum, trickedDataChecksum)
}

}
Loading