new: new can module for CAN-bus

This commit is contained in:
evilsocket 2024-08-09 15:42:03 +02:00
commit 5fe3ef3d52
12 changed files with 755 additions and 3 deletions

84
modules/can/can.go Normal file
View 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
View 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
View 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
View 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
})
}
}
}

View file

@ -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.") {

View 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)
}
}

View file

@ -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))