Compare commits

...

6 Commits

32 changed files with 254 additions and 151 deletions

View File

@ -1,5 +1,7 @@
# pancap
<img alt="pancap logo" src="pancap.png" width="250px" height="250px">
## Idea
If you get access to a [PCAP](https://en.wikipedia.org/wiki/Pcap) file, for example during a CTF or captured on your own, you usually have the problem of overlooking all the relevant information to get a basic idea of the capture file. This gets worse if the capture file includes lots of white noise or irrelevant traffic - often included in the capture file to cloak *interesting* packets in a bunch of packets to YouTube, Reddit, Twitter and others.
@ -10,7 +12,7 @@ If you get access to a [PCAP](https://en.wikipedia.org/wiki/Pcap) file, for exam
Simply run
`go get git.darknebu.la/maride/pancap`
`go get github.com/maride/pancap`
This will also build `pancap` and place it into your `GOBIN` directory - means you can directly execute it!

View File

@ -2,15 +2,16 @@ package analyze
import (
"fmt"
"git.darknebu.la/maride/pancap/output"
"git.darknebu.la/maride/pancap/protocol"
"github.com/google/gopacket"
"log"
"github.com/google/gopacket"
"github.com/maride/pancap/output"
"github.com/maride/pancap/protocol"
)
var (
// Store total amount and amount of visited packets
totalPackets int
totalPackets int
processedPackets int
)
@ -39,6 +40,9 @@ func Analyze(source *gopacket.PacketSource) error {
}
}
// Register communication for graph
output.AddPkgToGraph(packet)
// Raise statistics
totalPackets += 1
if processed {
@ -68,4 +72,3 @@ func handleErr(err error) {
log.Printf("Encountered error while examining packets, continuing anyway. Error: %s", err.Error())
}
}

View File

@ -1,42 +0,0 @@
package main
import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"testing"
)
func Test_analyzePCAP(t *testing.T) {
type args struct {
source *gopacket.PacketSource
linkType layers.LinkType
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Faulty link type",
args: args{
source: &gopacket.PacketSource{
DecodeOptions: gopacket.DecodeOptions{
Lazy: false,
NoCopy: false,
SkipDecodeRecovery: false,
DecodeStreamsAsDatagrams: false,
},
},
linkType: 2,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := analyzePCAP(tt.args.source, tt.args.linkType); (err != nil) != tt.wantErr {
t.Errorf("analyzePCAP() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -23,10 +23,10 @@ func GenerateTree(strarr []string) string {
// iterate over each element
for iter, elem := range strarr {
// check if we got the last element
if iter < len(strarr) - 1 {
if iter < len(strarr)-1 {
tmpstr = fmt.Sprintf("%s├ %s\n", tmpstr, elem)
} else {
tmpstr = fmt.Sprintf( "%s╰ %s\n", tmpstr, elem)
tmpstr = fmt.Sprintf("%s╰ %s\n", tmpstr, elem)
}
}

View File

@ -3,29 +3,30 @@ package main
import (
"flag"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
var (
filenameFlag *string
filenameFlag string
)
// Registers the flag --file
func registerFileFlags() {
filenameFlag = flag.String("file", "", "PCAP file to base analysis on")
flag.StringVar(&filenameFlag, "file", "", "PCAP file to base analysis on")
}
// Opens the PCAP, returns its packets and the link type or an error
func openPCAP() (*gopacket.PacketSource, layers.LinkType, error) {
// Check if we even got a file.
if *filenameFlag == "" {
if filenameFlag == "" {
return nil, 0, fmt.Errorf("missing file to analyze. Please specifiy it with --file")
}
// Open specified file
handle, openErr := pcap.OpenOffline(*filenameFlag)
handle, openErr := pcap.OpenOffline(filenameFlag)
if openErr != nil {
// There were some problems opening the file
return nil, 0, openErr

2
go.mod
View File

@ -1,4 +1,4 @@
module git.darknebu.la/maride/pancap
module github.com/maride/pancap
go 1.13

12
main.go
View File

@ -3,11 +3,12 @@ package main
import (
"flag"
"fmt"
"git.darknebu.la/maride/pancap/analyze"
"git.darknebu.la/maride/pancap/output"
"log"
"math/rand"
"time"
"github.com/maride/pancap/analyze"
"github.com/maride/pancap/output"
)
func main() {
@ -36,6 +37,9 @@ func main() {
// Extract found and requested files
output.StoreFiles()
// Create communication graph
output.CreateGraph()
// Show user analysis
analyze.PrintSummary()
@ -53,13 +57,13 @@ func printMOTD() {
"PanCAP: Analyzer for pancake files",
"You want some syrup with these packets?",
"Check out CONTRIBUTORS.md!",
"Push your commits to git.darknebu.la/maride/pancap",
"Push your commits to github.com/maride/pancap",
"Don't let the white noise traffic confuse you.",
"Grab a Club Mate if you don't have one yet.",
"In Soviet Russia, traffic analyzes you.",
"Who captures the captors?",
"Respect other's privacy. Always.",
"Make public data available, protect private data.", // https://www.ccc.de/en/hackerethik
"Make public data available, protect private data.", // https://www.ccc.de/en/hackerethik
"Most traffic is just there to confuse the russians.", // hat-tip to twitter.com/_harryr_
}

View File

@ -6,10 +6,10 @@ import (
)
type File struct {
name string
name string
content []byte
origin string
hash string
origin string
hash string
}
// Creates a new file object and calculates the hash of the given content

View File

@ -2,17 +2,18 @@ package output
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"io/ioutil"
"log"
"os"
"strings"
"github.com/maride/pancap/common"
)
var (
registeredFiles []File
notFound []string
extractedFiles int
notFound []string
extractedFiles int
)
// Registers a file with the given name and content.
@ -20,12 +21,18 @@ var (
// This means that a module should _always_ call this function when a file is encountered.
// origin is a descriptive string where the file comes from, e.g. the module name.
func RegisterFile(filename string, content []byte, origin string) {
// Check if there even is anything to register
if len(content) == 0 {
// File is empty, won't register the void
log.Printf("Avoided registering file from %s because it is empty.", origin)
return
}
thisFile := NewFile(filename, content, origin)
// To avoid doubles, we need to check if that hash is already present
for _, f := range registeredFiles {
if f.hash == thisFile.hash {
// Found - stop here
log.Printf("Avoided registering file '%s' because it has the same content as an already registered file ", f.name)
log.Printf("Avoided registering file from %s because it has the same content as an already registered file ", origin)
return
}
}
@ -39,12 +46,12 @@ func StoreFiles() {
var filesToExtract []File
// Check different flag scenarios
if *targetAllFiles {
if targetAllFiles {
// We should extract all files.
filesToExtract = registeredFiles
} else {
// We should extract only a given set of files
fileList := strings.Split(*targetFiles, ",")
fileList := strings.Split(targetFiles, ",")
for _, f := range fileList {
// Iterate over desired files
found := false
@ -73,7 +80,7 @@ func StoreFiles() {
// Writes the given file object to disk, along with a stats file placed next to it.
func writeOut(f File) {
targetName := fmt.Sprintf("%s%c%s", *targetOutput, os.PathSeparator, f.hash)
targetName := fmt.Sprintf("%s%c%s", targetOutput, os.PathSeparator, f.hash)
targetDescName := fmt.Sprintf("%s.info", targetName)
targetDescription := fmt.Sprintf("Filename: %s\nHash: %s\nOrigin: %s\nSize: %d", f.name, f.hash, f.origin, len(f.content))

View File

@ -3,19 +3,19 @@ package output
import "flag"
var (
fullOutput *bool
printEmptyBlocks *bool
targetFiles *string
targetAllFiles *bool
targetOutput *string
fullOutput bool
printEmptyBlocks bool
targetFiles string
targetAllFiles bool
targetOutput string
graphOutput string
)
func RegisterFlags() {
fullOutput = flag.Bool("full-output", false, "Show full output instead of limiting submodule output")
printEmptyBlocks = flag.Bool("print-empty-blocks", false, "Prints blocks (submodule output) even if the submodule doesn't have any content to print.")
targetFiles = flag.String("extract-these", "", "Comma-separated list of files to extract.")
targetAllFiles = flag.Bool("extract-all", false, "Extract all files found.")
targetOutput = flag.String("extract-to", "./extracted", "Directory to store extracted files in.")
flag.BoolVar(&fullOutput, "full-output", false, "Show full output instead of limiting submodule output")
flag.BoolVar(&printEmptyBlocks, "print-empty-blocks", false, "Prints blocks (submodule output) even if the submodule doesn't have any content to print.")
flag.StringVar(&targetFiles, "extract-these", "", "Comma-separated list of files to extract.")
flag.BoolVar(&targetAllFiles, "extract-all", false, "Extract all files found.")
flag.StringVar(&targetOutput, "extract-to", "./extracted", "Directory to store extracted files in.")
flag.StringVar(&graphOutput, "create-graph", "", "Create a Graphviz graph out of collected communication")
}

103
output/graph.go Normal file
View File

@ -0,0 +1,103 @@
package output
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"github.com/google/gopacket"
)
var graphPkgs []GraphPkg
// AddPkgToGraph adds the given packet as communication to the graph
func AddPkgToGraph(pkg gopacket.Packet) {
// Only proceed if pkg contains a network layer
if pkg.NetworkLayer() == nil {
return
}
src := pkg.NetworkLayer().NetworkFlow().Src().String()
dst := pkg.NetworkLayer().NetworkFlow().Dst().String()
// Search for the given communication pair
for _, p := range graphPkgs {
if p.from == src && p.to == dst {
// Communication pair found, add protocol and finish
p.AddProtocol("nil")
return
}
}
// Communcation pair was not in graphPkgs, add to it
graphPkgs = append(graphPkgs, GraphPkg{
from: src,
to: dst,
protocol: []string{""},
})
}
// CreateGraph writes out a Graphviz digraph
func CreateGraph() {
if graphOutput == "" {
// No graph requested
return
}
// Start with the Graphviz-specific header
dot := fmt.Sprintf("# Compile with `neato -Tpng %s > %s.png`\n", graphOutput, graphOutput)
dot += "digraph pancap {\n\toverlap = false;\n"
// First, gather all nodes as-is and write them out
dot += nodedef(graphPkgs)
// Iterate over communication
for _, p := range graphPkgs {
dot += fmt.Sprintf("\tn%s->n%s\n", hash(p.from), hash(p.to))
}
// Close
dot += "}\n"
// Write out
ioutil.WriteFile(graphOutput, []byte(dot), 0644)
}
// Creates a list of distinct nodes, Graphviz-compatible
func nodedef(pkgs []GraphPkg) string {
output := ""
nodes := []string{}
for _, p := range graphPkgs {
// Check if src and dst are already present in nodes array
srcFound := false
dstFound := false
for _, n := range nodes {
if p.from == n {
srcFound = true
}
if p.to == n {
dstFound = true
}
}
if !srcFound {
// src not yet present, add to node list
nodes = append(nodes, p.from)
}
if !dstFound {
// dst not yet present, add to node list
nodes = append(nodes, p.to)
}
}
// Output Graphviz-compatible node definition
for _, n := range nodes {
// As the Graphviz charset for nodes is rather small, rely on hashes
output += fmt.Sprintf("\tn%s[label=\"%s\"]\n", hash(n), n)
}
return output
}
func hash(s string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))[:6]
}

19
output/graphpkg.go Normal file
View File

@ -0,0 +1,19 @@
package output
// GraphPkg resembles a directed communication from one address to another
// It wraps up required information to draw a graph of the communication, including spoken protocols.
type GraphPkg struct {
from string
to string
protocol []string
}
// AddProtocol adds the given protocol to the list of protocols if not already present
func (p *GraphPkg) AddProtocol(protocol string) {
for _, p := range p.protocol {
if p == protocol {
return
}
}
p.protocol = append(p.protocol, protocol)
}

View File

@ -23,4 +23,10 @@ func Finalize() {
// User avoided the files
printer.Println("Files found in stream. Add --extract-all or --extract-these <list> to extract them.")
}
// Check if something graph-worthy was collected
if graphOutput == "" && len(graphPkgs) > 0 {
// User didn't want a graph
printer.Println("To summarize the communcation flow with a Graphviz graph, specify --create-graph <out.dot>.")
}
}

View File

@ -2,17 +2,18 @@ package output
import (
"fmt"
"github.com/fatih/color"
"strings"
"github.com/fatih/color"
)
const (
MaxContentLines = 50
SnipMark = "----- cut at 50 entries -----"
SnipMark = "----- cut at 50 entries -----"
)
var (
DidSnip bool
DidSnip bool
DidAvoidEmptyBlock bool
)
@ -21,7 +22,7 @@ var (
// If the content is longer than MaxContentLines, content is cut.
func PrintBlock(headline string, content string) {
// Avoid printing empty blocks - at least if user didn't specify it otherwise
if len(content) == 0 && !*printEmptyBlocks {
if len(content) == 0 && !printEmptyBlocks {
// No content and we are not forced to print empty blocks, return
DidAvoidEmptyBlock = true
return
@ -38,7 +39,7 @@ func PrintBlock(headline string, content string) {
}
// Cut to MaxContentLines if required
if !(*fullOutput) {
if !(fullOutput) {
// User states that they don't want to see the whole output - cut content.
content = cutContent(content)
}

BIN
pancap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

View File

@ -2,24 +2,24 @@ package arp
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"git.darknebu.la/maride/pancap/output"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
"github.com/maride/pancap/output"
"log"
"net"
)
var (
arpStatsList []arpStats
devices []arpDevice
arpStatsList []arpStats
devices []arpDevice
linkLocalBlock = net.IPNet{
IP: net.IPv4(169, 254, 0, 0),
Mask: net.IPv4Mask(255, 255, 0, 0),
}
)
type Protocol struct {}
type Protocol struct{}
// Checks if the given packet is an ARP packet we can process
func (p *Protocol) CanAnalyze(packet gopacket.Packet) bool {
@ -118,7 +118,7 @@ func (p *Protocol) getStatOrCreate(macaddr string) *arpStats {
// None found yet, we need to create a new one
arpStatsList = append(arpStatsList, arpStats{
macaddr: macaddr,
macaddr: macaddr,
})
// And return it

View File

@ -2,5 +2,5 @@ package arp
type arpDevice struct {
macaddr string
ipaddr string
ipaddr string
}

View File

@ -1,9 +1,9 @@
package arp
type arpStats struct {
macaddr string
asked int
answered int
askedList []string
macaddr string
asked int
answered int
askedList []string
answeredList []string
}

View File

@ -1,16 +1,16 @@
package dhcpv4
import (
"git.darknebu.la/maride/pancap/output"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/output"
)
type Protocol struct {
hostnames []hostname
hostnames []hostname
networkSetup map[layers.DHCPOpt][]byte
requestMAC []string
responses []dhcpResponse
requestMAC []string
responses []dhcpResponse
}
// Checks if the given packet is a DHCP packet we can process

View File

@ -1,9 +1,8 @@
package dhcpv4
type dhcpResponse struct {
destMACAddr string
newIPAddr string
destMACAddr string
newIPAddr string
serverMACAddr string
askedFor bool
askedFor bool
}

View File

@ -1,8 +1,8 @@
package dhcpv4
type hostname struct {
hostname string
hostname string
requestedByMAC string
granted bool
granted bool
deniedHostname string
}

View File

@ -2,8 +2,8 @@ package dhcpv4
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
"log"
)

View File

@ -12,13 +12,13 @@ import (
var (
watchedOpts = []layers.DHCPOpt{
layers.DHCPOptSubnetMask, // Option 1
layers.DHCPOptRouter, // Option 3
layers.DHCPOptDNS, // Option 6
layers.DHCPOptSubnetMask, // Option 1
layers.DHCPOptRouter, // Option 3
layers.DHCPOptDNS, // Option 6
layers.DHCPOptBroadcastAddr, // Option 28
layers.DHCPOptNTPServers, // Option 42
layers.DHCPOptLeaseTime, // Option 51
layers.DHCPOptT1, // Option 58
layers.DHCPOptNTPServers, // Option 42
layers.DHCPOptLeaseTime, // Option 51
layers.DHCPOptT1, // Option 58
}
)
@ -126,7 +126,7 @@ func formatDate(rawDate []byte) (string, bool) {
intDate := binary.LittleEndian.Uint32(rawDate)
seconds := intDate % 60
minutes := intDate / 60 % 60
hours := intDate / 60 / 60 % 60
hours := intDate / 60 / 60 % 60
formattedDate := ""
// Check which words we need to pick

View File

@ -2,8 +2,8 @@ package dhcpv4
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
)
// Processes the DHCP request packet handed over

View File

@ -2,8 +2,8 @@ package dhcpv4
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
"log"
)

View File

@ -2,20 +2,20 @@ package dns
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
"golang.org/x/net/publicsuffix"
"log"
)
var (
numAnswers int
answerDomains []string
answerBaseDomains []string
numAnswers int
answerDomains []string
answerBaseDomains []string
answerPrivateDomains []string
answerType = make(map[layers.DNSType]int)
answerPublicIPv4 []string
answerPrivateIPv4 []string
answerType = make(map[layers.DNSType]int)
answerPublicIPv4 []string
answerPrivateIPv4 []string
)
// Called on every DNS packet to process response(s)

View File

@ -8,7 +8,7 @@ import (
var (
privateBlocks = []net.IPNet{
{net.IPv4(10, 0, 0, 0), net.IPv4Mask(255, 0, 0, 0)}, // 10.0.0.0/8
{net.IPv4(10, 0, 0, 0), net.IPv4Mask(255, 0, 0, 0)}, // 10.0.0.0/8
{net.IPv4(172, 16, 0, 0), net.IPv4Mask(255, 240, 0, 0)}, // 172.16.0.0/12
{net.IPv4(192, 168, 0, 0), net.IPv4Mask(255, 255, 0, 0)}, // 192.168.0.0/24
{net.IPv4(100, 64, 0, 0), net.IPv4Mask(255, 192, 0, 0)}, // 100.64.0.0/10
@ -58,7 +58,7 @@ func (p *Protocol) generateDNSTypeSummary(typearr map[layers.DNSType]int) string
if iter == 0 {
// We don't need to append yet
answerstr = elem
} else if iter == len(answerarr) - 1 {
} else if iter == len(answerarr)-1 {
// Last element, use "and" instead of a comma
answerstr = fmt.Sprintf("%s and %s", answerstr, elem)
} else {

View File

@ -1,12 +1,12 @@
package dns
import (
"git.darknebu.la/maride/pancap/output"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/output"
)
type Protocol struct {}
type Protocol struct{}
func (p *Protocol) CanAnalyze(packet gopacket.Packet) bool {
return packet.Layer(layers.LayerTypeDNS) != nil

View File

@ -2,18 +2,18 @@ package dns
import (
"fmt"
"git.darknebu.la/maride/pancap/common"
"github.com/google/gopacket/layers"
"github.com/maride/pancap/common"
"golang.org/x/net/publicsuffix"
"log"
)
var (
numQuestions int
questionDomains []string
questionBaseDomains []string
numQuestions int
questionDomains []string
questionBaseDomains []string
questionPrivateDomains []string
questionType = make(map[layers.DNSType]int)
questionType = make(map[layers.DNSType]int)
)
// Called on every DNS packet to process questions

View File

@ -1,20 +1,20 @@
package http
import (
"git.darknebu.la/maride/pancap/common"
"git.darknebu.la/maride/pancap/output"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/tcpassembly"
"github.com/maride/pancap/common"
"github.com/maride/pancap/output"
)
type Protocol struct {
initialized bool
requestFactory *httpRequestFactory
responseFactory *httpResponseFactory
requestPool *tcpassembly.StreamPool
responsePool *tcpassembly.StreamPool
requestAssembler *tcpassembly.Assembler
initialized bool
requestFactory *httpRequestFactory
responseFactory *httpResponseFactory
requestPool *tcpassembly.StreamPool
responsePool *tcpassembly.StreamPool
requestAssembler *tcpassembly.Assembler
responseAssembler *tcpassembly.Assembler
}

View File

@ -3,17 +3,17 @@ package http
import (
"bufio"
"fmt"
"git.darknebu.la/maride/pancap/output"
"github.com/google/gopacket"
"github.com/google/gopacket/tcpassembly"
"github.com/google/gopacket/tcpassembly/tcpreader"
"github.com/maride/pancap/output"
"io"
"io/ioutil"
"net/http"
)
var (
responseSummaryLines []string
responseSummaryLines []string
)
type httpResponseFactory struct{}

View File

@ -1,10 +1,10 @@
package protocol
import (
"git.darknebu.la/maride/pancap/protocol/arp"
"git.darknebu.la/maride/pancap/protocol/dhcpv4"
"git.darknebu.la/maride/pancap/protocol/dns"
"git.darknebu.la/maride/pancap/protocol/http"
"github.com/maride/pancap/protocol/arp"
"github.com/maride/pancap/protocol/dhcpv4"
"github.com/maride/pancap/protocol/dns"
"github.com/maride/pancap/protocol/http"
)
var (