Add very basic fuzzing setup for discovery

This commit is contained in:
kdeme 2019-06-18 17:28:55 +02:00 committed by zah
parent 7cb5ac050b
commit d535bb1ade
5 changed files with 168 additions and 9 deletions

View File

@ -0,0 +1,68 @@
import
streams, posix, sequtils, strutils, chronicles, chronos, byteutils,
eth/p2p/[discovery, kademlia, enode], eth/[keys, rlp],
../../p2p/p2p_test_helper
template fuzz(body) =
# For code we want to fuzz.
try:
body
except:
let e = getCurrentException()
debug "Fuzzer input created exception", exception=e.name, trace=e.repr
discard kill(getpid(), SIGSEGV)
template noFuzz(body) =
# For code not in the scope of the test.
# Lets not have false negatives due to possible issues in this code.
try:
body
except:
let e = getCurrentException()
debug "Exception out of scope of the fuzzing target",
exception=e.name, trace=e.repr
return
const DefaultListeningPort = 30303
proc fuzzTest() =
var
msg: seq[byte]
address: Address
targetNode: DiscoveryProtocol
noFuzz:
# Set up a discovery node, this is the node we target with fuzzing
let
targetNodeKey = initPrivateKey("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")
targetNodeAddr = localAddress(DefaultListeningPort)
targetNode = newDiscoveryProtocol(targetNodeKey, targetNodeAddr, @[])
# Create the transport as else replies on the messages send will fail.
targetNode.open()
# Read input from stdin (fastest for AFL)
let s = newFileStream(stdin)
# We use binary files as with hex we can get lots of "not hex" failures
var input = s.readAll()
s.close()
# Remove newline if it is there
input.removeSuffix
# TODO: is there a better/faster way?
let payload = input.mapIt(it.byte)
# Sending raw payload is possible but won't find us much. We need a hash and
# a signature, and without it there is a big chance it will always result in
# "Wrong msg mac from" error.
let nodeKey = initPrivateKey("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")
msg = packData(payload, nodeKey)
address = localAddress(DefaultListeningPort + 1)
try:
targetNode.receive(address, msg)
# These errors are also catched in `processClient` in discovery.nim
# TODO: move them a layer down in discovery so we can do a cleaner test there?
except RlpError, DiscProtocolError:
debug "Receive failed", err = getCurrentExceptionMsg()
fuzz:
fuzzTest()

View File

@ -0,0 +1,73 @@
import
chronos, times, byteutils, stint, chronicles, streams, nimcrypto, os,
strformat, strutils, eth/p2p/[discovery, kademlia], eth/[keys, rlp],
../../p2p/p2p_test_helper
template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0]
const inputsDir = &"{sourceDir}{DirSep}generated-input{DirSep}"
proc toFile(data: Bytes, fn: string) =
var s = newFileStream(fn, fmWrite)
for x in data:
s.write(x)
s.close()
const EXPIRATION = 3600 * 24 * 365 * 10
proc expiration(): uint32 = uint32(epochTime() + EXPIRATION)
proc generate() =
## Generate some valid inputs where one can start fuzzing with
let
fromAddr = localAddress(30303)
toAddr = localAddress(30304)
peerKey = initPrivateKey("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")
# valid data for a Ping packet
block:
let payload = rlp.encode((4, fromAddr, toAddr, expiration())).toRange
let encodedData = @[1.byte] & payload.toSeq()
debug "Ping", data=byteutils.toHex(encodedData)
encodedData.toFile(inputsDir & "ping")
# valid data for a Pong packet
block:
let token = keccak256.digest(@[0])
let payload = rlp.encode((toAddr, token , expiration())).toRange
let encodedData = @[2.byte] & payload.toSeq()
debug "Pong", data=byteutils.toHex(encodedData)
encodedData.toFile(inputsDir & "pong")
# valid data for a FindNode packet
block:
var data: array[64, byte]
data[32 .. ^1] = peerKey.getPublicKey.toNodeId().toByteArrayBE()
let payload = rlp.encode((data, expiration())).toRange
let encodedData = @[3.byte] & payload.toSeq()
debug "FindNode", data=byteutils.toHex(encodedData)
encodedData.toFile(inputsDir & "findnode")
# valid data for a Neighbours packet
block:
let
n1Addr = localAddress(30305)
n2Addr = localAddress(30306)
n1Key = initPrivateKey("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")
n2Key = initPrivateKey("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a619")
type Neighbour = tuple[ip: IpAddress, udpPort, tcpPort: Port, pk: PublicKey]
var nodes = newSeqOfCap[Neighbour](2)
nodes.add((n1Addr.ip, n1Addr.udpPort, n1Addr.tcpPort, n1Key.getPublicKey()))
nodes.add((n2Addr.ip, n2Addr.udpPort, n2Addr.tcpPort, n2Key.getPublicKey()))
let payload = rlp.encode((nodes, expiration())).toRange
let encodedData = @[4.byte] & payload.toSeq()
debug "Neighbours", data=byteutils.toHex(encodedData)
encodedData.toFile(inputsDir & "neighbours")
discard existsOrCreateDir(inputsDir)
generate()

18
tests/fuzzing/fuzz.nims Normal file
View File

@ -0,0 +1,18 @@
# TODO: make this configurable when more fuzzing targets
cd "discovery"
if not dirExists("generated-input"):
exec "nim c -r generate"
if not fileExists("fuzz"):
# Requires afl-gcc to be installed
# TODO: add + test option for clang
exec "nim c --cc=gcc --gcc.exe=afl-gcc --gcc.linkerexe=afl-gcc fuzz"
if dirExists("output"):
exec "afl-fuzz -i - -o output -M fuzzer01 -- ./fuzz"
else:
exec "afl-fuzz -i generated-input -o output -M fuzzer01 -- ./fuzz"
# TODO: how to add slaves for multiple cores in nimscript?
# afl-fuzz -i generated-input -o output -S fuzzer02 -- ./fuzz

View File

@ -1,5 +1,5 @@
import
unittest, chronos, eth/[keys, p2p], eth/p2p/[discovery, enode]
unittest, chronos, nimcrypto, eth/[keys, p2p], eth/p2p/[discovery, enode]
var nextPort = 30303
@ -32,3 +32,9 @@ template asyncTest*(name, body: untyped) =
test name:
proc scenario {.async.} = body
waitFor scenario()
proc packData*(payload: seq[byte], pk: PrivateKey): seq[byte] =
let
signature = @(pk.signMessage(payload).getRaw())
msgHash = keccak256.digest(signature & payload)
result = @(msgHash.data) & signature & payload

View File

@ -8,7 +8,7 @@
#
import
sequtils, unittest, chronos, byteutils, nimcrypto,
sequtils, unittest, chronos, byteutils,
eth/[keys, rlp], eth/p2p/[discovery, kademlia, enode],
./p2p_test_helper
@ -16,12 +16,6 @@ proc nodeIdInNodes(id: NodeId, nodes: openarray[Node]): bool =
for n in nodes:
if id == n.id: return true
proc packData(payload: seq[byte], pk: PrivateKey): seq[byte] =
let
signature = @(pk.signMessage(payload).getRaw())
msgHash = keccak256.digest(signature & payload)
result = @(msgHash.data) & signature & payload
proc test() {.async.} =
suite "Discovery Tests":
let