Skip to content

Commit

Permalink
Implement ARP / SYN scanner
Browse files Browse the repository at this point in the history
This is a much faster and better implementation of a pure go scanner.
It uses gopacket to perform arp scanning to find devices on the network
along with their mac addresses. It then performs a SYN scan on those
found devices to detect when port 22 is open. Caveats are that this
requires ops to be run with root privileges and has a dependency on
libpcap.
  • Loading branch information
robgonnella committed Aug 8, 2023
1 parent 7fe6305 commit 6fd3b7f
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 161 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ jobs:
with:
go-version: '1.19'

- name: Install make
run: sudo apt update && sudo apt install -y make
- name: Install dependencies
run: sudo apt update && sudo apt install -y make libpcap-dev

- name: Build
run: make
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/apenella/go-ansible v1.1.7
github.com/gdamore/tcell/v2 v2.6.0
github.com/golang/mock v1.6.0
github.com/imdario/mergo v0.3.16
github.com/google/gopacket v1.1.19
github.com/jackpal/gateway v1.0.10
github.com/projectdiscovery/mapcidr v1.1.2
github.com/rivo/tview v0.0.0-20230621164836-6cc0565babaf
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
Expand Down Expand Up @@ -203,8 +205,6 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
6 changes: 4 additions & 2 deletions internal/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestCore(t *testing.T) {
mockDetailsScanner := mock_discovery.NewMockDetailScanner(ctrl)
mockConfig := mock_config.NewMockService(ctrl)
mockServerService := mock_server.NewMockService(ctrl)
resultChan := make(chan *discovery.DiscoveryResult)

networkInfo := &util.NetworkInfo{
Hostname: "hostname",
Expand All @@ -41,6 +42,7 @@ func TestCore(t *testing.T) {
mockScanner,
mockDetailsScanner,
mockServerService,
resultChan,
)

conf := config.Config{
Expand Down Expand Up @@ -229,11 +231,11 @@ func TestCore(t *testing.T) {
Do(func([]string) {
wg.Done()
})
mockScanner.EXPECT().Scan(gomock.Any()).DoAndReturn(func(rchan chan *discovery.DiscoveryResult) error {
mockScanner.EXPECT().Scan().DoAndReturn(func() error {
defer wg.Done()
go func() {
for _, r := range discoveryResults {
rchan <- r
resultChan <- r
}
}()
return nil
Expand Down
9 changes: 8 additions & 1 deletion internal/core/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ func CreateNewAppCore(networkInfo *util.NetworkInfo) (*Core, error) {
serverRepo := server.NewSqliteRepo(db)
serverService := server.NewService(*conf, serverRepo)

netScanner, err := discovery.NewNetScanner(conf.Targets)
resultChan := make(chan *discovery.DiscoveryResult)

netScanner, err := discovery.NewARPScanner(
networkInfo,
conf.Targets,
resultChan,
)

if err != nil {
return nil, err
Expand All @@ -95,6 +101,7 @@ func CreateNewAppCore(networkInfo *util.NetworkInfo) (*Core, error) {
netScanner,
detailScanner,
serverService,
resultChan,
)

return New(
Expand Down
275 changes: 275 additions & 0 deletions internal/discovery/arpscan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package discovery

import (
"bytes"
"context"
"net"
"regexp"

"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"github.com/projectdiscovery/mapcidr"
"github.com/robgonnella/ops/internal/logger"
"github.com/robgonnella/ops/internal/server"
"github.com/robgonnella/ops/internal/util"
"github.com/rs/zerolog/log"
)

var cidrSuffix = regexp.MustCompile(`\/\d{2}$`)

type ARPScanner struct {
ctx context.Context
cancel context.CancelFunc
targets []string
networkInfo *util.NetworkInfo
handle *pcap.Handle
packetSource *gopacket.PacketSource
arpMap map[string]net.HardwareAddr
resultChan chan *DiscoveryResult
log logger.Logger
}

func NewARPScanner(
networkInfo *util.NetworkInfo,
targets []string,
resultChan chan *DiscoveryResult,
) (*ARPScanner, error) {
ipList := []string{}

for _, t := range targets {
if cidrSuffix.MatchString(t) {
ips, err := mapcidr.IPAddresses(t)

if err != nil {
return nil, err
}

ipList = append(ipList, ips...)
} else {
ipList = append(ipList, t)
}
}

// Open up a pcap handle for packet reads/writes.
handle, err := pcap.OpenLive(
networkInfo.Interface.Name,
65536,
true,
pcap.BlockForever,
)

if err != nil {
return nil, err
}

src := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet)

ctx, cancel := context.WithCancel(context.Background())

scanner := &ARPScanner{
ctx: ctx,
cancel: cancel,
targets: ipList,
handle: handle,
packetSource: src,
networkInfo: networkInfo,
arpMap: map[string]net.HardwareAddr{},
resultChan: resultChan,
log: logger.New(),
}

go scanner.readPackets()

return scanner, nil
}

func (s *ARPScanner) Scan() error {
return s.writeARP()
}

func (s *ARPScanner) Stop() {
s.cancel()
s.handle.Close()
}

func (s *ARPScanner) readPackets() {
for {
select {
case <-s.ctx.Done():
return
case packet := <-s.packetSource.Packets():
arpLayer := packet.Layer(layers.LayerTypeARP)

if arpLayer != nil {
s.handleARPLayer(arpLayer.(*layers.ARP))
continue
}

s.handleNonARPPacket(packet)
}
}
}

func (s *ARPScanner) handleARPLayer(arp *layers.ARP) {
if arp.Operation != layers.ARPReply {
// not an arp reply
return
}

if bytes.Equal([]byte(s.networkInfo.Interface.HardwareAddr), arp.SourceHwAddress) {
// This is a packet I sent.
return
}

ip := net.IP(arp.SourceProtAddress)
mac := net.HardwareAddr(arp.SourceHwAddress)

s.arpMap[ip.String()] = mac

s.writeSyn(ip, mac)
}

func (s *ARPScanner) handleNonARPPacket(packet gopacket.Packet) {
// Find the packets we care about, and print out logging
// information about them. All others are ignored.
net := packet.NetworkLayer()

if net == nil {
return
}

srcIP := net.NetworkFlow().Src().String()

mac, ok := s.arpMap[srcIP]

if !ok {
return
}

tcpLayer := packet.Layer(layers.LayerTypeTCP)

if tcpLayer == nil {
return
}

tcp, ok := tcpLayer.(*layers.TCP)

if !ok {
// this should never happen.
return
}

if tcp.DstPort != 54321 {
return
}

if tcp.SrcPort != 22 {
return
}

res := &DiscoveryResult{
ID: mac.String(),
IP: srcIP,
Hostname: "",
OS: "",
Status: server.StatusOnline,
}

port := Port{
ID: 22,
Status: PortClosed,
}

if tcp.SYN && tcp.ACK {
port.Status = PortOpen
}

res.Ports = []Port{port}

s.resultChan <- res
}

func (s *ARPScanner) writeARP() error {
// Set up all the layers' fields we can.
eth := layers.Ethernet{
SrcMAC: s.networkInfo.Interface.HardwareAddr,
DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
EthernetType: layers.EthernetTypeARP,
}

arp := layers.ARP{
AddrType: layers.LinkTypeEthernet,
Protocol: layers.EthernetTypeIPv4,
HwAddressSize: 6,
ProtAddressSize: 4,
Operation: layers.ARPRequest,
SourceHwAddress: []byte(s.networkInfo.Interface.HardwareAddr),
SourceProtAddress: []byte(s.networkInfo.UserIP.To4()),
DstHwAddress: []byte{0, 0, 0, 0, 0, 0},
}

// Set up buffer and options for serialization.
buf := gopacket.NewSerializeBuffer()

opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}

// Send one packet for every address.
for _, ip := range s.targets {
arp.DstProtAddress = []byte(net.ParseIP(ip).To4())

if err := gopacket.SerializeLayers(buf, opts, &eth, &arp); err != nil {
return err
}

if err := s.handle.WritePacketData(buf.Bytes()); err != nil {
log.Error().Err(err).Msg("")
}
}

return nil
}

func (s *ARPScanner) writeSyn(ip net.IP, mac net.HardwareAddr) {
// Construct all the network layers we need.
eth := layers.Ethernet{
SrcMAC: s.networkInfo.Interface.HardwareAddr,
DstMAC: mac,
EthernetType: layers.EthernetTypeIPv4,
}

ip4 := layers.IPv4{
SrcIP: s.networkInfo.UserIP.To4(),
DstIP: ip.To4(),
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolTCP,
}

tcp := layers.TCP{
SrcPort: 54321,
DstPort: 22,
SYN: true,
}

tcp.SetNetworkLayerForChecksum(&ip4)

buf := gopacket.NewSerializeBuffer()

opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}

if err := gopacket.SerializeLayers(buf, opts, &eth, &ip4, &tcp); err != nil {
s.log.Error().Err(err).Msg("failed to serialize syn layers")
return
}

if err := s.handle.WritePacketData(buf.Bytes()); err != nil {
s.log.Error().Err(err).Msg("failed to send syn packet")
}
}
6 changes: 4 additions & 2 deletions internal/discovery/interface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package discovery

import "context"
import (
"context"
)

//go:generate mockgen -destination=../mock/discovery/mock_discovery.go -package=mock_discovery . DetailScanner,Scanner

Expand All @@ -11,7 +13,7 @@ type DetailScanner interface {

// Scanner interface for scanning a network for devices
type Scanner interface {
Scan(resultChan chan *DiscoveryResult) error
Scan() error
Stop()
}

Expand Down
Loading

0 comments on commit 6fd3b7f

Please sign in to comment.