misc: moved graph module to experimental branch for now

This commit is contained in:
Simone Margaritelli 2021-04-12 14:09:03 +02:00
commit f2b6d9b708
16 changed files with 0 additions and 1751 deletions

View file

@ -4,10 +4,6 @@ require("functions")
log("session script loaded, fake AP is " + fakeESSID);
// enable the graph module so we can extract more historical info
// for each device we see
run('graph on')
// create an empty ticker so we can run commands every few seconds
// this will inject decoy wifi client probes used to detect KARMA
// attacks and in general rogue access points
@ -35,8 +31,5 @@ onEvent('wifi.client.handshake', onHandshake);
// register for wifi.ap.new events (used to detect rogue APs)
onEvent('wifi.ap.new', onNewAP);
// register for new nodes in the graph
onEvent('graph.node.new', onNewNode);
// register for gateway changes
onEvent('gateway.change', onGatewayChange)

View file

@ -1,19 +1,9 @@
var fakeESSID = random.String(16, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
var fakeBSSID = random.Mac()
// uses graph.to_dot and graphviz to generate a png graph
function createGraph(who, where) {
// generates a .dot file with the graph for this mac
run('graph.to_dot ' + who);
// uses graphviz to make a png of it
run('!dot -Tpng bettergraph.dot > ' + where);
}
function onDeauthentication(event) {
var data = event.data;
createGraph(data.address1, '/tmp/graph_deauth.png');
var message = '🚨 Detected deauthentication frame:\n\n' +
// 'Time: ' + event.time + "\n" +
// 'GPS: lat=' + session.GPS.Latitude + " lon=" + session.GPS.Longitude + " updated_at=" +
@ -28,14 +18,11 @@ function onDeauthentication(event) {
// send to telegram bot
sendMessage(message);
sendPhoto("/tmp/graph_deauth.png");
}
function onNewAP(event){
var ap = event.data;
if(ap.hostname == fakeESSID) {
createGraph(ap.mac, '/tmp/graph_ap.png');
var message = '🦠 Detected rogue AP:\n\n' +
// 'Time: ' + event.time + "\n" +
// 'GPS: lat=' + session.GPS.Latitude + " lon=" + session.GPS.Longitude + " updated_at=" +
@ -44,7 +31,6 @@ function onNewAP(event){
// send to telegram bot
sendMessage(message);
sendPhoto("/tmp/graph_ap.png");
}
}
@ -52,8 +38,6 @@ function onHandshake(event){
var data = event.data;
var what = 'handshake';
createGraph(data.station, '/tmp/graph_handshake.png');
if(data.pmkid != null) {
what = "RSN PMKID";
} else if(data.full) {
@ -71,23 +55,6 @@ function onHandshake(event){
// send to telegram bot
sendMessage(message);
sendPhoto("/tmp/graph_handshake.png");
}
function onNewNode(event) {
var node = event.data;
if(node.type != 'ssid' && node.type != 'ble_server' && graph.IsConnected(node.type, node.id)) {
createGraph(node.id, '/tmp/graph_node.png');
var message = '🖥️ Detected previously unknown ' + node.type + ':\n\n' +
'Type: ' + node.type + "\n" +
'MAC: ' + node.id;
// send to telegram bot
sendMessage(message);
sendPhoto("/tmp/graph_node.png");
}
}
function onGatewayChange(event) {

View file

@ -1,195 +0,0 @@
<head>
<style> body {
margin: 0;
} </style>
<script src="//unpkg.com/jquery"></script>
<script src="//unpkg.com/3d-force-graph"></script>
<script src="//unpkg.com/three"></script>
<script src="//unpkg.com/three/examples/js/renderers/CSS2DRenderer.js"></script>
<script src="//unpkg.com/three-spritetext"></script>
<style>
.node-div {
font-size: 1rem;
padding: 1px 4px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
user-select: none;
}
.node-data-div {
background-color: rgba(0, 0, 0, 0.9);
}
</style>
</head>
<body>
<div id="3d-graph"></div>
<script id="vars">
const typeNodeColors = {
'ble_server': '#0066ff',
'ssid': 'transparent',
'station': '#ffcc33',
'access_point': '#ff9900',
'endpoint': '#33cc33',
'gateway': '#006600'
};
const typeColors = {
'ble_server': '#0066ff',
'ssid': '#ffff99',
'station': '#ffcc33',
'access_point': '#ff9900',
'endpoint': '#33cc33',
'gateway': '#006600'
};
var targetNode = null;
</script>
<script id="functions">
function renderNodeHTML(node) {
const nodeDiv = document.createElement('div');
switch (node.type) {
case 'ssid':
nodeDiv.innerHTML = `<small>${node.entity}</small>`
break;
case 'access_point':
var ap = node.entity;
nodeDiv.innerHTML = `
<center>
<b>${ap.hostname}</b> (${ap.encryption})
<br/>
${ap.mac}
${ap.vendor? '<br/>(' + ap.vendor + ')' : ''}
${ap.wps.length? '<br/>' + JSON.stringify(ap.wps) : ''}
</center>`;
break;
case 'station':
var sta = node.entity;
nodeDiv.innerHTML = `
<center>
${sta.mac}
${sta.vendor? '<br/>(' + sta.vendor + ')' : ''}
</center>`;
break;
case 'ble_server':
var dev = node.entity;
nodeDiv.innerHTML = `
<center>
${dev.mac}
${dev.vendor? '<br/>(' + dev.vendor + ')' : ''}
</center>`;
break;
case 'endpoint':
var ip = node.entity;
nodeDiv.innerHTML = `
<center>
${ip.hostname? '<b>' + ip.hostname + '</b><br/>' : ''}
${ip.ipv4? ip.ipv4 + '<br/>' : ''}
${ip.ipv6? ip.ipv6 + '<br/>' : ''}
<br/>
${ip.mac}
${ip.vendor? '<br/>(' + ip.vendor + ')' : ''}
${ip.meta.values.length? '<br/>' + JSON.stringify(ip.meta.values) : ''}
</center>`;
break;
case 'gateway':
var ip = node.entity;
nodeDiv.innerHTML = `
<center>
${ip.hostname? '<b>' + ip.hostname + '</b><br/>' : ''}
${ip.ipv4? ip.ipv4 + '<br/>' : ''}
${ip.ipv6? ip.ipv6 + '<br/>' : ''}
<br/>
${ip.mac}
${ip.vendor? '<br/>(' + ip.vendor + ')' : ''}
${ip.meta.values.length? '<br/>' + JSON.stringify(ip.meta.values) : ''}
</center>`;
break;
default:
nodeDiv.innerHTML = `<b>${node.id}</b>`
}
nodeDiv.style.color = typeColors[node.type];
nodeDiv.className = 'node-div';
const dataDiv = document.createElement('div');
dataDiv.id = 'datadiv_' + node.id;
dataDiv.className = 'node-data-div';
dataDiv.style.visibility = 'hidden';
dataDiv.style.display = 'none';
dataDiv.innerHTML = '<br/><pre>' + node.type + ' ' + JSON.stringify(node.entity, null, 2) + '</pre>';
nodeDiv.appendChild(dataDiv);
return new THREE.CSS2DObject(nodeDiv);
}
</script>
<script id="graph">
const Graph = ForceGraph3D({
extraRenderers: [new THREE.CSS2DRenderer()],
controlType: 'orbit'
})
(document.getElementById('3d-graph'))
.jsonUrl('bettergraph.json')
.nodeLabel('id')
.nodeColor(node => typeNodeColors[node.type])
.linkDirectionalArrowLength(3.5)
.linkDirectionalArrowRelPos(1)
/*
.linkThreeObjectExtend(true)
.linkThreeObject(link => {
const sprite = new SpriteText(link.edge.type);
sprite.color = 'lightgrey';
sprite.textHeight = 1.5;
return sprite;
})
.linkPositionUpdate((sprite, {start, end}) => {
const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({
[c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
})));
Object.assign(sprite.position, middlePos);
})
*/
.nodeThreeObject(renderNodeHTML)
.nodeThreeObjectExtend(true)
.onNodeClick(node => {
if( targetNode != null ) {
const prev = document.getElementById('datadiv_' + targetNode.id);
prev.style.visibility = 'hidden';
prev.style.display = 'none';
}
targetNode = node;
const curr = document.getElementById('datadiv_' + targetNode.id);
curr.style.visibility = 'visible';
curr.style.display = 'block';
// Aim at node from outside it
const distance = 40;
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
Graph.cameraPosition(
{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, // new position
node, // lookAt ({ x, y, z })
3000 // ms transition duration
);
});
</script>
</body>

View file

@ -127,8 +127,6 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
mod.viewSynScanEvent(output, e)
} else if e.Tag == "update.available" {
mod.viewUpdateEvent(output, e)
} else if strings.HasPrefix(e.Tag, "graph.") {
mod.viewGraphEvent(output, e)
} else if e.Tag == "gateway.change" {
mod.viewGatewayEvent(output, e)
} else if e.Tag != "tick" {

View file

@ -1,35 +0,0 @@
package events_stream
import (
"fmt"
"io"
"github.com/bettercap/bettercap/session"
"github.com/bettercap/bettercap/modules/graph"
"github.com/evilsocket/islazy/tui"
)
func (mod *EventsStream) viewGraphEvent(output io.Writer, e session.Event) {
if e.Tag == "graph.node.new" {
node := e.Data.(*graph.Node)
fmt.Fprintf(output, "[%s] [%s] %s %s\n",
e.Time.Format(mod.timeFormat),
tui.Green(e.Tag),
tui.Yellow(string(node.Type)),
node.ID)
} else if e.Tag == "graph.edge.new" {
data := e.Data.(graph.EdgeEvent)
fmt.Fprintf(output, "[%s] [%s] %s %s %s %s %s\n",
e.Time.Format(mod.timeFormat),
tui.Green(e.Tag),
tui.Dim(string(data.Left.Type)),
data.Left.ID,
tui.Bold(string(data.Edge.Type)),
tui.Dim(string(data.Right.Type)),
data.Right.ID)
}else {
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
}
}

View file

@ -1,184 +0,0 @@
package graph
import (
"fmt"
"github.com/bettercap/bettercap/network"
)
func (mod *Module) createIPGraph(endpoint *network.Endpoint) (*Node, bool, error) {
node, err := mod.db.FindNode(Endpoint, endpoint.HwAddress)
isNew := node == nil
if err != nil {
return nil, false, err
} else if isNew {
if node, err = mod.db.CreateNode(Endpoint, endpoint.HwAddress, endpoint, ""); err != nil {
return nil, false, err
}
} else {
if err = mod.db.UpdateNode(node); err != nil {
return nil, false, err
}
}
// create relations if needed
if manages, err := mod.db.FindLastRecentEdgeOfType(mod.gw, node, Manages, edgeStaleTime); err != nil {
return nil, false, err
} else if manages == nil {
if manages, err = mod.db.CreateEdge(mod.gw, node, Manages); err != nil {
return nil, false, err
}
}
if connects_to, err := mod.db.FindLastRecentEdgeOfType(node, mod.gw, ConnectsTo, edgeStaleTime); err != nil {
return nil, false, err
} else if connects_to == nil {
if connects_to, err = mod.db.CreateEdge(node, mod.gw, ConnectsTo); err != nil {
return nil, false, err
}
}
return node, isNew, nil
}
func (mod *Module) createDot11ApGraph(ap *network.AccessPoint) (*Node, bool, error) {
node, err := mod.db.FindNode(AccessPoint, ap.HwAddress)
isNew := node == nil
if err != nil {
return nil, false, err
} else if isNew {
if node, err = mod.db.CreateNode(AccessPoint, ap.HwAddress, ap, ""); err != nil {
return nil, false, err
}
} else if err = mod.db.UpdateNode(node); err != nil {
return nil, false, err
}
return node, isNew, nil
}
func (mod *Module) createDot11SSIDGraph(hex string, ssid string) (*Node, bool, error) {
node, err := mod.db.FindNode(SSID, hex)
isNew := node == nil
if err != nil {
return nil, false, err
} else if isNew {
if node, err = mod.db.CreateNode(SSID, hex, ssid, ""); err != nil {
return nil, false, err
}
} else if err = mod.db.UpdateNode(node); err != nil {
return nil, false, err
}
return node, isNew, nil
}
func (mod *Module) createDot11StaGraph(station *network.Station) (*Node, bool, error) {
node, err := mod.db.FindNode(Station, station.HwAddress)
isNew := node == nil
if err != nil {
return nil, false, err
} else if isNew {
if node, err = mod.db.CreateNode(Station, station.HwAddress, station, ""); err != nil {
return nil, false, err
}
} else if err = mod.db.UpdateNode(node); err != nil {
return nil, false, err
}
return node, isNew, nil
}
func (mod *Module) createDot11Graph(ap *network.AccessPoint, station *network.Station) (*Node, bool, *Node, bool, error) {
apNode, apIsNew, err := mod.createDot11ApGraph(ap)
if err != nil {
return nil, false, nil, false, err
}
staNode, staIsNew, err := mod.createDot11StaGraph(station)
if err != nil {
return nil, false, nil, false, err
}
// create relations if needed
if manages, err := mod.db.FindLastRecentEdgeOfType(apNode, staNode, Manages, edgeStaleTime); err != nil {
return nil, false, nil, false, err
} else if manages == nil {
if manages, err = mod.db.CreateEdge(apNode, staNode, Manages); err != nil {
return nil, false, nil, false, err
}
}
if connects_to, err := mod.db.FindLastRecentEdgeOfType(staNode, apNode, ConnectsTo, edgeStaleTime); err != nil {
return nil, false, nil, false, err
} else if connects_to == nil {
if connects_to, err = mod.db.CreateEdge(staNode, apNode, ConnectsTo); err != nil {
return nil, false, nil, false, err
}
}
return apNode, apIsNew, staNode, staIsNew, nil
}
func (mod *Module) createDot11ProbeGraph(ssid string, station *network.Station) (*Node, bool, *Node, bool, error) {
ssidNode, ssidIsNew, err := mod.createDot11SSIDGraph(fmt.Sprintf("%x", ssid), ssid)
if err != nil {
return nil, false, nil, false, err
}
staNode, staIsNew, err := mod.createDot11StaGraph(station)
if err != nil {
return nil, false, nil, false, err
}
// create relations if needed
if probes_for, err := mod.db.FindLastRecentEdgeOfType(staNode, ssidNode, ProbesFor, edgeStaleTime); err != nil {
return nil, false, nil, false, err
} else if probes_for == nil {
if probes_for, err = mod.db.CreateEdge(staNode, ssidNode, ProbesFor); err != nil {
return nil, false, nil, false, err
}
}
if probed_by, err := mod.db.FindLastRecentEdgeOfType(ssidNode, staNode, ProbedBy, edgeStaleTime); err != nil {
return nil, false, nil, false, err
} else if probed_by == nil {
if probed_by, err = mod.db.CreateEdge(ssidNode, staNode, ProbedBy); err != nil {
return nil, false, nil, false, err
}
}
return ssidNode, ssidIsNew, staNode, staIsNew, nil
}
func (mod *Module) createBLEServerGraph(dev *network.BLEDevice) (*Node, bool, error) {
mac := network.NormalizeMac(dev.Device.ID())
node, err := mod.db.FindNode(BLEServer, mac)
isNew := node == nil
if err != nil {
return nil, false, err
} else if isNew {
if node, err = mod.db.CreateNode(BLEServer, mac, dev, ""); err != nil {
return nil, false, err
}
} else if err = mod.db.UpdateNode(node); err != nil {
return nil, false, err
}
return node, isNew, nil
}
func (mod *Module) connectAsSame(a, b *Node) error {
if aIsB, err := mod.db.FindLastEdgeOfType(a, b, Is); err != nil {
return err
} else if aIsB == nil {
if aIsB, err = mod.db.CreateEdge(a, b, Is); err != nil {
return err
}
}
if bIsA, err := mod.db.FindLastEdgeOfType(b, a, Is); err != nil {
return err
} else if bIsA == nil {
if bIsA, err = mod.db.CreateEdge(b, a, Is); err != nil {
return err
}
}
return nil
}

View file

@ -1,42 +0,0 @@
package graph
import (
"fmt"
"github.com/bettercap/bettercap/session"
"time"
)
type EdgeType string
const (
Is EdgeType = "is"
ProbesFor EdgeType = "probes_for"
ProbedBy EdgeType = "probed_by"
ConnectsTo EdgeType = "connects_to"
Manages EdgeType = "manages"
)
type EdgeEvent struct {
Left *Node
Edge *Edge
Right *Node
}
type Edge struct {
Type EdgeType `json:"type"`
CreatedAt time.Time `json:"created_at"`
Position *session.GPS `json:"position,omitempty"`
}
func (e Edge) Dot(left, right *Node, width float64) string {
edgeLen := 1.0
if e.Type == Is {
edgeLen = 0.3
}
return fmt.Sprintf("\"%s\" -> \"%s\" [label=\"%s\", len=%.2f, penwidth=%.2f];",
left.String(),
right.String(),
e.Type,
edgeLen,
width)
}

View file

@ -1,158 +0,0 @@
package graph
import (
"encoding/json"
"github.com/evilsocket/islazy/fs"
"io/ioutil"
"os"
"path"
"sort"
"sync"
"time"
)
const edgesIndexName = "edges.json"
type EdgesTo map[string][]Edge
type EdgesCallback func(string, []Edge, string) error
type Edges struct {
sync.RWMutex
timestamp time.Time
fileName string
size int
from map[string]EdgesTo
}
type edgesJSON struct {
Timestamp time.Time `json:"timestamp"`
Size int `json:"size"`
Edges map[string]EdgesTo `json:"edges"`
}
func LoadEdges(basePath string) (*Edges, error) {
edges := Edges{
fileName: path.Join(basePath, edgesIndexName),
from: make(map[string]EdgesTo),
}
if fs.Exists(edges.fileName) {
var js edgesJSON
if raw, err := ioutil.ReadFile(edges.fileName); err != nil {
return nil, err
} else if err = json.Unmarshal(raw, &js); err != nil {
return nil, err
}
edges.timestamp = js.Timestamp
edges.from = js.Edges
edges.size = js.Size
}
return &edges, nil
}
func (e *Edges) flush() error {
e.timestamp = time.Now()
js := edgesJSON{
Timestamp: e.timestamp,
Size: e.size,
Edges: e.from,
}
if raw, err := json.Marshal(js); err != nil {
return err
} else if err = ioutil.WriteFile(e.fileName, raw, os.ModePerm); err != nil {
return err
}
return nil
}
func (e *Edges) Flush() error {
e.RLock()
defer e.RUnlock()
return e.flush()
}
func (e *Edges) ForEachEdge(cb EdgesCallback) error {
e.RLock()
defer e.RUnlock()
for from, edgesTo := range e.from {
for to, edges := range edgesTo {
if err := cb(from, edges, to); err != nil {
return err
}
}
}
return nil
}
func (e *Edges) ForEachEdgeFrom(nodeID string, cb EdgesCallback) error {
e.RLock()
defer e.RUnlock()
if edgesTo, found := e.from[nodeID]; found {
for to, edges := range edgesTo {
if err := cb(nodeID, edges, to); err != nil {
return err
}
}
}
return nil
}
func (e *Edges) IsConnected(nodeID string) bool {
e.RLock()
defer e.RUnlock()
if edgesTo, found := e.from[nodeID]; found {
return len(edgesTo) > 0
}
return false
}
func (e *Edges) FindEdges(fromID, toID string, doSort bool) []Edge {
e.RLock()
defer e.RUnlock()
if edgesTo, foundFrom := e.from[fromID]; foundFrom {
if edges, foundTo := edgesTo[toID]; foundTo {
if doSort {
// sort edges from oldest to newer
sort.Slice(edges, func(i, j int) bool {
return edges[i].CreatedAt.Before(edges[j].CreatedAt)
})
}
return edges
}
}
return nil
}
func (e *Edges) Connect(fromID, toID string, edge Edge) error {
e.Lock()
defer e.Unlock()
if edgesTo, foundFrom := e.from[fromID]; foundFrom {
edges := edgesTo[toID]
edges = append(edges, edge)
e.from[fromID][toID] = edges
} else {
// create the entire path
e.from[fromID] = EdgesTo{
toID: {edge},
}
}
e.size++
return e.flush()
}

View file

@ -1,425 +0,0 @@
package graph
import (
"encoding/json"
"fmt"
"github.com/bettercap/bettercap/session"
"github.com/evilsocket/islazy/fs"
"path"
"sync"
"time"
)
var Loaded = (* Graph)(nil)
type NodeCallback func(*Node)
type EdgeCallback func(*Node, []Edge, *Node)
type Graph struct {
sync.Mutex
path string
edges *Edges
}
func NewGraph(path string) (*Graph, error) {
if edges, err := LoadEdges(path); err != nil {
return nil, err
} else {
Loaded = &Graph{
path: path,
edges: edges,
}
return Loaded, nil
}
}
func (g *Graph) EachNode(cb NodeCallback) error {
g.Lock()
defer g.Unlock()
for _, nodeType := range NodeTypes {
err := fs.Glob(g.path, fmt.Sprintf("%s_*.json", nodeType), func(fileName string) error {
if node, err := ReadNode(fileName); err != nil {
return err
} else {
cb(node)
}
return nil
})
if err != nil {
return err
}
}
return nil
}
func (g *Graph) EachEdge(cb EdgeCallback) error {
g.Lock()
defer g.Unlock()
return g.edges.ForEachEdge(func(fromID string, edges []Edge, toID string) error {
var left, right *Node
var err error
leftFileName := path.Join(g.path, fromID+".json")
rightFileName := path.Join(g.path, toID+".json")
if left, err = ReadNode(leftFileName); err != nil {
return err
} else if right, err = ReadNode(rightFileName); err != nil {
return err
}
cb(left, edges, right)
return nil
})
}
func (g *Graph) Traverse(root string, onNode NodeCallback, onEdge EdgeCallback) error {
if root == "" {
// traverse the entire graph
if err := g.EachNode(onNode); err != nil {
return err
} else if err = g.EachEdge(onEdge); err != nil {
return err
}
} else {
// start by a specific node
roots, err := g.FindOtherTypes("", root)
if err != nil {
return err
}
stack := NewStack()
for _, root := range roots {
stack.Push(root)
}
type edgeBucket struct {
left *Node
edges []Edge
right *Node
}
allEdges := make([]edgeBucket, 0)
visited := make(map[string]bool)
for {
if last := stack.Pop(); last == nil {
break
} else {
node := last.(*Node)
nodeID := node.String()
if _, found := visited[nodeID]; found {
continue
} else {
visited[nodeID] = true
}
onNode(node)
// collect all edges starting from this node
err = g.edges.ForEachEdgeFrom(nodeID, func(_ string, edges []Edge, toID string) error {
rightFileName := path.Join(g.path, toID+".json")
if right, err := ReadNode(rightFileName); err != nil {
return err
} else {
// collect new node
if _, found := visited[toID]; !found {
stack.Push(right)
}
// collect all edges, we'll emit this later
allEdges = append(allEdges, edgeBucket{
left: node,
edges: edges,
right: right,
})
}
return nil
})
}
}
for _, edge := range allEdges {
onEdge(edge.left, edge.edges, edge.right)
}
}
return nil
}
func (g *Graph) IsConnected(nodeType string, nodeID string) bool {
return g.edges.IsConnected(fmt.Sprintf("%s_%s", nodeType, nodeID))
}
func (g *Graph) Dot(filter, layout, name string, disconnected bool) (string, int, int, error) {
size := 0
discarded := 0
data := fmt.Sprintf("digraph %s {\n", name)
data += fmt.Sprintf(" layout=%s\n", layout)
typeMap := make(map[NodeType]bool)
type typeCount struct {
edge Edge
count int
}
if err := g.Traverse(filter, func(node *Node) {
include := false
if disconnected {
include = true
} else {
include = g.edges.IsConnected(node.String())
}
if include {
size++
typeMap[node.Type] = true
data += fmt.Sprintf(" %s\n", node.Dot(filter == node.ID))
} else {
discarded++
}
}, func(left *Node, edges []Edge, right *Node) {
// collect counters by edge type in order to calculate proportional widths
byType := make(map[string]typeCount)
tot := len(edges)
for _, edge := range edges {
if c, found := byType[string(edge.Type)]; found {
c.count++
} else {
byType[string(edge.Type)] = typeCount{
edge: edge,
count: 1,
}
}
}
max := 2.0
for _, c := range byType {
w := max * float64(c.count/tot)
if w < 0.5 {
w = 0.5
}
data += fmt.Sprintf(" %s\n", c.edge.Dot(left, right, w))
}
}); err != nil {
return "", 0, 0, err
}
/*
data += "\n"
data += "node [style=filled height=0.55 fontname=\"Verdana\" fontsize=10];\n"
data += "subgraph legend {\n" +
"graph[style=dotted];\n" +
"label = \"Legend\";\n"
var types []NodeType
for nodeType, _ := range typeMap {
types = append(types, nodeType)
node := Node{
Type: nodeType,
Annotations: nodeTypeDescs[nodeType],
Dummy: true,
}
data += fmt.Sprintf(" %s\n", node.Dot(false))
}
ntypes := len(types)
for i := 0; i < ntypes - 1; i++ {
data += fmt.Sprintf(" \"%s\" -> \"%s\" [style=invis];\n", types[i], types[i + 1])
}
data += "}\n"
*/
data += "\n"
data += " overlap=false\n"
data += "}"
return data, size, discarded, nil
}
func (g *Graph) JSON(filter string, disconnected bool) (string, int, int, error) {
size := 0
discarded := 0
type link struct {
Source string `json:"source"`
Target string `json:"target"`
Edge interface{} `json:"edge"`
}
type data struct {
Nodes []map[string]interface{} `json:"nodes"`
Links []link `json:"links"`
}
jsData := data{
Nodes: make([]map[string]interface{}, 0),
Links: make([]link, 0),
}
if err := g.Traverse(filter, func(node *Node) {
include := false
if disconnected {
include = true
} else {
include = g.edges.IsConnected(node.String())
}
if include {
size++
if nm, err := node.ToMap(); err != nil {
panic(err)
} else {
// patch id
nm["id"] = node.String()
jsData.Nodes = append(jsData.Nodes, nm)
}
} else {
discarded++
}
}, func(left *Node, edges []Edge, right *Node) {
for _, edge := range edges {
jsData.Links = append(jsData.Links, link{
Source: left.String(),
Target: right.String(),
Edge: edge,
})
}
}); err != nil {
return "", 0, 0, err
}
if raw, err := json.Marshal(jsData); err != nil {
return "", 0, 0, err
} else {
return string(raw), size, discarded, nil
}
}
func (g *Graph) FindNode(t NodeType, id string) (*Node, error) {
g.Lock()
defer g.Unlock()
nodeFileName := path.Join(g.path, fmt.Sprintf("%s_%s.json", t, id))
if fs.Exists(nodeFileName) {
return ReadNode(nodeFileName)
}
return nil, nil
}
func (g *Graph) FindOtherTypes(t NodeType, id string) ([]*Node, error) {
g.Lock()
defer g.Unlock()
var otherNodes []*Node
for _, otherType := range NodeTypes {
if otherType != t {
if nodeFileName := path.Join(g.path, fmt.Sprintf("%s_%s.json", otherType, id)); fs.Exists(nodeFileName) {
if node, err := ReadNode(nodeFileName); err != nil {
return nil, err
} else {
otherNodes = append(otherNodes, node)
}
}
}
}
return otherNodes, nil
}
func (g *Graph) CreateNode(t NodeType, id string, entity interface{}, annotations string) (*Node, error) {
g.Lock()
defer g.Unlock()
node := &Node{
Type: t,
ID: id,
Entity: entity,
Annotations: annotations,
}
nodeFileName := path.Join(g.path, fmt.Sprintf("%s.json", node.String()))
if err := CreateNode(nodeFileName, node); err != nil {
return nil, err
}
session.I.Events.Add("graph.node.new", node)
return node, nil
}
func (g *Graph) UpdateNode(node *Node) error {
g.Lock()
defer g.Unlock()
nodeFileName := path.Join(g.path, fmt.Sprintf("%s.json", node.String()))
if err := UpdateNode(nodeFileName, node); err != nil {
return err
}
return nil
}
func (g *Graph) FindLastEdgeOfType(from, to *Node, edgeType EdgeType) (*Edge, error) {
edges := g.edges.FindEdges(from.String(), to.String(), true)
num := len(edges)
for i := range edges {
// loop backwards
idx := num - 1 - i
edge := edges[idx]
if edge.Type == edgeType {
return &edge, nil
}
}
return nil, nil
}
func (g *Graph) FindLastRecentEdgeOfType(from, to *Node, edgeType EdgeType, staleTime time.Duration) (*Edge, error) {
edges := g.edges.FindEdges(from.String(), to.String(), true)
num := len(edges)
for i := range edges {
// loop backwards
idx := num - 1 - i
edge := edges[idx]
if edge.Type == edgeType {
if time.Since(edge.CreatedAt) >= staleTime {
return nil, nil
}
return &edge, nil
}
}
return nil, nil
}
func (g *Graph) CreateEdge(from, to *Node, edgeType EdgeType) (*Edge, error) {
edge := Edge{
Type: edgeType,
CreatedAt: time.Now(),
}
if session.I.GPS.Updated.IsZero() == false {
edge.Position = &session.I.GPS
}
if err := g.edges.Connect(from.String(), to.String(), edge); err != nil {
return nil, err
}
session.I.Events.Add("graph.edge.new", EdgeEvent{
Left: from,
Edge: &edge,
Right: to,
})
return &edge, nil
}

View file

@ -1,15 +0,0 @@
package graph
import (
"github.com/bettercap/bettercap/log"
)
type graphPackage struct{}
func (g graphPackage) IsConnected(nodeType, nodeID string) bool {
if Loaded == nil {
log.Error("graph.IsConnected: graph not loaded")
return false
}
return Loaded.IsConnected(nodeType, nodeID)
}

View file

@ -1,348 +0,0 @@
package graph
import (
"github.com/bettercap/bettercap/caplets"
"github.com/bettercap/bettercap/modules/wifi"
"github.com/bettercap/bettercap/network"
"github.com/bettercap/bettercap/session"
"github.com/evilsocket/islazy/fs"
"github.com/evilsocket/islazy/plugin"
"github.com/evilsocket/islazy/str"
"os"
"path/filepath"
"regexp"
"sync"
"time"
)
const (
ifaceAnnotation = "<interface>"
edgeStaleTime = time.Hour * 24
)
var privacyFilter = regexp.MustCompile("(?i)([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2})")
type dotSettings struct {
layout string
name string
output string
}
type jsonSettings struct {
output string
}
type settings struct {
path string
dot dotSettings
json jsonSettings
disconnected bool
privacy bool
}
type Module struct {
session.SessionModule
settings settings
db *Graph
gw *Node
iface *Node
eventBus session.EventBus
wLock sync.Mutex
}
func init() {
plugin.Defines["graph"] = graphPackage{}
}
func NewModule(s *session.Session) *Module {
mod := &Module{
SessionModule: session.NewSessionModule("graph", s),
settings: settings{
path: filepath.Join(caplets.InstallBase, "graph"),
dot: dotSettings{
layout: "neato",
name: "bettergraph",
output: "bettergraph.dot",
},
json: jsonSettings{
output: "bettergraph.json",
},
},
}
mod.AddParam(session.NewStringParameter("graph.path",
mod.settings.path,
"",
"Base path for the graph database."))
mod.AddParam(session.NewStringParameter("graph.dot.name",
mod.settings.dot.name,
"",
"Graph name in the dot output."))
mod.AddParam(session.NewStringParameter("graph.dot.layout",
mod.settings.dot.layout,
"",
"Layout for dot output."))
mod.AddParam(session.NewStringParameter("graph.dot.output",
mod.settings.dot.output,
"",
"File name for dot output."))
mod.AddParam(session.NewStringParameter("graph.json.output",
mod.settings.json.output,
"",
"File name for JSON output."))
mod.AddParam(session.NewBoolParameter("graph.disconnected",
"false",
"Include disconnected edges in then output graph."))
mod.AddParam(session.NewBoolParameter("graph.privacy",
"false",
"Obfuscate mac addresses."))
mod.AddHandler(session.NewModuleHandler("graph on", "",
"Start the Module module.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("graph off", "",
"Stop the Module module.",
func(args []string) error {
return mod.Stop()
}))
mod.AddHandler(session.NewModuleHandler("graph.to_dot MAC?",
`graph\.to_dot\s*([^\s]*)`,
"Generate a dot graph file from the current graph.",
func(args []string) (err error) {
bssid := ""
if len(args) == 1 && args[0] != "" {
bssid = network.NormalizeMac(str.Trim(args[0]))
}
return mod.generateDotGraph(bssid)
}))
mod.AddHandler(session.NewModuleHandler("graph.to_json MAC?",
`graph\.to_json\s*([^\s]*)`,
"Generate a JSON graph file from the current graph.",
func(args []string) (err error) {
bssid := ""
if len(args) == 1 && args[0] != "" {
bssid = network.NormalizeMac(str.Trim(args[0]))
}
return mod.generateJSONGraph(bssid)
}))
return mod
}
func (mod *Module) Name() string {
return "graph"
}
func (mod *Module) Description() string {
return "A module to build a graph of WiFi and LAN nodes."
}
func (mod *Module) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *Module) updateSettings() error {
var err error
if err, mod.settings.dot.name = mod.StringParam("graph.dot.name"); err != nil {
return err
} else if err, mod.settings.dot.layout = mod.StringParam("graph.dot.layout"); err != nil {
return err
} else if err, mod.settings.dot.output = mod.StringParam("graph.dot.output"); err != nil {
return err
} else if err, mod.settings.json.output = mod.StringParam("graph.json.output"); err != nil {
return err
} else if err, mod.settings.disconnected = mod.BoolParam("graph.disconnected"); err != nil {
return err
} else if err, mod.settings.privacy = mod.BoolParam("graph.privacy"); err != nil {
return err
} else if err, mod.settings.path = mod.StringParam("graph.path"); err != nil {
return err
} else if mod.settings.path, err = filepath.Abs(mod.settings.path); err != nil {
return err
} else if !fs.Exists(mod.settings.path) {
if err = os.MkdirAll(mod.settings.path, os.ModePerm); err != nil {
return err
}
}
// only reload if needed
if mod.db != nil && mod.db.path != mod.settings.path {
mod.db = nil
}
if mod.db == nil {
if mod.db, err = NewGraph(mod.settings.path); err != nil {
return err
}
}
return nil
}
func (mod *Module) Configure() (err error) {
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if err = mod.updateSettings(); err != nil {
return err
}
// if have an IP
if mod.Session.Gateway != nil && mod.Session.Interface != nil {
// find or create interface node
iface := mod.Session.Interface
if mod.iface, err = mod.db.FindNode(Endpoint, iface.HwAddress); err != nil {
return err
} else if mod.iface == nil {
// create the interface node
if mod.iface, err = mod.db.CreateNode(Endpoint, iface.HwAddress, iface, ifaceAnnotation); err != nil {
return err
}
} else if err = mod.db.UpdateNode(mod.iface); err != nil {
return err
}
// find or create gateway node
gw := mod.Session.Gateway
if mod.gw, err = mod.db.FindNode(Gateway, gw.HwAddress); err != nil {
return err
} else if mod.gw == nil {
if mod.gw, err = mod.db.CreateNode(Gateway, gw.HwAddress, gw, ""); err != nil {
return err
}
} else {
if err = mod.db.UpdateNode(mod.gw); err != nil {
return err
}
}
// create relations if needed
if iface.HwAddress == gw.HwAddress {
if err = mod.connectAsSame(mod.gw, mod.iface); err != nil {
return err
}
} else {
if manages, err := mod.db.FindLastRecentEdgeOfType(mod.gw, mod.iface, Manages, edgeStaleTime); err != nil {
return err
} else if manages == nil {
if manages, err = mod.db.CreateEdge(mod.gw, mod.iface, Manages); err != nil {
return err
}
}
if connects_to, err := mod.db.FindLastEdgeOfType(mod.iface, mod.gw, ConnectsTo); err != nil {
return err
} else if connects_to == nil {
if connects_to, err = mod.db.CreateEdge(mod.iface, mod.gw, ConnectsTo); err != nil {
return err
}
}
}
}
mod.eventBus = mod.Session.Events.Listen()
return nil
}
func (mod *Module) onEvent(e session.Event) {
var entities []*Node
if e.Tag == "endpoint.new" {
endpoint := e.Data.(*network.Endpoint)
if entity, _, err := mod.createIPGraph(endpoint); err != nil {
mod.Error("%s", err)
} else {
entities = append(entities, entity)
}
} else if e.Tag == "wifi.ap.new" {
ap := e.Data.(*network.AccessPoint)
if entity, _, err := mod.createDot11ApGraph(ap); err != nil {
mod.Error("%s", err)
} else {
entities = append(entities, entity)
}
} else if e.Tag == "wifi.client.new" {
ce := e.Data.(wifi.ClientEvent)
if apEntity, _, staEntity, _, err := mod.createDot11Graph(ce.AP, ce.Client); err != nil {
mod.Error("%s", err)
} else {
entities = append(entities, apEntity)
entities = append(entities, staEntity)
}
} else if e.Tag == "wifi.client.probe" {
probe := e.Data.(wifi.ProbeEvent)
station := network.Station{
RSSI: probe.RSSI,
Endpoint: &network.Endpoint{
HwAddress: probe.FromAddr,
Vendor: probe.FromVendor,
Alias: probe.FromAlias,
},
}
if _, _, staEntity, _, err := mod.createDot11ProbeGraph(probe.SSID, &station); err != nil {
mod.Error("%s", err)
} else {
// don't add fake ap to entities, no need to correlate
entities = append(entities, staEntity)
}
} else if e.Tag == "ble.device.new" {
// surprisingly some devices, like DLink IPCams have BLE, Dot11 and LAN hardware address in common
dev := e.Data.(*network.BLEDevice)
if entity, _, err := mod.createBLEServerGraph(dev); err != nil {
mod.Error("%s", err)
} else {
entities = append(entities, entity)
}
}
// if there's at least an entity node, search for other nodes with the
// same mac address but different type and connect them as needed
for _, entity := range entities {
if others, err := mod.db.FindOtherTypes(entity.Type, entity.ID); err != nil {
mod.Error("%s", err)
} else if len(others) > 0 {
for _, other := range others {
if err = mod.connectAsSame(entity, other); err != nil {
mod.Error("%s", err)
}
}
}
}
}
func (mod *Module) Start() error {
if err := mod.Configure(); err != nil {
return err
}
return mod.SetRunning(true, func() {
mod.Info("started with database @ %s", mod.settings.path)
for mod.Running() {
select {
case e := <-mod.eventBus:
mod.onEvent(e)
}
}
})
}
func (mod *Module) Stop() error {
return mod.SetRunning(false, func() {
mod.Session.Events.Unlisten(mod.eventBus)
})
}

View file

@ -1,170 +0,0 @@
package graph
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"unicode"
)
type NodeType string
const (
SSID NodeType = "ssid"
BLEServer NodeType = "ble_server"
Station NodeType = "station"
AccessPoint NodeType = "access_point"
Endpoint NodeType = "endpoint"
Gateway NodeType = "gateway"
)
var NodeTypes = []NodeType{
SSID,
Station,
AccessPoint,
Endpoint,
Gateway,
BLEServer,
}
var nodeTypeDescs = map[NodeType]string{
SSID: "WiFI SSID probe",
BLEServer: "BLE Device",
Station: "WiFi Client",
AccessPoint: "WiFi AP",
Endpoint: "IP Client",
Gateway: "IP Gateway",
}
var nodeDotStyles = map[NodeType]string{
SSID: "shape=circle style=filled color=lightgray fillcolor=lightgray fixedsize=true penwidth=0.5",
BLEServer: "shape=box style=filled color=dodgerblue3",
Endpoint: "shape=box style=filled color=azure2",
Gateway: "shape=diamond style=filled color=azure4",
Station: "shape=box style=filled color=gold",
AccessPoint: "shape=diamond style=filled color=goldenrod3",
}
type Node struct {
Type NodeType `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Annotations string `json:"annotations"`
Entity interface{} `json:"entity"`
Dummy bool `json:"-"`
}
func ReadNode(fileName string) (*Node, error) {
var node Node
if raw, err := ioutil.ReadFile(fileName); err != nil {
return nil, fmt.Errorf("error while reading %s: %v", fileName, err)
} else if err = json.Unmarshal(raw, &node); err != nil {
return nil, fmt.Errorf("error while decoding %s: %v", fileName, err)
}
return &node, nil
}
func WriteNode(fileName string, node *Node, update bool) error {
if update {
node.UpdatedAt = time.Now()
} else {
node.CreatedAt = time.Now()
}
if raw, err := json.Marshal(node); err != nil {
return fmt.Errorf("error creating data for %s: %v", fileName, err)
} else if err = ioutil.WriteFile(fileName, raw, os.ModePerm); err != nil {
return fmt.Errorf("error creating %s: %v", fileName, err)
}
return nil
}
func CreateNode(fileName string, node *Node) error {
return WriteNode(fileName, node, false)
}
func UpdateNode(fileName string, node *Node) error {
return WriteNode(fileName, node, true)
}
func (n Node) String() string {
if n.Dummy == false {
return fmt.Sprintf("%s_%s", n.Type, n.ID)
}
return string(n.Type)
}
func (n Node) Label() string {
if n.Dummy {
return n.Annotations
}
switch n.Type {
case SSID:
s := n.Entity.(string)
allPrint := true
for _, rn := range s {
if !unicode.IsPrint(rune(rn)) {
allPrint = false
break
}
}
if !allPrint {
s = fmt.Sprintf("0x%x", s)
}
return s
case BLEServer:
return fmt.Sprintf("%s\\n(%s)",
n.Entity.(map[string]interface{})["mac"].(string),
n.Entity.(map[string]interface{})["vendor"].(string))
case Station:
return fmt.Sprintf("%s\\n(%s)",
n.Entity.(map[string]interface{})["mac"].(string),
n.Entity.(map[string]interface{})["vendor"].(string))
case AccessPoint:
return fmt.Sprintf("%s\\n%s\\n(%s)",
n.Entity.(map[string]interface{})["hostname"].(string),
n.Entity.(map[string]interface{})["mac"].(string),
n.Entity.(map[string]interface{})["vendor"].(string))
case Endpoint:
return fmt.Sprintf("%s\\n(%s %s)",
n.Entity.(map[string]interface{})["ipv4"].(string),
n.Entity.(map[string]interface{})["mac"].(string),
n.Entity.(map[string]interface{})["vendor"].(string))
case Gateway:
return fmt.Sprintf("%s\\n(%s %s)",
n.Entity.(map[string]interface{})["ipv4"].(string),
n.Entity.(map[string]interface{})["mac"].(string),
n.Entity.(map[string]interface{})["vendor"].(string))
}
return "?"
}
func (n Node) Dot(isTarget bool) string {
style := nodeDotStyles[n.Type]
if isTarget {
style += ", color=red"
}
return fmt.Sprintf("\"%s\" [%s, label=\"%s\"];",
n.String(),
style,
strings.ReplaceAll(n.Label(), "\"", "\\\""))
}
func (n Node) ToMap() (map[string]interface{}, error) {
var m map[string]interface{}
if raw, err := json.Marshal(n); err != nil {
return nil, err
} else if err = json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return m, nil
}

View file

@ -1,47 +0,0 @@
package graph
import "sync"
type entry struct {
data interface{}
next *entry
}
type Stack struct {
lock *sync.Mutex
head *entry
Size int
}
func (stk *Stack) Push(data interface{}) {
stk.lock.Lock()
element := new(entry)
element.data = data
temp := stk.head
element.next = temp
stk.head = element
stk.Size++
stk.lock.Unlock()
}
func (stk *Stack) Pop() interface{} {
if stk.head == nil {
return nil
}
stk.lock.Lock()
r := stk.head.data
stk.head = stk.head.next
stk.Size--
stk.lock.Unlock()
return r
}
func NewStack() *Stack {
stk := new(Stack)
stk.lock = &sync.Mutex{}
return stk
}

View file

@ -1,45 +0,0 @@
package graph
import (
"io/ioutil"
"os"
"time"
)
func (mod *Module) generateDotGraph(bssid string) error {
mod.wLock.Lock()
defer mod.wLock.Unlock()
start := time.Now()
if err := mod.updateSettings(); err != nil {
return err
}
data, size, discarded, err := mod.db.Dot(bssid,
mod.settings.dot.layout,
mod.settings.dot.name,
mod.settings.disconnected)
if err != nil {
return err
}
if size > 0 {
if mod.settings.privacy {
data = privacyFilter.ReplaceAllString(data, "$1:$2:xx:xx:xx:xx")
}
if err := ioutil.WriteFile(mod.settings.dot.output, []byte(data), os.ModePerm); err != nil {
return err
} else {
mod.Info("graph saved to %s in %v (%d edges, %d discarded)",
mod.settings.dot.output,
time.Since(start),
size,
discarded)
}
} else {
mod.Info("graph is empty")
}
return nil
}

View file

@ -1,43 +0,0 @@
package graph
import (
"io/ioutil"
"os"
"time"
)
func (mod *Module) generateJSONGraph(bssid string) error {
mod.wLock.Lock()
defer mod.wLock.Unlock()
start := time.Now()
if err := mod.updateSettings(); err != nil {
return err
}
data, size, discarded, err := mod.db.JSON(bssid, mod.settings.disconnected)
if err != nil {
return err
}
if size > 0 {
if mod.settings.privacy {
data = privacyFilter.ReplaceAllString(data, "$1:$2:xx:xx:xx:xx")
}
if err := ioutil.WriteFile(mod.settings.json.output, []byte(data), os.ModePerm); err != nil {
return err
} else {
mod.Info("graph saved to %s in %v (%d edges, %d discarded)",
mod.settings.json.output,
time.Since(start),
size,
discarded)
}
} else {
mod.Info("graph is empty")
}
return nil
}

View file

@ -11,7 +11,6 @@ import (
"github.com/bettercap/bettercap/modules/dns_spoof"
"github.com/bettercap/bettercap/modules/events_stream"
"github.com/bettercap/bettercap/modules/gps"
"github.com/bettercap/bettercap/modules/graph"
"github.com/bettercap/bettercap/modules/hid"
"github.com/bettercap/bettercap/modules/http_proxy"
"github.com/bettercap/bettercap/modules/http_server"
@ -64,7 +63,6 @@ func LoadModules(sess *session.Session) {
sess.Register(hid.NewHIDRecon(sess))
sess.Register(c2.NewC2(sess))
sess.Register(ndp_spoof.NewNDPSpoofer(sess))
sess.Register(graph.NewModule(sess))
sess.Register(caplets.NewCapletsModule(sess))
sess.Register(update.NewUpdateModule(sess))