opensnitch/daemon/procmon/audit/client.go

338 lines
8.7 KiB
Go
Raw Normal View History

// Package audit reads auditd events from the builtin af_unix plugin, and parses
// the messages in order to proactively monitor pids which make connections.
// Once a connection is made and redirected to us via NFQUEUE, we
// lookup the connection inode in /proc, and add the corresponding PID with all
// the information of the process to a list of known PIDs.
//
// TODO: Prompt the user to allow/deny a connection/program as soon as it's
// started.
//
// Requisities:
// - install auditd and audispd-plugins
// - enable af_unix plugin /etc/audisp/plugins.d/af_unix.conf (active = yes)
// - auditctl -a always,exit -F arch=b64 -S socket,connect,execve -k opensnitchd
// - increase /etc/audisp/audispd.conf q_depth if there're dropped events
// - set write_logs to no if you don't need/want audit logs to be stored in the disk.
//
// read messages from the pipe to verify that it's working:
// socat unix-connect:/var/run/audispd_events stdio
//
// Audit event fields:
// https://github.com/linux-audit/audit-documentation/blob/master/specs/fields/field-dictionary.csv
// Record types:
// https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Security_Guide/sec-Audit_Record_Types.html
//
// Documentation:
// https://github.com/linux-audit/audit-documentation
package audit
import (
"bufio"
"fmt"
"io"
"net"
"os"
2020-03-05 20:57:37 +01:00
"runtime"
"sort"
"sync"
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// Event represents an audit event, which in our case can be an event of type
// socket, execve, socketpair or connect.
type Event struct {
Timestamp string // audit(xxxxxxx:nnnn)
Serial string
ProcName string // comm
ProcPath string // exe
ProcCmdLine string // proctitle
ProcDir string // cwd
ProcMode string // mode
TTY string
Pid int
UID int
Gid int
PPid int
EUid int
EGid int
OUid int
OGid int
UserName string // auid
DstHost net.IP
DstPort int
NetFamily string // inet, inet6, local
Success string
INode int
Dev string
Syscall int
Exit int
EventType string
RawEvent string
LastSeen time.Time
}
// MaxEventAge is the maximum minutes an audit process can live without network activity.
const (
MaxEventAge = int(10)
)
var (
// Lock holds a mutex
Lock sync.RWMutex
ourPid = os.Getpid()
// cache of events
events []*Event
eventsCleaner *time.Ticker
eventsCleanerChan = make(chan bool)
// TODO: EventChan is an output channel where incoming auditd events will be written.
// If a client opens it.
EventChan = (chan Event)(nil)
auditConn net.Conn
// TODO: we may need arm arch
rule64 = []string{"exit,always", "-F", "arch=b64", "-F", fmt.Sprint("ppid!=", ourPid), "-F", fmt.Sprint("pid!=", ourPid), "-S", "socket,connect", "-k", "opensnitch"}
rule32 = []string{"exit,always", "-F", "arch=b32", "-F", fmt.Sprint("ppid!=", ourPid), "-F", fmt.Sprint("pid!=", ourPid), "-S", "socketcall", "-F", "a0=1", "-k", "opensnitch"}
audispdPath = "/var/run/audispd_events"
)
// OPENSNITCH_RULES_KEY is the mark we place on every event we are interested in.
const (
OpensnitchRulesKey = "key=\"opensnitch\""
)
// GetEvents returns the list of processes which have opened a connection.
func GetEvents() []*Event {
return events
}
// GetEventByPid returns an event given a pid.
func GetEventByPid(pid int) *Event {
Lock.RLock()
defer Lock.RUnlock()
for _, event := range events {
if pid == event.Pid {
return event
}
}
return nil
}
// sortEvents sorts received events by time and elapsed time since latest network activity.
// newest PIDs will be placed on top of the list.
func sortEvents() {
sort.Slice(events, func(i, j int) bool {
now := time.Now()
elapsedTimeT := now.Sub(events[i].LastSeen)
elapsedTimeU := now.Sub(events[j].LastSeen)
t := events[i].LastSeen.UnixNano()
u := events[j].LastSeen.UnixNano()
return t > u && elapsedTimeT < elapsedTimeU
})
}
// cleanOldEvents deletes the PIDs which do not exist or that are too old to
// live.
// We start searching from the oldest to the newest.
// If the last network activity of a PID has been greater than MaxEventAge,
// then it'll be deleted.
func cleanOldEvents() {
Lock.Lock()
defer Lock.Unlock()
for n := len(events) - 1; n >= 0; n-- {
now := time.Now()
elapsedTime := now.Sub(events[n].LastSeen)
if int(elapsedTime.Minutes()) >= MaxEventAge {
events = append(events[:n], events[n+1:]...)
continue
}
if core.Exists(fmt.Sprint("/proc/", events[n].Pid)) == false {
events = append(events[:n], events[n+1:]...)
}
}
}
func deleteEvent(pid int) {
for n := range events {
if events[n].Pid == pid || events[n].PPid == pid {
deleteEventByIndex(n)
break
}
}
}
func deleteEventByIndex(index int) {
Lock.Lock()
events = append(events[:index], events[index+1:]...)
Lock.Unlock()
}
// AddEvent adds new event to the list of PIDs which have generate network
// activity.
// If the PID is already in the list, the LastSeen field is updated, to keep
// it alive.
func AddEvent(aevent *Event) {
if aevent == nil {
return
}
Lock.Lock()
defer Lock.Unlock()
for n := 0; n < len(events); n++ {
if events[n].Pid == aevent.Pid && events[n].Syscall == aevent.Syscall {
if aevent.ProcCmdLine != "" || (aevent.ProcCmdLine == events[n].ProcCmdLine) {
events[n] = aevent
}
events[n].LastSeen = time.Now()
sortEvents()
return
}
}
aevent.LastSeen = time.Now()
events = append([]*Event{aevent}, events...)
}
// startEventsCleaner will review if the events in the cache need to be cleaned
// every 5 minutes.
func startEventsCleaner() {
for {
select {
case <-eventsCleanerChan:
goto Exit
case <-eventsCleaner.C:
cleanOldEvents()
}
}
Exit:
log.Info("cleanerRoutine stopped")
}
func addRules() bool {
r64 := append([]string{"-A"}, rule64...)
r32 := append([]string{"-A"}, rule32...)
_, err64 := core.Exec("auditctl", r64)
_, err32 := core.Exec("auditctl", r32)
if err64 == nil && err32 == nil {
return true
}
log.Error("Error adding audit rule, err32=%v, err=%v", err32, err64)
return false
}
2020-03-05 20:57:37 +01:00
func configureSyscalls() {
// XXX: what about a i386 process running on a x86_64 system?
if runtime.GOARCH == "386" {
syscallSOCKET = "1"
syscallCONNECT = "3"
syscallSOCKETPAIR = "8"
2020-03-05 20:57:37 +01:00
}
}
func deleteRules() bool {
r64 := []string{"-D", "-k", "opensnitch"}
r32 := []string{"-D", "-k", "opensnitch"}
_, err64 := core.Exec("auditctl", r64)
_, err32 := core.Exec("auditctl", r32)
if err64 == nil && err32 == nil {
return true
}
log.Error("Error deleting audit rules, err32=%v, err64=%v", err32, err64)
return false
}
func checkRules() bool {
// TODO
return true
}
func checkStatus() bool {
// TODO
return true
}
// Reader reads events from audisd af_unix pipe plugin.
// If the auditd daemon is stopped or restarted, the reader handle
// is closed, so we need to restablished the connection.
func Reader(r io.Reader, eventChan chan<- Event) {
if r == nil {
log.Error("Error reading auditd events. Is auditd running? is af_unix plugin enabled?")
return
}
reader := bufio.NewReader(r)
go startEventsCleaner()
for {
buf, _, err := reader.ReadLine()
if err != nil {
if err == io.EOF {
log.Error("AuditReader: auditd stopped, reconnecting in 30s", err)
if newReader, err := reconnect(); err == nil {
reader = bufio.NewReader(newReader)
log.Important("Auditd reconnected, continue reading")
}
continue
}
2020-10-24 19:49:16 +02:00
log.Warning("AuditReader: auditd error", err)
break
}
parseEvent(string(buf[0:len(buf)]), eventChan)
}
2020-10-24 19:49:16 +02:00
log.Info("audit.Reader() closed")
}
// StartChannel creates a channel to receive events from Audit.
// Launch audit.Reader() in a goroutine:
// go audit.Reader(c, (chan<- audit.Event)(audit.EventChan))
func StartChannel() {
EventChan = make(chan Event, 0)
}
func reconnect() (net.Conn, error) {
deleteRules()
time.Sleep(30 * time.Second)
return connect()
}
func connect() (net.Conn, error) {
addRules()
// TODO: make the unix socket path configurable
return net.Dial("unix", audispdPath)
}
// Stop stops listening for events from auditd and delete the auditd rules.
func Stop() {
eventsCleanerChan <- true
eventsCleaner.Stop()
if auditConn != nil {
2020-10-24 19:49:16 +02:00
if err := auditConn.Close(); err != nil {
log.Warning("audit.Stop() error closing socket: %v", err)
}
}
deleteRules()
if EventChan != nil {
close(EventChan)
}
}
// Start makes a new connection to the audisp af_unix socket.
func Start() (net.Conn, error) {
2020-10-24 19:49:16 +02:00
auditConn, err := connect()
if err != nil {
2020-10-24 19:49:16 +02:00
log.Error("auditd Start() connection error %v", err)
deleteRules()
return nil, err
}
2020-03-05 20:57:37 +01:00
configureSyscalls()
eventsCleaner = time.NewTicker(time.Minute * 5)
2020-10-24 19:49:16 +02:00
return auditConn, err
}