From d535bb1ade320d304cf1bfb17c4b4ec027d467c7 Mon Sep 17 00:00:00 2001 From: kdeme Date: Tue, 18 Jun 2019 17:28:55 +0200 Subject: [PATCH] Add very basic fuzzing setup for discovery --- tests/fuzzing/discovery/fuzz.nim | 68 ++++++++++++++++++++++++++ tests/fuzzing/discovery/generate.nim | 73 ++++++++++++++++++++++++++++ tests/fuzzing/fuzz.nims | 18 +++++++ tests/p2p/p2p_test_helper.nim | 8 ++- tests/p2p/test_discovery.nim | 10 +--- 5 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 tests/fuzzing/discovery/fuzz.nim create mode 100644 tests/fuzzing/discovery/generate.nim create mode 100644 tests/fuzzing/fuzz.nims diff --git a/tests/fuzzing/discovery/fuzz.nim b/tests/fuzzing/discovery/fuzz.nim new file mode 100644 index 0000000..6327c36 --- /dev/null +++ b/tests/fuzzing/discovery/fuzz.nim @@ -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() diff --git a/tests/fuzzing/discovery/generate.nim b/tests/fuzzing/discovery/generate.nim new file mode 100644 index 0000000..d0ba509 --- /dev/null +++ b/tests/fuzzing/discovery/generate.nim @@ -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() diff --git a/tests/fuzzing/fuzz.nims b/tests/fuzzing/fuzz.nims new file mode 100644 index 0000000..5ee0fd2 --- /dev/null +++ b/tests/fuzzing/fuzz.nims @@ -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 diff --git a/tests/p2p/p2p_test_helper.nim b/tests/p2p/p2p_test_helper.nim index a8d3ca7..ddbfc1f 100644 --- a/tests/p2p/p2p_test_helper.nim +++ b/tests/p2p/p2p_test_helper.nim @@ -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 diff --git a/tests/p2p/test_discovery.nim b/tests/p2p/test_discovery.nim index bfb9065..c2c7a54 100644 --- a/tests/p2p/test_discovery.nim +++ b/tests/p2p/test_discovery.nim @@ -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 @@ -96,4 +90,4 @@ proc test() {.async.} = # msg mac bootNode.receive(address, packData(@[], nodeKey)) -waitFor test() \ No newline at end of file +waitFor test()