diff --git a/go.mod b/go.mod index e25b3c7..f9fe8e5 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/spyre-project/spyre go 1.11 require ( + github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 github.com/hillu/go-archive-zip-crypto v0.0.0-20200712202847-bd5cf365dd44 github.com/hillu/go-ntdll v0.0.0-20210404124636-a6f426aa8d92 github.com/hillu/go-yara/v4 v4.1.0 diff --git a/go.sum b/go.sum index c1f2550..011007d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hillu/go-archive-zip-crypto v0.0.0-20200712202847-bd5cf365dd44 h1:seZZ+imS3iv6i0op0cqgt9K6BETRkXwUV9e8inrF+ws= github.com/hillu/go-archive-zip-crypto v0.0.0-20200712202847-bd5cf365dd44/go.mod h1:xUc/S/HgVuP6lhDvD0Wqtu/Npwx93VSXzbdyEDbqBb8= diff --git a/module_config/netscan.go b/module_config/netscan.go new file mode 100644 index 0000000..f607a38 --- /dev/null +++ b/module_config/netscan.go @@ -0,0 +1,5 @@ +// +build linux,windows + +package config + +import _ "github.com/spyre-project/spyre/scanner/netscan" diff --git a/report/formatter.go b/report/formatter.go index 47e3d51..2f79535 100644 --- a/report/formatter.go +++ b/report/formatter.go @@ -49,6 +49,10 @@ func (f *formatterPlain) formatProcEntry(w io.Writer, p ps.Process, description, w.Write([]byte{'\n'}) } +func (f *formatterPlain) formatNetstatEntry(w io.Writer, description, message string, extra ...string) { + fmt.Fprintf(w, "%s %s %s: %s%s\n", time.Now().Format(time.RFC3339), spyre.Hostname, description, message, fmtExtra(extra)) +} + func (f *formatterPlain) formatMessage(w io.Writer, format string, a ...interface{}) { f.emitTimeStamp(w) if format[len(format)-1] != '\n' { @@ -98,6 +102,11 @@ func (f *formatterTSJSON) formatProcEntry(w io.Writer, p ps.Process, description f.emitRecord(w, extra...) } +func (f *formatterTSJSON) formatNetstatEntry(w io.Writer, description, message string, extra ...string) { + extra = append([]string{"timestamp_desc", description, "message", message}, extra...) + f.emitRecord(w, extra...) +} + func (f *formatterTSJSON) formatMessage(w io.Writer, format string, a ...interface{}) { extra := []string{"timestamp_desc", "msg", "message", fmt.Sprintf(format, a...)} f.emitRecord(w, extra...) @@ -140,6 +149,11 @@ func (f *formatterTSJSONLines) formatProcEntry(w io.Writer, p ps.Process, descri f.emitRecord(w, extra...) } +func (f *formatterTSJSONLines) formatNetstatEntry(w io.Writer, description, message string, extra ...string) { + extra = append([]string{"timestamp_desc", description, "message", message}, extra...) + f.emitRecord(w, extra...) +} + func (f *formatterTSJSONLines) formatMessage(w io.Writer, format string, a ...interface{}) { extra := []string{"timestamp_desc", "msg", "message", fmt.Sprintf(format, a...)} f.emitRecord(w, extra...) diff --git a/report/report.go b/report/report.go index 88dbcc7..d3f135f 100644 --- a/report/report.go +++ b/report/report.go @@ -41,6 +41,12 @@ func AddProcInfo(proc ps.Process, description, message string, extra ...string) } } +func AddNetstatInfo(description, message string, extra ...string) { + for _, t := range targets { + t.formatNetstatEntry(t.writer, description, message, extra...) + } +} + // Close shuts down all reporting targets func Close() { for _, t := range targets { diff --git a/report/target.go b/report/target.go index 5d40cf0..4fd26a5 100644 --- a/report/target.go +++ b/report/target.go @@ -17,6 +17,7 @@ import ( type formatter interface { formatFileEntry(w io.Writer, f afero.File, description, message string, extra ...string) formatProcEntry(w io.Writer, p ps.Process, description, message string, extra ...string) + formatNetstatEntry(w io.Writer, description, message string, extra ...string) formatMessage(w io.Writer, format string, a ...interface{}) finish(w io.Writer) } diff --git a/scanner/netscan/netscan.go b/scanner/netscan/netscan.go new file mode 100644 index 0000000..fe262e0 --- /dev/null +++ b/scanner/netscan/netscan.go @@ -0,0 +1,198 @@ +// +build linux windows + +package netscan + +import ( + "fmt" + "strings" + + "github.com/cakturk/go-netstat/netstat" + "github.com/spyre-project/spyre/config" + "github.com/spyre-project/spyre/log" + "github.com/spyre-project/spyre/report" + "github.com/spyre-project/spyre/scanner" +) + +func init() { scanner.RegisterSystemScanner(&systemScanner{}) } + +type systemScanner struct { + IOCs map[string]eventIOC `yaml:"iocs"` +} + +type eventIOC struct { + Dip []string `yaml:"dip"` + Sip []string `yaml:"sip"` + Sport []int `yaml:"sport"` + Dport []int `yaml:"dport"` + Pname []string `yaml:"pname"` + NPname []string `yaml:"notpname"` + State []string `yaml:"state"` + Proto string `yaml:"proto"` +} + +func (s *systemScanner) FriendlyName() string { return "Netstat" } +func (s *systemScanner) ShortName() string { return "netstat" } + +func (s *systemScanner) Init(c *config.ScannerConfig) error { + if err := c.Config.Decode(s); err != nil { + return err + } + log.Debugf("%s: Initialized %d rules", s.ShortName(), len(s.IOCs)) + return nil +} + +func intInSlice(a int, list []int) bool { + if len(list) == 0 { + return true + } + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func stringInSlice(a string, list []string) bool { + if len(list) == 0 { + return true + } + for _, b := range list { + if strings.EqualFold(b, a) { + return true + } + } + return false +} + +func nstringInSlice(a string, list []string) bool { + if len(list) == 0 { + return false + } + for _, b := range list { + if strings.EqualFold(b, a) { + return true + } + } + return false +} + +func (s *systemScanner) Scan() error { + tsocks, err := netstat.TCPSocks(netstat.NoopFilter) + if err != nil { + log.Debugf("Error to get TCP socks : %s", err) + } + usocks, err := netstat.UDPSocks(netstat.NoopFilter) + if err != nil { + log.Debugf("Error to get UDP socks : %s", err) + } + for description, ioc := range s.IOCs { + //netCheck(ioc.Dip, ioc.Sip, ioc.Sport, ioc.Dport, ioc.Pname, ioc.State) + for _, e := range tsocks { + //fmt.Printf("%v\n", e) + pid := "unknown" + proc_name := "unknown" + port_src := fmt.Sprintf("%d", e.LocalAddr.Port) + port_dst := fmt.Sprintf("%d", e.RemoteAddr.Port) + uid := fmt.Sprintf("%d", e.UID) + + if !(strings.EqualFold(ioc.Proto, "tcp") || ioc.Proto == "*" || ioc.Proto == "") { + continue + } + if e.Process != nil { + proc_name = fmt.Sprintf("%s", e.Process.Name) + pid = fmt.Sprintf("%d", e.Process.Pid) + if !(stringInSlice(e.Process.Name, ioc.Pname)) { + continue + } + if nstringInSlice(e.Process.Name, ioc.NPname) { + continue + } + } + dip := e.RemoteAddr.IP.String() + if !(stringInSlice(dip, ioc.Dip)) { + continue + } + sip := e.LocalAddr.IP.String() + if !(stringInSlice(sip, ioc.Sip)) { + continue + } + if !(intInSlice(int(e.LocalAddr.Port), ioc.Sport)) { + continue + } + if !(intInSlice(int(e.RemoteAddr.Port), ioc.Dport)) { + continue + } + state := fmt.Sprintf("%s", e.State) + if !(stringInSlice(state, ioc.State)) { + continue + } + message := fmt.Sprintf("Found netstat rule: %s on TCP %v", description, e) + report.AddNetstatInfo( + "ioc_on_netstat", message, + "rule", description, + "State", state, + "ip_src", sip, + "ip_dst", dip, + "uid", uid, + "PID", pid, + "Process", proc_name, + "port_dst", port_dst, + "port_src", port_src, + "proto", "TCP") + } + for _, e := range usocks { + pid := "unknown" + proc_name := "unknown" + port_src := fmt.Sprintf("%d", e.LocalAddr.Port) + port_dst := fmt.Sprintf("%d", e.RemoteAddr.Port) + uid := fmt.Sprintf("%d", e.UID) + //fmt.Printf("%v\n", e) + if !(strings.EqualFold(ioc.Proto, "udp") || ioc.Proto == "*" || ioc.Proto == "") { + continue + } + if e.Process != nil { + proc_name = fmt.Sprintf("%s", e.Process.Name) + pid = fmt.Sprintf("%d", e.Process.Pid) + if !(stringInSlice(e.Process.Name, ioc.Pname)) { + continue + } + if nstringInSlice(e.Process.Name, ioc.NPname) { + continue + } + } + dip := e.RemoteAddr.IP.String() + if !(stringInSlice(dip, ioc.Dip)) { + continue + } + sip := e.LocalAddr.IP.String() + if !(stringInSlice(sip, ioc.Sip)) { + continue + } + if !(intInSlice(int(e.LocalAddr.Port), ioc.Sport)) { + continue + } + if !(intInSlice(int(e.RemoteAddr.Port), ioc.Dport)) { + continue + } + state := fmt.Sprintf("%s", e.State) + if !(stringInSlice(state, ioc.State)) { + continue + } + message := fmt.Sprintf("Found netstat rule: %s on UDP %v", description, e) + report.AddNetstatInfo( + "ioc_on_netstat", message, + "rule", description, + "State", state, + "ip_src", sip, + "ip_dst", dip, + "uid", uid, + "PID", pid, + "Process", proc_name, + "port_dst", port_dst, + "port_src", port_src, + "proto", "UDP") + } + } + return nil +}