diff --git a/README.md b/README.md index 0c7cc6b..207f268 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Transfer AFL files over a mesh to fuzz across multiple servers - Using DEFLATE compression format (see [RFC 1951](https://www.ietf.org/rfc/rfc1951.html)) - Automatically syncs the main fuzzer to secondary nodes, and all secondary fuzzers back to the main node +- Encrypts traffic between nodes using AES-256, dropping plaintext packets - Usable on UNIXoid (Linux, OSX) systems and Windows ## Usage @@ -22,3 +23,17 @@ As a countermeasure, use the `--restrict-to-peers` flags to only allow connectio - On your host 10.0.0.2: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1` - On your host 10.0.0.3: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1` +### Crypto + +If you want to encrypt your traffic between the nodes - which is advised, as it increases security and there is nearly no argument against it - you can do so by specifying a random key with `--key`. +To keep *afl-transmit* simple, the symmetric encryption algorithm AES256-GCM was chosen over an asymmetric variant. This means you need to specify the same key on all nodes. + +Key generation is fairly simple, you just need to get 32 random bytes from somewhere (buy them, or use `/dev/urandom`), and wrap them with base64. +For example like this: + +``` +dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tee transmit.key +./afl-transmit --key $(cat transmit.key) --fuzzer-directory ... +``` + +As already said, the same key must be used on all nodes. diff --git a/main.go b/main.go index 0f3ed3b..6503968 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ func main() { watchdog.RegisterWatchdogFlags() net.RegisterSenderFlags() net.RegisterListenFlags() + net.RegisterCryptFlags() RegisterGlobalFlags() flag.Parse() @@ -31,6 +32,13 @@ func main() { // Read peers file net.ReadPeers() + // Initialize crypto if desired + cryptErr := net.InitCrypt() + if cryptErr != nil { + fmt.Printf("Failed to initialize crypt function: %s", cryptErr) + return + } + // Start watchdog for local afl instances go watchdog.WatchFuzzers(outputDirectory) diff --git a/net/crypt.go b/net/crypt.go new file mode 100644 index 0000000..e59fed2 --- /dev/null +++ b/net/crypt.go @@ -0,0 +1,86 @@ +package net + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "flag" + "fmt" + "io" +) + +var ( + key string + sharedGCM cipher.AEAD + nonceSize int +) + +// RegisterCryptFlags Registers the flags required for cryptography +func RegisterCryptFlags() { + flag.StringVar(&key, "key", "", "32 random bytes, base64-wrapped, to AES-encrypt traffic between nodes") +} + +// InitCrypt creates a cipher object out of the key handed over via --key +func InitCrypt() error { + // Check if a key was handed over + if key == "" { + // no key, no service + return nil + } + + // Unwrap base64'ed crypto bytes + rawKey, base64Err := base64.StdEncoding.DecodeString(key) + if base64Err != nil { + return fmt.Errorf("failed to unpack base64'ed key: %s", base64Err) + } + + // Create cipher object using that key + block, cipherErr := aes.NewCipher(rawKey) + if cipherErr != nil { + return fmt.Errorf("failed to use your key as AES256 key: %s", cipherErr) + } + + // Create GCM with cipher object + gcm, gcmErr := cipher.NewGCM(block) + if gcmErr != nil { + return fmt.Errorf("failed to create GCM for your key: %s", gcmErr) + } + + // Set shared GCM instance + sharedGCM = gcm + nonceSize = gcm.NonceSize() + + // No error to report + return nil +} + +// CryptApplicable checks if we are able to encrypt or decrypt things, means if this instance was given a proper key +func CryptApplicable() bool { + // if the shared GCM object is set, we were able to get the key off user's hands, else we don't crypt + return sharedGCM != nil +} + +// Encrypt encrypts the given bytes using the symmetric key +func Encrypt(plain []byte) ([]byte, error) { + // create nonce and fill it with random bytes + nonce := make([]byte, nonceSize) + _, readErr := io.ReadFull(rand.Reader, nonce) + if readErr != nil { + return nil, fmt.Errorf("failed to get random bytes: %s", readErr) + } + + // Encrypt plaintext + return sharedGCM.Seal(nonce, nonce, plain, nil), nil +} + +// Decrypt decrypts the given bytes using the symmetric key +func Decrypt(enc []byte) ([]byte, error) { + // Sanity check on input + if len(enc) < sharedGCM.NonceSize() { + return nil, fmt.Errorf("failed to decrypt packet: too short") + } + + // Decrypt encrypted bytes + return sharedGCM.Open(nil, enc[:nonceSize], enc[nonceSize:], nil) +} diff --git a/net/listener.go b/net/listener.go index 2f8130a..ed2c54d 100644 --- a/net/listener.go +++ b/net/listener.go @@ -76,9 +76,20 @@ func handle(conn net.Conn, outputDirectory string) { // Read raw content cont, contErr := ioutil.ReadAll(conn) // bufio.NewReader(conn).ReadString('\x00') + // Check if we are able to decrypt + if CryptApplicable() { + // Decrypt packet + var decryptErr error + cont, decryptErr = Decrypt(cont) + if decryptErr != nil { + log.Printf("Failed to decrypt packet from %s: %s", conn.RemoteAddr().String(), decryptErr) + return + } + } + if contErr == nil || contErr == io.EOF { // We received the whole content, time to process it - unpackErr := logistic.UnpackInto([]byte(cont), outputDirectory) + unpackErr := logistic.UnpackInto(cont, outputDirectory) if unpackErr != nil { log.Printf("Encountered error processing packet from %s: %s", conn.RemoteAddr().String(), unpackErr) } diff --git a/net/peer.go b/net/peer.go index 07e54ac..26228f0 100644 --- a/net/peer.go +++ b/net/peer.go @@ -37,6 +37,17 @@ func CreatePeer(address string) Peer { // Sends the given content to the peer func (p *Peer) SendToPeer(content []byte) { + // Encrypt content if desired + if CryptApplicable() { + // Encrypt packet + var encryptErr error + content, encryptErr = Encrypt(content) + if encryptErr != nil { + log.Printf("Failed to decrypt packet from %s: %s", p.Address, encryptErr) + return + } + } + // Build up a connection tcpConn, dialErr := net.Dial("tcp", p.Address) if dialErr != nil {