mirror of
https://github.com/maride/pancap.git
synced 2024-11-24 01:34:26 +00:00
Compare commits
6 Commits
0430a1b214
...
21c956c545
Author | SHA1 | Date | |
---|---|---|---|
21c956c545 | |||
1989ae996f | |||
1aa3572c30 | |||
4a1c58b564 | |||
44739d4697 | |||
daf641c881 |
@ -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!
|
||||
|
||||
|
@ -2,10 +2,11 @@ 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 (
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
9
file.go
9
file.go
@ -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
2
go.mod
@ -1,4 +1,4 @@
|
||||
module git.darknebu.la/maride/pancap
|
||||
module github.com/maride/pancap
|
||||
|
||||
go 1.13
|
||||
|
||||
|
10
main.go
10
main.go
@ -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,7 +57,7 @@ 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.",
|
||||
|
@ -2,11 +2,12 @@ package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.darknebu.la/maride/pancap/common"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/maride/pancap/common"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -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))
|
||||
|
||||
|
@ -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
103
output/graph.go
Normal 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
19
output/graphpkg.go
Normal 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)
|
||||
}
|
@ -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>.")
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -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
BIN
pancap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 464 KiB |
@ -2,10 +2,10 @@ 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"
|
||||
)
|
||||
@ -19,7 +19,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
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 {
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 {
|
||||
|
@ -6,4 +6,3 @@ type dhcpResponse struct {
|
||||
serverMACAddr string
|
||||
askedFor bool
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -2,8 +2,8 @@ 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"
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -2,8 +2,8 @@ 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"
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
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 {
|
||||
|
@ -3,10 +3,10 @@ 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"
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user