mirror of
https://github.com/bettercap/bettercap
synced 2025-08-20 21:43:18 -07:00
new: new can module for CAN-bus
This commit is contained in:
parent
9937e797ae
commit
5fe3ef3d52
12 changed files with 755 additions and 3 deletions
84
modules/can/can.go
Normal file
84
modules/can/can.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"go.einride.tech/can/pkg/descriptor"
|
||||
"go.einride.tech/can/pkg/socketcan"
|
||||
)
|
||||
|
||||
type CANModule struct {
|
||||
session.SessionModule
|
||||
|
||||
deviceName string
|
||||
transport string
|
||||
dbcPath string
|
||||
dbc *descriptor.Database
|
||||
|
||||
conn net.Conn
|
||||
recv *socketcan.Receiver
|
||||
}
|
||||
|
||||
func NewCanModule(s *session.Session) *CANModule {
|
||||
mod := &CANModule{
|
||||
SessionModule: session.NewSessionModule("can", s),
|
||||
dbcPath: "",
|
||||
transport: "can",
|
||||
deviceName: "can0",
|
||||
}
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.device",
|
||||
mod.deviceName,
|
||||
"",
|
||||
"CAN-bus device."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.transport",
|
||||
mod.transport,
|
||||
"",
|
||||
"Network type, can be 'can' for SocketCAN or 'udp'."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.dbc_path",
|
||||
mod.dbcPath,
|
||||
"",
|
||||
"Optional path to DBC file for decoding."))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.recon on", "",
|
||||
"Start CAN-bus discovery.",
|
||||
func(args []string) error {
|
||||
return mod.Start()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.recon off", "",
|
||||
"Stop CAN-bus discovery.",
|
||||
func(args []string) error {
|
||||
return mod.Stop()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.clear", "",
|
||||
"Clear everything collected by the discovery module.",
|
||||
func(args []string) error {
|
||||
mod.Session.CAN.Clear()
|
||||
return nil
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.show", "",
|
||||
"Show a list of detected CAN devices.",
|
||||
func(args []string) error {
|
||||
return mod.Show()
|
||||
}))
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (mod *CANModule) Name() string {
|
||||
return "can"
|
||||
}
|
||||
|
||||
func (mod *CANModule) Description() string {
|
||||
return "A scanner and frames injection module for CAN-bus."
|
||||
}
|
||||
|
||||
func (mod *CANModule) Author() string {
|
||||
return "Simone Margaritelli <evilsocket@gmail.com>"
|
||||
}
|
131
modules/can/can_recon.go
Normal file
131
modules/can/can_recon.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"go.einride.tech/can"
|
||||
"go.einride.tech/can/pkg/socketcan"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Frame can.Frame
|
||||
Name string
|
||||
Source *network.CANDevice
|
||||
Signals map[string]string
|
||||
}
|
||||
|
||||
func (mod *CANModule) Configure() error {
|
||||
var err error
|
||||
|
||||
if mod.Running() {
|
||||
return session.ErrAlreadyStarted(mod.Name())
|
||||
} else if err, mod.deviceName = mod.StringParam("can.device"); err != nil {
|
||||
return err
|
||||
} else if err, mod.transport = mod.StringParam("can.transport"); err != nil {
|
||||
return err
|
||||
} else if mod.transport != "can" && mod.transport != "udp" {
|
||||
return errors.New("invalid transport")
|
||||
} else if err, mod.dbcPath = mod.StringParam("can.dbc_path"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mod.dbcPath != "" {
|
||||
input, err := os.ReadFile(mod.dbcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read %s: %v", mod.dbcPath, err)
|
||||
}
|
||||
|
||||
mod.Info("compiling %s ...", mod.dbcPath)
|
||||
|
||||
result, err := dbcCompile(mod.dbcPath, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't compile %s: %v", mod.dbcPath, err)
|
||||
}
|
||||
|
||||
for _, warning := range result.Warnings {
|
||||
mod.Warning("%v", warning)
|
||||
}
|
||||
|
||||
mod.dbc = result.Database
|
||||
} else {
|
||||
mod.Warning("no can.dbc_path specified, messages won't be parsed")
|
||||
}
|
||||
|
||||
if mod.conn, err = socketcan.DialContext(context.Background(), mod.transport, mod.deviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.recv = socketcan.NewReceiver(mod.conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *CANModule) Start() error {
|
||||
if err := mod.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mod.SetRunning(true, func() {
|
||||
mod.Info("started on %s ...", mod.deviceName)
|
||||
|
||||
for mod.recv.Receive() {
|
||||
frame := mod.recv.Frame()
|
||||
msg := Message{
|
||||
Frame: frame,
|
||||
}
|
||||
|
||||
if mod.dbc != nil {
|
||||
if message, found := mod.dbc.Message(frame.ID); found {
|
||||
msg.Name = message.Name
|
||||
|
||||
sourceName := message.SenderNode
|
||||
sourceDesc := ""
|
||||
if sender, found := mod.dbc.Node(message.SenderNode); found {
|
||||
sourceName = sender.Name
|
||||
sourceDesc = sender.Description
|
||||
}
|
||||
|
||||
_, msg.Source = mod.Session.CAN.AddIfNew(sourceName, sourceDesc, frame.Data[:])
|
||||
|
||||
msg.Signals = make(map[string]string)
|
||||
|
||||
for _, signal := range message.Signals {
|
||||
var value string
|
||||
|
||||
if signal.Length <= 32 && signal.IsFloat {
|
||||
value = fmt.Sprintf("%f", signal.UnmarshalFloat(frame.Data))
|
||||
} else if signal.Length == 1 {
|
||||
value = fmt.Sprintf("%v", signal.UnmarshalBool(frame.Data))
|
||||
} else if signal.IsSigned {
|
||||
value = fmt.Sprintf("%d", signal.UnmarshalSigned(frame.Data))
|
||||
} else {
|
||||
value = fmt.Sprintf("%d", signal.UnmarshalUnsigned(frame.Data))
|
||||
}
|
||||
|
||||
msg.Signals[signal.Name] = str.Trim(fmt.Sprintf("%s %s", value, signal.Unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod.Session.Events.Add("can.message", msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (mod *CANModule) Stop() error {
|
||||
if mod.conn != nil {
|
||||
mod.recv.Close()
|
||||
mod.conn.Close()
|
||||
mod.conn = nil
|
||||
mod.recv = nil
|
||||
mod.dbc = nil
|
||||
mod.dbcPath = ""
|
||||
}
|
||||
return nil
|
||||
}
|
50
modules/can/can_show.go
Normal file
50
modules/can/can_show.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
AliveTimeInterval = time.Duration(5) * time.Minute
|
||||
PresentTimeInterval = time.Duration(1) * time.Minute
|
||||
JustJoinedTimeInterval = time.Duration(10) * time.Second
|
||||
)
|
||||
|
||||
func (mod *CANModule) getRow(dev *network.CANDevice) []string {
|
||||
sinceLastSeen := time.Since(dev.LastSeen)
|
||||
seen := dev.LastSeen.Format("15:04:05")
|
||||
|
||||
if sinceLastSeen <= JustJoinedTimeInterval {
|
||||
seen = tui.Bold(seen)
|
||||
} else if sinceLastSeen > PresentTimeInterval {
|
||||
seen = tui.Dim(seen)
|
||||
}
|
||||
|
||||
return []string{
|
||||
dev.Name,
|
||||
dev.Description,
|
||||
humanize.Bytes(dev.Read),
|
||||
seen,
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *CANModule) Show() (err error) {
|
||||
devices := mod.Session.CAN.Devices()
|
||||
|
||||
rows := make([][]string, 0)
|
||||
for _, dev := range devices {
|
||||
rows = append(rows, mod.getRow(dev))
|
||||
}
|
||||
|
||||
tui.Table(mod.Session.Events.Stdout, []string{"Name", "Description", "Data", "Seen"}, rows)
|
||||
|
||||
if len(rows) > 0 {
|
||||
mod.Session.Refresh()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
227
modules/can/dbc.go
Normal file
227
modules/can/dbc.go
Normal file
|
@ -0,0 +1,227 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"go.einride.tech/can/pkg/dbc"
|
||||
"go.einride.tech/can/pkg/descriptor"
|
||||
)
|
||||
|
||||
type CompileResult struct {
|
||||
Database *descriptor.Database
|
||||
Warnings []error
|
||||
}
|
||||
|
||||
func dbcCompile(sourceFile string, data []byte) (result *CompileResult, err error) {
|
||||
p := dbc.NewParser(sourceFile, data)
|
||||
if err := p.Parse(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse DBC source file: %w", err)
|
||||
}
|
||||
defs := p.Defs()
|
||||
c := &compiler{
|
||||
db: &descriptor.Database{SourceFile: sourceFile},
|
||||
defs: defs,
|
||||
}
|
||||
c.collectDescriptors()
|
||||
c.addMetadata()
|
||||
c.sortDescriptors()
|
||||
return &CompileResult{Database: c.db, Warnings: c.warnings}, nil
|
||||
}
|
||||
|
||||
type compileError struct {
|
||||
def dbc.Def
|
||||
reason string
|
||||
}
|
||||
|
||||
func (e *compileError) Error() string {
|
||||
return fmt.Sprintf("failed to compile: %v (%v)", e.reason, e.def)
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
db *descriptor.Database
|
||||
defs []dbc.Def
|
||||
warnings []error
|
||||
}
|
||||
|
||||
func (c *compiler) addWarning(warning error) {
|
||||
c.warnings = append(c.warnings, warning)
|
||||
}
|
||||
|
||||
func (c *compiler) collectDescriptors() {
|
||||
for _, def := range c.defs {
|
||||
switch def := def.(type) {
|
||||
case *dbc.VersionDef:
|
||||
c.db.Version = def.Version
|
||||
case *dbc.MessageDef:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
message := &descriptor.Message{
|
||||
Name: string(def.Name),
|
||||
ID: def.MessageID.ToCAN(),
|
||||
IsExtended: def.MessageID.IsExtended(),
|
||||
Length: uint8(def.Size),
|
||||
SenderNode: string(def.Transmitter),
|
||||
}
|
||||
for _, signalDef := range def.Signals {
|
||||
signal := &descriptor.Signal{
|
||||
Name: string(signalDef.Name),
|
||||
IsBigEndian: signalDef.IsBigEndian,
|
||||
IsSigned: signalDef.IsSigned,
|
||||
IsMultiplexer: signalDef.IsMultiplexerSwitch,
|
||||
IsMultiplexed: signalDef.IsMultiplexed,
|
||||
MultiplexerValue: uint(signalDef.MultiplexerSwitch),
|
||||
Start: uint8(signalDef.StartBit),
|
||||
Length: uint8(signalDef.Size),
|
||||
Scale: signalDef.Factor,
|
||||
Offset: signalDef.Offset,
|
||||
Min: signalDef.Minimum,
|
||||
Max: signalDef.Maximum,
|
||||
Unit: signalDef.Unit,
|
||||
}
|
||||
for _, receiver := range signalDef.Receivers {
|
||||
signal.ReceiverNodes = append(signal.ReceiverNodes, string(receiver))
|
||||
}
|
||||
message.Signals = append(message.Signals, signal)
|
||||
}
|
||||
c.db.Messages = append(c.db.Messages, message)
|
||||
case *dbc.NodesDef:
|
||||
for _, node := range def.NodeNames {
|
||||
c.db.Nodes = append(c.db.Nodes, &descriptor.Node{Name: string(node)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) addMetadata() {
|
||||
for _, def := range c.defs {
|
||||
switch def := def.(type) {
|
||||
case *dbc.SignalValueTypeDef:
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
switch def.SignalValueType {
|
||||
case dbc.SignalValueTypeInt:
|
||||
signal.IsFloat = false
|
||||
case dbc.SignalValueTypeFloat32:
|
||||
if signal.Length == 32 {
|
||||
signal.IsFloat = true
|
||||
} else {
|
||||
reason := fmt.Sprintf("incorrect float signal length: %d", signal.Length)
|
||||
c.addWarning(&compileError{def: def, reason: reason})
|
||||
}
|
||||
default:
|
||||
reason := fmt.Sprintf("unsupported signal value type: %v", def.SignalValueType)
|
||||
c.addWarning(&compileError{def: def, reason: reason})
|
||||
}
|
||||
case *dbc.CommentDef:
|
||||
switch def.ObjectType {
|
||||
case dbc.ObjectTypeMessage:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
message, ok := c.db.Message(def.MessageID.ToCAN())
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared message"})
|
||||
continue
|
||||
}
|
||||
message.Description = def.Comment
|
||||
case dbc.ObjectTypeSignal:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
signal.Description = def.Comment
|
||||
case dbc.ObjectTypeNetworkNode:
|
||||
node, ok := c.db.Node(string(def.NodeName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared node"})
|
||||
continue
|
||||
}
|
||||
node.Description = def.Comment
|
||||
}
|
||||
case *dbc.ValueDescriptionsDef:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
if def.ObjectType != dbc.ObjectTypeSignal {
|
||||
continue // don't compile
|
||||
}
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
for _, valueDescription := range def.ValueDescriptions {
|
||||
signal.ValueDescriptions = append(signal.ValueDescriptions, &descriptor.ValueDescription{
|
||||
Description: valueDescription.Description,
|
||||
Value: int64(valueDescription.Value),
|
||||
})
|
||||
}
|
||||
case *dbc.AttributeValueForObjectDef:
|
||||
switch def.ObjectType {
|
||||
case dbc.ObjectTypeMessage:
|
||||
msg, ok := c.db.Message(def.MessageID.ToCAN())
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared message"})
|
||||
continue
|
||||
}
|
||||
switch def.AttributeName {
|
||||
case "GenMsgSendType":
|
||||
if err := msg.SendType.UnmarshalString(def.StringValue); err != nil {
|
||||
c.addWarning(&compileError{def: def, reason: err.Error()})
|
||||
continue
|
||||
}
|
||||
case "GenMsgCycleTime":
|
||||
msg.CycleTime = time.Duration(def.IntValue) * time.Millisecond
|
||||
case "GenMsgDelayTime":
|
||||
msg.DelayTime = time.Duration(def.IntValue) * time.Millisecond
|
||||
}
|
||||
case dbc.ObjectTypeSignal:
|
||||
sig, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
}
|
||||
if def.AttributeName == "GenSigStartValue" {
|
||||
sig.DefaultValue = int(def.IntValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) sortDescriptors() {
|
||||
// Sort nodes by name
|
||||
sort.Slice(c.db.Nodes, func(i, j int) bool {
|
||||
return c.db.Nodes[i].Name < c.db.Nodes[j].Name
|
||||
})
|
||||
// Sort messages by ID
|
||||
sort.Slice(c.db.Messages, func(i, j int) bool {
|
||||
return c.db.Messages[i].ID < c.db.Messages[j].ID
|
||||
})
|
||||
for _, m := range c.db.Messages {
|
||||
m := m
|
||||
// Sort signals by start (and multiplexer value)
|
||||
sort.Slice(m.Signals, func(j, k int) bool {
|
||||
if m.Signals[j].MultiplexerValue < m.Signals[k].MultiplexerValue {
|
||||
return true
|
||||
}
|
||||
return m.Signals[j].Start < m.Signals[k].Start
|
||||
})
|
||||
// Sort value descriptions by value
|
||||
for _, s := range m.Signals {
|
||||
s := s
|
||||
sort.Slice(s.ValueDescriptions, func(k, l int) bool {
|
||||
return s.ValueDescriptions[k].Value < s.ValueDescriptions[l].Value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,6 +120,8 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
|
|||
mod.viewBLEEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "hid.") {
|
||||
mod.viewHIDEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "can.") {
|
||||
mod.viewCANEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "gps.") {
|
||||
mod.viewGPSEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "mod.") {
|
||||
|
|
51
modules/events_stream/events_view_can.go
Normal file
51
modules/events_stream/events_view_can.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package events_stream
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/modules/can"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
func (mod *EventsStream) viewCANEvent(output io.Writer, e session.Event) {
|
||||
if e.Tag == "can.device.new" {
|
||||
dev := e.Data.(*network.CANDevice)
|
||||
fmt.Fprintf(output, "[%s] [%s] new CAN device %s (%s) detected.\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
tui.Bold(dev.Name),
|
||||
tui.Dim(dev.Description))
|
||||
} else if e.Tag == "can.message" {
|
||||
msg := e.Data.(can.Message)
|
||||
|
||||
// unparsed
|
||||
if msg.Name == "" {
|
||||
fmt.Fprintf(output, "[%s] [%s] <id %d> (%s): %s\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
msg.Frame.ID,
|
||||
tui.Dim(humanize.Bytes(uint64(msg.Frame.Length))),
|
||||
hex.EncodeToString(msg.Frame.Data[:msg.Frame.Length]))
|
||||
} else {
|
||||
fmt.Fprintf(output, "[%s] [%s] <id %d> %s (%s) from %s:\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
msg.Frame.ID,
|
||||
msg.Name,
|
||||
tui.Dim(humanize.Bytes(uint64(msg.Frame.Length))),
|
||||
tui.Bold(msg.Source.Name))
|
||||
|
||||
for name, value := range msg.Signals {
|
||||
fmt.Fprintf(output, " %s : %s\n", name, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/bettercap/bettercap/modules/arp_spoof"
|
||||
"github.com/bettercap/bettercap/modules/ble"
|
||||
"github.com/bettercap/bettercap/modules/c2"
|
||||
"github.com/bettercap/bettercap/modules/can"
|
||||
"github.com/bettercap/bettercap/modules/caplets"
|
||||
"github.com/bettercap/bettercap/modules/dhcp6_spoof"
|
||||
"github.com/bettercap/bettercap/modules/dns_spoof"
|
||||
|
@ -41,6 +42,7 @@ func LoadModules(sess *session.Session) {
|
|||
sess.Register(arp_spoof.NewArpSpoofer(sess))
|
||||
sess.Register(api_rest.NewRestAPI(sess))
|
||||
sess.Register(ble.NewBLERecon(sess))
|
||||
sess.Register(can.NewCanModule(sess))
|
||||
sess.Register(dhcp6_spoof.NewDHCP6Spoofer(sess))
|
||||
sess.Register(net_recon.NewDiscovery(sess))
|
||||
sess.Register(dns_spoof.NewDNSSpoofer(sess))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue