From 9e12a1a1414c70b7a1163748557ce05be884f976 Mon Sep 17 00:00:00 2001 From: Quentin JEROME Date: Tue, 22 Jun 2021 22:22:10 +0200 Subject: [PATCH] Refactoring: - hids package - hook functions taking hids as first parameter to easily access config from hooks - removed global variables shared between hooks and HIDS - manager command handler moved from api package to hids to easily access hids config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed issues: - Implement actionnable rules: #28 - Implement event count: #29 - Enrich events with signature information: #32 - Automatic canary folder management: #33 - Ability to configure audit policies from WHIDS config: #34 - Set File System Audit ACLs from config: #35 - Generate IR ready reports on detections: #36 - Dump process tree: #38 - Enrich event with Gene process scoring: #40 - Add Admin API to list and download artifacts dumped: #42 - Directory listing command: #44 - Implement hash command: #45 - Implement osquery command: #46 - Implement terminate command: #47 - Implement stat command: #48 - Implement walk command: #49 - Implement find command: #50 - Implement report command: #51 - Implement processes command: #52 - Implement drivers command: #53 --- api/adminapi_test.go | 10 +- api/{client.go => api_client.go} | 157 +- api/{client_test.go => api_client_test.go} | 7 +- api/endpoint.go | 146 +- api/forwarder.go | 24 +- api/{collector_test.go => forwarder_test.go} | 0 api/manager.go | 15 +- api/manager_admin_api.go | 14 +- api/manager_endpoint_api.go | 7 +- go.mod | 6 +- go.sum | 27 + {tools/whids => hids}/canary.go | 137 +- hids/commands.go | 181 ++ hids/config.go | 163 ++ hids/eventids.go | 37 + hids/filters.go | 72 + tools/whids/whids.go => hids/hids.go | 806 +++++---- {hooks/test => hids}/hook_test.go | 17 +- hids/hookdefs.go | 1222 +++++++++++++ {hooks => hids}/hooks.go | 48 +- hids/hookutils.go | 111 ++ hids/paths.go | 138 ++ hids/ptrack.go | 398 +++++ hids/reports.go | 119 ++ {hooks => hids}/test/events.json | 0 {hooks => hids}/test/new-events.json | 0 tools/whids/acquisition.go | 99 -- tools/whids/hookdefs.go | 1645 ------------------ tools/whids/main.go | 121 +- tools/whids/manage.bat | 33 +- tools/whids/service.go | 6 +- utils/files.go | 143 ++ utils/net.go | 29 + utils/sizes.go | 8 + utils/utils.go | 192 +- utils/windows.go | 38 + 36 files changed, 3515 insertions(+), 2661 deletions(-) rename api/{client.go => api_client.go} (82%) rename api/{client_test.go => api_client_test.go} (95%) rename api/{collector_test.go => forwarder_test.go} (100%) rename {tools/whids => hids}/canary.go (68%) create mode 100644 hids/commands.go create mode 100644 hids/config.go create mode 100644 hids/eventids.go create mode 100644 hids/filters.go rename tools/whids/whids.go => hids/hids.go (59%) rename {hooks/test => hids}/hook_test.go (82%) create mode 100644 hids/hookdefs.go rename {hooks => hids}/hooks.go (66%) create mode 100644 hids/hookutils.go create mode 100644 hids/paths.go create mode 100644 hids/ptrack.go create mode 100644 hids/reports.go rename {hooks => hids}/test/events.json (100%) rename {hooks => hids}/test/new-events.json (100%) delete mode 100644 tools/whids/acquisition.go delete mode 100644 tools/whids/hookdefs.go create mode 100644 utils/files.go create mode 100644 utils/net.go create mode 100644 utils/sizes.go create mode 100644 utils/windows.go diff --git a/api/adminapi_test.go b/api/adminapi_test.go index d10cf39..5c7dcca 100644 --- a/api/adminapi_test.go +++ b/api/adminapi_test.go @@ -182,7 +182,7 @@ func TestAdminAPIPostCommand(t *testing.T) { } r := post(format("%s/%s/command", AdmAPIEndpointsPath, euuid), JSON(ca)) failOnAdminAPIError(t, r) - if err := c.ExecuteCommand(); err != nil { + if _, err := c.ExecuteCommand(); err != nil { t.Errorf("Failed to execute command: %s", err) t.FailNow() } @@ -214,7 +214,7 @@ func TestAdminAPIGetCommandField(t *testing.T) { r := post(format("%s/%s/command", AdmAPIEndpointsPath, euuid), JSON(ca)) failOnAdminAPIError(t, r) - if err := c.ExecuteCommand(); err != nil { + if _, err := c.ExecuteCommand(); err != nil { t.Errorf("Failed to execute command: %s", err) t.FailNow() } @@ -289,7 +289,7 @@ func TestAdminAPIGetEndpointReport(t *testing.T) { t.Logf("Failed to prepare request: %s", err) t.FailNow() } - mc.httpClient.Do(r) + mc.HTTPClient.Do(r) } time.Sleep(1 * time.Second) @@ -340,7 +340,7 @@ func TestAdminAPIGetEndpointLogs(t *testing.T) { t.Logf("Failed to prepare request: %s", err) t.FailNow() } - mc.httpClient.Do(r) + mc.HTTPClient.Do(r) } time.Sleep(1 * time.Second) @@ -439,7 +439,7 @@ func TestAdminAPIGetEndpointAlerts(t *testing.T) { t.Logf("Failed to prepare request: %s", err) t.FailNow() } - mc.httpClient.Do(r) + mc.HTTPClient.Do(r) } time.Sleep(1 * time.Second) diff --git a/api/client.go b/api/api_client.go similarity index 82% rename from api/client.go rename to api/api_client.go index 8868fc1..5c22f78 100644 --- a/api/client.go +++ b/api/api_client.go @@ -4,10 +4,8 @@ import ( "bytes" "compress/gzip" "context" - "crypto/sha256" "crypto/tls" "crypto/x509" - "encoding/hex" "encoding/json" "fmt" "io" @@ -81,7 +79,7 @@ func (cc *ClientConfig) Transport() http.RoundTripper { return c, err } } - return c, fmt.Errorf("Server fingerprint not verified") + return c, fmt.Errorf("server fingerprint not verified") }, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, @@ -92,9 +90,10 @@ func (cc *ClientConfig) Transport() http.RoundTripper { // ManagerClient structure definition type ManagerClient struct { - httpClient http.Client - config ClientConfig - managerIP net.IP + config ClientConfig + ManagerIP net.IP + + HTTPClient http.Client } const ( @@ -102,8 +101,6 @@ const ( UserAgent = "Whids-API-Client/1.0" // Mega byte size Mega = 1 << 20 - // DefaultMaxUploadSize default maximum upload size - DefaultMaxUploadSize = 100 * Mega ) var ( @@ -115,34 +112,25 @@ func init() { var err error Hostname, err = os.Hostname() if err != nil { - id := data.Md5([]byte(fmt.Sprintf("%s", time.Now().Format(time.RFC3339Nano)))) + id := data.Md5([]byte(time.Now().Format(time.RFC3339Nano))) Hostname = fmt.Sprintf("HOST-%s", id) } } -// Sha256StringArray utility -func Sha256StringArray(array []string) string { - sha256 := sha256.New() - for _, e := range array { - sha256.Write([]byte(e)) - } - return hex.EncodeToString(sha256.Sum(nil)) -} - // NewManagerClient creates a new Client to interface with the manager func NewManagerClient(c *ClientConfig) (*ManagerClient, error) { tpt := c.Transport() mc := &ManagerClient{ - httpClient: http.Client{Transport: tpt}, + HTTPClient: http.Client{Transport: tpt}, config: *c, - managerIP: c.ManagerIP(), + ManagerIP: c.ManagerIP(), } // host if mc.config.Host == "" { - return nil, fmt.Errorf("Field \"host\" is missing from configuration") + return nil, fmt.Errorf("field \"host\" is missing from configuration") } // protocol if mc.config.Proto == "" { @@ -152,12 +140,12 @@ func NewManagerClient(c *ClientConfig) (*ManagerClient, error) { switch mc.config.Proto { case "http", "https": default: - return nil, fmt.Errorf("Protocol not supported (only http(s))") + return nil, fmt.Errorf("protocol not supported (only http(s))") } // key if mc.config.Key == "" { - return nil, fmt.Errorf("Field \"key\" is missing from configuration") + return nil, fmt.Errorf("field \"key\" is missing from configuration") } return mc, nil @@ -208,7 +196,7 @@ func (m *ManagerClient) IsServerUp() bool { log.Errorf("IsServerUp cannot create server key request: %s", err) return false } - resp, err := m.httpClient.Do(get) + resp, err := m.HTTPClient.Do(get) if err != nil { log.Errorf("IsServerUp cannot issue server key request: %s", err) return false @@ -229,7 +217,7 @@ func (m *ManagerClient) IsServerAuthenticated() (auth bool, up bool) { log.Errorf("IsServerAuthenticated cannot create server key request: %s", err) return false, false } - resp, err := m.httpClient.Do(get) + resp, err := m.HTTPClient.Do(get) if err != nil { log.Errorf("IsServerAuthenticated cannot issue server key request: %s", err) return false, false @@ -266,15 +254,15 @@ func (m *ManagerClient) GetRulesSha256() (string, error) { return "", fmt.Errorf("GetRulesSha256 failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { - return "", fmt.Errorf("GetRulesSha256 failed to issue HTTP request: %s", err) + return "", fmt.Errorf("SetRulesSha256 failed to issue HTTP request: %s", err) } if resp != nil { defer resp.Body.Close() if resp.StatusCode != 200 { - return "", fmt.Errorf("Failed to retrieve rules sha256, unexpected HTTP status code %d", resp.StatusCode) + return "", fmt.Errorf("failed to retrieve rules sha256, unexpected HTTP status code %d", resp.StatusCode) } sha256, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -296,7 +284,7 @@ func (m *ManagerClient) GetContainer(name string) ([]string, error) { return ctn, fmt.Errorf("GetContainer failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return ctn, fmt.Errorf("GetContainer failed to issue HTTP request: %s", err) } @@ -304,7 +292,7 @@ func (m *ManagerClient) GetContainer(name string) ([]string, error) { if resp != nil { defer resp.Body.Close() if resp.StatusCode != 200 { - return ctn, fmt.Errorf("Failed to retrieve container, unexpected HTTP status code %d", resp.StatusCode) + return ctn, fmt.Errorf("failed to retrieve container, unexpected HTTP status code %d", resp.StatusCode) } dec := json.NewDecoder(resp.Body) if err = dec.Decode(&ctn); err != nil { @@ -325,7 +313,7 @@ func (m *ManagerClient) GetContainersList() ([]string, error) { return ctn, fmt.Errorf("GetContainersList failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return ctn, fmt.Errorf("GetContainersList failed to issue HTTP request: %s", err) } @@ -333,7 +321,7 @@ func (m *ManagerClient) GetContainersList() ([]string, error) { if resp != nil { defer resp.Body.Close() if resp.StatusCode != 200 { - return ctn, fmt.Errorf("Failed to retrieve containers list, unexpected HTTP status code %d", resp.StatusCode) + return ctn, fmt.Errorf("failed to retrieve containers list, unexpected HTTP status code %d", resp.StatusCode) } dec := json.NewDecoder(resp.Body) if err = dec.Decode(&ctn); err != nil { @@ -353,7 +341,7 @@ func (m *ManagerClient) GetContainerSha256(name string) (string, error) { return "", fmt.Errorf("GetContainerSha256 failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("GetContainerSha256 failed to issue HTTP request: %s", err) } @@ -361,7 +349,7 @@ func (m *ManagerClient) GetContainerSha256(name string) (string, error) { if resp != nil { defer resp.Body.Close() if resp.StatusCode != 200 { - return "", fmt.Errorf("Failed to retrieve container sha256, unexpected HTTP status code %d", resp.StatusCode) + return "", fmt.Errorf("failed to retrieve container sha256, unexpected HTTP status code %d", resp.StatusCode) } sha256, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -381,7 +369,7 @@ func (m *ManagerClient) GetRules() (string, error) { return "", fmt.Errorf("GetRules failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("GetRules failed to issue HTTP request: %s", err) } @@ -416,7 +404,7 @@ func (m *ManagerClient) PrepareFileUpload(path, guid, evthash, filename string) } return &fu, nil } - return &fu, fmt.Errorf("Dump size above limit") + return &fu, fmt.Errorf("dump size above limit") } return &fu, os.ErrNotExist } @@ -446,7 +434,7 @@ func (m *ManagerClient) PostDump(f *FileUpload) error { return fmt.Errorf("PostDump failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return fmt.Errorf("PostDump failed to issue HTTP request: %s", err) } @@ -475,7 +463,7 @@ func (m *ManagerClient) PostLogs(r io.Reader) error { return fmt.Errorf("PostLogs failed to prepare request: %s", err) } - resp, err := m.httpClient.Do(req) + resp, err := m.HTTPClient.Do(req) if err != nil { return fmt.Errorf("PostLogs failed to issue HTTP request: %s", err) } @@ -494,79 +482,80 @@ func (m *ManagerClient) PostLogs(r io.Reader) error { return fmt.Errorf("PostLogs failed, server cannot be authenticated") } -// ExecuteCommand executes a Command on the endpoint and return the result -// to the manager. NB: this method is blocking due to Command.Run function call -func (m *ManagerClient) ExecuteCommand() error { +var ( + ErrNothingToDo = fmt.Errorf("nothing to do") +) + +func (m *ManagerClient) PostCommand(command *Command) error { if auth, _ := m.IsServerAuthenticated(); auth { - env := AliasEnv{m.managerIP} - command := NewCommandWithEnv(&env) + // stripping unecessary content to send back the command + command.Strip() - // getting command to be executed - req, err := m.Prepare("GET", EptAPICommandPath, nil) + // command should now contain stdout and stderr + jsonCommand, err := json.Marshal(command) if err != nil { - return fmt.Errorf("ExecuteCommand failed to prepare request: %s", err) + return fmt.Errorf("PostCommand failed to marshal command") } - resp, err := m.httpClient.Do(req) + // send back the response + req, err := m.PrepareGzip("POST", EptAPICommandPath, bytes.NewBuffer(jsonCommand)) if err != nil { - return fmt.Errorf("ExecuteCommand failed to issue HTTP request: %s", err) + return fmt.Errorf("PostCommand failed to prepare POST request") } - // if there is no command to execute, the server replies with this status code - if resp.StatusCode == http.StatusNoContent { - // nothing else to do - return nil - } - - jsonCommand, err := ioutil.ReadAll(resp.Body) + resp, err := m.HTTPClient.Do(req) if err != nil { - return fmt.Errorf("ExecuteCommand failed to read HTTP response body: %s", err) + return fmt.Errorf("PostCommand failed to issue HTTP request: %s", err) } - // unmarshal command to be executed - if err := json.Unmarshal(jsonCommand, &command); err != nil { - return fmt.Errorf("ExecuteCommand failed to unmarshal command: %s", err) + if resp != nil { + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("PostCommand failed to send command results, unexpected HTTP status code %d", resp.StatusCode) + } } + return nil + } + return fmt.Errorf("PostCommand failed, server cannot be authenticated") - // running the command, this is a blocking function, it waits the command to finish - if err := command.Run(); err != nil { - log.Errorf("ExecuteCommand failed to run command \"%s\": %s", command, err) - } +} - // stripping unecessary content to send back the command - command.Strip() - for fn, ff := range command.Fetch { - log.Infof("file: %s len: %d error: %s", fn, len(ff.Data), ff.Error) - } - // command should now contain stdout and stderr - jsonCommand, err = json.Marshal(command) +func (m *ManagerClient) FetchCommand() (*Command, error) { + command := NewCommand() + if auth, _ := m.IsServerAuthenticated(); auth { + // getting command to be executed + req, err := m.Prepare("GET", EptAPICommandPath, nil) if err != nil { - return fmt.Errorf("ExecuteCommand failed to marshal command") + return command, fmt.Errorf("FetchCommand failed to prepare request: %s", err) } - // send back the response - req, err = m.PrepareGzip("POST", EptAPICommandPath, bytes.NewBuffer(jsonCommand)) + resp, err := m.HTTPClient.Do(req) if err != nil { - return fmt.Errorf("ExecuteCommand failed to prepare POST request") + return command, fmt.Errorf("FetchCommand failed to issue HTTP request: %s", err) } - resp, err = m.httpClient.Do(req) + // if there is no command to execute, the server replies with this status code + if resp.StatusCode == http.StatusNoContent { + // nothing else to do + return command, ErrNothingToDo + } + + jsonCommand, err := ioutil.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("ExecuteCommand failed to issue HTTP request: %s", err) + return command, fmt.Errorf("FetchCommand failed to read HTTP response body: %s", err) } - if resp != nil { - defer resp.Body.Close() - if resp.StatusCode != 200 { - return fmt.Errorf("ExecuteCommand failed to send command results, unexpected HTTP status code %d", resp.StatusCode) - } + // unmarshal command to be executed + if err := json.Unmarshal(jsonCommand, &command); err != nil { + return command, fmt.Errorf("FetchCommand failed to unmarshal command: %s", err) } - return nil + + return command, nil } - return fmt.Errorf("ExecuteCommand failed, server cannot be authenticated") + return command, fmt.Errorf("FetchCommand failed, server cannot be authenticated") } // Close closes idle connections from underlying transport func (m *ManagerClient) Close() { - m.httpClient.CloseIdleConnections() + m.HTTPClient.CloseIdleConnections() } diff --git a/api/client_test.go b/api/api_client_test.go similarity index 95% rename from api/client_test.go rename to api/api_client_test.go index 67c8376..478d5cc 100644 --- a/api/client_test.go +++ b/api/api_client_test.go @@ -10,6 +10,7 @@ import ( "github.com/0xrawsec/golang-utils/crypto/file" "github.com/0xrawsec/golang-utils/datastructs" "github.com/0xrawsec/golang-utils/fsutil/fswalker" + "github.com/0xrawsec/whids/utils" ) var ( @@ -121,7 +122,7 @@ func TestClientContainer(t *testing.T) { t.Error(err) } - if sha256, err := c.GetContainerSha256(cont); sha256 != Sha256StringArray(bl) || err != nil { + if sha256, err := c.GetContainerSha256(cont); sha256 != utils.Sha256StringArray(bl) || err != nil { if err != nil { t.Error(err) } else { @@ -164,7 +165,7 @@ func TestClientExecuteCommand(t *testing.T) { t.Fail() } - if err := c.ExecuteCommand(); err != nil { + if _, err := c.ExecuteCommand(); err != nil { t.Errorf("Client failed to execute command") t.Fail() } @@ -231,7 +232,7 @@ func TestClientExecuteDroppedCommand(t *testing.T) { t.FailNow() } - if err := c.ExecuteCommand(); err != nil { + if _, err := c.ExecuteCommand(); err != nil { t.Errorf("Client failed to execute command") t.FailNow() } diff --git a/api/endpoint.go b/api/endpoint.go index 03de969..e86252e 100644 --- a/api/endpoint.go +++ b/api/endpoint.go @@ -2,9 +2,9 @@ package api import ( "context" + "encoding/json" "fmt" "io/ioutil" - "net" "os" "os/exec" "path/filepath" @@ -57,35 +57,26 @@ type Command struct { Drop []*EndpointFile `json:"drop"` // used to fetch files from the endpoint Fetch map[string]*EndpointFile `json:"fetch"` - Stdout []byte `json:"stdout"` + Stdout interface{} `json:"stdout"` Stderr []byte `json:"stderr"` Error string `json:"error"` Sent bool `json:"sent"` Background bool `json:"background"` Completed bool `json:"completed"` + ExpectJSON bool `json:"expect-json"` Timeout time.Duration `json:"timeout"` SentTime time.Time `json:"sent-time"` - aliasEnv *AliasEnv + runnable bool } // NewCommand creates a new Command to run on an endpoint func NewCommand() *Command { - id := UUIDGen() - cmd := &Command{ - UUID: id.String(), - Drop: make([]*EndpointFile, 0), - Fetch: make(map[string]*EndpointFile)} - return cmd -} - -// NewCommandWithEnv creates a new Command to run on an endpoint -func NewCommandWithEnv(env *AliasEnv) *Command { id := UUIDGen() cmd := &Command{ UUID: id.String(), Drop: make([]*EndpointFile, 0), Fetch: make(map[string]*EndpointFile), - aliasEnv: env} + runnable: true} return cmd } @@ -133,24 +124,34 @@ func (c *Command) AddFetchFile(filepath string) { c.Fetch[filepath] = &EndpointFile{UUID: UUIDGen().String()} } +func (c *Command) FromExecCmd(cmd *exec.Cmd) { + if cmd.Args != nil { + if len(cmd.Args) > 0 { + c.Name = cmd.Args[0] + if len(cmd.Args) > 1 { + c.Args = make([]string, len(cmd.Args[1:])) + copy(c.Args, cmd.Args[1:]) + } + } else { + c.Name = cmd.Path + } + } else { + c.Name = cmd.Path + } +} + // BuildCmd builds up an exec.Cmd from Command func (c *Command) BuildCmd() (*exec.Cmd, error) { - switch c.Name { - case "contain": - if cmd := ContainAlias(c.aliasEnv.ManagerIP); cmd != nil { - return cmd, nil - } - return nil, fmt.Errorf("Bad manager IP") - case "uncontain": - return UncontainAlias(), nil - default: - if c.Timeout > 0 { - // we create a command with a timeout context if needed - ctx, _ := context.WithTimeout(context.Background(), c.Timeout) - return exec.CommandContext(ctx, c.Name, c.Args...), nil - } - return exec.Command(c.Name, c.Args...), nil + if c.Timeout > 0 { + // we create a command with a timeout context if needed + ctx, _ := context.WithTimeout(context.Background(), c.Timeout) + return exec.CommandContext(ctx, c.Name, c.Args...), nil } + return exec.Command(c.Name, c.Args...), nil +} + +func (c *Command) Unrunnable() { + c.runnable = false } // Run runs the command according to the specified settings @@ -192,7 +193,7 @@ func (c *Command) Run() (err error) { } // we have something to run - if c.Name != "" { + if c.Name != "" && c.runnable { cmd, err = c.BuildCmd() if err == nil { c.Name = cmd.Path @@ -210,7 +211,16 @@ func (c *Command) Run() (err error) { } c.Error = fmt.Sprintf("%s", err) } - c.Stdout = stdout + + // if we expect JSON output + if c.ExpectJSON { + if err := json.Unmarshal(stdout, &c.Stdout); err != nil { + c.Stdout = stdout + } + } else { + c.Stdout = stdout + } + } else { // if we failed to build the command we set error field c.Error = fmt.Sprintf("Failed to build command: %s", err) @@ -253,79 +263,9 @@ func (c *Command) Complete(other *Command) error { c.Error = other.Error c.Drop = other.Drop c.Fetch = other.Fetch + c.ExpectJSON = other.ExpectJSON c.Completed = true return nil } - return fmt.Errorf("Commands do not have the same ID") -} - -////////////// Builtin commands - -// derived from: https://gist.github.com/kotakanbe/d3059af990252ba89a82 -func next(ip net.IP) net.IP { - nip := net.IP(make(net.IP, len(ip))) - copy(nip, ip) - for j := len(nip) - 1; j >= 0; j-- { - nip[j]++ - if nip[j] > 0 { - break - } - } - return nip -} - -// derived from: https://gist.github.com/kotakanbe/d3059af990252ba89a82 -func prev(ip net.IP) net.IP { - nip := net.IP(make(net.IP, len(ip))) - copy(nip, ip) - for j := len(nip) - 1; j >= 0; j-- { - nip[j]-- - if nip[j] < 255 { - break - } - } - return nip -} - -const ( - // ContainRuleName is the name of the Windows firewall rule used to contain endpoint - ContainRuleName = "EDR containment" -) - -// AliasEnv is a structure to hold variables needed by aliases -type AliasEnv struct { - ManagerIP net.IP -} - -// ContainAlias is an alias to contain an endpoint -func ContainAlias(ip net.IP) *exec.Cmd { - if ip != nil { - ip = ip.To4() - // building up netsh.exe arguments - args := []string{ - "advfirewall", - "firewall", - "add", - "rule", - fmt.Sprintf("name=%s", ContainRuleName), - "dir=out", - fmt.Sprintf("remoteip=0.0.0.0-%s,%s-255.255.255.255", prev(ip), next(ip)), - "action=block", - } - return exec.Command("netsh.exe", args...) - } - return nil -} - -// UncontainAlias builds a command to uncontain an endpoint -// NB: implementation must be in line with what is done in ContainAlias -func UncontainAlias() *exec.Cmd { - // building up netsh.exe arguments - args := []string{"advfirewall", - "firewall", - "delete", - "rule", - fmt.Sprintf("name=%s", ContainRuleName), - } - return exec.Command("netsh.exe", args...) + return fmt.Errorf("Command does not have the same ID") } diff --git a/api/forwarder.go b/api/forwarder.go index 4e92a5e..aa1e655 100644 --- a/api/forwarder.go +++ b/api/forwarder.go @@ -16,11 +16,10 @@ import ( "github.com/0xrawsec/golang-utils/fsutil/fswalker" "github.com/0xrawsec/golang-utils/fsutil/logfile" "github.com/0xrawsec/golang-utils/log" + "github.com/0xrawsec/whids/utils" ) const ( - // DefaultDirPerm default log directory permissions for forwarder - DefaultDirPerm = 0700 // DefaultLogfileSize default forwarder logfile size DefaultLogfileSize = logfile.MB * 5 // DiskSpaceThreshold allow 1GB of queued events @@ -29,14 +28,6 @@ const ( MinRotationInterval = time.Minute ) -var () - -func buildURI(proto, host, port, url string) string { - url = strings.Trim(url, "/") - return fmt.Sprintf("%s://%s:%s/%s", proto, host, port, url) - -} - // LoggingConfig structure to encode Logging configuration of the forwarder type LoggingConfig struct { Dir string `toml:"dir" comment:"Directory used to store logs"` @@ -85,20 +76,20 @@ func NewForwarder(c *ForwarderConfig) (*Forwarder, error) { if !co.Local { if co.Client, err = NewManagerClient(&c.Client); err != nil { - return nil, fmt.Errorf("Field to initialize manager client: %s", err) + return nil, fmt.Errorf("field to initialize manager client: %s", err) } } // queue directory if c.Logging.Dir == "" { - return nil, fmt.Errorf("Field \"logs-dir\" is missing from configuration") + return nil, fmt.Errorf("field \"logs-dir\" is missing from configuration") } // creating the queue directory if !fsutil.Exists(c.Logging.Dir) && !fsutil.IsDir(c.Logging.Dir) { // TOCTU may happen here so we double check error code - if err = os.Mkdir(c.Logging.Dir, DefaultDirPerm); err != nil && !os.IsExist(err) { - return nil, fmt.Errorf("Cannot create queue directory : %s", err) + if err = os.Mkdir(c.Logging.Dir, utils.DefaultPerms); err != nil && !os.IsExist(err) { + return nil, fmt.Errorf("cannot create queue directory : %s", err) } } @@ -158,7 +149,7 @@ func (f *Forwarder) Save() (err error) { lf := filepath.Join(f.fwdConfig.Logging.Dir, "alerts.log") ri := f.fwdConfig.Logging.RotationInterval log.Infof("Rotating logfile every %s", ri) - if f.logfile, err = logfile.OpenTimeRotateLogFile(lf, DefaultLogPerm, ri); err != nil { + if f.logfile, err = logfile.OpenTimeRotateLogFile(lf, utils.DefaultPerms, ri); err != nil { return } } @@ -270,8 +261,9 @@ func (f *Forwarder) ProcessQueue() { } switch { case strings.HasSuffix(fp, ".gz"): + var gzr *gzip.Reader // the file is gzip so we have to pass a gzip reader to prepCollectReq - gzr, err := gzip.NewReader(fd) + gzr, err = gzip.NewReader(fd) if err != nil { log.Errorf("Failed to create gzip reader for queued file (%s): %s", fp, err) // close file diff --git a/api/collector_test.go b/api/forwarder_test.go similarity index 100% rename from api/collector_test.go rename to api/forwarder_test.go diff --git a/api/manager.go b/api/manager.go index 2646da9..0dec8ca 100644 --- a/api/manager.go +++ b/api/manager.go @@ -22,6 +22,7 @@ import ( "time" "github.com/0xrawsec/golang-evtx/evtx" + "github.com/0xrawsec/whids/utils" "github.com/pelletier/go-toml" "github.com/0xrawsec/gene/reducer" @@ -42,13 +43,15 @@ const ( // DefaultLogPerm default logfile permission for Manager DefaultLogPerm = 0600 // DefaultManagerLogSize default size for Manager's logfiles - DefaultManagerLogSize = logfile.MB * 100 + DefaultManagerLogSize = utils.Mega * 100 // DefaultKeySize default size for API key generation DefaultKeySize = 64 // EptAPIDefaultPort default port used by manager's endpoint API EptAPIDefaultPort = 1519 // AdmAPIDefaultPort default port used by manager's admin API AdmAPIDefaultPort = 1520 + // DefaultMaxUploadSize default maximum upload size + DefaultMaxUploadSize = 100 * utils.Mega ) var ( @@ -150,7 +153,7 @@ func (f *FileUpload) Dump(dir string) (err error) { // Create directory if doesn't exist if !fsutil.IsDir(dirpath) { - if err = os.MkdirAll(dirpath, DefaultDirPerm); err != nil { + if err = os.MkdirAll(dirpath, utils.DefaultPerms); err != nil { return } } @@ -441,7 +444,7 @@ type Manager struct { endpointAPI *http.Server endpoints Endpoints adminAPI *http.Server - admins datastructs.SyncedMap + admins *datastructs.SyncedMap stop chan bool done bool // Gene related members @@ -468,7 +471,7 @@ func NewManager(c *ManagerConfig) (*Manager, error) { return nil, fmt.Errorf("Manager Admin API Error: invalid port to listen to %d", c.EndpointAPI.Port) } - if err := os.MkdirAll(c.Logging.Root, DefaultDirPerm); err != nil { + if err := os.MkdirAll(c.Logging.Root, utils.DefaultPerms); err != nil { return nil, fmt.Errorf("Failed at creating log directory: %s", err) } @@ -506,7 +509,7 @@ func NewManager(c *ManagerConfig) (*Manager, error) { // Dump Directory initialization if m.Config.DumpDir != "" && !fsutil.IsDir(m.Config.DumpDir) { - if err := os.MkdirAll(m.Config.DumpDir, DefaultDirPerm); err != nil { + if err := os.MkdirAll(m.Config.DumpDir, utils.DefaultPerms); err != nil { return &m, fmt.Errorf("Failed to created dump directory (%s): %s", m.Config.DumpDir, err) } } @@ -581,7 +584,7 @@ func (m *Manager) updateMispContainer() { } // Update the MISP container m.containers[mispContName] = mispContainer - m.containersSha256[mispContName] = Sha256StringArray(mispContainer) + m.containersSha256[mispContName] = utils.Sha256StringArray(mispContainer) } // AddEndpoint adds new endpoint to the manager diff --git a/api/manager_admin_api.go b/api/manager_admin_api.go index d15dae1..a72b2ad 100644 --- a/api/manager_admin_api.go +++ b/api/manager_admin_api.go @@ -213,10 +213,16 @@ func (m *Manager) admAPIEndpointCommand(wt http.ResponseWriter, rq *http.Request switch rq.Method { case "GET": + wait, _ := strconv.ParseBool(rq.URL.Query().Get("wait")) if euuid, err = muxGetVar(rq, "euuid"); err != nil { wt.Write(NewAdminAPIRespError(err).ToJSON()) } else { if endpt, ok := m.endpoints.GetByUUID(euuid); ok { + if endpt.Command != nil { + for wait && !endpt.Command.Completed { + time.Sleep(time.Millisecond * 50) + } + } wt.Write(NewAdminAPIResponse(endpt.Command).ToJSON()) } else { wt.Write(admErrStr(format("Unknown endpoint: %s", euuid))) @@ -342,10 +348,10 @@ func (m *Manager) admAPIEndpointLogs(wt http.ResponseWriter, rq *http.Request) { return } - // Default settings last 24h + // Default settings last hour if pStart == "" && pStop == "" && pPivot == "" && pDelta == "" { stop = time.Now() - start = stop.Add(-24 * time.Hour) + start = stop.Add(-1 * time.Hour) } // 10 min delta if delta is not provided @@ -838,13 +844,13 @@ func (m *Manager) runAdminAPI() { if m.Config.TLS.Empty() { // Bind to a port and pass our router in - log.Infof("Running HTTP server on: %s", uri) + log.Infof("Running admin HTTP API server on: %s", uri) if err := m.adminAPI.ListenAndServe(); err != http.ErrServerClosed { log.Panic(err) } } else { // Bind to a port and pass our router in - log.Infof("Running HTTPS server on: %s", uri) + log.Infof("Running admin HTTPS API server on: %s", uri) if err := m.adminAPI.ListenAndServeTLS(m.Config.TLS.Cert, m.Config.TLS.Key); err != http.ErrServerClosed { log.Panic(err) } diff --git a/api/manager_endpoint_api.go b/api/manager_endpoint_api.go index 415bd68..d3d2054 100644 --- a/api/manager_endpoint_api.go +++ b/api/manager_endpoint_api.go @@ -13,6 +13,7 @@ import ( "time" "github.com/0xrawsec/golang-evtx/evtx" + "github.com/0xrawsec/whids/utils" "github.com/0xrawsec/golang-utils/log" "github.com/0xrawsec/mux" @@ -148,13 +149,13 @@ func (m *Manager) runEndpointAPI() { if m.Config.TLS.Empty() { // Bind to a port and pass our router in - log.Infof("Running HTTP server on: %s", uri) + log.Infof("Running endpoint HTTP API server on: %s", uri) if err := m.endpointAPI.ListenAndServe(); err != http.ErrServerClosed { log.Panic(err) } } else { // Bind to a port and pass our router in - log.Infof("Running HTTPS server on: %s", uri) + log.Infof("Running endpoint HTTPS API server on: %s", uri) if err := m.endpointAPI.ListenAndServeTLS(m.Config.TLS.Cert, m.Config.TLS.Key); err != http.ErrServerClosed { log.Panic(err) } @@ -299,7 +300,7 @@ func (m *Manager) Collect(wt http.ResponseWriter, rq *http.Request) { } for _, path := range logPaths { if _, ok := paths[path]; !ok { - if err := os.MkdirAll(filepath.Dir(path), DefaultDirPerm); err != nil { + if err := os.MkdirAll(filepath.Dir(path), utils.DefaultPerms); err != nil { log.Errorf("Failed to create endpoint log directory %s: %s", path, err) } else { fd, err := os.OpenFile(path, os.O_APPEND|os.O_RDWR|os.O_CREATE, DefaultLogPerm) diff --git a/go.mod b/go.mod index af964b4..5196fb3 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/0xrawsec/whids require ( - github.com/0xrawsec/gene v1.6.11 - github.com/0xrawsec/golang-evtx v1.2.7 + github.com/0xrawsec/gene v1.6.13 + github.com/0xrawsec/golang-evtx v1.2.8 github.com/0xrawsec/golang-misp v1.0.3 - github.com/0xrawsec/golang-utils v1.1.8 + github.com/0xrawsec/golang-utils v1.2.0 github.com/0xrawsec/golang-win32 v1.0.9 github.com/0xrawsec/mux v1.6.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 diff --git a/go.sum b/go.sum index e9367c1..8ee2469 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,10 @@ github.com/0xrawsec/gene v1.6.10 h1:+VDQiktQuMTWoqYfu/hlDDI45zvOBdhg1zzKJrncoms= github.com/0xrawsec/gene v1.6.10/go.mod h1:BpgWF+kapIgjKJW/ZLq66mIHRSde6FRpiwpcXSbmUas= github.com/0xrawsec/gene v1.6.11 h1:E2A0tNIvbcGQfC9H52KXnI8GTUiJlcAdpt0bU74pwdk= github.com/0xrawsec/gene v1.6.11/go.mod h1:BpgWF+kapIgjKJW/ZLq66mIHRSde6FRpiwpcXSbmUas= +github.com/0xrawsec/gene v1.6.12 h1:3kVHcqDHTxDvcP9AEPDYQUWoZUzexqn5pUWI4gRDrXc= +github.com/0xrawsec/gene v1.6.12/go.mod h1:fJplwPEE/Pt593RvfgtaKzXwL6eSo/mbDM0VG29u5CA= +github.com/0xrawsec/gene v1.6.13 h1:Z4d0YLi7VMDVnMlDh9yL8PXIw1YS+FKFvNimMZT8IwE= +github.com/0xrawsec/gene v1.6.13/go.mod h1:zJ5jw49S81ju3+eDJbKMireX0ZmTZDV3tjoBVEqcYM0= github.com/0xrawsec/golang-evtx v1.2.4 h1:9ChVrqXWwZZ6NkN/xlWne32Qtkh23NSQYLJqH1DMMZ0= github.com/0xrawsec/golang-evtx v1.2.4/go.mod h1:RD+lv9ndoM/7XwvS5XViI51yAp5PDtVVJf8FM6Muro0= github.com/0xrawsec/golang-evtx v1.2.5 h1:PSri/6YSXlvcEZT1Ib6KNRauxpf74Bk94TJWpQbCE4Y= @@ -22,6 +26,8 @@ github.com/0xrawsec/golang-evtx v1.2.6 h1:3xVtjitLFGGIYyIDeDMeC/K0p9zJm5Hjxs+aaV github.com/0xrawsec/golang-evtx v1.2.6/go.mod h1:YamigAawNZtVBX39ksMQO3EWiXpTdrh7Hddo8pP65D8= github.com/0xrawsec/golang-evtx v1.2.7 h1:sHJ96Z4v6ypkTKwx1PlqvdB3dkpiJsMAykBpb6zhNkY= github.com/0xrawsec/golang-evtx v1.2.7/go.mod h1:YamigAawNZtVBX39ksMQO3EWiXpTdrh7Hddo8pP65D8= +github.com/0xrawsec/golang-evtx v1.2.8 h1:jbzuKsVq45oA/gEqu+E64cVgPpmOiiRzFHuOmYXG7/E= +github.com/0xrawsec/golang-evtx v1.2.8/go.mod h1:oN5j4PuiZ1cG5JJq5aW4FNeQrxFLdpIURg3XvbZjwdk= github.com/0xrawsec/golang-misp v1.0.3 h1:Y8fciKDbcRFPfmWOqlEaSOjJwe5Khx9v6FE5VDCCgNI= github.com/0xrawsec/golang-misp v1.0.3/go.mod h1:bF7MZPgPQFPtsXPvRLcIdrs09fZV7zYDRBKpLltd6oA= github.com/0xrawsec/golang-utils v1.1.0 h1:opQAwRONEfxOOl4nxhpPkXiTYgzAw0/wFATAffNjdII= @@ -30,6 +36,10 @@ github.com/0xrawsec/golang-utils v1.1.3 h1:ESJhyY4aGuiP4hmDcDNjoL/cc7SWDZVfgg4dE github.com/0xrawsec/golang-utils v1.1.3/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= github.com/0xrawsec/golang-utils v1.1.8 h1:9TzAKzC7+V2IXaV/3Y1aXiUFHeShL3BXcfL6BheAEH0= github.com/0xrawsec/golang-utils v1.1.8/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= +github.com/0xrawsec/golang-utils v1.1.9 h1:qhB8TfuQSphKjV0+8Kuc1vVmkIhXyo86Dq/U5ANpxm8= +github.com/0xrawsec/golang-utils v1.1.9/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= +github.com/0xrawsec/golang-utils v1.2.0 h1:wzPUcLLcx2NPV9txupkn7+KXOUuVG4zaKZ/Y5s7GJZQ= +github.com/0xrawsec/golang-utils v1.2.0/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= github.com/0xrawsec/golang-win32 v1.0.6 h1:wVvfd+trSeUkG6m5TFzeBtWHSHetfhPO3b5MVjTgsWk= github.com/0xrawsec/golang-win32 v1.0.6/go.mod h1:MAxVU7dr8lujwknuhf4TwjYm8tVEELi2zwx1zDTu/RM= github.com/0xrawsec/golang-win32 v1.0.7 h1:GJH0+QPzjNwMux+ziD7tdmmjO8lQklqdy4+Ktp5sPB8= @@ -41,7 +51,9 @@ github.com/0xrawsec/golang-win32 v1.0.9/go.mod h1:XBEh/JhPosCBiqLCoJsyyKF/+EdwbL github.com/0xrawsec/mux v1.6.2 h1:cc2OyJTxRmXxsmQe2ulp0VndXV8vZIRrc1JqQzJ4BMI= github.com/0xrawsec/mux v1.6.2/go.mod h1:CiOvEAd+RMn8YOtCs1b5QfWe7P8G4olvTmzzNbERonY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -51,30 +63,40 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= +github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/kafka-go v0.2.2 h1:KIUln5unPisRL2yyAkZsDR/coiymN9Djunv6JKGQ6JI= github.com/segmentio/kafka-go v0.2.2/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83 h1:mgAKeshyNqWKdENOnQsg+8dRTwZFIwFaO3HNl52sweA= golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190909003024-a7b16738d86b h1:XfVGCX+0T4WOStkaOsJRllbsiImhB2jgVBGc9L0lPGc= golang.org/x/net v0.0.0-20190909003024-a7b16738d86b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -83,13 +105,18 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190909082730-f460065e899a h1:mIzbOulag9/gXacgxKlFVwpCOWSfBT3/pDyyCwGA9as= golang.org/x/sys v0.0.0-20190909082730-f460065e899a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190320215829-36c10c0a621f/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190321211322-a94d7df2cbc8/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190625160430-252024b82959/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190909194007-75be6cdcda07 h1:lttDGkFxUqcdkT522GTSuVHkN+ZqZ16zIIJguFMBzuk= golang.org/x/tools v0.0.0-20190909194007-75be6cdcda07/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/tools/whids/canary.go b/hids/canary.go similarity index 68% rename from tools/whids/canary.go rename to hids/canary.go index 215e235..6262563 100644 --- a/tools/whids/canary.go +++ b/hids/canary.go @@ -1,4 +1,4 @@ -package main +package hids import ( "fmt" @@ -16,26 +16,29 @@ import ( "github.com/0xrawsec/whids/utils" ) +// Canary configuration type Canary struct { HideFiles bool `toml:"hide-files" comment:"Flag to set to hide files"` HideDirectories bool `toml:"hide-dirs" comment:"Flag to set to hide directories"` SetAuditACL bool `toml:"set-audit-acl" comment:"Set Audit ACL to the canary directories, sub-directories and files to generate File System audit events\n https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/audit-file-system"` - Directories []string `toml:"directories" comment:"Directory where canary files will be located"` + Directories []string `toml:"directories" comment:"Directories where canary files will be created"` Files []string `toml:"files" comment:"Canary files to monitor. Files will be created if not existing"` Delete bool `toml:"delete" comment:"Whether to delete or not the canary files when service stops"` - createdDir datastructs.SyncedSet + createdDir *datastructs.SyncedSet } -func (c *Canary) ExpandedDir() (dirs []string) { +// expands environment variables found in directories +func (c *Canary) expandDir() (dirs []string) { return utils.ExpandEnvs(c.Directories...) } -func (c *Canary) Create() (err error) { +// create the canary files and directories +func (c *Canary) create() (err error) { c.createdDir = datastructs.NewSyncedSet() - for _, dir := range c.ExpandedDir() { + for _, dir := range c.expandDir() { if !fsutil.Exists(dir) { - if err := os.MkdirAll(dir, 777); err != nil { + if err := os.MkdirAll(dir, 0777); err != nil { return err } if c.HideDirectories { @@ -47,7 +50,7 @@ func (c *Canary) Create() (err error) { } } - for _, fp := range c.Paths() { + for _, fp := range c.paths() { if !fsutil.Exists(fp) { var fd *os.File @@ -57,7 +60,7 @@ func (c *Canary) Create() (err error) { defer fd.Close() rand.Seed(time.Now().Unix()) buf := [1024]byte{} - size := rand.Int() % 50 * Mega + size := rand.Int() % 50 * utils.Mega written, n := 0, 0 for written < size && err == nil { if _, err = rand.Read(buf[:]); err != nil { @@ -79,29 +82,31 @@ func (c *Canary) Create() (err error) { return nil } -func (c *Canary) Clean() { +// clean the canary files +func (c *Canary) clean() { if c.Delete { // we remove canary files - for _, fp := range c.Paths() { + for _, fp := range c.paths() { os.Remove(fp) } // we remove only empty directories - for _, dir := range c.ExpandedDir() { + for _, dir := range c.expandDir() { os.Remove(dir) } // we remove directory which have been created - for _, i := range *(c.createdDir.List()) { + for _, i := range c.createdDir.List() { dir := i.(string) os.RemoveAll(dir) } } } -func (c *Canary) Paths() (files []string) { +// return a list containing the full paths of the canary files +func (c *Canary) paths() (files []string) { files = make([]string, 0, len(c.Files)*len(c.Directories)) - for _, dir := range c.ExpandedDir() { + for _, dir := range c.expandDir() { for _, fn := range c.Files { files = append(files, filepath.Join(dir, fn)) } @@ -109,23 +114,53 @@ func (c *Canary) Paths() (files []string) { return } +// CanariesConfig structure holding canary configuration type CanariesConfig struct { Enable bool `toml:"enable" comment:"Enable canary files management"` Actions []string `toml:"actions" comment:"Actions to apply when a canary file is touched"` Whitelist []string `toml:"whitelist" comment:"Process images being allowed to touch the canaries"` - Canaries []*Canary `toml:"canaries" comment:"Canary files to create at every run"` + Canaries []*Canary `toml:"group" comment:"Canary files to create at every run"` } -func (c *CanariesConfig) Initialize() { +func (c *CanariesConfig) canaryRegexp() string { + repaths := make([]string, 0) + for _, c := range c.Canaries { + + // adding list of created dir + for _, i := range c.createdDir.List() { + dir := fmt.Sprintf("%s%c", i.(string), os.PathSeparator) + repaths = append(repaths, regexp.QuoteMeta(dir)) + } + + for _, fp := range c.paths() { + dir := filepath.Dir(fp) + if !c.createdDir.Contains(dir) { + repaths = append(repaths, regexp.QuoteMeta(fp)) + } + } + } + return fmt.Sprintf("(?i:(%s))", strings.Join(repaths, "|")) +} + +func (c *CanariesConfig) whitelistRegexp() string { + wl := make([]string, 0, len(c.Whitelist)) + for _, im := range c.Whitelist { + wl = append(wl, regexp.QuoteMeta(im)) + } + return fmt.Sprintf("(?i:(%s))", strings.Join(wl, "|")) +} + +// Configure creates canaries and set ACLs if needed +func (c *CanariesConfig) Configure() { auditDirs := make([]string, 0) if c.Enable { for _, cf := range c.Canaries { // add the list of directories to audit if cf.SetAuditACL { - auditDirs = append(auditDirs, cf.ExpandedDir()...) + auditDirs = append(auditDirs, cf.expandDir()...) } - if err := cf.Create(); err != nil { + if err := cf.create(); err != nil { log.Errorf("Failed at creating canary: %s", err) } } @@ -139,12 +174,13 @@ func (c *CanariesConfig) Initialize() { } } +// RestoreACLs restore EDR configured ACLs func (c *CanariesConfig) RestoreACLs() { auditDirs := make([]string, 0) for _, cf := range c.Canaries { // add the list of directories to audit if cf.SetAuditACL { - auditDirs = append(auditDirs, cf.ExpandedDir()...) + auditDirs = append(auditDirs, cf.expandDir()...) } } if err := utils.RemoveEDRAuditACL(auditDirs...); err != nil { @@ -152,42 +188,7 @@ func (c *CanariesConfig) RestoreACLs() { } } -func (c *CanariesConfig) Clean() { - if c.Enable { - for _, cf := range c.Canaries { - cf.Clean() - } - } -} - -func (c *CanariesConfig) CanaryRegexp() string { - repaths := make([]string, 0) - for _, c := range c.Canaries { - - // adding list of created dir - for _, i := range *(c.createdDir.List()) { - dir := fmt.Sprintf("%s%c", i.(string), os.PathSeparator) - repaths = append(repaths, regexp.QuoteMeta(dir)) - } - - for _, fp := range c.Paths() { - dir := filepath.Dir(fp) - if !c.createdDir.Contains(dir) { - repaths = append(repaths, regexp.QuoteMeta(fp)) - } - } - } - return fmt.Sprintf("(?i:(%s))", strings.Join(repaths, "|")) -} - -func (c *CanariesConfig) WhitelistRegexp() string { - wl := make([]string, 0, len(c.Whitelist)) - for _, im := range c.Whitelist { - wl = append(wl, regexp.QuoteMeta(im)) - } - return fmt.Sprintf("(?i:(%s))", strings.Join(wl, "|")) -} - +// GenRuleFSAudit generate a rule matching FS Audit events for the configured canaries func (c *CanariesConfig) GenRuleFSAudit() (r rules.Rule) { r = rules.NewRule() r.Name = "Builtin:CanaryAccessed" @@ -197,15 +198,18 @@ func (c *CanariesConfig) GenRuleFSAudit() (r rules.Rule) { } r.Meta.Criticality = 10 r.Matches = []string{ - "$access: AccessMask &= '0x1'", - fmt.Sprintf("$wl_images: ProcessName ~= '%s'", c.WhitelistRegexp()), - fmt.Sprintf("$canary: ObjectName ~= '%s'", c.CanaryRegexp()), + "$read: AccessMask &= '0x1'", + "$write: AccessMask &= '0x2'", + "$append: AccessMask &= '0x4'", + fmt.Sprintf("$wl_images: ProcessName ~= '%s'", c.whitelistRegexp()), + fmt.Sprintf("$canary: ObjectName ~= '%s'", c.canaryRegexp()), } - r.Condition = "!$wl_images and $access and $canary" + r.Condition = "!$wl_images and ($read or $write or $append) and $canary" r.Actions = append(r.Actions, c.Actions...) return } +// GenRuleSysmon generate a rule matching sysmon events for the configured canaries func (c *CanariesConfig) GenRuleSysmon() (r rules.Rule) { r = rules.NewRule() r.Name = "Builtin:CanaryModified" @@ -216,10 +220,19 @@ func (c *CanariesConfig) GenRuleSysmon() (r rules.Rule) { } r.Meta.Criticality = 10 r.Matches = []string{ - fmt.Sprintf("$wl_images: Image ~= '%s'", c.WhitelistRegexp()), - fmt.Sprintf("$canary: TargetFilename ~= '%s'", c.CanaryRegexp()), + fmt.Sprintf("$wl_images: Image ~= '%s'", c.whitelistRegexp()), + fmt.Sprintf("$canary: TargetFilename ~= '%s'", c.canaryRegexp()), } r.Condition = "!$wl_images and $canary" r.Actions = append(r.Actions, c.Actions...) return } + +// Clean cleans up the canaries +func (c *CanariesConfig) Clean() { + if c.Enable { + for _, cf := range c.Canaries { + cf.clean() + } + } +} diff --git a/hids/commands.go b/hids/commands.go new file mode 100644 index 0000000..4a90666 --- /dev/null +++ b/hids/commands.go @@ -0,0 +1,181 @@ +package hids + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "io" + "io/fs" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "time" + + "github.com/0xrawsec/golang-utils/fsutil/fswalker" + "github.com/0xrawsec/whids/utils" +) + +func cmdHash(path string) (nfi FileInfo, err error) { + var fi fs.FileInfo + + if fi, err = os.Stat(path); err != nil { + return + } + + nfi.Dir = filepath.Dir(path) + nfi.FromFSFileInfo(fi) + err = nfi.Hash() + return +} + +type FileInfo struct { + Dir string `json:"dir"` + Name string `json:"name"` + Size int64 `json:"size"` + ModTime time.Time `json:"modtime"` + Type string `json:"type"` + Hashes map[string]string `json:"hashes,omitempty"` +} + +func (fi *FileInfo) Path() string { + return filepath.Join(fi.Dir, fi.Name) +} + +func (fi *FileInfo) Hash() error { + var buffer [4 * utils.Mega]byte + + file, err := os.Open(fi.Path()) + if err != nil { + return err + } + defer file.Close() + + fi.Hashes = make(map[string]string) + + md5 := md5.New() + sha1 := sha1.New() + sha256 := sha256.New() + sha512 := sha512.New() + + for read, err := file.Read(buffer[:]); err != io.EOF && read != 0; read, err = file.Read(buffer[:]) { + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return err + } + md5.Write(buffer[:read]) + sha1.Write(buffer[:read]) + sha256.Write(buffer[:read]) + sha512.Write(buffer[:read]) + } + + fi.Hashes["md5"] = hex.EncodeToString(md5.Sum(nil)) + fi.Hashes["sha1"] = hex.EncodeToString(sha1.Sum(nil)) + fi.Hashes["sha256"] = hex.EncodeToString(sha256.Sum(nil)) + fi.Hashes["sha512"] = hex.EncodeToString(sha512.Sum(nil)) + + return nil +} + +func (fi *FileInfo) FromFSFileInfo(fsfi fs.FileInfo) { + fi.Name = fsfi.Name() + fi.Size = fsfi.Size() + fi.ModTime = fsfi.ModTime() + switch { + case fsfi.IsDir(): + fi.Type = "dir" + case fsfi.Mode().IsRegular(): + fi.Type = "file" + case fsfi.Mode()&os.ModeSymlink == os.ModeSymlink: + fi.Type = "link" + } +} + +func cmdDir(path string) (sfi []FileInfo, err error) { + var ofi []fs.FileInfo + + if ofi, err = ioutil.ReadDir(path); err != nil { + return + } + + sfi = make([]FileInfo, len(ofi)) + for i, fi := range ofi { + sfi[i].Dir = path + sfi[i].FromFSFileInfo(fi) + } + + return +} + +type WalkItem struct { + Dirs []FileInfo `json:"dirs"` + Files []FileInfo `json:"files"` + Err string `json:"err"` +} + +func (wi *WalkItem) FromWalkerWalkItem(o fswalker.WalkItem) { + wi.Dirs = make([]FileInfo, len(o.Dirs)) + for i, fi := range o.Dirs { + wi.Dirs[i].Dir = o.Dirpath + wi.Dirs[i].FromFSFileInfo(fi) + } + + wi.Files = make([]FileInfo, len(o.Files)) + for i, fi := range o.Files { + wi.Files[i].Dir = o.Dirpath + wi.Files[i].FromFSFileInfo(fi) + } + + if o.Err != nil { + wi.Err = o.Err.Error() + } +} + +func cmdWalk(path string) []WalkItem { + out := make([]WalkItem, 0) + + for wi := range fswalker.Walk(path) { + new := WalkItem{} + new.FromWalkerWalkItem(wi) + out = append(out, new) + } + + return out +} + +func cmdFind(path string, pattern string) (out []FileInfo, err error) { + var pr *regexp.Regexp + + out = make([]FileInfo, 0) + + if pr, err = regexp.Compile(pattern); err != nil { + return + } + + for wi := range fswalker.Walk(path) { + for _, fi := range wi.Files { + path := filepath.Join(wi.Dirpath, fi.Name()) + if pr.MatchString(path) { + nfi := FileInfo{Dir: wi.Dirpath} + nfi.FromFSFileInfo(fi) + out = append(out, nfi) + } + + } + } + + return +} + +func cmdStat(path string) (nfi FileInfo, err error) { + var fi fs.FileInfo + + if fi, err = os.Stat(path); err != nil { + return + } + + nfi.Dir = filepath.Dir(path) + nfi.FromFSFileInfo(fi) + return +} diff --git a/hids/config.go b/hids/config.go new file mode 100644 index 0000000..82cc947 --- /dev/null +++ b/hids/config.go @@ -0,0 +1,163 @@ +package hids + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/0xrawsec/golang-utils/fsutil" + "github.com/0xrawsec/golang-utils/log" + "github.com/0xrawsec/whids/api" + "github.com/0xrawsec/whids/utils" + "github.com/pelletier/go-toml" +) + +// DumpConfig structure definition +type DumpConfig struct { + Mode string `toml:"mode" comment:"Dump mode (choices: file, registry, memory)\n Modes can be combined together, separated by |"` + Dir string `toml:"dir" comment:"Directory used to store dumps"` + Treshold int `toml:"treshold" comment:"Dumps only when event criticality is above this threshold"` + MaxDumps int `toml:"max-dumps" comment:"Maximum number of dumps per process"` // maximum number of dump per GUID + Compression bool `toml:"compression" comment:"Enable dumps compression"` + DumpUntracked bool `toml:"dump-untracked" comment:"Dumps untracked process. Untracked processes are missing\n enrichment information and may generate unwanted dumps"` // whether or not we should dump untracked processes, if true it would create many FPs +} + +// IsModeEnabled checks if dump mode is enabled +func (d *DumpConfig) IsModeEnabled(mode string) bool { + if strings.Contains(d.Mode, "all") { + return true + } + return strings.Contains(d.Mode, mode) +} + +// SysmonConfig holds Sysmon related configuration +type SysmonConfig struct { + Bin string `toml:"bin" comment:"Path to Sysmon binary"` + ArchiveDirectory string `toml:"archive-directory" comment:"Path to Sysmon Archive directory"` + CleanArchived bool `toml:"clean-archived" comment:"Delete files older than 5min archived by Sysmon"` +} + +// RulesConfig holds rules configuration +type RulesConfig struct { + RulesDB string `toml:"rules-db" comment:"Path to Gene rules database"` + ContainersDB string `toml:"containers-db" comment:"Path to Gene rules containers\n (c.f. Gene documentation)"` + UpdateInterval time.Duration `toml:"update-interval" comment:"Update interval at which rules should be pulled from manager\n NB: only applies if a manager server is configured"` +} + +// AuditConfig holds Windows audit configuration +type AuditConfig struct { + Enable bool `toml:"enable" comment:"Enable following Audit Policies or not"` + AuditPolicies []string `toml:"audit-policies" comment:"Audit Policies to enable (c.f. auditpol /get /category:* /r)"` + AuditDirs []string `toml:"audit-dirs" comment:"Set Audit ACL to directories, sub-directories and files to generate File System audit events\n https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/audit-file-system)"` +} + +// Configure configures the desired audit policies +func (c *AuditConfig) Configure() { + + if c.Enable { + for _, ap := range c.AuditPolicies { + if err := utils.EnableAuditPolicy(ap); err != nil { + log.Errorf("Failed to enable audit policy %s: %s", ap, err) + } else { + log.Infof("Enabled Audit Policy: %s", ap) + } + } + } + + // run this function async as it might take a little bit of time + go func() { + dirs := utils.StdDirs(utils.ExpandEnvs(c.AuditDirs...)...) + log.Infof("Setting ACLs for directories: %s", strings.Join(dirs, ", ")) + if err := utils.SetEDRAuditACL(dirs...); err != nil { + log.Errorf("Error while setting configured File System Audit ACLs: %s", err) + } + log.Infof("Finished setting up ACLs for directories: %s", strings.Join(dirs, ", ")) + }() +} + +// Restore the audit policies +func (c *AuditConfig) Restore() { + for _, ap := range c.AuditPolicies { + if err := utils.DisableAuditPolicy(ap); err != nil { + log.Errorf("Failed to disable audit policy %s: %s", ap, err) + } + } + + dirs := utils.StdDirs(utils.ExpandEnvs(c.AuditDirs...)...) + if err := utils.RemoveEDRAuditACL(dirs...); err != nil { + log.Errorf("Error while restoring File System Audit ACLs: %s", err) + } +} + +// Config structure +type Config struct { + Channels []string `toml:"channels" comment:"Windows log channels to listen to. Either channel names\n can be used (i.e. Microsoft-Windows-Sysmon/Operational) or aliases"` + CritTresh int `toml:"criticality-treshold" comment:"Dumps/forward only events above criticality threshold\n or filtered events (i.e. Gene filtering rules)"` + EnableHooks bool `toml:"en-hooks" comment:"Enable enrichment hooks and dump hooks"` + EnableFiltering bool `toml:"en-filters" comment:"Enable event filtering (log filtered events, not only alerts)\n See documentation: https://github.com/0xrawsec/gene"` + Logfile string `toml:"logfile" comment:"Logfile used to log messages generated by the engine"` // for WHIDS log messages (not alerts) + LogAll bool `toml:"log-all" comment:"Log any incoming event passing through the engine"` // log all events to logfile (used for debugging) + Endpoint bool `toml:"endpoint" comment:"True if current host is the endpoint on which logs are generated\n Example: turn this off if running on a WEC"` + FwdConfig *api.ForwarderConfig `toml:"forwarder" comment:"Forwarder configuration"` + Sysmon *SysmonConfig `toml:"sysmon" comment:"Sysmon related settings"` + Dump *DumpConfig `toml:"dump" comment:"Dump related settings"` + Report *ReportConfig `toml:"reporting" comment:"Reporting related settings"` + RulesConfig *RulesConfig `toml:"rules" comment:"Gene rules related settings\n Gene repo: https://github.com/0xrawsec/gene\n Gene rules repo: https://github.com/0xrawsec/gene-rules"` + AuditConfig *AuditConfig `toml:"audit" comment:"Windows auditing configuration"` + CanariesConfig *CanariesConfig `toml:"canaries" comment:"Canary files configuration"` +} + +// LoadsHIDSConfig loads a HIDS configuration from a file +func LoadsHIDSConfig(path string) (c Config, err error) { + fd, err := os.Open(path) + if err != nil { + return + } + defer fd.Close() + dec := toml.NewDecoder(fd) + err = dec.Decode(&c) + return +} + +// IsDumpEnabled returns true if any kind of dump is enabled +func (c *Config) IsDumpEnabled() bool { + // Dump can be enabled only in endpoint mode + return c.Endpoint && (c.Dump.IsModeEnabled("file") || c.Dump.IsModeEnabled("registry") || c.Dump.IsModeEnabled("memory")) +} + +// IsForwardingEnabled returns true if a forwarder is actually configured to forward logs +func (c *Config) IsForwardingEnabled() bool { + return *c.FwdConfig != emptyForwarderConfig && !c.FwdConfig.Local +} + +// Prepare creates directory used in the config if not existing +func (c *Config) Prepare() { + if !fsutil.Exists(c.RulesConfig.RulesDB) { + os.MkdirAll(c.RulesConfig.RulesDB, 0600) + } + if !fsutil.Exists(c.RulesConfig.ContainersDB) { + os.MkdirAll(c.RulesConfig.ContainersDB, 0600) + } + if !fsutil.Exists(c.Dump.Dir) { + os.MkdirAll(c.Dump.Dir, 0600) + } + if !fsutil.Exists(filepath.Dir(c.FwdConfig.Logging.Dir)) { + os.MkdirAll(filepath.Dir(c.FwdConfig.Logging.Dir), 0600) + } + if !fsutil.Exists(filepath.Dir(c.Logfile)) { + os.MkdirAll(filepath.Dir(c.Logfile), 0600) + } +} + +// Verify validate HIDS configuration object +func (c *Config) Verify() error { + if !fsutil.IsDir(c.RulesConfig.RulesDB) { + return fmt.Errorf("rules database must be a directory") + } + if !fsutil.IsDir(c.RulesConfig.ContainersDB) { + return fmt.Errorf("containers database must be a directory") + } + return nil +} diff --git a/hids/eventids.go b/hids/eventids.go new file mode 100644 index 0000000..e69b891 --- /dev/null +++ b/hids/eventids.go @@ -0,0 +1,37 @@ +package hids + +// Sysmon Event IDs +const ( + _ = iota + SysmonProcessCreate + SysmonFileTime + SysmonNetworkConnect + SysmonServiceStateChange + SysmonProcessTerminate + SysmonDriverLoad + SysmonImageLoad + SysmonCreateRemoteThread + SysmonRawAccessRead + SysmonAccessProcess + SysmonFileCreate + SysmonRegKey + SysmonRegSetValue + SysmonRegName + SysmonCreateStreamHash + SysmonServiceConfigurationChange + SysmonCreateNamedPipe + SysmonConnectNamedPipe + SysmonWMIFilter + SysmonWMIConsumer + SysmonWMIBinding + SysmonDNSQuery + SysmonFileDelete + SysmonClipboardChange + SysmonProcessTampering + SysmonFileDeleteDetected +) + +const ( + // https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4663 + SecurityAccessObject = 4663 +) diff --git a/hids/filters.go b/hids/filters.go new file mode 100644 index 0000000..30f1362 --- /dev/null +++ b/hids/filters.go @@ -0,0 +1,72 @@ +package hids + +import ( + "github.com/0xrawsec/golang-evtx/evtx" + "github.com/0xrawsec/golang-utils/datastructs" +) + +var ( + // sysmonChannel Sysmon windows event log channel + sysmonChannel = "Microsoft-Windows-Sysmon/Operational" + // securityChannel Security windows event log channel + securityChannel = "Security" + + // Filters definitions + fltAnyEvent = NewFilter([]int64{}, "") + + // Sysmon filters + fltAnySysmon = NewFilter([]int64{}, sysmonChannel) + fltProcessCreate = NewFilter([]int64{SysmonProcessCreate}, sysmonChannel) + fltTrack = NewFilter([]int64{SysmonProcessCreate, SysmonDriverLoad}, sysmonChannel) + fltProcTermination = NewFilter([]int64{SysmonProcessTerminate}, sysmonChannel) + fltImageLoad = NewFilter([]int64{SysmonImageLoad}, sysmonChannel) + fltRegSetValue = NewFilter([]int64{SysmonRegSetValue}, sysmonChannel) + //fltNetwork = NewFilter([]int64{SysmonNetworkConnect, SysmonDNSQuery}, sysmonChannel) + //fltDNS = NewFilter([]int64{SysmonDNSQuery}, sysmonChannel) + fltClipboard = NewFilter([]int64{SysmonClipboardChange}, sysmonChannel) + fltImageTampering = NewFilter([]int64{SysmonProcessTampering}, sysmonChannel) + + fltImageSize = NewFilter([]int64{ + SysmonProcessCreate, + SysmonDriverLoad, + SysmonImageLoad}, + sysmonChannel) + + fltStats = NewFilter([]int64{ + SysmonProcessCreate, + SysmonNetworkConnect, + SysmonFileCreate, + SysmonDNSQuery, + SysmonFileDelete, + SysmonFileDeleteDetected}, + sysmonChannel) + + // Security filters + fltFSObjectAccess = NewFilter([]int64{SecurityAccessObject}, securityChannel) +) + +// Filter structure +type Filter struct { + EventIDs *datastructs.SyncedSet + Channel string +} + +// NewFilter creates a new Filter structure +func NewFilter(eids []int64, channel string) *Filter { + f := &Filter{} + f.EventIDs = datastructs.NewInitSyncedSet(datastructs.ToInterfaceSlice(eids)...) + f.Channel = channel + return f +} + +// Match checks if an event matches the filter +func (f *Filter) Match(e *evtx.GoEvtxMap) bool { + if !f.EventIDs.Contains(e.EventID()) && f.EventIDs.Len() > 0 { + return false + } + // Don't check channel if empty string + if f.Channel != "" && f.Channel != e.Channel() { + return false + } + return true +} diff --git a/tools/whids/whids.go b/hids/hids.go similarity index 59% rename from tools/whids/whids.go rename to hids/hids.go index 55c192f..1d4f641 100644 --- a/tools/whids/whids.go +++ b/hids/hids.go @@ -1,4 +1,4 @@ -package main +package hids import ( "compress/gzip" @@ -9,13 +9,13 @@ import ( "os/exec" "path/filepath" "regexp" + "strconv" "strings" "sync" "time" "github.com/0xrawsec/golang-win32/win32" "github.com/0xrawsec/golang-win32/win32/kernel32" - "github.com/pelletier/go-toml" "github.com/0xrawsec/gene/engine" "github.com/0xrawsec/golang-evtx/evtx" @@ -24,14 +24,12 @@ import ( "github.com/0xrawsec/golang-utils/fsutil" "github.com/0xrawsec/golang-utils/fsutil/fswalker" "github.com/0xrawsec/golang-utils/log" + "github.com/0xrawsec/golang-utils/sync/semaphore" "github.com/0xrawsec/golang-win32/win32/wevtapi" "github.com/0xrawsec/whids/api" - "github.com/0xrawsec/whids/hooks" "github.com/0xrawsec/whids/utils" ) -/////////////////////////////////// Main /////////////////////////////////////// - // XMLEventToGoEvtxMap converts an XMLEvent as returned by wevtapi to a GoEvtxMap // object that Gene can use // TODO: Improve for more perf @@ -48,22 +46,19 @@ func XMLEventToGoEvtxMap(xe *wevtapi.XMLEvent) (*evtx.GoEvtxMap, error) { return &ge, nil } -/////////////////////////////////// Main /////////////////////////////////////// - const ( - // Default permissions for output files - defaultPerms = 0640 + /** Private const **/ // Container extension containerExt = ".cont.gz" ) var ( - abs, _ = filepath.Abs(filepath.Dir(os.Args[0])) + /** Public vars **/ - dumpOptions = []string{"registry", "memory", "file", "all"} + DumpOptions = []string{"registry", "memory", "file", "all"} - channelAliases = map[string]string{ + ChannelAliases = map[string]string{ "sysmon": "Microsoft-Windows-Sysmon/Operational", "security": "Security", "ps": "Microsoft-Windows-PowerShell/Operational", @@ -71,6 +66,12 @@ var ( "all": "All aliased channels", } + ContainRuleName = "EDR containment" + + /** Private vars **/ + + emptyForwarderConfig = api.ForwarderConfig{} + // extensions of files to upload to manager uploadExts = datastructs.NewInitSyncedSet(".gz", ".sha256") @@ -78,8 +79,8 @@ var ( ) func allChannels() []string { - channels := make([]string, 0, len(channelAliases)) - for alias, channel := range channelAliases { + channels := make([]string, 0, len(ChannelAliases)) + for alias, channel := range ChannelAliases { if alias != "all" { channels = append(channels, channel) } @@ -87,233 +88,36 @@ func allChannels() []string { return channels } -var ( - emptyForwarderConfig = api.ForwarderConfig{} - logDir = filepath.Join(abs, "Logs") - - // DefaultHIDSConfig is the default HIDS configuration - DefaultHIDSConfig = HIDSConfig{ - RulesConfig: &RulesConfig{ - RulesDB: filepath.Join(abs, "Database", "Rules"), - ContainersDB: filepath.Join(abs, "Database", "Containers"), - UpdateInterval: 60 * time.Second, - }, - - FwdConfig: &api.ForwarderConfig{ - Local: true, - Client: api.ClientConfig{ - MaxUploadSize: api.DefaultMaxUploadSize, - }, - Logging: api.LoggingConfig{ - Dir: filepath.Join(logDir, "Alerts"), - RotationInterval: time.Hour * 5, - }, - }, - Channels: []string{"all"}, - Sysmon: &SysmonConfig{ - Bin: "C:\\Windows\\Sysmon64.exe", - ArchiveDirectory: "C:\\Sysmon\\", - CleanArchived: true, - }, - Dump: &DumpConfig{ - Mode: "file|registry", - Dir: filepath.Join(abs, "Dumps"), - Compression: true, - MaxDumps: 4, - Treshold: 8, - DumpUntracked: false, - }, - AuditConfig: &AuditConfig{ - AuditPolicies: []string{"File System"}, - }, - CanariesConfig: &CanariesConfig{ - Enable: false, - Canaries: []*Canary{ - { - Directories: []string{"$SYSTEMDRIVE", "$SYSTEMROOT"}, - Files: []string{"readme.pdf", "readme.docx", "readme.txt"}, - Delete: true, - }, - }, - Actions: []string{"kill", "memdump", "filedump", "blacklist"}, - Whitelist: []string{"C:\\Windows\\explorer.exe"}, - }, - CritTresh: 5, - Logfile: filepath.Join(logDir, "whids.log"), - EnableHooks: true, - EnableFiltering: true, - Endpoint: true, - LogAll: false} -) - -// DumpConfig structure definition -type DumpConfig struct { - Mode string `toml:"mode" comment:"Dump mode (choices: file, registry, memory)\n Modes can be combined together, separated by |"` - Dir string `toml:"dir" comment:"Directory used to store dumps"` - Treshold int `toml:"treshold" comment:"Dumps only when event criticality is above this threshold"` - MaxDumps int `toml:"max-dumps" comment:"Maximum number of dumps per process"` // maximum number of dump per GUID - Compression bool `toml:"compression" comment:"Enable dumps compression"` - DumpUntracked bool `toml:"dump-untracked" comment:"Dumps untracked process. Untracked processes are missing\n enrichment information and may generate unwanted dumps"` // whether or not we should dump untracked processes, if true it would create many FPs -} - -// IsModeEnabled checks if dump mode is enabled -func (d *DumpConfig) IsModeEnabled(mode string) bool { - if strings.Index(d.Mode, "all") != -1 { - return true - } - return strings.Index(d.Mode, mode) != -1 -} - -type SysmonConfig struct { - Bin string `toml:"bin" comment:"Path to Sysmon binary"` - ArchiveDirectory string `toml:"archive-directory" comment:"Path to Sysmon Archive directory"` - CleanArchived bool `toml:"clean-archived" comment:"Delete files older than 5min archived by Sysmon"` -} - -type RulesConfig struct { - RulesDB string `toml:"rules-db" comment:"Path to Gene rules database"` - ContainersDB string `toml:"containers-db" comment:"Path to Gene rules containers\n (c.f. Gene documentation)"` - UpdateInterval time.Duration `toml:"update-interval" comment:"Update interval at which rules should be pulled from manager\n NB: only applies if a manager server is configured"` -} - -type AuditConfig struct { - Enable bool `toml:"enable" comment:"Enable following Audit Policies or not"` - AuditPolicies []string `toml:"audit-policies" comment:"Audit Policies to enable (c.f. auditpol /get /category:* /r)"` - AuditDirs []string `toml:"audit-dirs" comment:"Set Audit ACL to directories, sub-directories and files to generate File System audit events\n https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/audit-file-system)"` -} - -func (c *AuditConfig) Initialize() { - - if c.Enable { - for _, ap := range c.AuditPolicies { - if err := utils.EnableAuditPolicy(ap); err != nil { - log.Errorf("Failed to enable audit policy %s: %s", ap, err) - } else { - log.Infof("Enabled Audit Policy: %s", ap) - } - } - } - - // run this function async as it might take a little bit of time - go func() { - dirs := utils.StdDirs(utils.ExpandEnvs(c.AuditDirs...)...) - log.Infof("Setting ACLs for directories: %s", strings.Join(dirs, ", ")) - if err := utils.SetEDRAuditACL(dirs...); err != nil { - log.Errorf("Error while setting configured File System Audit ACLs: %s", err) - } - log.Infof("Finished setting up ACLs for directories: %s", strings.Join(dirs, ", ")) - }() -} - -func (c *AuditConfig) Restore() { - for _, ap := range c.AuditPolicies { - if err := utils.DisableAuditPolicy(ap); err != nil { - log.Errorf("Failed to disable audit policy %s: %s", ap, err) - } - } - - dirs := utils.StdDirs(utils.ExpandEnvs(c.AuditDirs...)...) - if err := utils.RemoveEDRAuditACL(dirs...); err != nil { - log.Errorf("Error while restoring File System Audit ACLs: %s", err) - } -} - -// HIDSConfig structure -type HIDSConfig struct { - Channels []string `toml:"channels" comment:"Windows log channels to listen to. Either channel names\n can be used (i.e. Microsoft-Windows-Sysmon/Operational) or aliases"` - CritTresh int `toml:"criticality-treshold" comment:"Dumps/forward only events above criticality threshold\n or filtered events (i.e. Gene filtering rules)"` - EnableHooks bool `toml:"en-hooks" comment:"Enable enrichment hooks and dump hooks"` - EnableFiltering bool `toml:"en-filters" comment:"Enable event filtering (log filtered events, not only alerts)\n See documentation: https://github.com/0xrawsec/gene"` - Logfile string `toml:"logfile" comment:"Logfile used to log messages generated by the engine"` // for WHIDS log messages (not alerts) - LogAll bool `toml:"log-all" comment:"Log any incoming event passing through the engine"` // log all events to logfile (used for debugging) - Endpoint bool `toml:"endpoint" comment:"True if current host is the endpoint on which logs are generated\n Example: turn this off if running on a WEC"` - FwdConfig *api.ForwarderConfig `toml:"forwarder" comment:"Forwarder configuration"` - Sysmon *SysmonConfig `toml:"sysmon" comment:"Sysmon related settings"` - Dump *DumpConfig `toml:"dump" comment:"Dump related settings"` - RulesConfig *RulesConfig `toml:"rules" comment:"Gene rules related settings\n Gene repo: https://github.com/0xrawsec/gene\n Gene rules repo: https://github.com/0xrawsec/gene-rules"` - AuditConfig *AuditConfig `toml:"audit-config" comment:"Windows auditing configuration"` - CanariesConfig *CanariesConfig `toml:"canary-config" comment:"Canary files configuration"` -} - -// LoadsHIDSConfig loads a HIDS configuration from a file -func LoadsHIDSConfig(path string) (c HIDSConfig, err error) { - fd, err := os.Open(path) - if err != nil { - return - } - defer fd.Close() - dec := toml.NewDecoder(fd) - err = dec.Decode(&c) - return -} - -// SetHooksGlobals sets the global variables used by hooks -func (c *HIDSConfig) SetHooksGlobals() { - dumpDirectory = c.Dump.Dir - dumpTresh = c.Dump.Treshold - flagDumpCompress = c.Dump.Compression - flagDumpUntracked = c.Dump.DumpUntracked - maxDumps = c.Dump.MaxDumps - sysmonArchiveDirectory = c.Sysmon.ArchiveDirectory -} - -// IsDumpEnabled returns true if any kind of dump is enabled -func (c *HIDSConfig) IsDumpEnabled() bool { - // Dump can be enabled only in endpoint mode - return c.Endpoint && (c.Dump.IsModeEnabled("file") || c.Dump.IsModeEnabled("registry") || c.Dump.IsModeEnabled("memory")) -} - -// IsForwardingEnabled returns true if a forwarder is actually configured to forward logs -func (c *HIDSConfig) IsForwardingEnabled() bool { - return *c.FwdConfig != emptyForwarderConfig && !c.FwdConfig.Local -} - -// Prepare creates directory used in the config if not existing -func (c *HIDSConfig) Prepare() { - if !fsutil.Exists(c.RulesConfig.RulesDB) { - os.MkdirAll(c.RulesConfig.RulesDB, 0600) - } - if !fsutil.Exists(c.RulesConfig.ContainersDB) { - os.MkdirAll(c.RulesConfig.ContainersDB, 0600) - } - if !fsutil.Exists(c.Dump.Dir) { - os.MkdirAll(c.Dump.Dir, 0600) - } - if !fsutil.Exists(filepath.Dir(c.FwdConfig.Logging.Dir)) { - os.MkdirAll(filepath.Dir(c.FwdConfig.Logging.Dir), 0600) - } - if !fsutil.Exists(filepath.Dir(c.Logfile)) { - os.MkdirAll(filepath.Dir(c.Logfile), 0600) - } -} - -// Verify validate HIDS configuration object -func (c *HIDSConfig) Verify() error { - if !fsutil.IsDir(c.RulesConfig.RulesDB) { - return fmt.Errorf("Rules database must be a directory") - } - if !fsutil.IsDir(c.RulesConfig.ContainersDB) { - return fmt.Errorf("Containers database must be a directory") - } - return nil -} - // HIDS structure type HIDS struct { sync.RWMutex // Mutex to lock the IDS when updating rules eventProvider wevtapi.EventProvider - engine engine.Engine - preHooks *hooks.HookManager - postHooks *hooks.HookManager + preHooks *HookManager + postHooks *HookManager forwarder *api.Forwarder - channels datastructs.SyncedSet // Windows log channels to listen to + channels *datastructs.SyncedSet // Windows log channels to listen to channelsSignals chan bool - config *HIDSConfig + config *Config eventScanned uint64 alertReported uint64 startTime time.Time waitGroup sync.WaitGroup + flagProcTermEn bool + bootCompleted bool + // Sysmon GUID of HIDS process + guid string + processTracker *ActivityTracker + memdumped *datastructs.SyncedSet + dumping *datastructs.SyncedSet + filedumped *datastructs.SyncedSet + hookSemaphore semaphore.Semaphore + + // Compression management + compressionIsRunning bool + compressionChannel chan string + + Engine engine.Engine DryRun bool PrintAll bool } @@ -325,16 +129,23 @@ func newActionnableEngine() (e engine.Engine) { } // NewHIDS creates a new HIDS object from configuration -func NewHIDS(c *HIDSConfig) (h *HIDS, err error) { +func NewHIDS(c *Config) (h *HIDS, err error) { h = &HIDS{ // PushEventProvider seems not to retrieve all the events (observed this at boot) - eventProvider: wevtapi.NewPullEventProvider(), - preHooks: hooks.NewHookMan(), - postHooks: hooks.NewHookMan(), - channels: datastructs.NewSyncedSet(), - channelsSignals: make(chan bool), - config: c, - waitGroup: sync.WaitGroup{}} + eventProvider: wevtapi.NewPullEventProvider(), + preHooks: NewHookMan(), + postHooks: NewHookMan(), + channels: datastructs.NewSyncedSet(), + channelsSignals: make(chan bool), + config: c, + waitGroup: sync.WaitGroup{}, + processTracker: NewActivityTracker(), + memdumped: datastructs.NewSyncedSet(), + dumping: datastructs.NewSyncedSet(), + filedumped: datastructs.NewSyncedSet(), + hookSemaphore: semaphore.New(4), + compressionChannel: make(chan string), + } // Creates missing directories c.Prepare() @@ -344,9 +155,6 @@ func NewHIDS(c *HIDSConfig) (h *HIDS, err error) { log.SetLogfile(c.Logfile, 0600) } - // Set the globals used by the Hooks - c.SetHooksGlobals() - // Verify configuration if err = c.Verify(); err != nil { return nil, err @@ -364,9 +172,9 @@ func NewHIDS(c *HIDSConfig) (h *HIDS, err error) { h.initChannels(c.Channels) h.initHooks(c.EnableHooks) // initializing canaries - h.config.CanariesConfig.Initialize() + h.config.CanariesConfig.Configure() // fixing local audit policies if necessary - h.config.AuditConfig.Initialize() + h.config.AuditConfig.Configure() // tries to update the engine if err := h.updateEngine(true); err != nil { @@ -375,13 +183,15 @@ func NewHIDS(c *HIDSConfig) (h *HIDS, err error) { return h, nil } +/** Private Methods **/ + func (h *HIDS) initChannels(channels []string) { for _, c := range channels { if c == "all" { h.channels.Add(datastructs.ToInterfaceSlice(allChannels())...) continue } - if rc, ok := channelAliases[c]; ok { + if rc, ok := ChannelAliases[c]; ok { h.channels.Add(rc) } else { h.channels.Add(c) @@ -395,13 +205,12 @@ func (h *HIDS) initHooks(advanced bool) { h.preHooks.Hook(hookSelfGUID, fltImageSize) h.preHooks.Hook(hookProcTerm, fltProcTermination) h.preHooks.Hook(hookStats, fltStats) - h.preHooks.Hook(hookTrack, fltProcessCreate) + h.preHooks.Hook(hookTrack, fltTrack) if advanced { // Process terminator hook, terminating blacklisted (by action) processes h.preHooks.Hook(hookTerminator, fltProcessCreate) h.preHooks.Hook(hookImageLoad, fltImageLoad) h.preHooks.Hook(hookSetImageSize, fltImageSize) - //h.preHooks.Hook(hookProcessIntegrity, fltImageSize) h.preHooks.Hook(hookProcessIntegrityProcTamp, fltImageTampering) h.preHooks.Hook(hookEnrichServices, fltAnySysmon) h.preHooks.Hook(hookClipboardEvents, fltClipboard) @@ -409,8 +218,8 @@ func (h *HIDS) initHooks(advanced bool) { // Must be run the last as it depends on other filters h.preHooks.Hook(hookEnrichAnySysmon, fltAnySysmon) // Not sastifying results with Sysmon 11.11 we should try enabling this on newer versions - // h.preHooks.Hook(hookDNS, fltDNS) - // h.preHooks.Hook(hookEnrichDNSSysmon, fltNetworkConnect) + //h.preHooks.Hook(HookDNS, fltDNS) + //h.preHooks.Hook(hookEnrichDNSSysmon, fltNetworkConnect) // Experimental //h.preHooks.Hook(hookSetValueSize, fltRegSetValue) @@ -428,74 +237,14 @@ func (h *HIDS) initHooks(advanced bool) { } } + // This hook must run before action handling as we want + // the gene score to be set before an eventual reporting + h.postHooks.Hook(hookUpdateGeneScore, fltAnyEvent) // Handles actions defined in rules for any Sysmon event h.postHooks.Hook(hookHandleActions, fltAnyEvent) } } -func (h *HIDS) cleanArchivedRoutine() bool { - if h.config.Sysmon.CleanArchived { - go func() { - log.Info("Starting routine to cleanup Sysmon archived files") - archivePath := h.config.Sysmon.ArchiveDirectory - - if archivePath == "" { - log.Error("Sysmon archive directory not found") - return - } - - if fsutil.IsDir(archivePath) { - // used to mark files for which we already reported errors - reported := datastructs.NewSyncedSet() - log.Infof("Starting archive cleanup loop for directory: %s", archivePath) - for { - // expiration fixed to five minutes - expired := time.Now().Add(time.Minute * -5) - for wi := range fswalker.Walk(archivePath) { - for _, fi := range wi.Files { - if archivedRe.MatchString(fi.Name()) { - path := filepath.Join(wi.Dirpath, fi.Name()) - if fi.ModTime().Before(expired) { - // we print out error only once - if err := os.Remove(path); err != nil && !reported.Contains(path) { - log.Errorf("Failed to remove archived file: %s", err) - reported.Add(path) - } - } - } - } - } - time.Sleep(time.Minute * 1) - } - } else { - log.Errorf(fmt.Sprintf("No such Sysmon archive directory: %s", archivePath)) - } - }() - return true - } - return false -} - -// returns true if the update routine is started -func (h *HIDS) updateRoutine() bool { - d := h.config.RulesConfig.UpdateInterval - if h.config.IsForwardingEnabled() { - if d > 0 { - go func() { - t := time.NewTimer(d) - for range t.C { - if err := h.updateEngine(false); err != nil { - log.Error(err) - } - t.Reset(d) - } - }() - return true - } - } - return false -} - func (h *HIDS) updateEngine(force bool) error { h.Lock() defer h.Unlock() @@ -522,12 +271,12 @@ func (h *HIDS) updateEngine(force bool) error { if reloadRules || reloadContainers || force { // We need to create a new engine if we received a rule/containers update - h.engine = newActionnableEngine() + h.Engine = newActionnableEngine() // containers must be loaded before the rules anyway log.Infof("Loading HIDS containers (used in rules) from: %s", h.config.RulesConfig.ContainersDB) if err := h.loadContainers(); err != nil { - return fmt.Errorf("Error loading containers: %s", err) + return fmt.Errorf("error loading containers: %s", err) } if reloadRules || force { @@ -536,28 +285,26 @@ func (h *HIDS) updateEngine(force bool) error { log.Infof("Loading canary rules") // Sysmon rule sr := h.config.CanariesConfig.GenRuleSysmon() - log.Info(utils.PrettyJSON(sr)) if scr, err := sr.Compile(nil); err != nil { log.Errorf("Failed to compile canary rule: %s", err) } else { - h.engine.AddRule(scr) + h.Engine.AddRule(scr) } // File System Audit Rule fsr := h.config.CanariesConfig.GenRuleFSAudit() - log.Info(utils.PrettyJSON(fsr)) if fscr, err := fsr.Compile(nil); err != nil { log.Errorf("Failed to compile canary rule: %s", err) } else { - h.engine.AddRule(fscr) + h.Engine.AddRule(fscr) } } log.Infof("Loading HIDS rules from: %s", h.config.RulesConfig.RulesDB) - if err := h.engine.LoadDirectory(h.config.RulesConfig.RulesDB); err != nil { - return fmt.Errorf("Failed to load rules: %s", err) + if err := h.Engine.LoadDirectory(h.config.RulesConfig.RulesDB); err != nil { + return fmt.Errorf("failed to load rules: %s", err) } - log.Infof("Number of rules loaded in engine: %d", h.engine.Count()) + log.Infof("Number of rules loaded in engine: %d", h.Engine.Count()) } } else { log.Debug("Neither rules nor containers need to be updated") @@ -570,7 +317,7 @@ func (h *HIDS) updateEngine(force bool) error { func (h *HIDS) needsRulesUpdate() bool { var err error var oldSha256, sha256 string - _, rulesSha256Path := h.rulesPaths() + _, rulesSha256Path := h.RulesPaths() if h.forwarder.Local { return false @@ -582,10 +329,7 @@ func (h *HIDS) needsRulesUpdate() bool { oldSha256, _ = utils.ReadFileString(rulesSha256Path) log.Debugf("Rules: remote=%s local=%s", sha256, oldSha256) - if oldSha256 != sha256 { - return true - } - return false + return oldSha256 != sha256 } // at least one container needs to be updated @@ -619,16 +363,13 @@ func (h *HIDS) needsContainerUpdate(remoteCont string) bool { remoteSha256, _ = h.forwarder.Client.GetContainerSha256(remoteCont) localSha256, _ = utils.ReadFileString(locContSha256Path) log.Infof("container %s: remote=%s local=%s", remoteCont, remoteSha256, localSha256) - if localSha256 != remoteSha256 { - return true - } - return false + return localSha256 != remoteSha256 } func (h *HIDS) fetchRulesFromManager() (err error) { var rules, sha256 string - rulePath, sha256Path := h.rulesPaths() + rulePath, sha256Path := h.RulesPaths() // if we are not connected to a manager we return if h.config.FwdConfig.Local { @@ -645,11 +386,11 @@ func (h *HIDS) fetchRulesFromManager() (err error) { } if sha256 != data.Sha256([]byte(rules)) { - return fmt.Errorf("Failed to verify rules integrity") + return fmt.Errorf("failed to verify rules integrity") } - ioutil.WriteFile(sha256Path, []byte(sha256), 600) - return ioutil.WriteFile(rulePath, []byte(rules), 600) + ioutil.WriteFile(sha256Path, []byte(sha256), 0600) + return ioutil.WriteFile(rulePath, []byte(rules), 0600) } // containerPaths returns the path to the container and the path to its sha256 file @@ -659,13 +400,6 @@ func (h *HIDS) containerPaths(container string) (path, sha256Path string) { return } -// rulesPaths returns the path used by WHIDS to save gene rules -func (h *HIDS) rulesPaths() (path, sha256Path string) { - path = filepath.Join(h.config.RulesConfig.RulesDB, "database.gen") - sha256Path = fmt.Sprintf("%s.sha256", path) - return -} - func (h *HIDS) fetchContainersFromManager() (err error) { var containers []string cl := h.forwarder.Client @@ -688,10 +422,10 @@ func (h *HIDS) fetchContainersFromManager() (err error) { } // we compare the integrity of the container received - compSha256 := api.Sha256StringArray(cont) + compSha256 := utils.Sha256StringArray(cont) sha256, _ := cl.GetContainerSha256(contName) if compSha256 != sha256 { - return fmt.Errorf("Failed to verify container \"%s\" integrity", contName) + return fmt.Errorf("failed to verify container \"%s\" integrity", contName) } // we dump the container @@ -708,7 +442,7 @@ func (h *HIDS) fetchContainersFromManager() (err error) { w.Close() fd.Close() // Dump current container sha256 to a file - ioutil.WriteFile(contSha256Path, []byte(compSha256), 600) + ioutil.WriteFile(contSha256Path, []byte(compSha256), 0600) } } return nil @@ -727,14 +461,15 @@ func (h *HIDS) loadContainers() (lastErr error) { lastErr = err continue } - defer fd.Close() r, err := gzip.NewReader(fd) if err != nil { lastErr = err + // we close file descriptor + fd.Close() continue } log.Infof("Loading container: %s", cont) - h.engine.LoadContainer(cont, r) + h.Engine.LoadContainer(cont, r) r.Close() fd.Close() } @@ -756,10 +491,110 @@ func (h *HIDS) cleanup() { } } +////////////////// Routines + +// schedules the different routines to be ran +func (h *HIDS) cronRoutine() { + now := time.Now() + // timestamps + lastUpdateTs := now + lastUploadTs := now + lastArchDelTs := now + lastCmdRunTs := now + + go func() { + for { + now = time.Now() + switch { + // handle updates + case now.Sub(lastUpdateTs) >= h.config.RulesConfig.UpdateInterval: + // put here function to update + lastUpdateTs = now + // handle uploads + case now.Sub(lastUploadTs) >= time.Minute: + lastUploadTs = now + // handle sysmon archive cleaning + case now.Sub(lastArchDelTs) >= time.Minute: + // put here code to delete archived files + lastArchDelTs = now + // handle command to run + case now.Sub(lastCmdRunTs) >= 5*time.Second: + // put here code to run commands + lastCmdRunTs = now + } + + time.Sleep(1 * time.Second) + } + }() +} + +func (h *HIDS) cleanArchivedRoutine() bool { + if h.config.Sysmon.CleanArchived { + go func() { + log.Info("Starting routine to cleanup Sysmon archived files") + archivePath := h.config.Sysmon.ArchiveDirectory + + if archivePath == "" { + log.Error("Sysmon archive directory not found") + return + } + + if fsutil.IsDir(archivePath) { + // used to mark files for which we already reported errors + reported := datastructs.NewSyncedSet() + log.Infof("Starting archive cleanup loop for directory: %s", archivePath) + for { + // expiration fixed to five minutes + expired := time.Now().Add(time.Minute * -5) + for wi := range fswalker.Walk(archivePath) { + for _, fi := range wi.Files { + if archivedRe.MatchString(fi.Name()) { + path := filepath.Join(wi.Dirpath, fi.Name()) + if fi.ModTime().Before(expired) { + // we print out error only once + if err := os.Remove(path); err != nil && !reported.Contains(path) { + log.Errorf("Failed to remove archived file: %s", err) + reported.Add(path) + } + } + } + } + } + time.Sleep(time.Minute * 1) + } + } else { + log.Errorf(fmt.Sprintf("No such Sysmon archive directory: %s", archivePath)) + } + }() + return true + } + return false +} + +// returns true if the update routine is started +func (h *HIDS) updateRoutine() bool { + d := h.config.RulesConfig.UpdateInterval + if h.config.IsForwardingEnabled() { + if d > 0 { + go func() { + t := time.NewTimer(d) + for range t.C { + if err := h.updateEngine(false); err != nil { + log.Error(err) + } + t.Reset(d) + } + }() + return true + } + } + return false +} + func (h *HIDS) uploadRoutine() bool { if h.config.IsDumpEnabled() && h.config.IsForwardingEnabled() { // force compression in this case - flagDumpCompress = true + h.config.Dump.Compression = true go func() { for { // Sending dump files over to the manager @@ -796,14 +631,165 @@ func (h *HIDS) uploadRoutine() bool { return false } +func (h *HIDS) containCmd() *exec.Cmd { + ip := h.forwarder.Client.ManagerIP + // only allow connection to the manager configured + return exec.Command("netsh.exe", + "advfirewall", + "firewall", + "add", + "rule", + fmt.Sprintf("name=%s", ContainRuleName), + "dir=out", + fmt.Sprintf("remoteip=0.0.0.0-%s,%s-255.255.255.255", utils.PrevIP(ip), utils.NextIP(ip)), + "action=block") +} + +func (h *HIDS) uncontainCmd() *exec.Cmd { + return exec.Command("netsh.exe", "advfirewall", + "firewall", + "delete", + "rule", + fmt.Sprintf("name=%s", ContainRuleName), + ) +} + +func (h *HIDS) handleManagerCommand(cmd *api.Command) { + + // Switch processing the commands + switch cmd.Name { + // Aliases + case "contain": + cmd.FromExecCmd(h.containCmd()) + case "uncontain": + cmd.FromExecCmd(h.uncontainCmd()) + case "osquery": + if fsutil.IsFile(h.config.Report.OSQuery.Bin) { + cmd.Name = h.config.Report.OSQuery.Bin + cmd.Args = append([]string{"--json", "-A"}, cmd.Args...) + cmd.ExpectJSON = true + } else { + cmd.Unrunnable() + cmd.Error = fmt.Sprintf("OSQuery binary file configured does not exist: %s", h.config.Report.OSQuery.Bin) + } + // HIDs internal commands + case "terminate": + cmd.Unrunnable() + if len(cmd.Args) > 0 { + spid := cmd.Args[0] + if pid, err := strconv.Atoi(spid); err != nil { + cmd.Error = fmt.Sprintf("failed to parse pid: %s", err) + } else if err := terminate(pid); err != nil { + cmd.Error = err.Error() + } + } + case "hash": + cmd.Unrunnable() + cmd.ExpectJSON = true + if len(cmd.Args) > 0 { + if out, err := cmdHash(cmd.Args[0]); err != nil { + cmd.Error = err.Error() + } else { + cmd.Stdout = out + } + } + case "stat": + cmd.Unrunnable() + cmd.ExpectJSON = true + if len(cmd.Args) > 0 { + if out, err := cmdStat(cmd.Args[0]); err != nil { + cmd.Error = err.Error() + } else { + cmd.Stdout = out + } + } + case "dir": + cmd.Unrunnable() + cmd.ExpectJSON = true + if len(cmd.Args) > 0 { + if out, err := cmdDir(cmd.Args[0]); err != nil { + cmd.Error = err.Error() + } else { + cmd.Stdout = out + } + } + case "walk": + cmd.Unrunnable() + cmd.ExpectJSON = true + if len(cmd.Args) > 0 { + cmd.Stdout = cmdWalk(cmd.Args[0]) + } + case "find": + cmd.Unrunnable() + cmd.ExpectJSON = true + if len(cmd.Args) == 2 { + if out, err := cmdFind(cmd.Args[0], cmd.Args[1]); err != nil { + cmd.Error = err.Error() + } else { + cmd.Stdout = out + } + } + case "report": + cmd.Unrunnable() + cmd.ExpectJSON = true + cmd.Stdout = h.Report() + case "processes": + h.processTracker.RLock() + cmd.Unrunnable() + cmd.ExpectJSON = true + cmd.Stdout = h.processTracker.PS() + h.processTracker.RUnlock() + case "drivers": + h.processTracker.RLock() + cmd.Unrunnable() + cmd.ExpectJSON = true + cmd.Stdout = h.processTracker.Drivers + h.processTracker.RUnlock() + } + + // we finally run the command + if err := cmd.Run(); err != nil { + log.Errorf("failed to run command sent by manager \"%s\": %s", cmd.String(), err) + } +} + +// routine which manages command to be executed on the endpoint +// it is made in such a way that we can send burst of commands func (h *HIDS) commandRunnerRoutine() bool { if h.config.IsForwardingEnabled() { go func() { + + defaultSleep := time.Second * 5 + sleep := defaultSleep + + burstDur := time.Duration(0) + tgtBurstDur := time.Second * 30 + burstSleep := time.Millisecond * 500 + for { - if err := h.forwarder.Client.ExecuteCommand(); err != nil { + if cmd, err := h.forwarder.Client.FetchCommand(); err != nil && err != api.ErrNothingToDo { log.Error(err) + } else if err == nil { + // reduce sleeping time if a command was received + sleep = burstSleep + burstDur = 0 + log.Infof("Handling command: %s", cmd.String()) + h.handleManagerCommand(cmd) + if err := h.forwarder.Client.PostCommand(cmd); err != nil { + log.Error(err) + } + } + + // if we reached the targetted burst duration + if burstDur >= tgtBurstDur { + sleep = defaultSleep } - time.Sleep(5 * time.Second) + + if sleep == burstSleep { + burstDur += sleep + } + + time.Sleep(sleep) } }() return true @@ -811,6 +797,89 @@ func (h *HIDS) commandRunnerRoutine() bool { return false } +func (h *HIDS) compress(path string) { + if h.config.Dump.Compression { + if !h.compressionIsRunning { + // start compression routine + go func() { + h.compressionIsRunning = true + for path := range compressionChannel { + log.Infof("Compressing %s", path) + if err := utils.GzipFileBestSpeed(path); err != nil { + log.Errorf("Cannot compress %s: %s", path, err) + } + } + h.compressionIsRunning = false + }() + } + compressionChannel <- path + } +} + +/** Public Methods **/ + +// IsHIDSEvent returns true if the event is generated by IDS activity +func (h *HIDS) IsHIDSEvent(e *evtx.GoEvtxMap) bool { + if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { + if pguid == h.guid { + return true + } + } + + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if guid == h.guid { + return true + } + // search for parent in processTracker + if pt := h.processTracker.GetByGuid(guid); pt != nil { + if pt.ParentProcessGUID == h.guid { + return true + } + } + } + if sguid, err := e.GetString(&pathSysmonSourceProcessGUID); err == nil { + if sguid == h.guid { + return true + } + // search for parent in processTracker + if pt := h.processTracker.GetByGuid(sguid); pt != nil { + if pt.ParentProcessGUID == h.guid { + return true + } + } + } + return false +} + +// Report generate a forensic ready report (meant to be dumped) +// this method is blocking as it runs commands and wait after those +func (h *HIDS) Report() (r Report) { + r.StartTime = time.Now() + + // generate a report for running processes or those terminated still having one child or more + // do this step first not to polute report with commands to run + r.Processes = h.processTracker.PS() + + // Drivers loaded + r.Drivers = h.processTracker.Drivers + + // run all the commands configured to inculde in the report + r.Commands = h.config.Report.PrepareCommands() + for i := range r.Commands { + r.Commands[i].Run() + } + + r.StopTime = time.Now() + return +} + +// RulesPaths returns the path used by WHIDS to save gene rules +func (h *HIDS) RulesPaths() (path, sha256Path string) { + path = filepath.Join(h.config.RulesConfig.RulesDB, "database.gen") + sha256Path = fmt.Sprintf("%s.sha256", path) + return +} + // Run starts the WHIDS engine and waits channel listening is stopped func (h *HIDS) Run() { // Running all the threads @@ -860,26 +929,28 @@ func (h *HIDS) Run() { } // Warning message in certain circumstances - if h.config.EnableHooks && !flagProcTermEn && h.eventScanned > 0 && h.eventScanned%1000 == 0 { + if h.config.EnableHooks && !h.flagProcTermEn && h.eventScanned > 0 && h.eventScanned%1000 == 0 { log.Warn("Sysmon process termination events seem to be missing. WHIDS won't work as expected.") } + h.RLock() + + // Runs pre detection hooks + // putting this before next condition makes the processTracker registering + // HIDS events and allows detecting ProcessAccess events from HIDS childs + h.preHooks.RunHooksOn(h, event) + // We skip if it is one of IDS event - // we keep process termination event because it is used to control wether process termination is enabled - if isSelf(event) && !isSysmonProcessTerminate(event) { + // we keep process termination event because it is used to control if process termination is enabled + if h.IsHIDSEvent(event) && !isSysmonProcessTerminate(event) { if h.PrintAll { fmt.Println(utils.JSON(event)) } - continue + goto LoopTail } - // Runs pre detection hooks - h.preHooks.RunHooksOn(event) - - h.RLock() - // if the event has matched at least one signature or is filtered - if n, crit, filtered := h.engine.MatchOrFilter(event); len(n) > 0 || filtered { + if n, crit, filtered := h.Engine.MatchOrFilter(event); len(n) > 0 || filtered { switch { case crit >= h.config.CritTresh: if !h.PrintAll && !h.config.LogAll { @@ -887,7 +958,7 @@ func (h *HIDS) Run() { } // Pipe the event to be sent to the forwarder // Run hooks post detection - h.postHooks.RunHooksOn(event) + h.postHooks.RunHooksOn(h, event) h.alertReported++ case filtered && h.config.EnableFiltering && !h.PrintAll && !h.config.LogAll: event.Del(&engine.GeneInfoPath) @@ -906,9 +977,12 @@ func (h *HIDS) Run() { h.forwarder.PipeEvent(event) } - h.RUnlock() h.eventScanned++ + + LoopTail: + h.RUnlock() } + log.Infof("HIDS main loop terminated") }() // Run bogus command so that at least one Process Terminate @@ -923,7 +997,7 @@ func (h *HIDS) LogStats() { log.Infof("Count Event Scanned: %d", h.eventScanned) log.Infof("Average Event Rate: %.2f EPS", float64(h.eventScanned)/(stop.Sub(h.startTime).Seconds())) log.Infof("Alerts Reported: %d", h.alertReported) - log.Infof("Count Rules Used (loaded + generated): %d", h.engine.Count()) + log.Infof("Count Rules Used (loaded + generated): %d", h.Engine.Count()) } // Stop stops the IDS @@ -947,3 +1021,13 @@ func (h *HIDS) Stop() { func (h *HIDS) Wait() { h.waitGroup.Wait() } + +// WaitWithTimeout waits the IDS to finish +func (h *HIDS) WaitWithTimeout(timeout time.Duration) { + t := time.NewTimer(timeout) + go func() { + h.waitGroup.Wait() + t.Stop() + }() + <-t.C +} diff --git a/hooks/test/hook_test.go b/hids/hook_test.go similarity index 82% rename from hooks/test/hook_test.go rename to hids/hook_test.go index 83069fd..632f1b2 100644 --- a/hooks/test/hook_test.go +++ b/hids/hook_test.go @@ -1,4 +1,4 @@ -package main +package hids import ( "encoding/json" @@ -8,17 +8,16 @@ import ( "testing" "github.com/0xrawsec/golang-evtx/evtx" - "github.com/0xrawsec/whids/hooks" "github.com/0xrawsec/golang-utils/log" "github.com/0xrawsec/golang-utils/readers" ) var ( // DNSFilter filters any Windows-DNS-Client log - DNSFilter = hooks.NewFilter([]int64{}, "Microsoft-Windows-DNS-Client/Operational") + DNSFilter = NewFilter([]int64{}, "Microsoft-Windows-DNS-Client/Operational") // SysmonNetConnFilter filters any Sysmon network connection - SysmonNetConnFilter = hooks.NewFilter([]int64{3}, "Microsoft-Windows-Sysmon/Operational") - eventSource = "new-events.json" + SysmonNetConnFilter = NewFilter([]int64{3}, "Microsoft-Windows-Sysmon/Operational") + eventSource = "test/new-events.json" queryValue = evtx.Path("/Event/EventData/QueryName") queryType = evtx.Path("/Event/EventData/QueryType") queryResults = evtx.Path("/Event/EventData/QueryResults") @@ -27,7 +26,7 @@ var ( dnsResolution = make(map[string]string) ) -func hookDNS(e *evtx.GoEvtxMap) { +func hookDNS(h *HIDS, e *evtx.GoEvtxMap) { if qtype, err := e.GetInt(&queryType); err == nil { // request for A or AAAA records if qtype == 1 || qtype == 28 { @@ -47,7 +46,7 @@ func hookDNS(e *evtx.GoEvtxMap) { } } -func hookNetConn(e *evtx.GoEvtxMap) { +func hookNetConn(h *HIDS, e *evtx.GoEvtxMap) { if ip, err := e.GetString(&destIP); err == nil { if dom, ok := dnsResolution[ip]; ok { e.Set(&destHostname, dom) @@ -56,7 +55,7 @@ func hookNetConn(e *evtx.GoEvtxMap) { } func TestHook(t *testing.T) { - hm := hooks.NewHookMan() + hm := NewHookMan() hm.Hook(hookDNS, DNSFilter) hm.Hook(hookNetConn, SysmonNetConnFilter) f, err := os.Open(eventSource) @@ -72,7 +71,7 @@ func TestHook(t *testing.T) { t.Logf("JSON deserialization issue") t.Fail() } - if hm.RunHooksOn(&e) { + if hm.RunHooksOn(nil, &e) { t.Log(string(evtx.ToJSON(e))) } } diff --git a/hids/hookdefs.go b/hids/hookdefs.go new file mode 100644 index 0000000..729e281 --- /dev/null +++ b/hids/hookdefs.go @@ -0,0 +1,1222 @@ +package hids + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math" + "net" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/0xrawsec/gene/engine" + "github.com/0xrawsec/golang-utils/crypto/file" + + "github.com/0xrawsec/golang-evtx/evtx" + "github.com/0xrawsec/golang-utils/fsutil" + "github.com/0xrawsec/golang-utils/log" + "github.com/0xrawsec/golang-win32/win32" + "github.com/0xrawsec/golang-win32/win32/advapi32" + "github.com/0xrawsec/golang-win32/win32/dbghelp" + "github.com/0xrawsec/golang-win32/win32/kernel32" + "github.com/0xrawsec/whids/utils" +) + +////////////////////////////////// Hooks ////////////////////////////////// + +const ( + // Empty GUID + nullGUID = "{00000000-0000-0000-0000-000000000000}" +) + +const ( + // Actions + ActionKill = "kill" + ActionBlacklist = "blacklist" + ActionMemdump = "memdump" + ActionFiledump = "filedump" + ActionRegdump = "regdump" + ActionReport = "report" +) + +var ( + selfPath, _ = filepath.Abs(os.Args[0]) +) + +var ( + compressionChannel = make(chan string) + + errServiceResolution = fmt.Errorf("error resolving service name") +) + +// hook applying on Sysmon events containing image information and +// adding a new field containing the image size +func hookSetImageSize(h *HIDS, e *evtx.GoEvtxMap) { + var path *evtx.GoEvtxPath + var modpath *evtx.GoEvtxPath + switch e.EventID() { + case SysmonProcessCreate: + path = &pathSysmonImage + modpath = &pathImSize + default: + path = &pathSysmonImageLoaded + modpath = &pathImLoadedSize + } + if image, err := e.GetString(path); err == nil { + if fsutil.IsFile(image) { + if stat, err := os.Stat(image); err == nil { + e.Set(modpath, toString(stat.Size())) + } + } + } +} + +func hookImageLoad(h *HIDS, e *evtx.GoEvtxMap) { + e.Set(&pathImageLoadParentImage, "?") + e.Set(&pathImageLoadParentCommandLine, "?") + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if track := h.processTracker.GetByGuid(guid); track != nil { + if image, err := e.GetString(&pathSysmonImage); err == nil { + // make sure that we are taking signature of the image and not + // one of its DLL + if image == track.Image { + if signed, err := e.GetBool(&pathSysmonSigned); err == nil { + track.Signed = signed + } + if signature, err := e.GetString(&pathSysmonSignature); err == nil { + track.Signature = signature + } + if sigStatus, err := e.GetString(&pathSysmonSignatureStatus); err == nil { + track.SignatureStatus = sigStatus + } + } + } + e.Set(&pathImageLoadParentImage, track.ParentImage) + e.Set(&pathImageLoadParentCommandLine, track.ParentCommandLine) + } + } +} + +// hooks Windows DNS client logs and maintain a domain name resolution table +/*func hookDNS(h *HIDS, e *evtx.GoEvtxMap) { + if qresults, err := e.GetString(&pathQueryResults); err == nil { + if qresults != "" && qresults != "-" { + records := strings.Split(qresults, ";") + for _, r := range records { + // check if it is a valid IP + if net.ParseIP(r) != nil { + if qvalue, err := e.GetString(&pathQueryName); err == nil { + dnsResolution[r] = qvalue + } + } + } + } + } +}*/ + +// hook tracking processes +func hookTrack(h *HIDS, e *evtx.GoEvtxMap) { + switch e.EventID() { + case SysmonProcessCreate: + // Default values + e.Set(&pathAncestors, "?") + e.Set(&pathParentUser, "?") + e.Set(&pathParentIntegrityLevel, "?") + e.Set(&pathParentServices, "?") + // We need to be sure that process termination is enabled + // before initiating process tracking not to fill up memory + // with structures that will never be freed + if h.flagProcTermEn || !h.bootCompleted { + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { + if image, err := e.GetString(&pathSysmonImage); err == nil { + // Boot sequence is completed when LogonUI.exe is strarted + if strings.EqualFold(image, "C:\\Windows\\System32\\LogonUI.exe") { + log.Infof("Boot sequence completed") + h.bootCompleted = true + } + if commandLine, err := e.GetString(&pathSysmonCommandLine); err == nil { + if pCommandLine, err := e.GetString(&pathSysmonParentCommandLine); err == nil { + if pImage, err := e.GetString(&pathSysmonParentImage); err == nil { + if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { + if user, err := e.GetString(&pathSysmonUser); err == nil { + if il, err := e.GetString(&pathSysmonIntegrityLevel); err == nil { + if cd, err := e.GetString(&pathSysmonCurrentDirectory); err == nil { + if hashes, err := e.GetString(&pathSysmonHashes); err == nil { + + track := NewProcessTrack(image, pguid, guid, pid) + track.ParentImage = pImage + track.CommandLine = commandLine + track.ParentCommandLine = pCommandLine + track.CurrentDirectory = cd + track.User = user + track.IntegrityLevel = il + track.SetHashes(hashes) + + if parent := h.processTracker.GetByGuid(pguid); parent != nil { + track.Ancestors = append(parent.Ancestors, parent.Image) + track.ParentUser = parent.User + track.ParentIntegrityLevel = parent.IntegrityLevel + track.ParentServices = parent.Services + track.ParentCurrentDirectory = parent.CurrentDirectory + } else { + // For processes created by System + if pimage, err := e.GetString(&pathSysmonParentImage); err == nil { + track.Ancestors = append(track.Ancestors, pimage) + } + } + h.processTracker.Add(track) + e.Set(&pathAncestors, strings.Join(track.Ancestors, "|")) + if track.ParentUser != "" { + e.Set(&pathParentUser, track.ParentUser) + } + if track.ParentIntegrityLevel != "" { + e.Set(&pathParentIntegrityLevel, track.ParentIntegrityLevel) + } + if track.ParentServices != "" { + e.Set(&pathParentServices, track.ParentServices) + } + } + } + } + } + } + } + } + } + } + } + } + } + case SysmonDriverLoad: + d := DriverInfo{"?", nil, "?", "?", "?", false} + if hashes, err := e.GetString(&pathSysmonHashes); err == nil { + d.SetHashes(hashes) + } + if imloaded, err := e.GetString(&pathSysmonImageLoaded); err == nil { + d.Image = imloaded + } + if signature, err := e.GetString(&pathSysmonSignature); err == nil { + d.Signature = signature + } + if sigstatus, err := e.GetString(&pathSysmonSignatureStatus); err == nil { + d.SignatureStatus = sigstatus + } + if signed, err := e.GetBool(&pathSysmonSigned); err == nil { + d.Signed = signed + } + h.processTracker.Drivers = append(h.processTracker.Drivers, d) + } +} + +// hook managing statistics about some events +func hookStats(h *HIDS, e *evtx.GoEvtxMap) { + // We do not store stats if process termination is not enabled + if h.flagProcTermEn { + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if pt := h.processTracker.GetByGuid(guid); pt != nil { + switch e.EventID() { + case SysmonProcessCreate: + pt.Stats.CreateProcessCount++ + + case SysmonNetworkConnect: + if ip, err := e.GetString(&pathSysmonDestIP); err == nil { + if port, err := e.GetInt(&pathSysmonDestPort); err == nil { + if ts, err := e.GetString(&pathSysmonUtcTime); err == nil { + pt.Stats.UpdateCon(ts, ip, uint16(port)) + } + } + } + case SysmonDNSQuery: + if ts, err := e.GetString(&pathSysmonUtcTime); err == nil { + if qvalue, err := e.GetString(&pathQueryName); err == nil { + if qresults, err := e.GetString(&pathQueryResults); err == nil { + if qresults != "" && qresults != "-" { + records := strings.Split(qresults, ";") + for _, r := range records { + // check if it is a valid IP + if net.ParseIP(r) != nil { + pt.Stats.UpdateNetResolve(ts, r, qvalue) + } + } + } + } + } + } + case SysmonFileCreate: + now := time.Now() + + // Set new fields + e.Set(&pathFileCount, "?") + e.Set(&pathFileCountByExt, "?") + e.Set(&pathFileExtension, "?") + + if pt.Stats.Files.TimeFirstFileCreated.IsZero() { + pt.Stats.Files.TimeFirstFileCreated = now + } + + if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { + ext := filepath.Ext(target) + pt.Stats.Files.CountFilesCreatedByExt[ext]++ + // Setting file count by extension + e.Set(&pathFileCountByExt, toString(pt.Stats.Files.CountFilesCreatedByExt[ext])) + // Setting file extension + e.Set(&pathFileExtension, ext) + } + pt.Stats.Files.CountFilesCreated++ + // Setting total file count + e.Set(&pathFileCount, toString(pt.Stats.Files.CountFilesCreated)) + // Setting frequency + freq := now.Sub(pt.Stats.Files.TimeFirstFileCreated) + if freq != 0 { + eps := pt.Stats.Files.CountFilesCreated * int64(math.Pow10(9)) / freq.Nanoseconds() + e.Set(&pathFileFrequency, toString(int64(eps))) + } else { + e.Set(&pathFileFrequency, toString(0)) + } + // Finally set last event timestamp + pt.Stats.Files.TimeLastFileCreated = now + + case SysmonFileDelete, SysmonFileDeleteDetected: + now := time.Now() + + // Set new fields + e.Set(&pathFileCount, "?") + e.Set(&pathFileCountByExt, "?") + e.Set(&pathFileExtension, "?") + + if pt.Stats.Files.TimeFirstFileDeleted.IsZero() { + pt.Stats.Files.TimeFirstFileDeleted = now + } + + if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { + ext := filepath.Ext(target) + pt.Stats.Files.CountFilesDeletedByExt[ext]++ + // Setting file count by extension + e.Set(&pathFileCountByExt, toString(pt.Stats.Files.CountFilesDeletedByExt[ext])) + // Setting file extension + e.Set(&pathFileExtension, ext) + } + pt.Stats.Files.CountFilesDeleted++ + // Setting total file count + e.Set(&pathFileCount, toString(pt.Stats.Files.CountFilesDeleted)) + + // Setting frequency + freq := now.Sub(pt.Stats.Files.TimeFirstFileDeleted) + if freq != 0 { + eps := pt.Stats.Files.CountFilesDeleted * int64(math.Pow10(9)) / freq.Nanoseconds() + e.Set(&pathFileFrequency, toString(int64(eps))) + } else { + e.Set(&pathFileFrequency, toString(0)) + } + + // Finally set last event timestamp + pt.Stats.Files.TimeLastFileDeleted = time.Now() + } + } + } + } +} + +func hookUpdateGeneScore(h *HIDS, e *evtx.GoEvtxMap) { + if h.IsHIDSEvent(e) { + return + } + + if t := processTrackFromEvent(h, e); t != nil { + if i, err := e.Get(&engine.SignaturePath); err == nil { + t.GeneScore.UpdateCriticality(int64(getCriticality(e))) + if signatures, ok := (*i).([]string); ok { + t.GeneScore.UpdateSignature(signatures) + } + } + } +} + +func hookHandleActions(h *HIDS, e *evtx.GoEvtxMap) { + var kill, memdump bool + + // We have to check that if we are handling one of + // our event and we don't want to kill ourself + if h.IsHIDSEvent(e) { + return + } + + // the only requirement to be able to handle action + // is to have a process guuid + if uuid := srcGUIDFromEvent(e); uuid != nullGUID { + if i, err := e.Get(&engine.ActionsPath); err == nil { + if actions, ok := (*i).([]string); ok { + for _, action := range actions { + switch action { + case ActionKill: + kill = true + if pt := processTrackFromEvent(h, e); pt != nil { + // additional check not to suspend agent + if pt.PID != int64(os.Getpid()) { + // before we kill we suspend the process + kernel32.SuspendProcess(int(pt.PID)) + } + } + case ActionBlacklist: + if pt := processTrackFromEvent(h, e); pt != nil { + // additional check not to blacklist agent + if int(pt.PID) != os.Getpid() { + h.processTracker.Blacklist(pt.CommandLine) + } + } + case ActionMemdump: + memdump = true + dumpProcessRtn(h, e) + case ActionRegdump: + dumpRegistryRtn(h, e) + case ActionFiledump: + dumpFilesRtn(h, e) + case ActionReport: + dumpReportRtn(h, e) + default: + log.Errorf("Cannot handle %s action as it is unknown", action) + } + } + } + + // handle kill operation after the other actions + if kill { + if pt := processTrackFromEvent(h, e); pt != nil { + if pt.PID != int64(os.Getpid()) { + if memdump { + // Wait we finish dumping before killing the process + go func() { + guid := pt.ProcessGUID + for i := 0; i < 60 && !h.memdumped.Contains(guid); i++ { + time.Sleep(1 * time.Second) + } + if err := pt.TerminateProcess(); err != nil { + log.Errorf("Failed to terminate process PID=%d GUID=%s", pt.PID, pt.ProcessGUID) + } + }() + } else if err := pt.TerminateProcess(); err != nil { + log.Errorf("Failed to terminate process PID=%d GUID=%s", pt.PID, pt.ProcessGUID) + } + } + } + } + } + } else { + log.Errorf("Failed to handle actions for event (channel: %s, id: %d): no process GUID available", e.Channel(), e.EventID()) + } +} + +// hook terminating previously blacklisted processes (according to their CommandLine) +func hookTerminator(h *HIDS, e *evtx.GoEvtxMap) { + if e.EventID() == SysmonProcessCreate { + if commandLine, err := e.GetString(&pathSysmonCommandLine); err == nil { + if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { + if h.processTracker.IsBlacklisted(commandLine) { + log.Warnf("Terminating blacklisted process PID=%d CommandLine=\"%s\"", pid, commandLine) + if err := terminate(int(pid)); err != nil { + log.Errorf("Failed to terminate process PID=%d: %s", pid, err) + } + } + } + } + } +} + +// hook setting flagProcTermEn variable +// it is also used to cleanup any structures needing to be cleaned +func hookProcTerm(h *HIDS, e *evtx.GoEvtxMap) { + log.Debug("Process termination events are enabled") + h.flagProcTermEn = true + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + // Releasing resources + h.processTracker.Terminate(guid) + h.memdumped.Del(guid) + } +} + +func hookSelfGUID(h *HIDS, e *evtx.GoEvtxMap) { + if h.guid == "" { + if e.EventID() == SysmonProcessCreate { + // Sometimes it happens that other events are generated before process creation + // Check parent image first because we launch whids.exe -h to test process termination + // and we catch it up if we check image first + if pimage, err := e.GetString(&pathSysmonParentImage); err == nil { + if pimage == selfPath { + if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { + h.guid = pguid + log.Infof("Found self GUID from PGUID: %s", h.guid) + return + } + } + } + if image, err := e.GetString(&pathSysmonImage); err == nil { + if image == selfPath { + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + h.guid = guid + log.Infof("Found self GUID: %s", h.guid) + return + } + } + } + } + } +} + +func hookFileSystemAudit(h *HIDS, e *evtx.GoEvtxMap) { + e.Set(&pathSysmonCommandLine, "?") + e.Set(&pathSysmonProcessGUID, nullGUID) + e.Set(&pathImageHashes, "?") + if pid, err := e.GetInt(&pathFSAuditProcessId); err == nil { + if pt := h.processTracker.GetByPID(pid); pt != nil { + if pt.CommandLine != "" { + e.Set(&pathSysmonCommandLine, pt.CommandLine) + } + if pt.hashes != "" { + e.Set(&pathImageHashes, pt.hashes) + } + if pt.ProcessGUID != "" { + e.Set(&pathSysmonProcessGUID, pt.ProcessGUID) + } + + if obj, err := e.GetString(&pathFSAuditObjectName); err == nil { + if fsutil.IsFile(obj) { + pt.Stats.Files.LastAccessed.Add(obj) + } + } + } + } +} + +func hookProcessIntegrityProcTamp(h *HIDS, e *evtx.GoEvtxMap) { + // Default values + e.Set(&pathProcessIntegrity, toString(-1.0)) + + // Sysmon Create Process + if e.EventID() == SysmonProcessTampering { + if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { + // prevent stopping our own process, it may happen in some + // cases when selfGuid is not found fast enough + if pid != int64(os.Getpid()) { + if kernel32.IsPIDRunning(int(pid)) { + // we first need to wait main process thread + mainTid := kernel32.GetFirstTidOfPid(int(pid)) + // if we found the main thread of pid + if mainTid > 0 { + hThread, err := kernel32.OpenThread(kernel32.THREAD_SUSPEND_RESUME, win32.FALSE, win32.DWORD(mainTid)) + if err != nil { + log.Errorf("Cannot open main thread before checking integrity of PID=%d", pid) + } else { + defer kernel32.CloseHandle(hThread) + if ok := kernel32.WaitThreadRuns(hThread, time.Millisecond*50, time.Millisecond*500); !ok { + // We check whether the thread still exists + checkThread, err := kernel32.OpenThread(kernel32.PROCESS_SUSPEND_RESUME, win32.FALSE, win32.DWORD(mainTid)) + if err == nil { + log.Warnf("Timeout reached while waiting main thread of PID=%d", pid) + } + kernel32.CloseHandle(checkThread) + } else { + da := win32.DWORD(kernel32.PROCESS_VM_READ | kernel32.PROCESS_QUERY_INFORMATION) + hProcess, err := kernel32.OpenProcess(da, win32.FALSE, win32.DWORD(pid)) + + if err != nil { + log.Errorf("Cannot open process to check integrity of PID=%d: %s", pid, err) + } else { + defer kernel32.CloseHandle(hProcess) + bdiff, slen, err := kernel32.CheckProcessIntegrity(hProcess) + if err != nil { + log.Errorf("Cannot check integrity of PID=%d: %s", pid, err) + } else { + if slen != 0 { + integrity := utils.Round(float64(bdiff)*100/float64(slen), 2) + e.Set(&pathProcessIntegrity, toString(integrity)) + } + } + } + } + } + } + } + } + } else { + log.Debugf("Cannot check integrity of PID=%d: process terminated", pid) + } + } +} + +// too big to be put in hookEnrichAnySysmon +func hookEnrichServices(h *HIDS, e *evtx.GoEvtxMap) { + // We do this only if we can cleanup resources + eventID := e.EventID() + if h.flagProcTermEn { + switch eventID { + case SysmonDriverLoad, SysmonWMIBinding, SysmonWMIConsumer, SysmonWMIFilter: + // Nothing to do + break + case SysmonCreateRemoteThread, SysmonAccessProcess: + e.Set(&pathSourceServices, "?") + e.Set(&pathTargetServices, "?") + + sguidPath := &pathSysmonSourceProcessGUID + tguidPath := &pathSysmonTargetProcessGUID + + if eventID == 8 { + sguidPath = &pathSysmonCRTSourceProcessGuid + tguidPath = &pathSysmonCRTTargetProcessGuid + } + + if sguid, err := e.GetString(sguidPath); err == nil { + // First try to resolve it by tracked process + if t := h.processTracker.GetByGuid(sguid); t != nil { + e.Set(&pathSourceServices, t.Services) + } else { + // If it fails we resolve the services by PID + if spid, err := e.GetInt(&pathSysmonSourceProcessId); err == nil { + if svcs, err := advapi32.ServiceWin32NamesByPid(uint32(spid)); err == nil { + e.Set(&pathSourceServices, svcs) + } else { + log.Errorf("Failed to resolve service from PID=%d: %s", spid, err) + e.Set(&pathSourceServices, errServiceResolution.Error()) + } + } + } + } + + // First try to resolve it by tracked process + if tguid, err := e.GetString(tguidPath); err == nil { + if t := h.processTracker.GetByGuid(tguid); t != nil { + e.Set(&pathTargetServices, t.Services) + } else { + // If it fails we resolve the services by PID + if tpid, err := e.GetInt(&pathSysmonTargetProcessId); err == nil { + if svcs, err := advapi32.ServiceWin32NamesByPid(uint32(tpid)); err == nil { + e.Set(&pathTargetServices, svcs) + } else { + log.Errorf("Failed to resolve service from PID=%d: %s", tpid, err) + e.Set(&pathTargetServices, errServiceResolution) + } + } + } + } + default: + e.Set(&pathServices, "?") + // image, guid and pid are supposed to be available for all the remaining Sysmon logs + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { + if track := h.processTracker.GetByGuid(guid); track != nil { + if track.Services == "" { + track.Services, err = advapi32.ServiceWin32NamesByPid(uint32(pid)) + if err != nil { + log.Errorf("Failed to resolve service from PID=%d: %s", pid, err) + track.Services = errServiceResolution.Error() + } + } + e.Set(&pathServices, track.Services) + } else { + services, err := advapi32.ServiceWin32NamesByPid(uint32(pid)) + if err != nil { + log.Errorf("Failed to resolve service from PID=%d: %s", pid, err) + services = errServiceResolution.Error() + } + e.Set(&pathServices, services) + } + } + } + } + } +} + +func hookSetValueSize(h *HIDS, e *evtx.GoEvtxMap) { + e.Set(&pathValueSize, toString(-1)) + if targetObject, err := e.GetString(&pathSysmonTargetObject); err == nil { + size, err := advapi32.RegGetValueSizeFromString(targetObject) + if err != nil { + log.Errorf("Failed to get value size \"%s\": %s", targetObject, err) + } + e.Set(&pathValueSize, toString(size)) + } +} + +// hook that replaces the destination hostname of Sysmon Network connection +// event with the one previously found in the DNS logs +/*func hookEnrichDNSSysmon(h *HIDS, e *evtx.GoEvtxMap) { + if ip, err := e.GetString(&pathSysmonDestIP); err == nil { + if dom, ok := dnsResolution[ip]; ok { + e.Set(&pathSysmonDestHostname, dom) + } + } +}*/ + +func hookEnrichAnySysmon(h *HIDS, e *evtx.GoEvtxMap) { + eventID := e.EventID() + switch eventID { + case SysmonProcessCreate, SysmonDriverLoad: + // ProcessCreation is already processed in hookTrack + // DriverLoad does not contain any GUID information + break + + case SysmonCreateRemoteThread, SysmonAccessProcess: + // Handling CreateRemoteThread and ProcessAccess events + // Default Values for the fields + e.Set(&pathSourceUser, "?") + e.Set(&pathSourceIntegrityLevel, "?") + e.Set(&pathTargetUser, "?") + e.Set(&pathTargetIntegrityLevel, "?") + e.Set(&pathTargetParentProcessGuid, "?") + e.Set(&pathSourceHashes, "?") + e.Set(&pathTargetHashes, "?") + e.Set(&pathSrcProcessGeneScore, "-1") + e.Set(&pathTgtProcessGeneScore, "-1") + + sguidPath := &pathSysmonSourceProcessGUID + tguidPath := &pathSysmonTargetProcessGUID + + if eventID == SysmonCreateRemoteThread { + sguidPath = &pathSysmonCRTSourceProcessGuid + tguidPath = &pathSysmonCRTTargetProcessGuid + } + if sguid, err := e.GetString(sguidPath); err == nil { + if tguid, err := e.GetString(tguidPath); err == nil { + if strack := h.processTracker.GetByGuid(sguid); strack != nil { + if strack.User != "" { + e.Set(&pathSourceUser, strack.User) + } + if strack.IntegrityLevel != "" { + e.Set(&pathSourceIntegrityLevel, strack.IntegrityLevel) + } + if strack.hashes != "" { + e.Set(&pathSourceHashes, strack.hashes) + } + // Source process score + e.Set(&pathSrcProcessGeneScore, toString(strack.GeneScore.Score)) + } + if ttrack := h.processTracker.GetByGuid(tguid); ttrack != nil { + if ttrack.User != "" { + e.Set(&pathTargetUser, ttrack.User) + } + if ttrack.IntegrityLevel != "" { + e.Set(&pathTargetIntegrityLevel, ttrack.IntegrityLevel) + } + if ttrack.ParentProcessGUID != "" { + e.Set(&pathTargetParentProcessGuid, ttrack.ParentProcessGUID) + } + if ttrack.hashes != "" { + e.Set(&pathTargetHashes, ttrack.hashes) + } + // Target process score + e.Set(&pathTgtProcessGeneScore, toString(ttrack.GeneScore.Score)) + } + } + } + + default: + + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + // Default value + e.Set(&pathProcessGeneScore, "-1") + + if track := h.processTracker.GetByGuid(guid); track != nil { + // if event does not have CommandLine field + if !eventHas(e, &pathSysmonCommandLine) { + e.Set(&pathSysmonCommandLine, "?") + if track.CommandLine != "" { + e.Set(&pathSysmonCommandLine, track.CommandLine) + } + } + + // if event does not have User field + if !eventHas(e, &pathSysmonUser) { + e.Set(&pathSysmonUser, "?") + if track.User != "" { + e.Set(&pathSysmonUser, track.User) + } + } + + // if event does not have IntegrityLevel field + if !eventHas(e, &pathSysmonIntegrityLevel) { + e.Set(&pathSysmonIntegrityLevel, "?") + if track.IntegrityLevel != "" { + e.Set(&pathSysmonIntegrityLevel, track.IntegrityLevel) + } + } + + // if event does not have CurrentDirectory field + if !eventHas(e, &pathSysmonCurrentDirectory) { + e.Set(&pathSysmonCurrentDirectory, "?") + if track.CurrentDirectory != "" { + e.Set(&pathSysmonCurrentDirectory, track.CurrentDirectory) + } + } + + // event never has ImageHashes field since it is not Sysmon standard + e.Set(&pathImageHashes, "?") + if track.hashes != "" { + e.Set(&pathImageHashes, track.hashes) + } + + // Signature information + e.Set(&pathImageSigned, toString(track.Signed)) + e.Set(&pathImageSignature, track.Signature) + e.Set(&pathImageSignatureStatus, track.SignatureStatus) + + // Overal criticality score + e.Set(&pathProcessGeneScore, toString(track.GeneScore.Score)) + } + } + } +} + +func hookClipboardEvents(h *HIDS, e *evtx.GoEvtxMap) { + e.Set(&pathSysmonClipboardData, "?") + if hashes, err := e.GetString(&pathSysmonHashes); err == nil { + fname := fmt.Sprintf("CLIP-%s", sysmonArcFileRe.ReplaceAllString(hashes, "")) + path := filepath.Join(h.config.Sysmon.ArchiveDirectory, fname) + if fi, err := os.Stat(path); err == nil { + // limit size of ClipboardData to 1 Mega + if fi.Mode().IsRegular() && fi.Size() < utils.Mega { + if data, err := ioutil.ReadFile(path); err == nil { + // We try to decode utf16 content because regexp can only match utf8 + // Thus doing this is needed to apply detection rule on clipboard content + if enc, err := utils.Utf16ToUtf8(data); err == nil { + e.Set(&pathSysmonClipboardData, string(enc)) + } else { + e.Set(&pathSysmonClipboardData, fmt.Sprintf("%q", data)) + } + } + } + } + } +} + +//////////////////// Hooks' helpers ///////////////////// + +func dumpPidAndCompress(h *HIDS, pid int, guid, id string) { + // prevent stopping ourself (><) + if kernel32.IsPIDRunning(pid) && pid != os.Getpid() && !h.memdumped.Contains(guid) && !h.dumping.Contains(guid) { + + // To avoid dumping the same process twice, possible if two alerts + // comes from the same GUID in a short period of time + h.dumping.Add(guid) + defer h.dumping.Del(guid) + + tmpDumpDir := filepath.Join(h.config.Dump.Dir, guid, id) + os.MkdirAll(tmpDumpDir, utils.DefaultPerms) + module, err := kernel32.GetModuleFilenameFromPID(int(pid)) + if err != nil { + log.Errorf("Cannot get module filename for memory dump PID=%d: %s", pid, err) + } + dumpFilename := fmt.Sprintf("%s_%d_%d.dmp", filepath.Base(module), pid, time.Now().UnixNano()) + dumpPath := filepath.Join(tmpDumpDir, dumpFilename) + log.Infof("Trying to dump memory of process PID=%d Image=\"%s\"", pid, module) + //log.Infof("Mock dump: %s", dumpFilename) + err = dbghelp.FullMemoryMiniDump(pid, dumpPath) + if err != nil { + log.Errorf("Failed to dump process PID=%d Image=%s: %s", pid, module, err) + } else { + // dump was successfull + h.memdumped.Add(guid) + h.compress(dumpPath) + } + } else { + log.Warnf("Cannot dump process PID=%d, the process is already terminated", pid) + } + +} + +func dumpFileAndCompress(h *HIDS, src, path string) error { + var err error + os.MkdirAll(path, utils.DefaultPerms) + sha256, err := file.Sha256(src) + if err != nil { + return err + } + // replace : in case we are dumping an ADS + base := strings.Replace(filepath.Base(src), ":", "_ADS_", -1) + dst := filepath.Join(path, fmt.Sprintf("%d_%s.bin", time.Now().UnixNano(), base)) + // dump sha256 of file anyway + ioutil.WriteFile(fmt.Sprintf("%s.sha256", dst), []byte(sha256), 0600) + if !h.filedumped.Contains(sha256) { + log.Debugf("Dumping file: %s->%s", src, dst) + if err = fsutil.CopyFile(src, dst); err == nil { + h.compress(dst) + h.filedumped.Add(sha256) + } + } + return err +} + +func dumpEventAndCompress(h *HIDS, e *evtx.GoEvtxMap, guid string) (err error) { + dumpPath := dumpPrepareDumpFilename(e, h.config.Dump.Dir, guid, "event.json") + + if !h.dumping.Contains(dumpPath) && !h.filedumped.Contains(dumpPath) { + h.dumping.Add(dumpPath) + defer h.dumping.Del(dumpPath) + + var f *os.File + + f, err = os.Create(dumpPath) + if err != nil { + return + } + f.Write(evtx.ToJSON(e)) + f.Close() + h.compress(dumpPath) + h.filedumped.Add(dumpPath) + } + return +} + +//////////////////// Post Detection Hooks ///////////////////// + +// variables specific to post-detection hooks +var ( + sysmonArcFileRe = regexp.MustCompile("(((SHA1|MD5|SHA256|IMPHASH)=)|,)") +) + +func dumpPrepareDumpFilename(e *evtx.GoEvtxMap, dir, guid, filename string) string { + id := idFromEvent(e) + tmpDumpDir := filepath.Join(dir, guid, id) + os.MkdirAll(tmpDumpDir, utils.DefaultPerms) + return filepath.Join(tmpDumpDir, filename) +} + +func hookDumpProcess(h *HIDS, e *evtx.GoEvtxMap) { + // We have to check that if we are handling one of + // our event and we don't want to dump ourself + if h.IsHIDSEvent(e) { + return + } + + // we dump only if alert is relevant + if getCriticality(e) < h.config.Dump.Treshold { + return + } + + // if memory got already dumped + if hasAction(e, ActionMemdump) { + return + } + + dumpProcessRtn(h, e) +} + +// this hook can run async +func dumpProcessRtn(h *HIDS, e *evtx.GoEvtxMap) { + // make it non blocking + go func() { + h.hookSemaphore.Acquire() + defer h.hookSemaphore.Release() + var guid string + + // it would be theoretically possible to dump a process + // only from a PID (with a null GUID) but dumpPidAndCompress + // is not designed for it. + if guid = srcGUIDFromEvent(e); guid != nullGUID { + // check if we should go on + if !h.processTracker.CheckDumpCountOrInc(guid, h.config.Dump.MaxDumps, h.config.Dump.DumpUntracked) { + log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) + return + } + + if pt := h.processTracker.GetByGuid(guid); pt != nil { + // if the process track is not nil we are sure PID is set + dumpPidAndCompress(h, int(pt.PID), guid, idFromEvent(e)) + } + } + dumpEventAndCompress(h, e, guid) + }() +} + +func hookDumpRegistry(h *HIDS, e *evtx.GoEvtxMap) { + // We have to check that if we are handling one of + // our event and we don't want to dump ourself + if h.IsHIDSEvent(e) { + return + } + + // we dump only if alert is relevant + if getCriticality(e) < h.config.Dump.Treshold { + return + } + + // if registry got already dumped + if hasAction(e, ActionRegdump) { + return + } + + dumpRegistryRtn(h, e) +} + +func dumpRegistryRtn(h *HIDS, e *evtx.GoEvtxMap) { + // make it non blocking + go func() { + h.hookSemaphore.Acquire() + defer h.hookSemaphore.Release() + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + + // check if we should go on + if !h.processTracker.CheckDumpCountOrInc(guid, h.config.Dump.MaxDumps, h.config.Dump.DumpUntracked) { + log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) + return + } + + if targetObject, err := e.GetString(&pathSysmonTargetObject); err == nil { + if details, err := e.GetString(&pathSysmonDetails); err == nil { + // We dump only if Details is "Binary Data" since the other kinds can be seen in the raw event + if details == "Binary Data" { + dumpPath := filepath.Join(h.config.Dump.Dir, guid, idFromEvent(e), "reg.txt") + key, value := filepath.Split(targetObject) + dumpEventAndCompress(h, e, guid) + content, err := utils.RegQuery(key, value) + if err != nil { + log.Errorf("Failed to run reg query: %s", err) + content = fmt.Sprintf("Error Dumping %s: %s", targetObject, err) + } + err = ioutil.WriteFile(dumpPath, []byte(content), 0600) + if err != nil { + log.Errorf("Failed to write registry content to file: %s", err) + return + } + h.compress(dumpPath) + return + } + return + } + } + } + log.Errorf("Failed to dump registry from event") + }() +} + +func dumpCommandLine(h *HIDS, e *evtx.GoEvtxMap, dumpPath string) { + if cl, err := e.GetString(&pathSysmonCommandLine); err == nil { + if cwd, err := e.GetString(&pathSysmonCurrentDirectory); err == nil { + if argv, err := utils.ArgvFromCommandLine(cl); err == nil { + if len(argv) > 1 { + for _, arg := range argv[1:] { + if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { + if err = dumpFileAndCompress(h, arg, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) + } + } + // try to dump a path relative to CWD + relarg := filepath.Join(cwd, arg) + if fsutil.IsFile(relarg) && !utils.IsPipePath(relarg) { + if err = dumpFileAndCompress(h, relarg, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), relarg, err) + } + } + } + } + } + } + } +} + +func dumpParentCommandLine(h *HIDS, e *evtx.GoEvtxMap, dumpPath string) { + if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { + if track := h.processTracker.GetByGuid(guid); track != nil { + if argv, err := utils.ArgvFromCommandLine(track.ParentCommandLine); err == nil { + if len(argv) > 1 { + for _, arg := range argv[1:] { + if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { + if err = dumpFileAndCompress(h, arg, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) + } + } + // try to dump a path relative to parent CWD + if track.ParentCurrentDirectory != "" { + relarg := filepath.Join(track.ParentCurrentDirectory, arg) + if fsutil.IsFile(relarg) && !utils.IsPipePath(relarg) { + if err = dumpFileAndCompress(h, relarg, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), relarg, err) + } + } + } + } + } + } + } + } +} + +func hookDumpFiles(h *HIDS, e *evtx.GoEvtxMap) { + // We have to check that if we are handling one of + // our event and we don't want to dump ourself + if h.IsHIDSEvent(e) { + return + } + + // we dump only if alert is relevant + if getCriticality(e) < h.config.Dump.Treshold { + return + } + + // if file got already dumped + if hasAction(e, ActionFiledump) { + return + } + + dumpFilesRtn(h, e) +} + +func dumpFilesRtn(h *HIDS, e *evtx.GoEvtxMap) { + // make it non blocking + go func() { + h.hookSemaphore.Acquire() + defer h.hookSemaphore.Release() + guid := srcGUIDFromEvent(e) + + // check if we should go on + if !h.processTracker.CheckDumpCountOrInc(guid, h.config.Dump.MaxDumps, h.config.Dump.DumpUntracked) { + log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) + return + } + + // build up dump path + dumpPath := filepath.Join(h.config.Dump.Dir, guid, idFromEvent(e)) + // dump event who triggered the dump + dumpEventAndCompress(h, e, guid) + + // dump CommandLine fields regardless of the event + // this would actually work best when hooks are enabled and enrichment occurs + // in the worst case it would only work for Sysmon CreateProcess events + dumpCommandLine(h, e, dumpPath) + dumpParentCommandLine(h, e, dumpPath) + + // Handling different kinds of event IDs + switch e.EventID() { + + case SysmonFileTime, SysmonFileCreate, SysmonCreateStreamHash: + if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { + if err = dumpFileAndCompress(h, target, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), target, err) + } + } + + case SysmonDriverLoad: + if im, err := e.GetString(&pathSysmonImageLoaded); err == nil { + if err = dumpFileAndCompress(h, im, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) + } + } + + case SysmonAccessProcess: + if sim, err := e.GetString(&pathSysmonSourceImage); err == nil { + if err = dumpFileAndCompress(h, sim, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), sim, err) + } + } + + case SysmonRegSetValue, SysmonWMIConsumer: + // for event ID 13 + path := &pathSysmonDetails + if e.EventID() == SysmonWMIConsumer { + path = &pathSysmonDestination + } + if cl, err := e.GetString(path); err == nil { + // try to parse details as a command line + if argv, err := utils.ArgvFromCommandLine(cl); err == nil { + for _, arg := range argv { + if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { + if err = dumpFileAndCompress(h, arg, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) + } + } + } + } + } + + case SysmonFileDelete: + if im, err := e.GetString(&pathSysmonImage); err == nil { + if err = dumpFileAndCompress(h, im, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) + } + } + + archived, err := e.GetBool(&pathSysmonArchived) + if err == nil && archived { + if !fsutil.IsDir(h.config.Sysmon.ArchiveDirectory) { + log.Errorf("Aborting deleted file dump: %s archive directory does not exist", h.config.Sysmon.ArchiveDirectory) + return + } + log.Info("Will try to dump deleted file") + if hashes, err := e.GetString(&pathSysmonHashes); err == nil { + if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { + fname := fmt.Sprintf("%s%s", sysmonArcFileRe.ReplaceAllString(hashes, ""), filepath.Ext(target)) + path := filepath.Join(h.config.Sysmon.ArchiveDirectory, fname) + if err = dumpFileAndCompress(h, path, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), path, err) + } + } + } + } + + default: + if im, err := e.GetString(&pathSysmonImage); err == nil { + if err = dumpFileAndCompress(h, im, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) + } + } + if pim, err := e.GetString(&pathSysmonParentImage); err == nil { + if err = dumpFileAndCompress(h, pim, dumpPath); err != nil { + log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), pim, err) + } + } + } + }() +} + +func hookDumpReport(h *HIDS, e *evtx.GoEvtxMap) { + // We have to check that if we are handling one of + // our event and we don't want to dump ourself + if h.IsHIDSEvent(e) { + return + } + + // we dump only if alert is relevant + if getCriticality(e) < h.config.Dump.Treshold { + return + } + + // if file got already dumped + if hasAction(e, ActionReport) { + return + } + + dumpReportRtn(h, e) +} + +func dumpReportRtn(h *HIDS, e *evtx.GoEvtxMap) { + // make it non blocking + go func() { + h.hookSemaphore.Acquire() + defer h.hookSemaphore.Release() + + c := h.config.Report + guid := srcGUIDFromEvent(e) + + // check if we should go on + if !h.processTracker.CheckDumpCountOrInc(guid, h.config.Dump.MaxDumps, h.config.Dump.DumpUntracked) { + log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) + return + } + reportPath := dumpPrepareDumpFilename(e, h.config.Dump.Dir, guid, "report.json") + //psPath := dumpPrepareDumpFilename(e, h.config.Dump.Dir, guid, "ps.json") + dumpEventAndCompress(h, e, guid) + if c.EnableReporting { + log.Infof("Generating IR report: %s", guid) + if b, err := json.Marshal(h.Report()); err != nil { + log.Errorf("Failed to JSON encode report: %s", guid) + } else { + utils.HidsWriteFile(reportPath, b) + h.compress(reportPath) + } + log.Infof("Finished generating report: %s", guid) + } + + }() +} diff --git a/hooks/hooks.go b/hids/hooks.go similarity index 66% rename from hooks/hooks.go rename to hids/hooks.go index 28824a7..87bb501 100644 --- a/hooks/hooks.go +++ b/hids/hooks.go @@ -1,49 +1,19 @@ -package hooks +package hids import ( "fmt" + "reflect" + "runtime" "sync" "github.com/0xrawsec/golang-evtx/evtx" - "github.com/0xrawsec/golang-utils/datastructs" ) -type FilterDefinition struct { - EventIDs []int - Channel string -} - -// Filter structure -type Filter struct { - EventIDs datastructs.SyncedSet - Channel string -} - -// NewFilter creates a new Filter structure -func NewFilter(eids []int64, channel string) *Filter { - f := &Filter{} - f.EventIDs = datastructs.NewInitSyncedSet(datastructs.ToInterfaceSlice(eids)...) - f.Channel = channel - return f -} - -// Match checks if an event matches the filter -func (f *Filter) Match(e *evtx.GoEvtxMap) bool { - if !f.EventIDs.Contains(e.EventID()) && f.EventIDs.Len() > 0 { - return false - } - // Don't check channel if empty string - if f.Channel != "" && f.Channel != e.Channel() { - return false - } - return true -} - // Hook structure definition // hooking functions are supposed to run quickly since it is // run synchronously with the Gene scanner. Likewise, the // hooking functions should never panic the program. -type Hook func(*evtx.GoEvtxMap) +type Hook func(*HIDS, *evtx.GoEvtxMap) // HookManager structure definition to easier handle hooks type HookManager struct { @@ -71,7 +41,7 @@ func eventIdentifier(e *evtx.GoEvtxMap) string { } // RunHooksOn runs the hook on a given event -func (hm *HookManager) RunHooksOn(e *evtx.GoEvtxMap) (ret bool) { +func (hm *HookManager) RunHooksOn(h *HIDS, e *evtx.GoEvtxMap) (ret bool) { // Don't waste resources if nothing to do if len(hm.Filters) == 0 { return @@ -96,10 +66,16 @@ func (hm *HookManager) RunHooksOn(e *evtx.GoEvtxMap) (ret bool) { // hi: hook index for _, hi := range hm.memory[key] { hook := hm.Hooks[hi] - hook(e) + // debug hooks + //log.Infof("Running hook: %s", getFunctionName(hook)) + hook(h, e) // We set return value to true if a hook has been applied ret = true } hm.RUnlock() return } + +func getFunctionName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} diff --git a/hids/hookutils.go b/hids/hookutils.go new file mode 100644 index 0000000..40630e6 --- /dev/null +++ b/hids/hookutils.go @@ -0,0 +1,111 @@ +package hids + +import ( + "fmt" + "os" + "sort" + "syscall" + + "github.com/0xrawsec/gene/engine" + "github.com/0xrawsec/golang-evtx/evtx" + "github.com/0xrawsec/golang-utils/crypto/data" + "github.com/0xrawsec/golang-win32/win32" + "github.com/0xrawsec/golang-win32/win32/kernel32" + "github.com/0xrawsec/whids/utils" +) + +func toString(i interface{}) string { + return fmt.Sprintf("%v", i) +} + +func terminate(pid int) error { + // prevents from terminating our own process + if os.Getpid() != pid { + pHandle, err := kernel32.OpenProcess(kernel32.PROCESS_ALL_ACCESS, win32.FALSE, win32.DWORD(pid)) + if err != nil { + return err + } + err = syscall.TerminateProcess(syscall.Handle(pHandle), 0) + if err != nil { + return err + } + } + return nil +} + +// helper function which checks if the event belongs to current WHIDS +func isSysmonProcessTerminate(e *evtx.GoEvtxMap) bool { + return e.Channel() == sysmonChannel && e.EventID() == SysmonProcessTerminate +} + +func srcPIDFromEvent(e *evtx.GoEvtxMap) int64 { + + if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { + return pid + } + + if pid, err := e.GetInt(&pathSysmonSourceProcessId); err == nil { + return pid + } + + return -1 +} + +func srcGUIDFromEvent(e *evtx.GoEvtxMap) string { + var procGUIDPath *evtx.GoEvtxPath + + // the interesting pid to dump depends on the event + switch e.EventID() { + case SysmonAccessProcess: + procGUIDPath = &pathSysmonSourceProcessGUID + case SysmonCreateRemoteThread: + procGUIDPath = &pathSysmonCRTSourceProcessGuid + default: + procGUIDPath = &pathSysmonProcessGUID + } + + if guid, err := e.GetString(procGUIDPath); err == nil { + return guid + } + + return nullGUID +} + +func processTrackFromEvent(h *HIDS, e *evtx.GoEvtxMap) *ProcessTrack { + if uuid := srcGUIDFromEvent(e); uuid != nullGUID { + return h.processTracker.GetByGuid(uuid) + } + return nil +} + +func hasAction(e *evtx.GoEvtxMap, action string) bool { + if i, err := e.Get(&engine.ActionsPath); err == nil { + if actions, ok := (*i).([]string); ok { + for _, a := range actions { + if a == action { + return true + } + } + } + } + return false +} + +// Todo: move this function into evtx package +func eventHas(e *evtx.GoEvtxMap, p *evtx.GoEvtxPath) bool { + _, err := e.GetString(p) + return err == nil +} + +func getCriticality(e *evtx.GoEvtxMap) int { + if c, err := e.Get(&pathGeneCriticality); err == nil { + return (*c).(int) + } + return 0 +} + +func idFromEvent(e *evtx.GoEvtxMap) string { + bs := utils.ByteSlice(evtx.ToJSON(e)) + sort.Stable(bs) + return data.Md5(bs) +} diff --git a/hids/paths.go b/hids/paths.go new file mode 100644 index 0000000..59db7ae --- /dev/null +++ b/hids/paths.go @@ -0,0 +1,138 @@ +package hids + +import "github.com/0xrawsec/golang-evtx/evtx" + +var ( + // Path definitions + ////////////////////////// Getters /////////////////////////// + // DNS-Client logs + pathDNSQueryValue = evtx.Path("/Event/EventData/QueryName") + pathDNSQueryType = evtx.Path("/Event/EventData/QueryType") + pathDNSQueryResults = evtx.Path("/Event/EventData/QueryResults") + + // FileSystemAudit + pathFSAuditProcessId = pathSysmonProcessId + pathFSAuditObjectName = evtx.Path("/Event/EventData/ObjectName") + + // Sysmon related paths + // Common to several events + pathSysmonUtcTime = evtx.Path("/Event/EventData/UtcTime") + pathSysmonImage = evtx.Path("/Event/EventData/Image") + pathSysmonHashes = evtx.Path("/Event/EventData/Hashes") + pathSysmonTargetFilename = evtx.Path("/Event/EventData/TargetFilename") + pathSysmonProcessGUID = evtx.Path("/Event/EventData/ProcessGuid") + pathSysmonProcessId = evtx.Path("/Event/EventData/ProcessId") + + // EventID 1: ProcessCreate + pathSysmonCommandLine = evtx.Path("/Event/EventData/CommandLine") + pathSysmonParentCommandLine = evtx.Path("/Event/EventData/ParentCommandLine") + pathSysmonParentImage = evtx.Path("/Event/EventData/ParentImage") + pathSysmonParentProcessGUID = evtx.Path("/Event/EventData/ParentProcessGuid") + pathSysmonParentProcessId = evtx.Path("/Event/EventData/ParentProcessId") + pathSysmonUser = evtx.Path("/Event/EventData/User") + pathSysmonIntegrityLevel = evtx.Path("/Event/EventData/IntegrityLevel") + pathSysmonCurrentDirectory = evtx.Path("/Event/EventData/CurrentDirectory") + + // EventID 3: NetworkConnect + pathSysmonDestIP = evtx.Path("/Event/EventData/DestinationIp") + pathSysmonDestPort = evtx.Path("/Event/EventData/DestinationPort") + pathSysmonDestHostname = evtx.Path("/Event/EventData/DestinationHostname") + + // EventID 6/7 + pathSysmonSignature = evtx.Path("/Event/EventData/Signature") + pathSysmonSigned = evtx.Path("/Event/EventData/Signed") + pathSysmonSignatureStatus = evtx.Path("/Event/EventData/SignatureStatus") + + // EventID 7 + pathSysmonImageLoaded = evtx.Path("/Event/EventData/ImageLoaded") + + // EventID 8: CreateRemoteThread + pathSysmonCRTSourceProcessGuid = evtx.Path("/Event/EventData/SourceProcessGuid") + pathSysmonCRTTargetProcessGuid = evtx.Path("/Event/EventData/TargetProcessGuid") + + // EventID 8/10 + pathSysmonSourceProcessId = evtx.Path("/Event/EventData/SourceProcessId") + pathSysmonTargetProcessId = evtx.Path("/Event/EventData/TargetProcessId") + + // EventID 10: ProcessAccess + pathSysmonSourceProcessGUID = evtx.Path("/Event/EventData/SourceProcessGUID") + pathSysmonTargetProcessGUID = evtx.Path("/Event/EventData/TargetProcessGUID") + pathSysmonSourceImage = evtx.Path("/Event/EventData/SourceImage") + pathSysmonTargetImage = evtx.Path("/Event/EventData/TargetImage") + + // EventID 12,13,14: Registry + pathSysmonTargetObject = evtx.Path("/Event/EventData/TargetObject") + pathSysmonDetails = evtx.Path("/Event/EventData/Details") + + // EventID 20 + pathSysmonDestination = evtx.Path("/Event/EventData/Destination") + + // EventID 22: DNSQuery + pathQueryName = evtx.Path("/Event/EventData/QueryName") + pathQueryResults = evtx.Path("/Event/EventData/QueryResults") + + // EventID 23: + pathSysmonArchived = evtx.Path("/Event/EventData/Archived") + + // Gene criticality path + pathGeneCriticality = evtx.Path("/Event/GeneInfo/Criticality") + + ///////////////////////// Setters ////////////////////////////////////// + pathProcessGeneScore = evtx.Path("/Event/EventData/ProcessGeneScore") + pathSrcProcessGeneScore = evtx.Path("/Event/EventData/SourceProcessGeneScore") + pathTgtProcessGeneScore = evtx.Path("/Event/EventData/TargetProcessGeneScore") + + pathAncestors = evtx.Path("/Event/EventData/Ancestors") + pathParentUser = evtx.Path("/Event/EventData/ParentUser") + pathParentIntegrityLevel = evtx.Path("/Event/EventData/ParentIntegrityLevel") + + // Use to store image sizes information by hook + pathImSize = evtx.Path("/Event/EventData/ImageSize") + pathImLoadedSize = evtx.Path("/Event/EventData/ImageLoadedSize") + + // Use to store process information by hook + pathParentIntegrity = evtx.Path("/Event/EventData/ParentProcessIntegrity") + pathProcessIntegrity = evtx.Path("/Event/EventData/ProcessIntegrity") + pathIntegrityTimeout = evtx.Path("/Event/EventData/ProcessIntegrityTimeout") + + // Use to store pathServices information by hook + pathServices = evtx.Path("/Event/EventData/Services") + pathParentServices = evtx.Path("/Event/EventData/ParentServices") + pathSourceServices = evtx.Path("/Event/EventData/SourceServices") + pathTargetServices = evtx.Path("/Event/EventData/TargetServices") + + // Use to store process by hook + pathSourceIsParent = evtx.Path("/Event/EventData/SourceIsParent") + + // Use to store value size by hooking on SetValue events + pathValueSize = evtx.Path("/Event/EventData/ValueSize") + + // Use to store parent image and command line in image load events + pathImageLoadParentImage = evtx.Path("/Event/EventData/ParentImage") + pathImageLoadParentCommandLine = evtx.Path("/Event/EventData/ParentCommandLine") + + // Used to store user and integrity information in sysmon CreateRemoteThread and ProcessAccess events + pathSourceUser = evtx.Path("/Event/EventData/SourceUser") + pathSourceIntegrityLevel = evtx.Path("/Event/EventData/SourceIntegrityLevel") + pathTargetUser = evtx.Path("/Event/EventData/TargetUser") + pathTargetIntegrityLevel = evtx.Path("/Event/EventData/TargetIntegrityLevel") + pathTargetParentProcessGuid = evtx.Path("/Event/EventData/TargetParentProcessGuid") + + // Used to store Image Hashes information into any Sysmon Event + pathImageHashes = evtx.Path("/Event/EventData/ImageHashes") + pathSourceHashes = evtx.Path("/Event/EventData/SourceHashes") + pathTargetHashes = evtx.Path("/Event/EventData/TargetHashes") + + // Used to store image signature related information + pathImageSignature = evtx.Path("/Event/EventData/ImageSignature") + pathImageSigned = evtx.Path("/Event/EventData/ImageSigned") + pathImageSignatureStatus = evtx.Path("/Event/EventData/ImageSignatureStatus") + + // Use to enrich Clipboard events + pathSysmonClipboardData = evtx.Path("/Event/EventData/ClipboardData") + + pathFileCount = evtx.Path("/Event/EventData/Count") + pathFileCountByExt = evtx.Path("/Event/EventData/CountByExt") + pathFileExtension = evtx.Path("/Event/EventData/Extension") + pathFileFrequency = evtx.Path("/Event/EventData/FrequencyEPS") +) diff --git a/hids/ptrack.go b/hids/ptrack.go new file mode 100644 index 0000000..bcc1619 --- /dev/null +++ b/hids/ptrack.go @@ -0,0 +1,398 @@ +package hids + +import ( + "strings" + "sync" + "time" + + "github.com/0xrawsec/golang-utils/datastructs" +) + +type ConStat struct { + FirstSeen string `json:"first-seen"` + LastSeen string `json:"last-seen"` + Resolved map[string]uint `json:"resolved"` + Ports map[uint16]uint `json:"ports"` + Count int `json:"count"` +} + +type FileStats struct { + LastAccessed *datastructs.RingSet `json:"last-accessed"` + CountFilesCreated int64 `json:"file-create-count"` + CountFilesCreatedByExt map[string]int64 `json:"file-create-count-by-ext"` + TimeFirstFileCreated time.Time `json:"first-file-create"` + TimeLastFileCreated time.Time `json:"last-file-create"` + CountFilesDeleted int64 `json:"file-delete-count"` + CountFilesDeletedByExt map[string]int64 `json:"file-delete-count-by-ext"` + TimeFirstFileDeleted time.Time `json:"first-file-delete"` + TimeLastFileDeleted time.Time `json:"last-file-delete"` +} + +type ProcStats struct { + CreateProcessCount int64 `json:"create-process-count"` + Connections map[string]*ConStat `json:"connections"` + Files FileStats `json:"files"` +} + +func NewProcStats() ProcStats { + return ProcStats{ + Connections: make(map[string]*ConStat), + Files: FileStats{ + LastAccessed: datastructs.NewRingSet(50), + CountFilesCreatedByExt: make(map[string]int64), + CountFilesDeletedByExt: make(map[string]int64), + }, + } +} + +func (p *ProcStats) UpdateNetResolve(timestamp, ip, qname string) { + cs := p.ConStat(ip) + if cs.FirstSeen == "" { + cs.FirstSeen = timestamp + } + cs.LastSeen = timestamp + cs.Resolved[qname]++ +} + +func (p *ProcStats) UpdateCon(timestamp, ip string, port uint16) { + cs := p.ConStat(ip) + if cs.FirstSeen == "" { + cs.FirstSeen = timestamp + } + cs.LastSeen = timestamp + cs.Ports[port]++ + cs.Count++ +} + +func (p *ProcStats) ConStat(ip string) *ConStat { + if _, ok := p.Connections[ip]; !ok { + p.Connections[ip] = &ConStat{Resolved: make(map[string]uint), Ports: make(map[uint16]uint)} + } + return p.Connections[ip] +} + +type GeneScore struct { + Signatures map[string]uint `json:"signatures"` + Score int64 `json:"score"` +} + +func NewGeneScore() GeneScore { + return GeneScore{make(map[string]uint), 0} +} + +func (g *GeneScore) UpdateCriticality(criticality int64) { + g.Score += criticality +} + +func (g *GeneScore) UpdateSignature(signature []string) { + for _, s := range signature { + g.Signatures[s]++ + } +} + +func (g *GeneScore) Update(criticality int64, signature []string) { + for _, s := range signature { + g.Signatures[s]++ + } + g.Score += criticality +} + +func sysmonHashesToMap(hashes string) map[string]string { + m := make(map[string]string) + for _, h := range strings.Split(hashes, ",") { + i := strings.Index(h, "=") + if i+1 < len(h) { + v := strings.ToLower(h[i+1:]) + + // it is sha1 + if len(v) == 40 { + m["sha1"] = v + continue + } + + // it is sha256 + if len(v) == 64 { + m["sha256"] = v + } + + // md5 or imphash + if len(v) == 32 { + switch { + case strings.HasPrefix(h, "MD5="): + m["md5"] = v + case strings.HasPrefix(h, "IMPHASH="): + m["imphash"] = v + } + } + } + } + return m +} + +type ProcessTrack struct { + /* Private */ + hashes string + + /* Public */ + Image string `json:"image"` + ParentImage string `json:"parent-image"` + PID int64 `json:"pid"` + CommandLine string `json:"command-line"` + ParentCommandLine string `json:"parent-command-line"` + CurrentDirectory string `json:"cwd"` + ParentCurrentDirectory string `json:"parent-cwd"` + ProcessGUID string `json:"process-guid"` + User string `json:"user"` + ParentUser string `json:"parent-user"` + IntegrityLevel string `json:"integrity-lvl"` + ParentIntegrityLevel string `json:"parent-integrity-lvl"` + ParentProcessGUID string `json:"parent-process-guid"` + Services string `json:"services"` + ParentServices string `json:"parent-services"` + HashesMap map[string]string `json:"hashes"` + Signature string `json:"signature"` + SignatureStatus string `json:"signature-status"` + Signed bool `json:"signed"` + Ancestors []string `json:"ancestors"` + Integrity float64 `json:"integrity"` + IntegrityTimeout bool `json:"integrity-timeout"` + MemDumped bool `json:"memory-dumped"` + DumpCount int `json:"dump-count"` + ChildCount int `json:"child-count"` // number of currently running child proceses + Stats ProcStats `json:"statistics"` + GeneScore GeneScore `json:"gene-score"` + Terminated bool `json:"terminated"` + TimeTerminated time.Time `json:"time-terminated"` +} + +// NewProcessTrack creates a new processTrack structure enforcing +// that minimal information is encoded (image, guid, pid) +func NewProcessTrack(image, pguid, guid string, pid int64) *ProcessTrack { + return &ProcessTrack{ + Image: image, + ParentProcessGUID: pguid, + ProcessGUID: guid, + PID: pid, + Signature: "?", + SignatureStatus: "?", + Ancestors: make([]string, 0), + Integrity: -1.0, + Stats: NewProcStats(), + GeneScore: NewGeneScore(), + } +} + +func (t *ProcessTrack) SetHashes(hashes string) { + t.hashes = hashes + t.HashesMap = sysmonHashesToMap(hashes) +} + +func (t *ProcessTrack) TerminateProcess() error { + if !t.Terminated { + return terminate(int(t.PID)) + } + return nil +} + +type DriverInfo struct { + /* Private */ + hashes string + + /* Public */ + HashesMap map[string]string `json:"hashes"` + Image string `json:"image"` + Signature string `json:"signature"` + SignatureStatus string `json:"signature-status"` + Signed bool `json:"signed"` +} + +func (di *DriverInfo) SetHashes(hashes string) { + di.hashes = hashes + di.HashesMap = sysmonHashesToMap(hashes) +} + +type ActivityTracker struct { + sync.RWMutex + // to store process track by parent process GUID + //pguids map[string]int + guids map[string]*ProcessTrack + // PIDs can be re-used so we have to jungle with two data structures + rpids map[int64]*ProcessTrack // for running processes + tpids map[int64]*ProcessTrack // for terminated processes + blacklisted *datastructs.SyncedSet + free *datastructs.Fifo + + // driver loaded + Drivers []DriverInfo +} + +func NewActivityTracker() *ActivityTracker { + pt := &ActivityTracker{ + //pguids: make(map[string]int), + guids: make(map[string]*ProcessTrack), + rpids: make(map[int64]*ProcessTrack), + tpids: make(map[int64]*ProcessTrack), + blacklisted: datastructs.NewSyncedSet(), + free: &datastructs.Fifo{}, + Drivers: make([]DriverInfo, 0), + } + // startup the routine to free resources + pt.freeRtn() + return pt +} + +func (pt *ActivityTracker) delete(t *ProcessTrack) { + pt.Lock() + defer pt.Unlock() + + if t := pt.guids[t.ParentProcessGUID]; t != nil { + t.ChildCount-- + } + + delete(pt.guids, t.ProcessGUID) + // delete from terminated processes + delete(pt.tpids, t.PID) +} + +func (pt *ActivityTracker) freeRtn() { + go func() { + for { + for e := pt.free.Pop(); e != nil; e = pt.free.Pop() { + t := e.Value.(*ProcessTrack) + now := time.Now() + // delete the track only after some time because some + // events come after process terminate events and we don't + // want to miss correlation + timeToDel := t.TimeTerminated.Add(time.Second * 10) + if timeToDel.After(now) { + delta := timeToDel.Sub(now) + time.Sleep(delta) + } + // we don't free the process structure if it still has a child + // this is mostly to keep track of parent processes when generating + // a report + if t.ChildCount > 0 { + pt.free.Push(t) + // we need to sleep there because we + // can end up reprocessing the same + // track over and over + time.Sleep(1 * time.Second) + continue + } + // delete ProcessTrack from ProcessTracker + pt.delete(t) + } + // we have to wait here not to go in an + // empty endless loop (if nothing in free list) + time.Sleep(1 * time.Second) + } + }() +} + +// returns true if DumpCount member of processTrack is below max argument +// and increments if necessary. This function is used to check whether we +// should still dump information given a guid +func (pt *ActivityTracker) CheckDumpCountOrInc(guid string, max int, deflt bool) bool { + pt.Lock() + defer pt.Unlock() + if track, ok := pt.guids[guid]; ok { + if track.DumpCount < max { + track.DumpCount++ + return true + } + return false + } + // we return a parametrized default value (cleaner than returning global) + return deflt +} + +func (pt *ActivityTracker) Add(t *ProcessTrack) { + pt.Lock() + defer pt.Unlock() + //pt.pguids[t.ParentProcessGUID]++ + if t := pt.guids[t.ParentProcessGUID]; t != nil { + t.ChildCount++ + } + pt.guids[t.ProcessGUID] = t + pt.rpids[t.PID] = t +} + +func (pt *ActivityTracker) PS() map[string]ProcessTrack { + pt.RLock() + defer pt.RUnlock() + ps := make(map[string]ProcessTrack) + for guid, t := range pt.guids { + t.HashesMap = sysmonHashesToMap(t.hashes) + ps[guid] = *t + } + return ps +} + +func (pt *ActivityTracker) Blacklist(cmdLine string) { + pt.blacklisted.Add(cmdLine) +} + +func (pt *ActivityTracker) IsBlacklisted(cmdLine string) bool { + return pt.blacklisted.Contains(cmdLine) +} + +func (pt *ActivityTracker) GetParentByGuid(guid string) *ProcessTrack { + pt.RLock() + defer pt.RUnlock() + if c, ok := pt.guids[guid]; ok { + return pt.guids[c.ParentProcessGUID] + } + return nil +} + +func (pt *ActivityTracker) GetByGuid(guid string) *ProcessTrack { + pt.RLock() + defer pt.RUnlock() + return pt.guids[guid] +} + +func (pt *ActivityTracker) GetByPID(pid int64) *ProcessTrack { + pt.RLock() + defer pt.RUnlock() + // if we find processes in running processes + if t := pt.rpids[pid]; t != nil { + return t + } + // if we find process in terminated processes + return pt.tpids[pid] +} + +func (pt *ActivityTracker) ContainsGuid(guid string) bool { + pt.RLock() + defer pt.RUnlock() + _, ok := pt.guids[guid] + return ok +} + +func (pt *ActivityTracker) ContainsPID(pid int64) bool { + pt.RLock() + defer pt.RUnlock() + _, ok := pt.rpids[pid] + return ok +} + +func (pt *ActivityTracker) IsTerminated(guid string) bool { + if t := pt.GetByGuid(guid); t != nil { + return t.Terminated + } + return true +} + +func (pt *ActivityTracker) Terminate(guid string) error { + if t := pt.GetByGuid(guid); t != nil { + t.Terminated = true + t.TimeTerminated = time.Now() + // PID entry must be cleared as soon as possible + // to avoid issues like deleting a re-used PID in delete method + delete(pt.rpids, t.PID) + // we put it in the map of terminated processes + pt.tpids[t.PID] = t + pt.free.Push(t) + } + return nil +} diff --git a/hids/reports.go b/hids/reports.go new file mode 100644 index 0000000..a287e8c --- /dev/null +++ b/hids/reports.go @@ -0,0 +1,119 @@ +package hids + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "time" +) + +// Report structure +type Report struct { + Processes map[string]ProcessTrack `json:"processes"` + Drivers []DriverInfo `json:"drivers"` + Commands []ReportCommand `json:"commands"` + StartTime time.Time `json:"start-timestamp"` // time at which report generation started + StopTime time.Time `json:"stop-timestamp"` // time at which report generation stopped +} + +// ReportCommand is a structure both to configure commands to run in a report +// but also to store the outcome of the command after it ran +type ReportCommand struct { + Description string `json:"description" toml:"description" comment:"Description of the command to run, for reporting purposes"` + Name string `json:"name" toml:"name" comment:"Name of the command to execute (can be a binary)"` + Args []string `json:"args" toml:"args" comment:"Argument of the command line"` + ExpectJSON bool `json:"expect-json" toml:"expect-json" comment:"Expect JSON formated output on stdout"` + Stdout interface{} `json:"stdout" toml:",omitempty"` + Stderr []byte `json:"stderr" toml:",omitempty"` + Error string `json:"error" toml:",omitempty"` + Timestamp time.Time `json:"timestamp" toml:",omitempty"` + Timeout time.Duration `json:"timeout" toml:"timeout" comment:"Timeout to apply to the command (if > 0 this takes precedence over the global report timeout setting)"` +} + +// Run the desired command +func (c *ReportCommand) Run() { + var cmd *exec.Cmd + var err error + var stdout []byte + var cancel context.CancelFunc + + ctx := context.Background() + if c.Timeout > 0 { + ctx, cancel = context.WithTimeout(context.Background(), c.Timeout) + defer cancel() + } + + cmd = exec.CommandContext(ctx, c.Name, c.Args...) + // set timestamp + c.Timestamp = time.Now() + if stdout, err = cmd.Output(); err != nil { + c.Error = err.Error() + if ee, ok := err.(*exec.ExitError); ok { + c.Stderr = ee.Stderr + } + } + + if c.ExpectJSON { + if err = json.Unmarshal(stdout, &(c.Stdout)); err != nil { + c.Stdout = string(stdout) + c.Error = err.Error() + } + } else { + c.Stdout = stdout + } +} + +var ( + osqueryiArgs = []string{"--json", "-A"} +) + +// OSQueryConfig holds configuration about OSQuery tool +type OSQueryConfig struct { + Bin string `toml:"bin" comment:"Path to osqueryi binary"` + Tables []string `toml:"tables" comment:"OSQuery tables to add to the report"` +} + +// PrepareCommands builds up osquery commands +func (c *OSQueryConfig) PrepareCommands() (cmds []ReportCommand) { + cmds = make([]ReportCommand, len(c.Tables)) + + for i, t := range c.Tables { + cmds[i].Description = fmt.Sprintf("OSQuery %s table", t) + cmds[i].Name = c.Bin + cmds[i].Args = osqueryiArgs + cmds[i].Args = append(cmds[i].Args, t) + + cmds[i].ExpectJSON = true + } + + return +} + +// ReportConfig holds report configuration +type ReportConfig struct { + EnableReporting bool `toml:"en-reporting" comment:"Enables IR reporting"` + OSQuery OSQueryConfig `toml:"osquery" comment:"OSQuery configuration"` + Commands []ReportCommand `toml:"commands" comment:"Commands to execute in addition to the OSQuery ones" commented:"true"` + CommandTimeout time.Duration `toml:"timeout" comment:"Timeout after which every command expires (to prevent too long commands)"` +} + +// PrepareCommands builds up all commands to run +func (c *ReportConfig) PrepareCommands() (cmds []ReportCommand) { + + cmds = make([]ReportCommand, 0, len(c.OSQuery.Tables)+len(c.Commands)) + // OSQuery commands processed first + for _, rc := range c.OSQuery.PrepareCommands() { + rc.Timeout = c.CommandTimeout + cmds = append(cmds, rc) + } + + // other commands + for _, rc := range c.Commands { + if rc.Timeout == 0 { + rc.Timeout = c.CommandTimeout + } + cmds = append(cmds, rc) + } + return +} diff --git a/hooks/test/events.json b/hids/test/events.json similarity index 100% rename from hooks/test/events.json rename to hids/test/events.json diff --git a/hooks/test/new-events.json b/hids/test/new-events.json similarity index 100% rename from hooks/test/new-events.json rename to hids/test/new-events.json diff --git a/tools/whids/acquisition.go b/tools/whids/acquisition.go deleted file mode 100644 index 04b0eb3..0000000 --- a/tools/whids/acquisition.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os/exec" - "time" - - "github.com/google/shlex" -) - -type ReportCommand struct { - Description string `toml:"description" comment:"Description of the command to run, for reporting purposes"` - CommandLine string `toml:"cmd-line" comment:"Command line to be executed"` - ExpectJSON bool - Stdout interface{} - Stderr []byte - Timeout time.Duration -} - -func (c *ReportCommand) Run() { - var cmd *exec.Cmd - args := make([]string, 0) - stdout := make([]byte, 0) - - if cl, err := shlex.Split(c.CommandLine); err == nil { - ctx := context.Background() - if c.Timeout > 0 { - ctx, _ = context.WithTimeout(context.Background(), c.Timeout) - } - if len(cl) > 0 { - if len(cl) > 1 { - args = append(args, cl[1:]...) - } - - cmd = exec.CommandContext(ctx, cl[0], args...) - if stdout, err = cmd.Output(); err != nil { - if ee, ok := err.(*exec.ExitError); ok { - c.Stderr = ee.Stderr - } - } - - if c.ExpectJSON { - c.Stdout = string(stdout) - } - } - } -} - -type OSQueryConfig struct { - Bin string `toml:"bin" comment:"Path to osqueryi binary"` - Tables []string `toml:"tables" comment:"OSQuery tables to add to the report"` -} - -const ( - osqueryiArgs = "--json -A %s" -) - -func (c *OSQueryConfig) Commands() (cmds []ReportCommand) { - cmds = make([]ReportCommand, len(c.Tables)) - - for i, t := range c.Tables { - args := fmt.Sprintf(osqueryiArgs, t) - cmds[i].CommandLine = fmt.Sprintf("%s %s", c.Bin, args) - cmds[i].ExpectJSON = true - } - - return -} - -type IRReportConfig struct { - EnableReporting bool `toml:"en-reporting" comment:"Enables IR reporting"` - OSQuery OSQueryConfig `toml:"osquery-config" comment:"OSQuery configuration"` - Commands []ReportCommand `toml:"acqu-commands" comment:"Acquisition commands to execute in addition to the OSQuery ones"` - CommandTimeout time.Duration `toml:"timeout" comment:"Timeout after which every command expires (to prevent too long commands)"` -} - -func (c *IRReportConfig) AllCommands() (cmds []ReportCommand) { - - cmds = make([]ReportCommand, 0, len(c.OSQuery.Tables)+len(c.Commands)) - // OSQuery commands processed first - for _, rc := range c.OSQuery.Commands() { - cmds = append(cmds, rc) - } - - // other commands - for _, rc := range c.Commands { - cmds = append(cmds, rc) - } - return -} - -func Report(c *IRReportConfig) (r []ReportCommand) { - r = c.AllCommands() - for i := range r { - r[i].Run() - } - return -} diff --git a/tools/whids/hookdefs.go b/tools/whids/hookdefs.go deleted file mode 100644 index e9369e8..0000000 --- a/tools/whids/hookdefs.go +++ /dev/null @@ -1,1645 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "math" - "net" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "sync" - "syscall" - "time" - - "github.com/0xrawsec/gene/engine" - "github.com/0xrawsec/golang-utils/crypto/file" - - "github.com/0xrawsec/golang-evtx/evtx" - "github.com/0xrawsec/golang-utils/crypto/data" - "github.com/0xrawsec/golang-utils/datastructs" - "github.com/0xrawsec/golang-utils/fsutil" - "github.com/0xrawsec/golang-utils/log" - "github.com/0xrawsec/golang-utils/sync/semaphore" - "github.com/0xrawsec/golang-win32/win32" - "github.com/0xrawsec/golang-win32/win32/advapi32" - "github.com/0xrawsec/golang-win32/win32/dbghelp" - "github.com/0xrawsec/golang-win32/win32/kernel32" - "github.com/0xrawsec/whids/hooks" - "github.com/0xrawsec/whids/utils" -) - -func terminate(pid int) error { - // prevents from terminating our own process - if os.Getpid() != pid { - pHandle, err := kernel32.OpenProcess(kernel32.PROCESS_ALL_ACCESS, win32.FALSE, win32.DWORD(pid)) - if err != nil { - return err - } - err = syscall.TerminateProcess(syscall.Handle(pHandle), 0) - if err != nil { - return err - } - } - return nil -} - -/////////////////////////// ProcessTracker //////////////////////////////// - -type stats struct { - CountProcessCreated int64 - CountNetConn int64 - CountFilesCreated int64 - CountFilesCreatedByExt map[string]int64 - TimeFirstFileCreated time.Time - TimeLastFileCreated time.Time - CountFilesDeleted int64 - CountFilesDeletedByExt map[string]int64 - TimeFirstFileDeleted time.Time - TimeLastFileDeleted time.Time -} - -func NewStats() stats { - return stats{ - CountFilesCreatedByExt: make(map[string]int64), - CountFilesDeletedByExt: make(map[string]int64), - } -} - -type processTrack struct { - Image string - ParentImage string - PID int64 - CommandLine string - ParentCommandLine string - CurrentDirectory string - ParentCurrentDirectory string - ProcessGUID string - User string - ParentUser string - IntegrityLevel string - ParentIntegrityLevel string - ParentProcessGUID string - Services string - ParentServices string - Hashes string - Signature string - SignatureStatus string - Signed bool - History []string - Integrity float64 - IntegrityTimeout bool - IntegrityComputed bool - Stats stats - MemDumped bool - DumpsCount int - TimeTerminated time.Time -} - -func NewProcessTrack() *processTrack { - return &processTrack{ - Signature: "?", - SignatureStatus: "?", - History: make([]string, 0), - Integrity: -1.0, - Stats: NewStats(), - } -} - -func (t *processTrack) IsTerminated() bool { - return !t.TimeTerminated.IsZero() -} - -func (t *processTrack) TerminateProcess() error { - if !t.IsTerminated() { - return terminate(int(t.PID)) - } - return nil -} - -type ProcessTracker struct { - sync.RWMutex - guids map[string]*processTrack - pids map[int64]*processTrack - blacklisted datastructs.SyncedSet - free *datastructs.Fifo -} - -func NewProcessTracker() *ProcessTracker { - pt := &ProcessTracker{ - guids: make(map[string]*processTrack), - pids: make(map[int64]*processTrack), - blacklisted: datastructs.NewSyncedSet(), - free: &datastructs.Fifo{}, - } - // startup the routine to free resources - pt.freeRtn() - return pt -} - -func (pt *ProcessTracker) freeRtn() { - go func() { - for { - for e := pt.free.Pop(); e != nil; e = pt.free.Pop() { - t := e.Value.(*processTrack) - now := time.Now() - // delete the track only after some time because some - // events come after process terminate events - timeToDel := t.TimeTerminated.Add(time.Second * 10) - if timeToDel.After(now) { - delta := timeToDel.Sub(now) - time.Sleep(delta) - } - pt.Del(t.ProcessGUID) - } - time.Sleep(1 * time.Second) - } - }() -} - -// returns true if DumpCount member of processTrack is below max argument -// and increments if necessary. This function is used to check whether we -// should still dump information given a guid -func (pt *ProcessTracker) CheckDumpCountOrInc(guid string, max int, deflt bool) bool { - pt.Lock() - defer pt.Unlock() - if track, ok := pt.guids[guid]; ok { - if track.DumpsCount < max { - track.DumpsCount++ - return true - } - return false - } - // we return a parametrized default value (cleaner than returning global) - return deflt -} - -func (pt *ProcessTracker) Add(t *processTrack) { - pt.Lock() - defer pt.Unlock() - pt.guids[t.ProcessGUID] = t - pt.pids[t.PID] = t -} - -func (pt *ProcessTracker) Blacklist(cmdLine string) { - pt.blacklisted.Add(cmdLine) -} - -func (pt *ProcessTracker) IsBlacklisted(cmdLine string) bool { - return pt.blacklisted.Contains(cmdLine) -} - -func (pt *ProcessTracker) GetParentByGuid(guid string) *processTrack { - pt.RLock() - defer pt.RUnlock() - if c, ok := pt.guids[guid]; ok { - return pt.guids[c.ParentProcessGUID] - } - return nil -} - -func (pt *ProcessTracker) GetByGuid(guid string) *processTrack { - pt.RLock() - defer pt.RUnlock() - return pt.guids[guid] -} - -func (pt *ProcessTracker) GetByPID(pid int64) *processTrack { - pt.RLock() - defer pt.RUnlock() - return pt.pids[pid] -} - -func (pt *ProcessTracker) ContainsGuid(guid string) bool { - pt.RLock() - defer pt.RUnlock() - _, ok := pt.guids[guid] - return ok -} - -func (pt *ProcessTracker) ContainsPID(pid int64) bool { - pt.RLock() - defer pt.RUnlock() - _, ok := pt.pids[pid] - return ok -} - -func (pt *ProcessTracker) IsTerminated(guid string) bool { - if t := pt.GetByGuid(guid); t != nil { - return t.IsTerminated() - } - return true -} - -func (pt *ProcessTracker) Terminate(guid string) error { - if t := pt.GetByGuid(guid); t != nil { - t.TimeTerminated = time.Now() - pt.free.Push(t) - } - return nil -} - -func (pt *ProcessTracker) TerminateProcess(guid string) error { - if t := pt.GetByGuid(guid); t != nil { - // We terminate process only if not already terminated - t.TerminateProcess() - } - return nil -} - -func (pt *ProcessTracker) Del(guid string) { - if t := pt.GetByGuid(guid); t != nil { - pt.Lock() - defer pt.Unlock() - delete(pt.guids, guid) - delete(pt.pids, t.PID) - } -} - -////////////////////////////////// Hooks ////////////////////////////////// - -const ( - _ = 1 << (iota * 10) - Kilo - Mega -) - -const ( - // Sysmon Event IDs - _ = iota - IDProcessCreate - IDFileTime - IDNetworkConnect - IDServiceStateChange - IDProcessTerminate - IDDriverLoad - IDImageLoad - IDCreateRemoteThread - IDRawAccessRead - IDAccessProcess - IDFileCreate - IDRegKey - IDRegSetValue - IDRegName - IDCreateStreamHash - IDServiceConfigurationChange - IDCreateNamedPipe - IDConnectNamedPipe - IDWMIFilter - IDWMIConsumer - IDWMIBinding - IDDNSQuery - IDFileDelete - IDClipboardChange - IDProcessTampering - IDFileDeleteDetected - - // Empty GUID - nullGUID = "{00000000-0000-0000-0000-000000000000}" -) - -const ( - // Actions - ActionMemdump = "memdump" - ActionFiledump = "filedump" - ActionRegdump = "regdump" -) - -var ( - // Globals needed by Hooks - dumpDirectory string - selfGUID string - sysmonArchiveDirectory string - bootCompleted bool - flagProcTermEn bool // set the flag to true if process termination is enabled - flagDumpCompress bool - flagDumpUntracked bool - maxDumps int - - selfPath, _ = filepath.Abs(os.Args[0]) - selfPid = os.Getpid() - - cryptoLockerFilecreateLimit = int64(50) - dumpTresh = 8 -) - -var ( - // SysmonChannel Sysmon windows event log channel - SysmonChannel = "Microsoft-Windows-Sysmon/Operational" - // SecurityChannel Security windows event log channel - SecurityChannel = "Security" - - // Filters definitions - fltAnyEvent = hooks.NewFilter([]int64{}, "") - fltAnySysmon = hooks.NewFilter([]int64{}, SysmonChannel) - fltProcessCreate = hooks.NewFilter([]int64{1}, SysmonChannel) - fltNetworkConnect = hooks.NewFilter([]int64{3}, SysmonChannel) - fltProcTermination = hooks.NewFilter([]int64{5}, SysmonChannel) - fltImageLoad = hooks.NewFilter([]int64{7}, SysmonChannel) - fltProcessAccess = hooks.NewFilter([]int64{10}, SysmonChannel) - fltRegSetValue = hooks.NewFilter([]int64{13}, SysmonChannel) - fltNetwork = hooks.NewFilter([]int64{3, 22}, SysmonChannel) - fltImageSize = hooks.NewFilter([]int64{1, 6, 7}, SysmonChannel) - fltStats = hooks.NewFilter([]int64{1, 3, 11, 23, 26}, SysmonChannel) - fltDNS = hooks.NewFilter([]int64{22}, SysmonChannel) - fltClipboard = hooks.NewFilter([]int64{24}, SysmonChannel) - fltImageTampering = hooks.NewFilter([]int64{25}, SysmonChannel) - - fltFSObjectAccess = hooks.NewFilter([]int64{4663}, SecurityChannel) -) - -var ( - // Path definitions - ////////////////////////// Getters /////////////////////////// - // DNS-Client logs - pathDNSQueryValue = evtx.Path("/Event/EventData/QueryName") - pathDNSQueryType = evtx.Path("/Event/EventData/QueryType") - pathDNSQueryResults = evtx.Path("/Event/EventData/QueryResults") - - // FileSystemAudit logs - pathFSAuditProcessId = pathSysmonProcessId - - // Sysmon related paths - pathSysmonDestIP = evtx.Path("/Event/EventData/DestinationIp") - pathSysmonDestHostname = evtx.Path("/Event/EventData/DestinationHostname") - pathSysmonImage = evtx.Path("/Event/EventData/Image") - pathSysmonHashes = evtx.Path("/Event/EventData/Hashes") - pathSysmonCommandLine = evtx.Path("/Event/EventData/CommandLine") - pathSysmonParentCommandLine = evtx.Path("/Event/EventData/ParentCommandLine") - pathSysmonParentImage = evtx.Path("/Event/EventData/ParentImage") - pathSysmonImageLoaded = evtx.Path("/Event/EventData/ImageLoaded") - pathSysmonSignature = evtx.Path("/Event/EventData/Signature") - pathSysmonSigned = evtx.Path("/Event/EventData/Signed") - pathSysmonSignatureStatus = evtx.Path("/Event/EventData/SignatureStatus") - - // EventID 8: CreateRemoteThread - pathSysmonCRTSourceProcessGuid = evtx.Path("/Event/EventData/SourceProcessGuid") - pathSysmonCRTTargetProcessGuid = evtx.Path("/Event/EventData/TargetProcessGuid") - - // EventID 10: ProcessAccess - pathSysmonSourceProcessGUID = evtx.Path("/Event/EventData/SourceProcessGUID") - pathSysmonTargetProcessGUID = evtx.Path("/Event/EventData/TargetProcessGUID") - - // EventID 12,13,14: Registry - pathSysmonTargetObject = evtx.Path("/Event/EventData/TargetObject") - - pathSysmonProcessGUID = evtx.Path("/Event/EventData/ProcessGuid") - pathSysmonParentProcessGUID = evtx.Path("/Event/EventData/ParentProcessGuid") - pathSysmonParentProcessId = evtx.Path("/Event/EventData/ParentProcessId") - pathSysmonProcessId = evtx.Path("/Event/EventData/ProcessId") - pathSysmonSourceProcessId = evtx.Path("/Event/EventData/SourceProcessId") - pathSysmonTargetProcessId = evtx.Path("/Event/EventData/TargetProcessId") - pathSysmonTargetFilename = evtx.Path("/Event/EventData/TargetFilename") - pathSysmonCurrentDirectory = evtx.Path("/Event/EventData/CurrentDirectory") - pathSysmonDetails = evtx.Path("/Event/EventData/Details") - pathSysmonDestination = evtx.Path("/Event/EventData/Destination") - pathSysmonSourceImage = evtx.Path("/Event/EventData/SourceImage") - pathSysmonTargetImage = evtx.Path("/Event/EventData/TargetImage") - pathSysmonUser = evtx.Path("/Event/EventData/User") - pathSysmonIntegrityLevel = evtx.Path("/Event/EventData/IntegrityLevel") - - // EventID 22: DNSQuery - pathQueryName = evtx.Path("/Event/EventData/QueryName") - pathQueryResults = evtx.Path("/Event/EventData/QueryResults") - - // EventID 23: - pathSysmonArchived = evtx.Path("/Event/EventData/Archived") - - // Gene criticality path - pathGeneCriticality = evtx.Path("/Event/GeneInfo/Criticality") - - ///////////////////////// Setters ////////////////////////////////////// - pathAncestors = evtx.Path("/Event/EventData/Ancestors") - pathParentUser = evtx.Path("/Event/EventData/ParentUser") - pathParentIntegrityLevel = evtx.Path("/Event/EventData/ParentIntegrityLevel") - - // Use to store image sizes information by hook - pathImSize = evtx.Path("/Event/EventData/ImageSize") - pathImLoadedSize = evtx.Path("/Event/EventData/ImageLoadedSize") - - // Use to store process information by hook - pathParentIntegrity = evtx.Path("/Event/EventData/ParentProcessIntegrity") - pathProcessIntegrity = evtx.Path("/Event/EventData/ProcessIntegrity") - pathIntegrityTimeout = evtx.Path("/Event/EventData/ProcessIntegrityTimeout") - - // Use to store pathServices information by hook - pathServices = evtx.Path("/Event/EventData/Services") - pathParentServices = evtx.Path("/Event/EventData/ParentServices") - pathSourceServices = evtx.Path("/Event/EventData/SourceServices") - pathTargetServices = evtx.Path("/Event/EventData/TargetServices") - - // Use to store process by hook - pathSourceIsParent = evtx.Path("/Event/EventData/SourceIsParent") - - // Use to store value size by hooking on SetValue events - pathValueSize = evtx.Path("/Event/EventData/ValueSize") - - // Use to store parent image and command line in image load events - pathImageLoadParentImage = evtx.Path("/Event/EventData/ParentImage") - pathImageLoadParentCommandLine = evtx.Path("/Event/EventData/ParentCommandLine") - - // Used to store user and integrity information in sysmon CreateRemoteThread and ProcessAccess events - pathSourceUser = evtx.Path("/Event/EventData/SourceUser") - pathSourceIntegrityLevel = evtx.Path("/Event/EventData/SourceIntegrityLevel") - pathTargetUser = evtx.Path("/Event/EventData/TargetUser") - pathTargetIntegrityLevel = evtx.Path("/Event/EventData/TargetIntegrityLevel") - pathTargetParentProcessGuid = evtx.Path("/Event/EventData/TargetParentProcessGuid") - - // Used to store Image Hashes information into any Sysmon Event - pathImageHashes = evtx.Path("/Event/EventData/ImageHashes") - pathSourceHashes = evtx.Path("/Event/EventData/SourceHashes") - pathTargetHashes = evtx.Path("/Event/EventData/TargetHashes") - - // Used to store image signature related information - pathImageSignature = evtx.Path("/Event/EventData/ImageSignature") - pathImageSigned = evtx.Path("/Event/EventData/ImageSigned") - pathImageSignatureStatus = evtx.Path("/Event/EventData/ImageSignatureStatus") - - // Use to enrich Clipboard events - pathSysmonClipboardData = evtx.Path("/Event/EventData/ClipboardData") - - pathFileCount = evtx.Path("/Event/EventData/Count") - pathFileCountByExt = evtx.Path("/Event/EventData/CountByExt") - pathFileExtension = evtx.Path("/Event/EventData/Extension") - pathFileFrequency = evtx.Path("/Event/EventData/FrequencyEPS") -) - -var ( - processTracker = NewProcessTracker() - - dnsResolution = make(map[string]string) - - memdumped = datastructs.NewSyncedSet() - dumping = datastructs.NewSyncedSet() - - filedumped = datastructs.NewSyncedSet() - - parallelHooks = semaphore.New(4) - - compressionIsRunning = false - compressionChannel = make(chan string) -) - -func toString(i interface{}) string { - return fmt.Sprintf("%v", i) -} - -// helper function which checks if the event belongs to current WHIDS -func isSelf(e *evtx.GoEvtxMap) bool { - if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { - if pguid == selfGUID { - return true - } - } - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if guid == selfGUID { - return true - } - } - if sguid, err := e.GetString(&pathSysmonSourceProcessGUID); err == nil { - if sguid == selfGUID { - return true - } - } - return false -} - -// helper function which checks if the event belongs to current WHIDS -func isSysmonProcessTerminate(e *evtx.GoEvtxMap) bool { - return e.Channel() == SysmonChannel && e.EventID() == IDProcessTerminate -} - -// hook applying on Sysmon events containing image information and -// adding a new field containing the image size -func hookSetImageSize(e *evtx.GoEvtxMap) { - var path *evtx.GoEvtxPath - var modpath *evtx.GoEvtxPath - switch e.EventID() { - case IDProcessCreate: - path = &pathSysmonImage - modpath = &pathImSize - default: - path = &pathSysmonImageLoaded - modpath = &pathImLoadedSize - } - if image, err := e.GetString(path); err == nil { - if fsutil.IsFile(image) { - if stat, err := os.Stat(image); err == nil { - e.Set(modpath, toString(stat.Size())) - } - } - } -} - -func hookImageLoad(e *evtx.GoEvtxMap) { - e.Set(&pathImageLoadParentImage, "?") - e.Set(&pathImageLoadParentCommandLine, "?") - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if track := processTracker.GetByGuid(guid); track != nil { - if image, err := e.GetString(&pathSysmonImage); err == nil { - // make sure that we are taking signature of the image and not - // one of its DLL - if image == track.Image { - if signed, err := e.GetBool(&pathSysmonSigned); err == nil { - track.Signed = signed - } - if signature, err := e.GetString(&pathSysmonSignature); err == nil { - track.Signature = signature - } - if sigStatus, err := e.GetString(&pathSysmonSignatureStatus); err == nil { - track.SignatureStatus = sigStatus - } - } - } - e.Set(&pathImageLoadParentImage, track.ParentImage) - e.Set(&pathImageLoadParentCommandLine, track.ParentCommandLine) - } - } -} - -// hooks Windows DNS client logs and maintain a domain name resolution table -func hookDNS(e *evtx.GoEvtxMap) { - if qresults, err := e.GetString(&pathQueryResults); err == nil { - if qresults != "" && qresults != "-" { - records := strings.Split(qresults, ";") - for _, r := range records { - // check if it is a valid IP - if net.ParseIP(r) != nil { - if qvalue, err := e.GetString(&pathQueryName); err == nil { - dnsResolution[r] = qvalue - } - } - } - } - } -} - -// hook tracking processes -func hookTrack(e *evtx.GoEvtxMap) { - // Default values - e.Set(&pathAncestors, "?") - e.Set(&pathParentUser, "?") - e.Set(&pathParentIntegrityLevel, "?") - e.Set(&pathParentServices, "?") - // We need to be sure that process termination is enabled - // before initiating process tracking not to fill up memory - // with structures that will never be freed - if flagProcTermEn || !bootCompleted { - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { - if image, err := e.GetString(&pathSysmonImage); err == nil { - // Boot sequence is completed when LogonUI.exe is strarted - if strings.ToLower(image) == strings.ToLower("C:\\Windows\\System32\\LogonUI.exe") { - log.Infof("Boot sequence completed") - bootCompleted = true - } - if commandLine, err := e.GetString(&pathSysmonCommandLine); err == nil { - if pCommandLine, err := e.GetString(&pathSysmonParentCommandLine); err == nil { - if pImage, err := e.GetString(&pathSysmonParentImage); err == nil { - if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { - if user, err := e.GetString(&pathSysmonUser); err == nil { - if il, err := e.GetString(&pathSysmonIntegrityLevel); err == nil { - if cd, err := e.GetString(&pathSysmonCurrentDirectory); err == nil { - if hashes, err := e.GetString(&pathSysmonHashes); err == nil { - - track := NewProcessTrack() - track.Image = image - track.ParentImage = pImage - track.CommandLine = commandLine - track.ParentCommandLine = pCommandLine - track.CurrentDirectory = cd - track.PID = pid - track.User = user - track.IntegrityLevel = il - track.ProcessGUID = guid - track.ParentProcessGUID = pguid - track.Hashes = hashes - - if parent := processTracker.GetByGuid(pguid); parent != nil { - track.History = append(parent.History, parent.Image) - track.ParentUser = parent.User - track.ParentIntegrityLevel = parent.IntegrityLevel - track.ParentServices = parent.Services - track.ParentCurrentDirectory = parent.CurrentDirectory - } else { - // For processes created by System - if pimage, err := e.GetString(&pathSysmonParentImage); err == nil { - track.History = append(track.History, pimage) - } - } - processTracker.Add(track) - e.Set(&pathAncestors, strings.Join(track.History, "|")) - if track.ParentUser != "" { - e.Set(&pathParentUser, track.ParentUser) - } - if track.ParentIntegrityLevel != "" { - e.Set(&pathParentIntegrityLevel, track.ParentIntegrityLevel) - } - if track.ParentServices != "" { - e.Set(&pathParentServices, track.ParentServices) - } - } - } - } - } - } - } - } - } - } - } - } - } -} - -// hook managing statistics about some events -func hookStats(e *evtx.GoEvtxMap) { - // We do not store stats if process termination is not enabled - if flagProcTermEn { - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if pt := processTracker.GetByGuid(guid); pt != nil { - switch e.EventID() { - case IDProcessCreate: - pt.Stats.CountProcessCreated++ - case IDNetworkConnect: - pt.Stats.CountNetConn++ - case IDFileCreate: - now := time.Now() - - // Set new fields - e.Set(&pathFileCount, "?") - e.Set(&pathFileCountByExt, "?") - e.Set(&pathFileExtension, "?") - - if pt.Stats.TimeFirstFileCreated.IsZero() { - pt.Stats.TimeFirstFileCreated = now - } - - if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { - ext := filepath.Ext(target) - pt.Stats.CountFilesCreatedByExt[ext]++ - // Setting file count by extension - e.Set(&pathFileCountByExt, toString(pt.Stats.CountFilesCreatedByExt[ext])) - // Setting file extension - e.Set(&pathFileExtension, ext) - } - pt.Stats.CountFilesCreated++ - // Setting total file count - e.Set(&pathFileCount, toString(pt.Stats.CountFilesCreated)) - // Setting frequency - freq := now.Sub(pt.Stats.TimeFirstFileCreated) - if freq != 0 { - eps := pt.Stats.CountFilesCreated * int64(math.Pow10(9)) / freq.Nanoseconds() - e.Set(&pathFileFrequency, toString(int64(eps))) - } else { - e.Set(&pathFileFrequency, toString(0)) - } - // Finally set last event timestamp - pt.Stats.TimeLastFileCreated = now - - case IDFileDelete, IDFileDeleteDetected: - now := time.Now() - - // Set new fields - e.Set(&pathFileCount, "?") - e.Set(&pathFileCountByExt, "?") - e.Set(&pathFileExtension, "?") - - if pt.Stats.TimeFirstFileDeleted.IsZero() { - pt.Stats.TimeFirstFileDeleted = now - } - - if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { - ext := filepath.Ext(target) - pt.Stats.CountFilesDeletedByExt[ext]++ - // Setting file count by extension - e.Set(&pathFileCountByExt, toString(pt.Stats.CountFilesDeletedByExt[ext])) - // Setting file extension - e.Set(&pathFileExtension, ext) - } - pt.Stats.CountFilesDeleted++ - // Setting total file count - e.Set(&pathFileCount, toString(pt.Stats.CountFilesDeleted)) - - // Setting frequency - freq := now.Sub(pt.Stats.TimeFirstFileDeleted) - if freq != 0 { - eps := pt.Stats.CountFilesDeleted * int64(math.Pow10(9)) / freq.Nanoseconds() - e.Set(&pathFileFrequency, toString(int64(eps))) - } else { - e.Set(&pathFileFrequency, toString(0)) - } - - // Finally set last event timestamp - pt.Stats.TimeLastFileDeleted = time.Now() - } - } - } - } -} - -func guidFromEvent(e *evtx.GoEvtxMap) string { - if uuid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - return uuid - } - if uuid, err := e.GetString(&pathSysmonSourceProcessGUID); err == nil { - return uuid - } - if uuid, err := e.GetString(&pathSysmonCRTSourceProcessGuid); err == nil { - return uuid - } - return "" -} - -func processTrackFromEvent(e *evtx.GoEvtxMap) *processTrack { - if uuid := guidFromEvent(e); uuid != "" { - return processTracker.GetByGuid(uuid) - } - return nil -} - -func hasAction(e *evtx.GoEvtxMap, action string) bool { - if i, err := e.Get(&engine.ActionsPath); err == nil { - if actions, ok := (*i).([]string); ok { - for _, a := range actions { - if a == action { - return true - } - } - } - } - return false -} - -func hookHandleActions(e *evtx.GoEvtxMap) { - var kill, memdump bool - - // We have to check that if we are handling one of - // our event and we don't want to kill ourself - if isSelf(e) { - return - } - - // the only requirement to be able to handle action - // is to have a process guuid - if uuid := guidFromEvent(e); uuid != "" { - if i, err := e.Get(&engine.ActionsPath); err == nil { - if actions, ok := (*i).([]string); ok { - for _, action := range actions { - switch action { - case "kill": - kill = true - if pt := processTrackFromEvent(e); pt != nil { - // additional check not to suspend agent - if int(pt.PID) != os.Getpid() { - // before we kill we suspend the process - kernel32.SuspendProcess(int(pt.PID)) - } - } - case "blacklist": - if pt := processTrackFromEvent(e); pt != nil { - // additional check not to blacklist agent - if int(pt.PID) != os.Getpid() { - processTracker.Blacklist(pt.CommandLine) - } - } - case ActionMemdump: - memdump = true - dumpProcessRtn(e) - case ActionRegdump: - dumpRegistryRtn(e) - case ActionFiledump: - dumpFilesRtn(e) - default: - log.Errorf("Cannot handle %s action as it is unknown", action) - } - } - } - - // handle kill operation after the other actions - if kill { - if pt := processTrackFromEvent(e); pt != nil { - if memdump { - // Wait we finish dumping before killing the process - go func() { - guid := pt.ProcessGUID - for i := 0; i < 60 && !memdumped.Contains(guid); i++ { - time.Sleep(1 * time.Second) - } - if err := pt.TerminateProcess(); err != nil { - log.Errorf("Failed to terminate process PID=%d GUID=%s", pt.PID, pt.ProcessGUID) - } - }() - } else if err := pt.TerminateProcess(); err != nil { - log.Errorf("Failed to terminate process PID=%d GUID=%s", pt.PID, pt.ProcessGUID) - } - } - } - } - } else { - log.Errorf("Failed to handle actions for event (channel: %s, id: %d): no process GUID available", e.Channel(), e.EventID()) - } -} - -// hook terminating previously blacklisted processes (according to their CommandLine) -func hookTerminator(e *evtx.GoEvtxMap) { - if e.EventID() == IDProcessCreate { - if commandLine, err := e.GetString(&pathSysmonCommandLine); err == nil { - if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { - if processTracker.IsBlacklisted(commandLine) { - log.Warnf("Terminating blacklisted process PID=%d CommandLine=\"%s\"", pid, commandLine) - if err := terminate(int(pid)); err != nil { - log.Errorf("Failed to terminate process PID=%d: %s", pid, err) - } - } - } - } - } -} - -// hook setting flagProcTermEn variable -// it is also used to cleanup any structures needing to be cleaned -func hookProcTerm(e *evtx.GoEvtxMap) { - log.Debug("Process termination events are enabled") - flagProcTermEn = true - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - // Releasing resources - processTracker.Terminate(guid) - memdumped.Del(guid) - } -} - -func hookSelfGUID(e *evtx.GoEvtxMap) { - if selfGUID == "" { - if e.EventID() == IDProcessCreate { - // Sometimes it happens that other events are generated before process creation - // Check parent image first because we launch whids.exe -h to test process termination - // and we catch it up if we check image first - if pimage, err := e.GetString(&pathSysmonParentImage); err == nil { - if pimage == selfPath { - if pguid, err := e.GetString(&pathSysmonParentProcessGUID); err == nil { - selfGUID = pguid - log.Infof("Found self GUID from PGUID: %s", selfGUID) - return - } - } - } - if image, err := e.GetString(&pathSysmonImage); err == nil { - if image == selfPath { - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - selfGUID = guid - log.Infof("Found self GUID: %s", selfGUID) - return - } - } - } - } - } -} - -func isIntegrityComputed(pt *processTrack) bool { - if pt == nil { - return false - } - return pt.IntegrityComputed -} - -func hookFileSystemAudit(e *evtx.GoEvtxMap) { - e.Set(&pathSysmonCommandLine, "?") - e.Set(&pathSysmonProcessGUID, nullGUID) - e.Set(&pathImageHashes, "?") - if pid, err := e.GetInt(&pathFSAuditProcessId); err == nil { - if pt := processTracker.GetByPID(pid); pt != nil { - if pt.CommandLine != "" { - e.Set(&pathSysmonCommandLine, pt.CommandLine) - } - if pt.Hashes != "" { - e.Set(&pathImageHashes, pt.Hashes) - } - if pt.ProcessGUID != "" { - e.Set(&pathSysmonProcessGUID, pt.ProcessGUID) - } - } - } -} - -func hookProcessIntegrityProcTamp(e *evtx.GoEvtxMap) { - // Default values - e.Set(&pathProcessIntegrity, toString(-1.0)) - - // Sysmon Create Process - if e.EventID() == IDProcessTampering { - if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { - // prevent stopping our own process, it may happen in some - // cases when selfGuid is not found fast enough - if pid != int64(os.Getpid()) { - if kernel32.IsPIDRunning(int(pid)) { - // we first need to wait main process thread - mainTid := kernel32.GetFirstTidOfPid(int(pid)) - // if we found the main thread of pid - if mainTid > 0 { - hThread, err := kernel32.OpenThread(kernel32.THREAD_SUSPEND_RESUME, win32.FALSE, win32.DWORD(mainTid)) - if err != nil { - log.Errorf("Cannot open main thread before checking integrity of PID=%d", pid) - } else { - defer kernel32.CloseHandle(hThread) - if ok := kernel32.WaitThreadRuns(hThread, time.Millisecond*50, time.Millisecond*500); !ok { - // We check whether the thread still exists - checkThread, err := kernel32.OpenThread(kernel32.PROCESS_SUSPEND_RESUME, win32.FALSE, win32.DWORD(mainTid)) - if err == nil { - log.Warnf("Timeout reached while waiting main thread of PID=%d", pid) - } - kernel32.CloseHandle(checkThread) - } else { - da := win32.DWORD(kernel32.PROCESS_VM_READ | kernel32.PROCESS_QUERY_INFORMATION) - hProcess, err := kernel32.OpenProcess(da, win32.FALSE, win32.DWORD(pid)) - - if err != nil { - log.Errorf("Cannot open process to check integrity of PID=%d: %s", pid, err) - } else { - defer kernel32.CloseHandle(hProcess) - bdiff, slen, err := kernel32.CheckProcessIntegrity(hProcess) - if err != nil { - log.Errorf("Cannot check integrity of PID=%d: %s", pid, err) - } else { - if slen != 0 { - integrity := utils.Round(float64(bdiff)*100/float64(slen), 2) - e.Set(&pathProcessIntegrity, toString(integrity)) - } - } - } - } - } - } - } - } - } else { - log.Debugf("Cannot check integrity of PID=%d: process terminated", pid) - } - } -} - -// too big to be put in hookEnrichAnySysmon -func hookEnrichServices(e *evtx.GoEvtxMap) { - // We do this only if we can cleanup resources - eventID := e.EventID() - if flagProcTermEn { - switch eventID { - case IDDriverLoad, IDWMIBinding, IDWMIConsumer, IDWMIFilter: - // Nothing to do - break - case IDCreateRemoteThread, IDAccessProcess: - e.Set(&pathSourceServices, "?") - e.Set(&pathTargetServices, "?") - - sguidPath := &pathSysmonSourceProcessGUID - tguidPath := &pathSysmonTargetProcessGUID - - if eventID == 8 { - sguidPath = &pathSysmonCRTSourceProcessGuid - tguidPath = &pathSysmonCRTTargetProcessGuid - } - - if sguid, err := e.GetString(sguidPath); err == nil { - // First try to resolve it by tracked process - if t := processTracker.GetByGuid(sguid); t != nil { - e.Set(&pathSourceServices, t.Services) - } else { - // If it fails we resolve the services by PID - if spid, err := e.GetInt(&pathSysmonSourceProcessId); err == nil { - if svcs, err := advapi32.ServiceWin32NamesByPid(uint32(spid)); err == nil { - e.Set(&pathSourceServices, svcs) - } else { - log.Errorf("Failed to resolve service from PID=%d: %s", spid, err) - } - } - } - } - - // First try to resolve it by tracked process - if tguid, err := e.GetString(tguidPath); err == nil { - if t := processTracker.GetByGuid(tguid); t != nil { - e.Set(&pathTargetServices, t.Services) - } else { - // If it fails we resolve the services by PID - if tpid, err := e.GetInt(&pathSysmonTargetProcessId); err == nil { - if svcs, err := advapi32.ServiceWin32NamesByPid(uint32(tpid)); err == nil { - e.Set(&pathTargetServices, svcs) - } else { - log.Errorf("Failed to resolve service from PID=%d: %s", tpid, err) - } - } - } - } - default: - e.Set(&pathServices, "?") - // image, guid and pid are supposed to be available for all the remaining Sysmon logs - if image, err := e.GetString(&pathSysmonImage); err == nil { - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if pid, err := e.GetInt(&pathSysmonProcessId); err == nil { - track := processTracker.GetByGuid(guid) - // we missed process creation so we create a minimal track - if track == nil { - track = NewProcessTrack() - track.Image = image - track.ProcessGUID = guid - track.PID = pid - processTracker.Add(track) - } - - if track.Services == "" { - track.Services, err = advapi32.ServiceWin32NamesByPid(uint32(pid)) - if err != nil { - log.Errorf("Failed to resolve service from PID=%d: %s", pid, err) - track.Services = "Error" - } - } - e.Set(&pathServices, track.Services) - } - } - } - } - } -} - -func hookSetValueSize(e *evtx.GoEvtxMap) { - e.Set(&pathValueSize, toString(-1)) - if targetObject, err := e.GetString(&pathSysmonTargetObject); err == nil { - size, err := advapi32.RegGetValueSizeFromString(targetObject) - if err != nil { - log.Errorf("Failed to get value size \"%s\": %s", targetObject, err) - } - e.Set(&pathValueSize, toString(size)) - } -} - -// hook that replaces the destination hostname of Sysmon Network connection -// event with the one previously found in the DNS logs -func hookEnrichDNSSysmon(e *evtx.GoEvtxMap) { - if ip, err := e.GetString(&pathSysmonDestIP); err == nil { - if dom, ok := dnsResolution[ip]; ok { - e.Set(&pathSysmonDestHostname, dom) - } - } -} - -// Todo: move this function into evtx package -func eventHas(e *evtx.GoEvtxMap, p *evtx.GoEvtxPath) bool { - _, err := e.GetString(p) - return err == nil -} - -func hookEnrichAnySysmon(e *evtx.GoEvtxMap) { - eventID := e.EventID() - switch eventID { - case IDProcessCreate, IDDriverLoad: - // ProcessCreation is already processed in hookTrack - // DriverLoad does not contain any GUID information - break - - case IDCreateRemoteThread, IDAccessProcess: - // Handling CreateRemoteThread and ProcessAccess events - // Default Values for the fields - e.Set(&pathSourceUser, "?") - e.Set(&pathSourceIntegrityLevel, "?") - e.Set(&pathTargetUser, "?") - e.Set(&pathTargetIntegrityLevel, "?") - e.Set(&pathTargetParentProcessGuid, "?") - e.Set(&pathSourceHashes, "?") - e.Set(&pathTargetHashes, "?") - - sguidPath := &pathSysmonSourceProcessGUID - tguidPath := &pathSysmonTargetProcessGUID - - if eventID == IDCreateRemoteThread { - sguidPath = &pathSysmonCRTSourceProcessGuid - tguidPath = &pathSysmonCRTTargetProcessGuid - } - - if sguid, err := e.GetString(sguidPath); err == nil { - if tguid, err := e.GetString(tguidPath); err == nil { - if strack := processTracker.GetByGuid(sguid); strack != nil { - if strack.User != "" { - e.Set(&pathSourceUser, strack.User) - } - if strack.IntegrityLevel != "" { - e.Set(&pathSourceIntegrityLevel, strack.IntegrityLevel) - } - if strack.Hashes != "" { - e.Set(&pathSourceHashes, strack.Hashes) - } - } - if ttrack := processTracker.GetByGuid(tguid); ttrack != nil { - if ttrack.User != "" { - e.Set(&pathTargetUser, ttrack.User) - } - if ttrack.IntegrityLevel != "" { - e.Set(&pathTargetIntegrityLevel, ttrack.IntegrityLevel) - } - if ttrack.ParentProcessGUID != "" { - e.Set(&pathTargetParentProcessGuid, ttrack.ParentProcessGUID) - } - if ttrack.Hashes != "" { - e.Set(&pathTargetHashes, ttrack.Hashes) - } - } - } - } - break - - default: - - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if track := processTracker.GetByGuid(guid); track != nil { - // if event does not have CommandLine field - if !eventHas(e, &pathSysmonCommandLine) { - e.Set(&pathSysmonCommandLine, "?") - if track.CommandLine != "" { - e.Set(&pathSysmonCommandLine, track.CommandLine) - } - } - - // if event does not have User field - if !eventHas(e, &pathSysmonUser) { - e.Set(&pathSysmonUser, "?") - if track.User != "" { - e.Set(&pathSysmonUser, track.User) - } - } - - // if event does not have IntegrityLevel field - if !eventHas(e, &pathSysmonIntegrityLevel) { - e.Set(&pathSysmonIntegrityLevel, "?") - if track.IntegrityLevel != "" { - e.Set(&pathSysmonIntegrityLevel, track.IntegrityLevel) - } - } - - // if event does not have CurrentDirectory field - if !eventHas(e, &pathSysmonCurrentDirectory) { - e.Set(&pathSysmonCurrentDirectory, "?") - if track.CurrentDirectory != "" { - e.Set(&pathSysmonCurrentDirectory, track.CurrentDirectory) - } - } - - // event never has ImageHashes field since it is not Sysmon standard - e.Set(&pathImageHashes, "?") - if track.Hashes != "" { - e.Set(&pathImageHashes, track.Hashes) - } - - // Signature information - e.Set(&pathImageSigned, toString(track.Signed)) - e.Set(&pathImageSignature, track.Signature) - e.Set(&pathImageSignatureStatus, track.SignatureStatus) - } - } - } -} - -func hookClipboardEvents(e *evtx.GoEvtxMap) { - e.Set(&pathSysmonClipboardData, "?") - if hashes, err := e.GetString(&pathSysmonHashes); err == nil { - fname := fmt.Sprintf("CLIP-%s", sysmonArcFileRe.ReplaceAllString(hashes, "")) - path := filepath.Join(sysmonArchiveDirectory, fname) - if fi, err := os.Stat(path); err == nil { - // limit size of ClipboardData to 1 Mega - if fi.Mode().IsRegular() && fi.Size() < Mega { - if data, err := ioutil.ReadFile(path); err == nil { - // We try to decode utf16 content because regexp can only match utf8 - // Thus doing this is needed to apply detection rule on clipboard content - if enc, err := utils.Utf16ToUtf8(data); err == nil { - e.Set(&pathSysmonClipboardData, string(enc)) - } else { - e.Set(&pathSysmonClipboardData, fmt.Sprintf("%q", data)) - } - } - } - } - } -} - -//////////////////// Hooks' helpers ///////////////////// - -func getCriticality(e *evtx.GoEvtxMap) int { - if c, err := e.Get(&pathGeneCriticality); err == nil { - return (*c).(int) - } - return 0 -} - -func compress(path string) { - if flagDumpCompress { - if !compressionIsRunning { - // start compression routine - go func() { - compressionIsRunning = true - for path := range compressionChannel { - log.Infof("Compressing %s", path) - if err := utils.GzipFile(path); err != nil { - log.Errorf("Cannot compress %s: %s", path, err) - } - } - compressionIsRunning = false - }() - } - compressionChannel <- path - } -} - -func dumpPidAndCompress(pid int, guid, id string) { - // prevent stopping ourself (><) - if kernel32.IsPIDRunning(pid) && pid != selfPid && !memdumped.Contains(guid) && !dumping.Contains(guid) { - - // To avoid dumping the same process twice, possible if two alerts - // comes from the same GUID in a short period of time - dumping.Add(guid) - defer dumping.Del(guid) - - tmpDumpDir := filepath.Join(dumpDirectory, guid, id) - os.MkdirAll(tmpDumpDir, defaultPerms) - module, err := kernel32.GetModuleFilenameFromPID(int(pid)) - if err != nil { - log.Errorf("Cannot get module filename for memory dump PID=%d: %s", pid, err) - } - dumpFilename := fmt.Sprintf("%s_%d_%d.dmp", filepath.Base(module), pid, time.Now().UnixNano()) - dumpPath := filepath.Join(tmpDumpDir, dumpFilename) - log.Infof("Trying to dump memory of process PID=%d Image=\"%s\"", pid, module) - //log.Infof("Mock dump: %s", dumpFilename) - err = dbghelp.FullMemoryMiniDump(pid, dumpPath) - if err != nil { - log.Errorf("Failed to dump process PID=%d Image=%s: %s", pid, module, err) - } else { - // dump was successfull - memdumped.Add(guid) - compress(dumpPath) - } - } else { - log.Warnf("Cannot dump process PID=%d, the process is already terminated", pid) - } - -} - -func dumpFileAndCompress(src, path string) error { - var err error - os.MkdirAll(path, defaultPerms) - sha256, err := file.Sha256(src) - if err != nil { - return err - } - // replace : in case we are dumping an ADS - base := strings.Replace(filepath.Base(src), ":", "_ADS_", -1) - dst := filepath.Join(path, fmt.Sprintf("%d_%s.bin", time.Now().UnixNano(), base)) - // dump sha256 of file anyway - ioutil.WriteFile(fmt.Sprintf("%s.sha256", dst), []byte(sha256), 600) - if !filedumped.Contains(sha256) { - log.Debugf("Dumping file: %s->%s", src, dst) - if err = fsutil.CopyFile(src, dst); err == nil { - compress(dst) - filedumped.Add(sha256) - } - } - return err -} - -func idFromEvent(e *evtx.GoEvtxMap) string { - bs := utils.ByteSlice(evtx.ToJSON(e)) - sort.Stable(bs) - return data.Md5(bs) -} - -func dumpEventAndCompress(e *evtx.GoEvtxMap, guid string) (err error) { - id := idFromEvent(e) - tmpDumpDir := filepath.Join(dumpDirectory, guid, id) - os.MkdirAll(tmpDumpDir, defaultPerms) - dumpPath := filepath.Join(tmpDumpDir, fmt.Sprintf("%s_event.json", id)) - - if !dumping.Contains(id) && !filedumped.Contains(id) { - dumping.Add(id) - defer dumping.Del(id) - - var f *os.File - - f, err = os.Create(dumpPath) - if err != nil { - return - } - f.Write(evtx.ToJSON(e)) - f.Close() - compress(dumpPath) - filedumped.Add(id) - } - return -} - -//////////////////// Post Detection Hooks ///////////////////// - -// variables specific to post-detection hooks -var ( - sysmonArcFileRe = regexp.MustCompile("(((SHA1|MD5|SHA256|IMPHASH)=)|,)") -) - -func hookDumpProcess(e *evtx.GoEvtxMap) { - // We have to check that if we are handling one of - // our event and we don't want to dump ourself - if isSelf(e) { - return - } - - // we dump only if alert is relevant - if getCriticality(e) < dumpTresh { - return - } - - // if memory got already dumped - if hasAction(e, ActionMemdump) { - return - } - - dumpProcessRtn(e) -} - -// this hook can run async -func dumpProcessRtn(e *evtx.GoEvtxMap) { - - parallelHooks.Acquire() - go func() { - defer parallelHooks.Release() - var pidPath *evtx.GoEvtxPath - var procGUIDPath *evtx.GoEvtxPath - - // the interesting pid to dump depends on the event - switch e.EventID() { - case IDCreateRemoteThread, IDAccessProcess: - pidPath = &pathSysmonSourceProcessId - procGUIDPath = &pathSysmonSourceProcessGUID - default: - pidPath = &pathSysmonProcessId - procGUIDPath = &pathSysmonProcessGUID - } - - if guid, err := e.GetString(procGUIDPath); err == nil { - - // check if we should go on - if !processTracker.CheckDumpCountOrInc(guid, maxDumps, flagDumpUntracked) { - log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) - return - } - - if pid, err := e.GetInt(pidPath); err == nil { - dumpEventAndCompress(e, guid) - dumpPidAndCompress(int(pid), guid, idFromEvent(e)) - } - } - }() -} - -func hookDumpRegistry(e *evtx.GoEvtxMap) { - // We have to check that if we are handling one of - // our event and we don't want to dump ourself - if isSelf(e) { - return - } - - // we dump only if alert is relevant - if getCriticality(e) < dumpTresh { - return - } - - // if registry got already dumped - if hasAction(e, ActionRegdump) { - return - } - - dumpRegistryRtn(e) -} - -// ToDo: test this function -func dumpRegistryRtn(e *evtx.GoEvtxMap) { - parallelHooks.Acquire() - go func() { - defer parallelHooks.Release() - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - - // check if we should go on - if !processTracker.CheckDumpCountOrInc(guid, maxDumps, flagDumpUntracked) { - log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) - return - } - - if targetObject, err := e.GetString(&pathSysmonTargetObject); err == nil { - if details, err := e.GetString(&pathSysmonDetails); err == nil { - // We dump only if Details is "Binary Data" since the other kinds can be seen in the raw event - if details == "Binary Data" { - dumpPath := filepath.Join(dumpDirectory, guid, idFromEvent(e), "reg.txt") - key, value := filepath.Split(targetObject) - dumpEventAndCompress(e, guid) - content, err := utils.RegQuery(key, value) - if err != nil { - log.Errorf("Failed to run reg query: %s", err) - content = fmt.Sprintf("Error Dumping %s: %s", targetObject, err) - } - err = ioutil.WriteFile(dumpPath, []byte(content), 0600) - if err != nil { - log.Errorf("Failed to write registry content to file: %s", err) - return - } - compress(dumpPath) - return - } - return - } - } - } - log.Errorf("Failed to dump registry from event") - }() -} - -func dumpCommandLine(e *evtx.GoEvtxMap, dumpPath string) { - if cl, err := e.GetString(&pathSysmonCommandLine); err == nil { - if cwd, err := e.GetString(&pathSysmonCurrentDirectory); err == nil { - if argv, err := utils.ArgvFromCommandLine(cl); err == nil { - if len(argv) > 1 { - for _, arg := range argv[1:] { - if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { - if err = dumpFileAndCompress(arg, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) - } - } - // try to dump a path relative to CWD - relarg := filepath.Join(cwd, arg) - if fsutil.IsFile(relarg) && !utils.IsPipePath(relarg) { - if err = dumpFileAndCompress(relarg, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), relarg, err) - } - } - } - } - } - } - } -} - -func dumpParentCommandLine(e *evtx.GoEvtxMap, dumpPath string) { - if guid, err := e.GetString(&pathSysmonProcessGUID); err == nil { - if track := processTracker.GetByGuid(guid); track != nil { - if argv, err := utils.ArgvFromCommandLine(track.ParentCommandLine); err == nil { - if len(argv) > 1 { - for _, arg := range argv[1:] { - if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { - if err = dumpFileAndCompress(arg, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) - } - } - // try to dump a path relative to parent CWD - if track.ParentCurrentDirectory != "" { - relarg := filepath.Join(track.ParentCurrentDirectory, arg) - if fsutil.IsFile(relarg) && !utils.IsPipePath(relarg) { - if err = dumpFileAndCompress(relarg, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), relarg, err) - } - } - } - } - } - } - } - } -} - -func hookDumpFiles(e *evtx.GoEvtxMap) { - // We have to check that if we are handling one of - // our event and we don't want to dump ourself - if isSelf(e) { - return - } - - // we dump only if alert is relevant - if getCriticality(e) < dumpTresh { - return - } - - // if file got already dumped - if hasAction(e, ActionFiledump) { - return - } - - dumpFilesRtn(e) -} - -func dumpFilesRtn(e *evtx.GoEvtxMap) { - parallelHooks.Acquire() - go func() { - defer parallelHooks.Release() - guid := nullGUID - tmpGUID, err := e.GetString(&pathSysmonProcessGUID) - if err != nil { - if tmpGUID, err = e.GetString(&pathSysmonSourceProcessGUID); err == nil { - guid = tmpGUID - } - } else { - guid = tmpGUID - } - - // check if we should go on - if !processTracker.CheckDumpCountOrInc(guid, maxDumps, flagDumpUntracked) { - log.Warnf("Not dumping, reached maximum dumps count for guid %s", guid) - return - } - - // build up dump path - dumpPath := filepath.Join(dumpDirectory, guid, idFromEvent(e)) - // dump event who triggered the dump - dumpEventAndCompress(e, guid) - - // dump CommandLine fields regardless of the event - // this would actually work best when hooks are enabled and enrichment occurs - // in the worst case it would only work for Sysmon CreateProcess events - dumpCommandLine(e, dumpPath) - dumpParentCommandLine(e, dumpPath) - - // Handling different kinds of event IDs - switch e.EventID() { - - case IDFileTime, IDFileCreate, IDCreateStreamHash: - if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { - if err = dumpFileAndCompress(target, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), target, err) - } - } - - case IDDriverLoad: - if im, err := e.GetString(&pathSysmonImageLoaded); err == nil { - if err = dumpFileAndCompress(im, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) - } - } - - case IDAccessProcess: - if sim, err := e.GetString(&pathSysmonSourceImage); err == nil { - if err = dumpFileAndCompress(sim, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), sim, err) - } - } - - case IDRegSetValue, IDWMIConsumer: - // for event ID 13 - path := &pathSysmonDetails - if e.EventID() == IDWMIConsumer { - path = &pathSysmonDestination - } - if cl, err := e.GetString(path); err == nil { - // try to parse details as a command line - if argv, err := utils.ArgvFromCommandLine(cl); err == nil { - for _, arg := range argv { - if fsutil.IsFile(arg) && !utils.IsPipePath(arg) { - if err = dumpFileAndCompress(arg, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), arg, err) - } - } - } - } - } - - case IDFileDelete: - if im, err := e.GetString(&pathSysmonImage); err == nil { - if err = dumpFileAndCompress(im, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) - } - } - - archived, err := e.GetBool(&pathSysmonArchived) - if err == nil && archived { - if !fsutil.IsDir(sysmonArchiveDirectory) { - log.Errorf("Aborting deleted file dump: %s archive directory does not exist", sysmonArchiveDirectory) - return - } - log.Info("Will try to dump deleted file") - if hashes, err := e.GetString(&pathSysmonHashes); err == nil { - if target, err := e.GetString(&pathSysmonTargetFilename); err == nil { - fname := fmt.Sprintf("%s%s", sysmonArcFileRe.ReplaceAllString(hashes, ""), filepath.Ext(target)) - path := filepath.Join(sysmonArchiveDirectory, fname) - if err = dumpFileAndCompress(path, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), path, err) - } - } - } - } - - default: - if im, err := e.GetString(&pathSysmonImage); err == nil { - if err = dumpFileAndCompress(im, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), im, err) - } - } - if pim, err := e.GetString(&pathSysmonParentImage); err == nil { - if err = dumpFileAndCompress(pim, dumpPath); err != nil { - log.Errorf("Error dumping file from EventID=%d \"%s\": %s", e.EventID(), pim, err) - } - } - } - }() -} diff --git a/tools/whids/main.go b/tools/whids/main.go index 6071fb6..6345241 100644 --- a/tools/whids/main.go +++ b/tools/whids/main.go @@ -12,8 +12,12 @@ import ( "path/filepath" "runtime/pprof" "strings" + "time" "github.com/0xrawsec/gene/engine" + "github.com/0xrawsec/whids/api" + "github.com/0xrawsec/whids/hids" + "github.com/0xrawsec/whids/utils" "github.com/pelletier/go-toml" "golang.org/x/sys/windows/svc" @@ -41,6 +45,79 @@ const ( svcName = "WHIDS" ) +var ( + abs, _ = filepath.Abs(filepath.Dir(os.Args[0])) + + logDir = filepath.Join(abs, "Logs") + + // DefaultHIDSConfig is the default HIDS configuration + DefaultHIDSConfig = hids.Config{ + RulesConfig: &hids.RulesConfig{ + RulesDB: filepath.Join(abs, "Database", "Rules"), + ContainersDB: filepath.Join(abs, "Database", "Containers"), + UpdateInterval: 60 * time.Second, + }, + + FwdConfig: &api.ForwarderConfig{ + Local: true, + Client: api.ClientConfig{ + MaxUploadSize: api.DefaultMaxUploadSize, + }, + Logging: api.LoggingConfig{ + Dir: filepath.Join(logDir, "Alerts"), + RotationInterval: time.Hour * 5, + }, + }, + Channels: []string{"all"}, + Sysmon: &hids.SysmonConfig{ + Bin: "C:\\Windows\\Sysmon64.exe", + ArchiveDirectory: "C:\\Sysmon\\", + CleanArchived: true, + }, + Dump: &hids.DumpConfig{ + Mode: "file|registry", + Dir: filepath.Join(abs, "Dumps"), + Compression: true, + MaxDumps: 4, + Treshold: 8, + DumpUntracked: false, + }, + Report: &hids.ReportConfig{ + EnableReporting: false, + OSQuery: hids.OSQueryConfig{ + Bin: "C:\\Program Files\\osquery\\osqueryi.exe", + Tables: []string{"processes", "services", "scheduled_tasks", "drivers", "startup_items", "process_open_sockets"}}, + Commands: []hids.ReportCommand{{ + Description: "Example command", + Name: "osqueryi.exe", + Args: []string{"--json", "-A", "processes"}, + ExpectJSON: true, + }}, + CommandTimeout: 60 * time.Second, + }, + AuditConfig: &hids.AuditConfig{ + AuditPolicies: []string{"File System"}, + }, + CanariesConfig: &hids.CanariesConfig{ + Enable: false, + Canaries: []*hids.Canary{ + { + Directories: []string{"$SYSTEMDRIVE", "$SYSTEMROOT"}, + Files: []string{"readme.pdf", "readme.docx", "readme.txt"}, + Delete: true, + }, + }, + Actions: []string{"kill", "memdump", "filedump", "blacklist", "report"}, + Whitelist: []string{"C:\\Windows\\explorer.exe"}, + }, + CritTresh: 5, + Logfile: filepath.Join(logDir, "whids.log"), + EnableHooks: true, + EnableFiltering: true, + Endpoint: true, + LogAll: false} +) + var ( flagDumpDefault bool flagDryRun bool @@ -51,7 +128,7 @@ var ( flagProfile bool flagRestore bool - hids *HIDS + hostIDS *hids.HIDS importRules string @@ -65,8 +142,8 @@ func printInfo(writer io.Writer) { } func fmtAliases() string { - aliases := make([]string, 0, len(channelAliases)) - for alias, channel := range channelAliases { + aliases := make([]string, 0, len(hids.ChannelAliases)) + for alias, channel := range hids.ChannelAliases { aliases = append(aliases, fmt.Sprintf("\t\t%s : %s", alias, channel)) } return strings.Join(aliases, "\n") @@ -74,22 +151,22 @@ func fmtAliases() string { func runHids(service bool) { var err error - var hidsConf HIDSConfig + var hidsConf hids.Config log.Infof("Running HIDS as Windows service: %t", service) - hidsConf, err = LoadsHIDSConfig(config) + hidsConf, err = hids.LoadsHIDSConfig(config) if err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to load configuration: %s", err)) } - hids, err = NewHIDS(&hidsConf) + hostIDS, err = hids.NewHIDS(&hidsConf) if err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to create HIDS: %s", err)) } - hids.DryRun = flagDryRun - hids.PrintAll = flagPrintAll + hostIDS.DryRun = flagDryRun + hostIDS.PrintAll = flagPrintAll // If not a service we need to be able to stop the HIDS if !service { @@ -99,14 +176,14 @@ func runHids(service bool) { <-osSignals log.Infof("Received SIGINT") // runs stop on sigint - hids.Stop() + hostIDS.Stop() }() } // Runs HIDS and wait for the output - hids.Run() + hostIDS.Run() if !service { - hids.Wait() + hostIDS.Wait() } } @@ -149,7 +226,7 @@ func main() { printInfo(os.Stderr) fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", filepath.Base(os.Args[0])) fmt.Fprintf(os.Stderr, "\nAvailable Channel Aliases:\n%s\n", fmtAliases()) - fmt.Fprintf(os.Stderr, "\nAvailable Dump modes: %s\n", strings.Join(dumpOptions, ", ")) + fmt.Fprintf(os.Stderr, "\nAvailable Dump modes: %s\n", strings.Join(hids.DumpOptions, ", ")) flag.PrintDefaults() os.Exit(exitSuccess) } @@ -204,7 +281,7 @@ func main() { log.InitLogger(log.LDebug) } - hidsConf, err := LoadsHIDSConfig(config) + hidsConf, err := hids.LoadsHIDSConfig(config) if err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to load configuration: %s", err)) } @@ -224,31 +301,31 @@ func main() { // in order not to write logs into file // TODO: add a stream handler to log facility hidsConf.Logfile = "" - hids, err = NewHIDS(&hidsConf) + hostIDS, err = hids.NewHIDS(&hidsConf) if err != nil { log.LogErrorAndExit(fmt.Errorf("Failed create HIDS: %s", err)) } log.Infof("Importing rules from %s", importRules) - hids.engine = engine.NewEngine(false) - hids.engine.SetDumpRaw(true) + hostIDS.Engine = engine.NewEngine(false) + hostIDS.Engine.SetDumpRaw(true) - if err := hids.engine.LoadDirectory(importRules); err != nil { + if err := hostIDS.Engine.LoadDirectory(importRules); err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to import rules: %s", err)) } - prules, psha256 := hids.rulesPaths() + prules, psha256 := hostIDS.RulesPaths() rules := new(bytes.Buffer) - for rule := range hids.engine.GetRawRule(".*") { + for rule := range hostIDS.Engine.GetRawRule(".*") { if _, err := rules.Write([]byte(rule + "\n")); err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to import rules: %s", err)) } } - if err := ioutil.WriteFile(prules, rules.Bytes(), defaultPerms); err != nil { + if err := ioutil.WriteFile(prules, rules.Bytes(), utils.DefaultPerms); err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to import rules: %s", err)) } - if err := ioutil.WriteFile(psha256, []byte(data.Sha256(rules.Bytes())), defaultPerms); err != nil { + if err := ioutil.WriteFile(psha256, []byte(data.Sha256(rules.Bytes())), utils.DefaultPerms); err != nil { log.LogErrorAndExit(fmt.Errorf("Failed to import rules: %s", err)) } @@ -257,5 +334,5 @@ func main() { } runHids(false) - hids.LogStats() + hostIDS.LogStats() } diff --git a/tools/whids/manage.bat b/tools/whids/manage.bat index 16daccb..232a0c8 100644 --- a/tools/whids/manage.bat +++ b/tools/whids/manage.bat @@ -6,6 +6,7 @@ set UNINSTALL_SCRIPT=%INSTALL_DIR%\Uninstall.bat set BINPATH=%INSTALL_DIR%\%BASENAME% set CONFIG=%INSTALL_DIR%\config.toml REM default during installation used to clean +set LOGS=%INSTALL_DIR%\Logs set ALERTS=%INSTALL_DIR%\Logs\Alerts set DUMPS=%INSTALL_DIR%\Dumps set VERSION="REPLACED BY MAKEFILE" @@ -16,16 +17,17 @@ set SYSMON=Sysmon64 set RULES_IMPORT="%~dp0\rules" :choice -echo [i] Install WHIDS from scratch (removes older installation) -echo [un] Uninstall previous installation -echo [up] Update WHIDS binary and rules (keeps current config) -echo [st] Start services -echo [sp] Stop services -echo [r] Restart services -echo [g] Remove alerts logs and dumps -echo [e] Edit WHIDS configuration -echo [c] Clear screen -echo [q] Quit +echo [i] Install WHIDS from scratch (removes older installation) +echo [un] Uninstall previous installation +echo [up] Update WHIDS binary and rules (keeps current config) +echo [st] Start services +echo [sp] Stop services +echo [r] Restart services +echo [g] Remove alerts logs and dumps +echo [e] Edit WHIDS configuration with a registered application +echo [en] Edit WHIDS configuration with notepad +echo [c] Clear screen +echo [q] Quit echo. echo Whids version: %VERSION% (commit: %COMMITID%) @@ -68,6 +70,10 @@ FOR /F "tokens=1* delims=+" %%A IN (%_in_ch%) DO ( call :Groom ) IF "%%A"=="e" ( + start "" "%CONFIG%" + cls + ) + IF "%%A"=="en" ( notepad "%CONFIG%" cls ) @@ -87,11 +93,12 @@ echo. GOTO :choice :Groom -IF exist "%ALERTS%" ( +IF exist "%LOGS%" ( echo. - echo [+] Removing directory: "%ALERTS%" - rmdir /S /Q "%ALERTS%" + echo [+] Removing directory: "%LOGS%" + rmdir /S /Q "%LOGS%" ) + IF exist "%DUMPS%" ( echo [+] Removing directory: "%DUMPS%" rmdir /S /Q "%DUMPS%" diff --git a/tools/whids/service.go b/tools/whids/service.go index ef48c4e..04efbeb 100644 --- a/tools/whids/service.go +++ b/tools/whids/service.go @@ -26,9 +26,9 @@ loop: changes <- c.CurrentStatus case svc.Stop: // Stop WHIDS there - hids.Stop() - hids.Wait() - hids.LogStats() + hostIDS.Stop() + hostIDS.Wait() + hostIDS.LogStats() break loop } } diff --git a/utils/files.go b/utils/files.go new file mode 100644 index 0000000..e5431e5 --- /dev/null +++ b/utils/files.go @@ -0,0 +1,143 @@ +package utils + +import ( + "archive/zip" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/0xrawsec/golang-utils/fsutil/fswalker" + "github.com/0xrawsec/golang-utils/log" +) + +const ( + // DefaultPerms default permissions for output files + DefaultPerms = 0740 +) + +// CountFiles counts files in a directory +func CountFiles(directory string) (cnt int) { + for wi := range fswalker.Walk(directory) { + cnt += len(wi.Files) + } + return +} + +// GzipFileBestSpeed compresses a file to gzip and deletes the original file +func GzipFileBestSpeed(path string) (err error) { + fname := fmt.Sprintf("%s.gz", path) + partname := fmt.Sprintf("%s.part", fname) + + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + + of, err := os.Create(partname) + if err != nil { + return + } + defer of.Close() + + // if valid level error returned is nil so no need to handle it + w, _ := gzip.NewWriterLevel(of, gzip.BestSpeed) + defer w.Close() + if _, err := io.Copy(w, f); err != nil { + return err + } + + // gzip writer + w.Flush() + w.Close() + // original file + f.Close() + // part file + of.Close() + log.Infof("Removing original dumpfile: %s", path) + if err := os.Remove(path); err != nil { + log.Errorf("Cannot remove original dumpfile: %s", err) + } + // rename the file to its final name + return os.Rename(partname, fname) +} + +// HidsCreateFile creates a file with the good permissions +func HidsCreateFile(filename string) (*os.File, error) { + return os.OpenFile(filename, os.O_CREATE|os.O_RDWR, DefaultPerms) +} + +// HidsWriteFile is a wrapper around ioutil.WriteFile to write a file +// with the good permissions +func HidsWriteFile(filename string, data []byte) error { + return ioutil.WriteFile(filename, data, DefaultPerms) +} + +// IsPipePath checks whether the argument path is a pipe +func IsPipePath(path string) bool { + return strings.HasPrefix(path, `\\.\`) +} + +// ReadFileString reads bytes from a file +func ReadFileString(path string) (string, error) { + b, err := ioutil.ReadFile(path) + return string(b), err +} + +// StdDir makes a directory ending with os separator +func StdDir(dir string) string { + sep := string(os.PathSeparator) + return fmt.Sprintf("%s%s", strings.TrimSuffix(dir, sep), sep) +} + +// StdDirs makes a directories are ending with os separator +func StdDirs(directories ...string) (o []string) { + o = make([]string, len(directories)) + for i, d := range directories { + o[i] = StdDir(d) + } + return +} + +// Unzip helper function to unzip a file to a destination folder +// source code from : https://stackoverflow.com/questions/20357223/easy-way-to-unzip-file-with-golang +func Unzip(zipfile, dest string) (err error) { + r, err := zip.OpenReader(zipfile) + if err != nil { + return err + } + defer r.Close() + + // Creating directory + os.MkdirAll(dest, 0700) + + for _, f := range r.File { + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + path := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(path, f.Mode()) + } else { + f, err := os.OpenFile( + path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, rc) + if err != nil { + return err + } + } + } + return nil +} diff --git a/utils/net.go b/utils/net.go new file mode 100644 index 0000000..ae8ffe2 --- /dev/null +++ b/utils/net.go @@ -0,0 +1,29 @@ +package utils + +import "net" + +// derived from: https://gist.github.com/kotakanbe/d3059af990252ba89a82 +func NextIP(ip net.IP) net.IP { + nip := net.IP(make(net.IP, len(ip))) + copy(nip, ip) + for j := len(nip) - 1; j >= 0; j-- { + nip[j]++ + if nip[j] > 0 { + break + } + } + return nip +} + +// derived from: https://gist.github.com/kotakanbe/d3059af990252ba89a82 +func PrevIP(ip net.IP) net.IP { + nip := net.IP(make(net.IP, len(ip))) + copy(nip, ip) + for j := len(nip) - 1; j >= 0; j-- { + nip[j]-- + if nip[j] < 255 { + break + } + } + return nip +} diff --git a/utils/sizes.go b/utils/sizes.go new file mode 100644 index 0000000..1bde351 --- /dev/null +++ b/utils/sizes.go @@ -0,0 +1,8 @@ +package utils + +const ( + _ = 1 << (iota * 10) + Kilo + Mega + Giga +) diff --git a/utils/utils.go b/utils/utils.go index 69d98d4..d176de5 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,144 +1,25 @@ package utils import ( - "archive/zip" "bytes" - "compress/gzip" + "crypto/sha256" "encoding/csv" + "encoding/hex" "encoding/json" "fmt" - "io" - "io/ioutil" "math" - "net/http" "os" "os/exec" - "path/filepath" "strings" - "syscall" "time" "unicode/utf16" "unicode/utf8" "github.com/0xrawsec/golang-utils/datastructs" - "github.com/0xrawsec/golang-utils/fsutil/fswalker" "github.com/0xrawsec/golang-utils/log" "github.com/0xrawsec/whids/utils/powershell" ) -const ( - Mega = 1 << 20 -) - -// HTTPGet helper function to issue a simple HTTP GET method -func HTTPGet(client *http.Client, url, outPath string) (err error) { - out, err := os.Create(outPath) - if err != nil { - return err - } - defer out.Close() - - // Building new request - req, err := http.NewRequest("GET", url, new(bytes.Buffer)) - if err != nil { - return err - } - - // Issuing the query - resp, err := client.Do(req) - if err != nil { - return - } - - if resp.StatusCode != 200 { - return fmt.Errorf("Bad status code: %d", resp.StatusCode) - } - - // Dumping the content of the response - log.Debugf("Dumping the content of the response -> %s", outPath) - r := -1 - buf := make([]byte, 4096) - for err == nil && r != 0 { - r, err = resp.Body.Read(buf) - out.Write(buf[:r]) - } - return nil -} - -// Unzip helper function to unzip a file to a destination folder -// source code from : https://stackoverflow.com/questions/20357223/easy-way-to-unzip-file-with-golang -func Unzip(zipfile, dest string) (err error) { - r, err := zip.OpenReader(zipfile) - if err != nil { - return err - } - defer r.Close() - - // Creating directory - os.MkdirAll(dest, 0700) - - for _, f := range r.File { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - path := filepath.Join(dest, f.Name) - if f.FileInfo().IsDir() { - os.MkdirAll(path, f.Mode()) - } else { - f, err := os.OpenFile( - path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - defer f.Close() - - _, err = io.Copy(f, rc) - if err != nil { - return err - } - } - } - return nil -} - -// GzipFile compresses a file to gzip and deletes the original file -func GzipFile(path string) (err error) { - var buf [Mega]byte - f, err := os.Open(path) - if err != nil { - return - } - //defer f.Close() - fname := fmt.Sprintf("%s.gz", path) - partname := fmt.Sprintf("%s.part", fname) - of, err := os.Create(partname) - if err != nil { - return - } - - w := gzip.NewWriter(of) - for n, err := f.Read(buf[:]); err != io.EOF; { - w.Write(buf[:n]) - n, err = f.Read(buf[:]) - } - w.Flush() - // gzip writer - w.Close() - // original file - f.Close() - // part file - of.Close() - log.Infof("Removing original dumpfile: %s", path) - if err := os.Remove(path); err != nil { - log.Errorf("Cannot remove original dumpfile: %s", err) - } - // rename the file to its final name - return os.Rename(partname, fname) -} - // EnableDNSLogs through wevutil command line func EnableDNSLogs() error { cmd := exec.Command("wevtutil.exe", "sl", "Microsoft-Windows-DNS-Client/Operational", "/e:true") @@ -151,12 +32,6 @@ func FlushDNSCache() error { return cmd.Run() } -// ReadFileString reads bytes from a file -func ReadFileString(path string) (string, error) { - b, err := ioutil.ReadFile(path) - return string(b), err -} - // PrettyJSON returns a JSON pretty string out of i func PrettyJSON(i interface{}) string { b, err := json.MarshalIndent(i, "", " ") @@ -175,28 +50,6 @@ func JSON(i interface{}) string { return string(b) } -// CountFiles counts files in a directory -func CountFiles(directory string) (cnt int) { - for wi := range fswalker.Walk(directory) { - cnt += len(wi.Files) - } - return -} - -// HideFile hides a file in Windows explorer -// source: https://stackoverflow.com/questions/54139606/how-to-create-a-hidden-file-in-windows-mac-linux -func HideFile(filename string) error { - filenameW, err := syscall.UTF16PtrFromString(filename) - if err != nil { - return err - } - err = syscall.SetFileAttributes(filenameW, syscall.FILE_ATTRIBUTE_HIDDEN) - if err != nil { - return err - } - return nil -} - // ExpandEnvs expands several strings with environment variable // it is just a loop calling os.ExpandEnv for every element func ExpandEnvs(s ...string) (o []string) { @@ -207,18 +60,13 @@ func ExpandEnvs(s ...string) (o []string) { return } -func StdDir(dir string) string { - sep := string(os.PathSeparator) - return fmt.Sprintf("%s%s", strings.TrimSuffix(dir, sep), sep) -} - -func StdDirs(directories ...string) (o []string) { - // make sure directories ends with \ - o = make([]string, len(directories)) - for i, d := range directories { - o[i] = StdDir(d) +// Sha256StringArray utility +func Sha256StringArray(array []string) string { + sha256 := sha256.New() + for _, e := range array { + sha256.Write([]byte(e)) } - return + return hex.EncodeToString(sha256.Sum(nil)) } /////////////////////////////// Windows Logger //////////////////////////////// @@ -279,25 +127,6 @@ func Round(f float64, precision int) float64 { return float64(int64(f*pow)) / pow } -// ArgvFromCommandLine returns an argv slice given a command line -// provided in argument -func ArgvFromCommandLine(cl string) (argv []string, err error) { - argc := int32(0) - utf16ClPtr, err := syscall.UTF16PtrFromString(cl) - if err != nil { - return - } - utf16Argv, err := syscall.CommandLineToArgv(utf16ClPtr, &argc) - if err != nil { - return - } - argv = make([]string, argc) - for i, utf16Ptr := range utf16Argv[:argc] { - argv[i] = syscall.UTF16ToString((*utf16Ptr)[:]) - } - return -} - // SvcFromPid returns the list of services hosted by a given PID // interesting to know what service is hosted by svchost func SvcFromPid(pid int32) string { @@ -334,11 +163,6 @@ func RegQuery(key, value string) (string, error) { return string(out), nil } -// IsPipePath checks whether the argument path is a pipe -func IsPipePath(path string) bool { - return strings.HasPrefix(path, `\\.\`) -} - // Utf16ToUtf8 converts a utf16 encoded byte slice to utf8 byte slice // it returns error if there is any decoding / encoding issue // Inspired by: https://gist.github.com/bradleypeabody/185b1d7ed6c0c2ab6cec#file-gistfile1-go diff --git a/utils/windows.go b/utils/windows.go new file mode 100644 index 0000000..b35d7bb --- /dev/null +++ b/utils/windows.go @@ -0,0 +1,38 @@ +// +build windows + +package utils + +import "syscall" + +// ArgvFromCommandLine returns an argv slice given a command line +// provided in argument +func ArgvFromCommandLine(cl string) (argv []string, err error) { + argc := int32(0) + utf16ClPtr, err := syscall.UTF16PtrFromString(cl) + if err != nil { + return + } + utf16Argv, err := syscall.CommandLineToArgv(utf16ClPtr, &argc) + if err != nil { + return + } + argv = make([]string, argc) + for i, utf16Ptr := range utf16Argv[:argc] { + argv[i] = syscall.UTF16ToString((*utf16Ptr)[:]) + } + return +} + +// HideFile hides a file in Windows explorer +// source: https://stackoverflow.com/questions/54139606/how-to-create-a-hidden-file-in-windows-mac-linux +func HideFile(filename string) error { + filenameW, err := syscall.UTF16PtrFromString(filename) + if err != nil { + return err + } + err = syscall.SetFileAttributes(filenameW, syscall.FILE_ATTRIBUTE_HIDDEN) + if err != nil { + return err + } + return nil +}