Only sync main nodes across servers

This commit is contained in:
maride 2021-05-17 15:20:37 +02:00
parent a95b6ce3fe
commit 7807b45ed5
4 changed files with 38 additions and 106 deletions

View File

@ -4,12 +4,11 @@ Transfer AFL files over a mesh to fuzz across multiple servers
## Features
- Automatically syncs the fuzzers over all nodes
- No obscure dependencies, no painful setup process - just a single, self-contained binary
- 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 UNIX-like systems (Linux, OSX) and Windows
- Reduces the amount of transmitted test cases to a bare minimum
## Usage
@ -23,9 +22,9 @@ As a countermeasure, use the `--restrict-to-peers` flags to only allow connectio
Let's assume you have three servers running with some instances of AFL, all in secondary (`-S`) mode, except the main fuzzer running on the box 10.0.0.1.
To sync test cases across those servers, you'd need to run
- on 10.0.0.1: `./afl-transmit --fuzzer-directory /ram/output --main --peers 10.0.0.2,10.0.0.3`
- on 10.0.0.2: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1`
- on 10.0.0.3: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1`
- on 10.0.0.1: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.2,10.0.0.3`
- on 10.0.0.2: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1,10.0.0.3`
- on 10.0.0.3: `./afl-transmit --fuzzer-directory /ram/output --peers 10.0.0.1,10.0.0.2`
Because *afl-transmit* stays in the foreground, you should probably run it in a `tmux` window or something comparable.
@ -43,24 +42,3 @@ dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tee transmit.key
```
As already said, the same key must be used on all nodes.
### Traffic reduction
On default, *afl-transmit* avoids sending files with the same name present in different fuzzer directories.
This will greatly reduce the traffic between your nodes (I measured 621 kB to 1.3 kB, for example).
Please note that there might be some edge cases when you don't want that behaviour, e.g.
- you want to preserve the queue of each fuzzer
- you expect your fuzzers to give the same (file) name to different test cases, in which case *afl-transmit* would mistakenly assume that the file has the same *contents* and not only the same *name*
- you don't care for traffic
To avoid reducing the transmitted files by comparing filenames, add `--no-duplicates=false` as argument.
Also on default, *afl-transmit* tries to check if the queue of the observed fuzzers contain test cases which originated from another fuzzer instance.
In that case, the file name contains the keyword "sync" in it, and looks e.g. like this: `id:001815,time:0,orig:id:001805,sync:main,src:001794`
If it was copied from another fuzzer, it means that the file is already present in the fuzzer cluster, and can safely be skipped on those fuzzer instances which copied it.
Please note that this will produce false positives if the filename of your testcases contain `,sync:` for whatever reason.
To avoid reducing the transmitted files by filtering synced files out, add `--avoid-synced=false` as argument.
If you still have trouble paying the invoice for your ISP due to heavy traffic usage, try increasing the `--rescan` value, so files are transmitted less often.

View File

@ -4,7 +4,6 @@ import (
"archive/tar"
"bytes"
"compress/flate"
"flag"
"fmt"
"io/ioutil"
"log"
@ -12,30 +11,17 @@ import (
"strings"
)
var noDuplicates bool
// avoidSynced is the flag to set if files containing "sync" should be packed or not.
// Those files are from another fuzzer - no need to pack them twice.
var avoidSynced bool
// RegisterPackerFlags registers flags which are required by the packer
func RegisterPackerFlags() {
flag.BoolVar(&noDuplicates, "no-duplicates", true, "Avoid transmitting the same file multiple times, e.g. because it is present in multiple fuzzer's queues")
flag.BoolVar(&avoidSynced, "avoid-synced", true, "Avoid transmitting files containing the keyword 'sync', as they are from other fuzzers anyways, and should be included by their afl-transmit instance")
}
// PackFuzzers packs all targeted fuzzers into a TAR - at least queue/, fuzz_bitmap, fuzzer_stats
func PackFuzzers(fuzzers []string, fuzzerDirectory string) ([]byte, error) {
// PackFuzzer packs the fuzzer into a TAR: queue/, fuzz_bitmap, fuzzer_stats
func PackFuzzer(fuzzer string, fuzzerDirectory string) ([]byte, error) {
// Create TAR archive
var tarBuffer bytes.Buffer
tarWriter := tar.NewWriter(&tarBuffer)
// Essentially we want to pack three things from each targeted fuzzer:
// Essentially we want to pack three things from the targeted fuzzer:
// - the fuzz_bitmap file
// - the fuzzer_stats file
// - the queue/ directory - but avoiding duplicates
var pkgCont []string // list of queue files already present in the archive
for _, fuzzer := range fuzzers {
// We need full paths to read, but will write relative paths into the TAR archive
absFuzzerPath := fuzzerDirectory
relFuzzerPath := strings.TrimPrefix(fuzzer, fuzzerDirectory)
@ -43,8 +29,7 @@ func PackFuzzers(fuzzers []string, fuzzerDirectory string) ([]byte, error) {
// Read-n-Pack™
packSingleFile(tarWriter, absFuzzerPath, relFuzzerPath, "fuzz_bitmap")
packSingleFile(tarWriter, absFuzzerPath, relFuzzerPath, "fuzzer_stats")
packQueueFiles(tarWriter, absFuzzerPath, relFuzzerPath, &pkgCont)
}
packQueueFiles(tarWriter, absFuzzerPath, relFuzzerPath)
// Close TAR archive
tarWriter.Close()
@ -90,7 +75,7 @@ func packSingleFile(tarWriter *tar.Writer, absPath string, relPath string, fileN
}
// Packs the files in the given directory into a tar archive
func packQueueFiles(tarWriter *tar.Writer, absPath string, relPath string, pkgCont *[]string) {
func packQueueFiles(tarWriter *tar.Writer, absPath string, relPath string) {
// Get list of queue files
queuePath := fmt.Sprintf("%s%c%s%cqueue", absPath, os.PathSeparator, relPath, os.PathSeparator)
filesInDir, readErr := ioutil.ReadDir(queuePath)
@ -107,37 +92,7 @@ func packQueueFiles(tarWriter *tar.Writer, absPath string, relPath string, pkgCo
continue
}
// Check if we should care fore duplicates
if noDuplicates && checkDuplicate(f.Name(), pkgCont) {
// that file is already present in the package - avoid packing it again
continue
}
// Check if we should care for the keyword 'sync' in file name
if avoidSynced && strings.Contains(f.Name(), ",sync:") {
// seems like this file was put into the queue of this fuzzer by syncing it from another fuzzer. We don't
// need to transmit it then, because the fuzzer which found that case will have the same file but without
// the keyword "sync" in it. Simply put, we avoid sending the same file multiple times with different names.
continue
}
// Pack into the archive
packSingleFile(tarWriter, absPath, relPath, fmt.Sprintf("queue%c%s", os.PathSeparator, f.Name()))
if noDuplicates {
// Append added file name to the list of things included in the package
*pkgCont = append(*pkgCont, f.Name())
}
}
}
// checkDuplicate checks if name is already present in pkgCont
func checkDuplicate(name string, pkgCont *[]string) bool {
for _, p := range *pkgCont {
if p == name {
return true
}
}
return true
}

View File

@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
"github.com/maride/afl-transmit/logistic"
"github.com/maride/afl-transmit/net"
"github.com/maride/afl-transmit/stats"
"github.com/maride/afl-transmit/watchdog"
@ -20,7 +19,6 @@ func main() {
net.RegisterSenderFlags()
net.RegisterListenFlags()
net.RegisterCryptFlags()
logistic.RegisterPackerFlags()
stats.RegisterStatsFlags()
RegisterGlobalFlags()
flag.Parse()

View File

@ -13,21 +13,26 @@ import (
var (
rescan int
isMainNode bool
)
// RegisterWatchdogFlags registers required flags for the watchdog
func RegisterWatchdogFlags() {
flag.IntVar(&rescan, "rescan", 30, "Minutes to wait before rescanning local fuzzer directories")
flag.BoolVar(&isMainNode, "main", false, "Set this option if this afl-transmit instance is running on the node running the main afl-fuzz instance")
flag.IntVar(&rescan, "rescan", 30, "Minutes to wait before rescanning local fuzzer directory")
}
// Watch over the specified directory, send updates to peers and re-scan after the specified amount of seconds
// WatchFuzzers watches over the specified directory, sends updates to peers and re-scans after the specified amount of seconds
func WatchFuzzers(outputDirectory string) {
// Loop forever
for {
// Pack important parts of the fuzzers into an archive
packedFuzzers, packerErr := logistic.PackFuzzers(getTargetFuzzer(outputDirectory), outputDirectory)
// Search for main fuzzer
targetFuzzer, targetErr := getTargetFuzzer(outputDirectory)
if targetErr != nil {
log.Printf("Failed to detect main fuzzer: %s", targetErr)
continue
}
// Pack important parts of the fuzzer into an archive
packedFuzzers, packerErr := logistic.PackFuzzer(targetFuzzer, outputDirectory)
if packerErr != nil {
log.Printf("Failed to pack fuzzer: %s", packerErr)
continue
@ -41,24 +46,24 @@ func WatchFuzzers(outputDirectory string) {
}
}
// Searches in the specified output directory for fuzzers which we want to pack.
// Identifying the main fuzzer is done by searching for the file "is_main_node"
// This relies on the "election process" done by secondary fuzzers if they don't find a local main node.
// If that is detected, a secondary fuzzer becomes the main node in terms of syncing. That means we need to focus on
// the main node even if there is no real main node running locally.
// Searches in the specified output directory for the main fuzzer.
// Identifying the main fuzzer is done by searching for the file "is_main_node". On secondary-only servers, this relies
// on the "election process" done by secondary fuzzers if they don't find a local main node. In that election process, a
// secondary fuzzer becomes the main node in terms of syncing. That means we need to focus on the main node even if
// there is no real main node running locally. Good thing is that the elected node will create a "is_main_node" file, so
// we really just need to search for that.
// However it is important to avoid transmitting the is_main_node file - else, the secondaries will think that there is
// a main node running locally. Skipping the is_main_node file means the other fuzzers (and especially the elected main
// fuzzer) detect the transmitted fuzzer as a normal (and dead) secondary. ... blabla
// fuzzer) detect the transmitted fuzzer as a normal (and dead) secondary.
//
// (see AFLplusplus/src/afl-fuzz.run.c:542)
func getTargetFuzzer(outputDirectory string) []string {
func getTargetFuzzer(outputDirectory string) (string, error) {
// find main fuzzer directory - its the only one required by the secondaries
// List files (read: fuzzers) in output directory
filesInDir, readErr := ioutil.ReadDir(outputDirectory)
if readErr != nil {
log.Printf("Failed to list directory content of %s: %s", outputDirectory, readErr)
return nil
return "", fmt.Errorf("Failed to list directory content of %s: %s", outputDirectory, readErr)
}
for _, f := range filesInDir {
@ -75,13 +80,9 @@ func getTargetFuzzer(outputDirectory string) []string {
}
// is_main_node file exists, so we found the correct fuzzer directory - return it
fullPath := fmt.Sprintf("%s%c%s", outputDirectory, os.PathSeparator, f.Name())
return []string{
fullPath,
}
return fmt.Sprintf("%s%c%s", outputDirectory, os.PathSeparator, f.Name()), nil
}
// Failed to find the main node - probably we are in --main mode by accident
log.Printf("Unable to find main node in %s - sure afl-transmit should run in --main mode?", outputDirectory)
return nil
return "", fmt.Errorf("Unable to find main node in %s", outputDirectory)
}