diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 91551f5cc..299163e1f 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,7 +16,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Checkout uses: actions/checkout@v2 - name: Adjust pkg-config prefix @@ -49,7 +49,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' shell: bash env: - VERSION: "4.0.x" + VERSION: "4.2.x" run: | git clone --depth 1 --branch $VERSION https://github.com/VirusTotal/yara.git - name: Configure yara @@ -105,7 +105,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Checkout uses: actions/checkout@v2 - name: Build @@ -134,7 +134,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Setup msys2 uses: msys2/setup-msys2@v2 with: @@ -184,7 +184,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Setup msys2 uses: msys2/setup-msys2@v2 with: @@ -216,7 +216,7 @@ jobs: run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VER env: - GOLANGCI_LINT_VER: v1.35.2 + GOLANGCI_LINT_VER: v1.50.1 - name: Lint shell: bash run: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dbdfc9a31..dd357f233 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Checkout uses: actions/checkout@v2 - name: Adjust pkg-config prefix @@ -47,7 +47,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' shell: bash env: - VERSION: "4.0.x" + VERSION: "4.2.x" run: | git clone --depth 1 --branch $VERSION https://github.com/VirusTotal/yara.git - name: Configure yara @@ -87,7 +87,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Setup msys2 uses: msys2/setup-msys2@v2 with: @@ -137,7 +137,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Setup msys2 uses: msys2/setup-msys2@v2 with: @@ -169,7 +169,7 @@ jobs: run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VER env: - GOLANGCI_LINT_VER: v1.35.2 + GOLANGCI_LINT_VER: v1.50.1 - name: Lint shell: bash run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83bc385d4..3f4b28635 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Checkout uses: actions/checkout@v2 - name: Adjust pkg-config prefix @@ -45,7 +45,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' shell: bash env: - VERSION: "4.0.x" + VERSION: "4.2.x" run: | git clone --depth 1 --branch $VERSION https://github.com/VirusTotal/yara.git - name: Configure yara @@ -102,7 +102,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.x + go-version: 1.19.x - name: Checkout uses: actions/checkout@v2 - name: Get version diff --git a/cmd/fibratus/app/list.go b/cmd/fibratus/app/list.go index a88976cac..5c7ed336b 100644 --- a/cmd/fibratus/app/list.go +++ b/cmd/fibratus/app/list.go @@ -27,7 +27,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/spf13/cobra" - "io/ioutil" "os" "path/filepath" "strings" @@ -82,7 +81,7 @@ func listFilaments(cmd *cobra.Command, args []string) error { return err } - filaments, err := ioutil.ReadDir(dir) + filaments, err := os.ReadDir(dir) if err != nil { return err } diff --git a/cmd/fibratus/main_windows.go b/cmd/fibratus/main_windows.go index 315296881..29baa5393 100644 --- a/cmd/fibratus/main_windows.go +++ b/cmd/fibratus/main_windows.go @@ -26,13 +26,13 @@ import ( ) func main() { - // determine if we are running in an interactive session - in, err := svc.IsAnInteractiveSession() + // determine if we are running as a Windows Service + isWinService, err := svc.IsWindowsService() if err != nil { fmt.Printf("interactive session check failed: %v\n", err) os.Exit(-1) } - if !in { + if isWinService { app.RunService() return } diff --git a/configs/fibratus.yml b/configs/fibratus.yml index 2239f926d..4c230a0e2 100644 --- a/configs/fibratus.yml +++ b/configs/fibratus.yml @@ -25,7 +25,7 @@ alertsenders: #host: # Represents the port of the SMTP server - #port: 25 + #port: 587 # Specifies the user name when authenticating to the SMTP server #user: @@ -38,7 +38,9 @@ alertsenders: # Specifies all the recipients that'll receive the alert #to: - # - "" + + # Specifies the email body content type + #content-type: text/html # Slack sender transports the alerts to the Slack workspace. slack: @@ -95,14 +97,18 @@ filament: # =============================== Filters =============================================== -# Contains the definition of filter rules. Filter expressions are contained in filter group files. +# Contains the definition of detection rules. Rules are contained within rule group files. # Rule definitions can reside in the local file system or also can be served over HTTP/S. +# For local file system rule paths, it is possible to use the glob expression to load the +# rules from different directory locations. filters: rules: from-paths: - # - C:\Program Files\Fibratus\Config\Rules\Default\default.yml + # - C:\Program Files\Fibratus\Rules\*.yml #from-urls: - # - https://raw.githubusercontent.com/rabbitstack/fibratus/master/configs/rules/default/default.yml + macros: + from-paths: + - C:\Program Files\Fibratus\Rules\Macros\*.yml # =============================== Handle =============================================== diff --git a/configs/rules/default/default.yml b/configs/rules/default/default.yml deleted file mode 100644 index 9423f64c2..000000000 --- a/configs/rules/default/default.yml +++ /dev/null @@ -1,380 +0,0 @@ -# ==================================================================================================== -# -# These filter rules try to mimic the subset of the sysmon config template -# created by SwiftOnSecurity (https://github.com/SwiftOnSecurity/sysmon-config). -# -# All credits for digging the rule definitions go to the above author/contributors. -# -# Obviously, some events can't be directly translated to Fibratus equivalent -# since Fibratus is not aware of them. In the same way, -# some filter fields are still missing in Fibratus, so those sysmon rules were -# omitted. -# -# ======================= Process creation =========================================================== -# -# All processes launched will be logged, except for what matches a rule below. -# It's best to be as specific as possible, to avoid user-mode executables imitating -# other process names to avoid logging, or if malware drops files in an existing directory. -# -- group: Windows userspace and common apps processes - selector: - type: CreateProcess - enabled: true - policy: exclude - relation: or - rules: - - name: Windows error reporting/telemetry, WMI provider host - condition: ps.comm istartswith - ( - ' \"C:\\Windows\\system32\\wermgr.exe\\" \"-queuereporting_svc\" ', - 'C:\\Windows\\system32\\DllHost.exe /Processid', - 'C:\\Windows\\system32\\wbem\\wmiprvse.exe -Embedding', - 'C:\\Windows\\system32\\wbem\\wmiprvse.exe -secured -Embedding' - ) - - name: Windows error reporting/telemetry, Search Indexer, Session Manager, Auto check utility - condition: ps.comm iin - ( - 'C:\\Windows\\system32\\wermgr.exe -upload', - 'C:\\Windows\\system32\\SearchIndexer.exe /Embedding', - 'C:\\windows\\system32\\wermgr.exe -queuereporting', - '\\??\\C:\\Windows\\system32\\autochk.exe *', - '\\SystemRoot\\System32\\smss.exe', - 'C:\\Windows\\System32\\RuntimeBroker.exe -Embedding' - ) - - name: Various Windows processes - condition: ps.exe iin - ( - 'C:\\Program Files (x86)\\Common Files\\microsoft shared\\ink\\TabTip32.exe', - 'C:\\Windows\\System32\\TokenBrokerCookies.exe', - 'C:\\Windows\\System32\\plasrv.exe', - 'C:\\Windows\\System32\\wifitask.exe', - 'C:\\Windows\\system32\\CompatTelRunner.exe', - 'C:\\Windows\\system32\\PrintIsolationHost.exe', - 'C:\\Windows\\system32\\SppExtComObj.Exe', - 'C:\\Windows\\system32\\audiodg.exe', - 'C:\\Windows\\system32\\conhost.exe', - 'C:\\Windows\\system32\\mobsync.exe', - 'C:\\Windows\\system32\\musNotification.exe', - 'C:\\Windows\\system32\\musNotificationUx.exe', - 'C:\\Windows\\system32\\powercfg.exe', - 'C:\\Windows\\system32\\sndVol.exe', - 'C:\\Windows\\system32\\sppsvc.exe', - 'C:\\Windows\\system32\\wbem\\WmiApSrv.exe' - ) - or - ps.comm iin - ( - 'C:\\WINDOWS\\system32\\devicecensus.exe UserCxt', - 'C:\\Windows\\System32\\usocoreworker.exe -Embedding' - ) - - name: svchost - condition: ps.comm iin {{ .Values.processes.comm.svchost | stringify }} - - name: Microsoft edge - condition: ps.comm istartswith '\"C:\\Program Files (x86)\\Microsoft\\Edge Dev\\Application\\msedge.exe\" --type=' - - name: Microsoft dotNet - condition: ps.comm istartswith - ( - 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\ngen.exe', - 'C:\\WINDOWS\\Microsoft.NET\\Framework64\\v4.0.30319\\Ngen.exe' - ) - or - ps.exe imatches - ( - 'C:\\Windows\\Microsoft.NET\\Framework64\\*\\mscorsvw.exe', - 'C:\\Windows\\Microsoft.NET\\Framework\\*\\mscorsvw.exe', - 'C:\\Windows\\Microsoft.Net\\Framework64\\*\\WPF\\PresentationFontCache.exe' - ) - - name: Microsoft Office - condition: ps.exe iin - ( - 'C:\\Program Files\\Microsoft Office\\Office16\\MSOSYNC.EXE', - 'C:\\Program Files (x86)\\Microsoft Office\\Office16\\MSOSYNC.EXE', - 'C:\\Program Files\\Common Files\\Microsoft Shared\\OfficeSoftwareProtectionPlatform\\OSPPSVC.EXE', - 'C:\\Program Files\\Microsoft Office\\Office16\\msoia.exe', - 'C:\\Program Files (x86)\\Microsoft Office\\root\\Office16\\officebackgroundtaskhandler.exe', - 'C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeC2RClient.exe' - ) - - name: Media Player - condition: ps.exe = 'C:\\Program Files\\Windows Media Player\\wmpnscfg.exe' - - name: Google - condition: ps.comm istartswith - ( - '\"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe\\\" --type=', - '\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --type=' - ) - -# ======================= Process termination ======================================================== -# -- group: Suspicious process terminations - selector: - type: TerminateProcess - policy: include - rules: - - name: User binaries - condition: ps.name istartswith ('C:\\Users', '\\') - - -# ======================= Remote thread creation ===================================================== -# -# Monitor for processes injecting code into other processes. Often used by malware to cloak their actions. -# Also when Firefox loads Flash. -# -- group: Suspicious remote thread creations - selector: - type: CreateThread - policy: include - rules: - - name: Fishy remote threads - condition: kevt.pid != thread.pid - and - ps.exe not iin - ( - 'C:\\Windows\\system32\\wbem\\WmiPrvSE.exe', - 'C:\\Windows\\system32\\svchost.exe', - 'C:\\Windows\\system32\\wininit.exe', - 'C:\\Windows\\system32\\csrss.exe', - 'C:\\Windows\\system32\\services.exe', - 'C:\\Windows\\system32\\winlogon.exe', - 'C:\\Windows\\system32\\audiodg.exe', - 'C:\\Windows\\system32\\kernel32.dll', - 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' - ) - action: > - {{ emit - (printf "Detected remote thread creation in %s" .Kevt.Kparams.exe) - (printf "Possible code injection by %s" .Kevt.PS.Exe) - }} - -# ======================= Network connection initiated =============================================== -# -# By default this configuration takes a very conservative approach to network logging, -# limited to only extremely high-signal events. -# -- group: Suspicious network-connecting binaries - selector: - type: Connect - policy: include - rules: - - name: Suspicious sources for network-connecting binaries - condition: ps.exe istartswith - ( - 'C:\\Users', - 'C:\\Recycle', - 'C:\\ProgramData', - 'C:\\Windows\\Temp', - '\\', - 'C:\\perflogs', - 'C:\\intel', - 'C:\\Windows\\fonts', - 'C:\\Windows\\system32\\config' - ) - - name: Suspicious Windows tools network-connecting binaries - condition: ps.name in - ( - 'at.exe', - 'certutil.exe', - 'cmd.exe', - 'cmstp.exe', - 'cscript.exe', - 'driverquery.exe', - 'dsquery.exe', - 'hh.exe', - 'infconditionaultInstall.exe', - 'java.exe', - 'javaw.exe', - 'javaws.exe', - 'mmc.exe', - 'msbuild.exe', - 'mshta.exe', - 'msiexec.exe', - 'nbtstat.exe', - 'net.exe', - 'net1.exe', - 'notepad.exe', - 'nslookup.exe', - 'powershell.exe', - 'qprocess.exe', - 'qwinsta.exe', - 'reg.exe', - 'regsvcs.exe', - 'regsvr32.exe', - 'rundll32.exe', - 'rwinsta.exe', - 'sc.exe', - 'schtasks.exe', - 'taskkill.exe', - 'tasklist.exe', - 'wmic.exe', - 'wscript.exe' - ) - - name: Relevant 3rd Party Tools - condition: ps.name in - ( - 'nc.exe', - 'ncat.exe', - 'psexec.exe', - 'psexesvc.exe', - 'tor.exe', - 'vnc.exe', - 'vncservice.exe', - 'vncviewer.exe', - 'winexesvc.exe', - 'nmap.exe', - 'psinfo.exe' - ) - - name: Suspicious ports - condition: net.dport in - ( - 22, - 23, - 25, - 143, - 3389, - 5800, - 5900, - 444, - 1080, - 3128, - 8080, - 1723, - 9001, - 9030 - ) - -- group: Microsoft binaries and known addresses - selector: - type: Connect - policy: exclude - rules: - - name: Microsoft binaries - condition: ps.exe istartswith 'C:\\ProgramData\\Microsoft\\Windows conditionender\\Platform\\' - or - ps.exe endswith 'AppData\\Local\\Microsoft\\Teams\\current\\Teams.exe' - or - net.dip.names endswith - ( - '.microsoft.com', - 'microsoft.com.akadns.net', - 'microsoft.com.nsatc.net' - ) - - name: OCSP protocol known addresses - condition: net.dip in (23.4.43.27, 72.21.91.29) - - name: Loopback addresses - condition: net.dip = 127.0.0.1 or net.dip startswith 'fe80:0:0:0' - -# ======================= File created =============================================================== -# -- group: Suspicious file creation operations - selector: - type: CreateFile - policy: include - rules: - - name: Startup links and shortcut modifications - condition: file.operation = 'create' - and - file.name icontains - ( - '\\Start Menu', - '\\Startup\\' - ) - - name: Microsoft Outlook attachments - condition: file.operation = 'create' and file.name icontains '\\Content.Outlook\\' - - name: Downloaded files - condition: file.operation = 'create' and file.name icontains '\\Downloads\\' - - name: Microsoft ClickOnce application - condition: file.operation = 'create' - and - file.extension in - ( - '.application', - '.appref-ms' - ) - - name: Batch scripting - condition: file.operation = 'create' - and - file.extension in - ( - '.bat', - '.chm', - '.cmd', - '.cmdline' - ) - - name: Fishy extensions - condition: file.operation = 'create' - and - file.extension in - ( - '.dll', - '.exe', - '.exe.log', - '.jar', - '.jnlp', - '.jse', - '.hta', - '.job', - '.pptm', - '.ps1', - '.sys', - '.scr', - '.vbe', - '.vbs', - '.xlsm', - '.ocx', - '.sln', - '.xls' - ) - - name: Powershell persistence - condition: file.operation = 'create' - and - file.name imatches 'C:\\Windows\\*\\WindowsPowerShell' - -# ======================= Registry modification ====================================================== -# -- group: Suspicious registry key modifications - selector: - category: registry - policy: include - rules: - - name: Core Windows keys - condition: > - kevt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') - and - registry.key.name icontains - ( - 'CurrentVersion\\Run', - 'Policies\\Explorer\\Run', - 'Group Policy\\Scripts', - 'Windows\\System\\Scripts', - 'CurrentVersion\\Windows\\Load', - 'CurrentVersion\\Windows\\Run', - 'CurrentVersion\\Winlogon\\Shell', - 'CurrentVersion\\Winlogon\\System', - 'UserInitMprLogonScript' - ) - or - registry.key.name istartswith - ( - 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Notify', - 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Shell', - 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Userinit', - 'HKEY_LOCAL_MACHINE\\Software\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Drivers32', - 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\BootExecute', - 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug' - ) - or - registry.key.name iendswith - ( - 'user shell folders\\startup' - ) - - - name: Services - condition: kevt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') - and - registry.key.name iendswith - ( - '\\ServiceDll', - '\\ServiceManifest', - '\\ImagePath', - '\\Start', - 'CurrentVersion\\Run' - ) diff --git a/configs/rules/default/stateful.yml b/configs/rules/default/stateful.yml deleted file mode 100644 index 4f65353f4..000000000 --- a/configs/rules/default/stateful.yml +++ /dev/null @@ -1,26 +0,0 @@ -- group: remote connection and command shell execution - policy: sequence - rules: - - name: establish remote connection - condition: > - kevt.name = 'Connect' - and - not - cidr_contains( - net.dip, - '10.0.0.0/8', - '172.16.0.0/12', - '172.17.0.0/16', - '192.168.0.0/16') - - name: spawn command shell - max-span: 1m - condition: > - kevt.name = 'CreateProcess' - and - ps.pid = $1.ps.pid - and - ps.sibling.name in ('cmd.exe', 'powershell.exe') - action: > - {{ emit "Command shell spawned after remote connection" - (printf "%s process spawned a command shell after connecting to %s" .Kevts.k2.PS.Exe .Kevts.k1.Kparams.dip) - }} diff --git a/configs/rules/default/values.yml b/configs/rules/default/values.yml deleted file mode 100644 index 43a12bccc..000000000 --- a/configs/rules/default/values.yml +++ /dev/null @@ -1,52 +0,0 @@ -# ========================= Values.yml ================================================== -# -# This file contains process image names, command line signatures, file names or registry -# keys that are utilized in filter group files to make them more readable and avoid cluttering -# with massive payloads. -# -processes: - comm: - svchost: - - C:\\Windows\\system32\\svchost.exe -k appmodel -s StateRepository - - C:\\Windows\\system32\\svchost.exe -k appmodel -p -s camsvc - - C:\\Windows\\system32\\svchost.exe -k appmodel - - C:\\Windows\\system32\\svchost.exe -k appmodel -p -s tiledatamodelsvc - - C:\\Windows\\system32\\svchost.exe -k camera -s FrameServer - - C:\\Windows\\system32\\svchost.exe -k dcomlaunch -s LSM - - C:\\Windows\\system32\\svchost.exe -k dcomlaunch -s PlugPlay - # Windows defragmentation - - C:\\Windows\\system32\\svchost.exe -k defragsvc - - C:\\Windows\\system32\\svchost.exe -k devicesflow -s DevicesFlowUserSvc - # Microsoft: The Windows Image Acquisition Service - - C:\\Windows\\system32\\svchost.exe -k imgsvc - - C:\\Windows\\system32\\svchost.exe -k localService -s EventSystem - - C:\\Windows\\system32\\svchost.exe -k localService -s bthserv - - C:\\Windows\\system32\\svchost.exe -k LocalService -p -s BthAvctpSvc - - C:\\Windows\\system32\\svchost.exe -k localService -s nsi - - C:\\Windows\\system32\\svchost.exe -k localService -s w32Time - # Windows: Network services - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s Dhcp - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s EventLog - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s TimeBrokerSvc - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s WFDSConMgrSvc - - C:\\Windows\\system32\\svchost.exe -k LocalServiceNetworkRestricted -s BTAGService - # Win10:1903: Network Connection Broker - - C:\\Windows\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation -s SensrSvc - # Windows: SSDP [ https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol ] - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation -p -s SSDPSRV - - C:\\Windows\\system32\\svchost.exe -k localServiceNoNetwork - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s WPDBusEnum - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s fhsvc - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s DeviceAssociationService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s NcbService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s SensorService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s TabletInputService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s UmRdpService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s WPDBusEnum - # Microsoft: Passport - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s NgcSvc - # Microsoft: Passport Container - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -p -s NgcCtnrSvc \ No newline at end of file diff --git a/go.mod b/go.mod index 66041a9c3..f36ac037a 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/rabbitstack/fibratus require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/Microsoft/go-winio v0.4.14 + github.com/antchfx/htmlquery v1.2.5 github.com/briandowns/spinner v1.12.0 github.com/dustin/go-humanize v1.0.0 github.com/hashicorp/go-version v1.2.1 - github.com/hillu/go-yara/v4 v4.0.6 - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/hillu/go-yara/v4 v4.2.4 github.com/jedib0t/go-pretty/v6 v6.2.1 github.com/lithammer/fuzzysearch v1.1.2 github.com/magiconair/properties v1.8.1 @@ -15,24 +15,61 @@ require ( github.com/olivere/elastic/v7 v7.0.20 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 - github.com/qmuntal/stateless v1.6.0 // indirect + github.com/qmuntal/stateless v1.6.0 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.2 github.com/streadway/amqp v1.0.0 - github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/gozstd v1.11.0 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b - golang.org/x/text v0.3.5 - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + github.com/yuin/goldmark v1.5.2 + golang.org/x/sys v0.0.0-20210423082822-04245dca01da + golang.org/x/text v0.3.6 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) -go 1.16 +require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/antchfx/xpath v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.3.1 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 // indirect + golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/ini.v1 v1.51.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) + +go 1.19 diff --git a/go.sum b/go.sum index cf2eda19b..0f2fe9361 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,10 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antchfx/htmlquery v1.2.5 h1:1lXnx46/1wtv1E/kzmH8vrfMuUKYgkdDBA9pIdMJnk4= +github.com/antchfx/htmlquery v1.2.5/go.mod h1:2MCVBzYVafPBmKbrmwB9F5xdd+IEgRY61ci2oOsOQVw= +github.com/antchfx/xpath v1.2.1 h1:qhp4EW6aCOVr5XIkT+l6LJ9ck/JsUH/yyauNgTQkBF8= +github.com/antchfx/xpath v1.2.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.34.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -50,6 +54,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -70,8 +76,8 @@ github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pB github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hillu/go-yara/v4 v4.0.6 h1:2fHGPatCXQL1RgWWvPJDdaCkXAzvMx8SpVtKpqVMHDo= -github.com/hillu/go-yara/v4 v4.0.6/go.mod h1:rkb/gSAoO8qcmj+pv6fDZN4tOa3N7R+qqGlEkzT4iys= +github.com/hillu/go-yara/v4 v4.2.4 h1:r3KB1XV+h6q+N8bvK6/gLpxAVcd6baYzmOSYHzNo9QQ= +github.com/hillu/go-yara/v4 v4.2.4/go.mod h1:AHEs/FXVMQKVVlT6iG9d+q1BRr0gq0WoAWZQaZ0gS7s= github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= @@ -184,8 +190,9 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -202,6 +209,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= +github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -225,6 +234,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -239,12 +251,15 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -279,6 +294,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/make.bat b/make.bat index 8209b62a3..391fb3072 100644 --- a/make.bat +++ b/make.bat @@ -95,7 +95,7 @@ set RELEASE_DIR=.\build\msi\fibratus-%VERSION% mkdir "%~dp0\%RELEASE_DIR%" mkdir "%~dp0\%RELEASE_DIR%\Bin" mkdir "%~dp0\%RELEASE_DIR%\Config" -mkdir "%~dp0\%RELEASE_DIR%\Config\Rules" +mkdir "%~dp0\%RELEASE_DIR%\Rules" mkdir "%~dp0\%RELEASE_DIR%\Python" mkdir "%~dp0\%RELEASE_DIR%\Filaments" @@ -106,7 +106,7 @@ copy /y ".\configs\fibratus.yml" "%RELEASE_DIR%\Config\fibratus.yml" copy /y ".\pkg\outputs\eventlog\mc\fibratus.dll" "%RELEASE_DIR%\fibratus.dll" robocopy ".\filaments" "%RELEASE_DIR%\Filaments" /E /S /XF *.md /XD __pycache__ .idea -robocopy ".\configs\rules" "%RELEASE_DIR%\Config\Rules" /E /S +robocopy ".\rules" "%RELEASE_DIR%\Rules" /E /S :: download the embedded Python distribution echo Downloading Python %PYTHON_VER%... @@ -152,7 +152,7 @@ set RELEASE_DIR=.\build\msi\fibratus-%VERSION%-slim mkdir "%~dp0\%RELEASE_DIR%" mkdir "%~dp0\%RELEASE_DIR%\Bin" mkdir "%~dp0\%RELEASE_DIR%\Config" -mkdir "%~dp0\%RELEASE_DIR%\Config\Rules" +mkdir "%~dp0\%RELEASE_DIR%\Rules" echo "Copying artifacts..." :: copy artifacts @@ -160,7 +160,7 @@ copy /y ".\cmd\fibratus\fibratus.exe" "%RELEASE_DIR%\Bin" copy /y ".\configs\fibratus.yml" "%RELEASE_DIR%\Config\fibratus.yml" copy /y ".\pkg\outputs\eventlog\mc\fibratus.dll" "%RELEASE_DIR%\fibratus.dll" -robocopy ".\configs\rules" "%RELEASE_DIR%\Config\Rules" /E /S +robocopy ".\rules" "%RELEASE_DIR%\Rules" /E /S echo "Building MSI package..." heat dir %RELEASE_DIR%\ -cg Fibratus -dr INSTALLDIR -suid -gg -sfrag -srd -var var.FibratusDir -out build/msi/components.wxs || exit /b diff --git a/pkg/aggregator/transformers/remove/remove.go b/pkg/aggregator/transformers/remove/remove.go index 61e4da16d..1b81c71af 100644 --- a/pkg/aggregator/transformers/remove/remove.go +++ b/pkg/aggregator/transformers/remove/remove.go @@ -26,7 +26,7 @@ import ( var removedCount = expvar.NewInt("transformers.removed.params") -//remove transformer deletes kparams that are given in the list. +// remove transformer deletes kparams that are given in the list. type remove struct { c Config } diff --git a/pkg/alertsender/alert.go b/pkg/alertsender/alert.go index 95a623c62..917e98520 100644 --- a/pkg/alertsender/alert.go +++ b/pkg/alertsender/alert.go @@ -18,7 +18,13 @@ package alertsender -import "fmt" +import ( + "bytes" + "fmt" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/renderer/html" +) // Severity is the type alias for alert's severity level. type Severity uint8 @@ -36,7 +42,7 @@ const ( func (s Severity) String() string { switch s { case Normal: - return "normal" + return "low" case Medium: return "medium" case Critical: @@ -49,11 +55,11 @@ func (s Severity) String() string { // ParseSeverityFromString parses the severity from the string representation. func ParseSeverityFromString(sever string) Severity { switch sever { - case "normal", "Normal": + case "normal", "Normal", "NORMAL", "low", "LOW": return Normal - case "medium", "Medium": + case "medium", "Medium", "MEDIUM": return Medium - case "critical", "Critical": + case "critical", "Critical", "high", "High", "HIGH": return Critical default: return Normal @@ -77,6 +83,21 @@ func (a Alert) String() string { return fmt.Sprintf("Title: %s, Text: %s, Severity: %s, Tags: %v", a.Title, a.Text, a.Severity, a.Tags) } +// MDToHTML converts alert's text Markdown elements to HTML blocks. +func (a *Alert) MDToHTML() error { + md := goldmark.New( + goldmark.WithExtensions(extension.GFM), + goldmark.WithRendererOptions(html.WithUnsafe()), + ) + var w bytes.Buffer + err := md.Convert([]byte(a.Text), &w) + if err != nil { + return err + } + a.Text = w.String() + return nil +} + // NewAlert builds a new alert. func NewAlert(title, text string, tags []string, severity Severity) Alert { return Alert{Title: title, Text: text, Tags: tags, Severity: severity} diff --git a/pkg/alertsender/mail/config.go b/pkg/alertsender/mail/config.go index 368c5778d..3a8facbe7 100644 --- a/pkg/alertsender/mail/config.go +++ b/pkg/alertsender/mail/config.go @@ -21,13 +21,14 @@ package mail import "github.com/spf13/pflag" const ( - host = "alertsenders.mail.host" - port = "alertsenders.mail.port" - user = "alertsenders.mail.user" - pass = "alertsenders.mail.password" - from = "alertsenders.mail.from" - to = "alertsenders.mail.to" - enabled = "alertsenders.mail.enabled" + host = "alertsenders.mail.host" + port = "alertsenders.mail.port" + user = "alertsenders.mail.user" + pass = "alertsenders.mail.password" + from = "alertsenders.mail.from" + to = "alertsenders.mail.to" + enabled = "alertsenders.mail.enabled" + contentType = "alertsenders.mail.content-type" ) // Config contains the configuration for the mail alert sender. @@ -44,8 +45,10 @@ type Config struct { From string `mapstructure:"from"` // To specifies recipients that receive the alert. To []string `mapstructure:"to"` - // Enabled indicates whether mail alert sender is enabled + // Enabled indicates whether mail alert sender is enabled. Enabled bool `mapstructure:"enabled"` + // ContentType represents the email body content type. + ContentType string `mapstructure:"content-type"` } // AddFlags registers persistent flags. @@ -57,4 +60,5 @@ func AddFlags(flags *pflag.FlagSet) { flags.String(from, "", "Specifies the sender's address") flags.StringSlice(to, []string{}, "Specifies all the recipients that'll receive the alert") flags.Bool(enabled, false, "Indicates whether mail alert sender is enabled") + flags.String(contentType, "text/html", "Represents the email body content type") } diff --git a/pkg/alertsender/mail/mail.go b/pkg/alertsender/mail/mail.go index acd46f120..6b789d903 100644 --- a/pkg/alertsender/mail/mail.go +++ b/pkg/alertsender/mail/mail.go @@ -48,14 +48,16 @@ func (s mail) Send(alert alertsender.Alert) error { return err } defer sender.Close() - return gomail.Send(sender, composeMessage(s.c.From, s.c.To, alert)) + return gomail.Send(sender, s.composeMessage(s.c.From, s.c.To, alert)) } -func composeMessage(from string, to []string, alert alertsender.Alert) *gomail.Message { +func (s mail) Type() alertsender.Type { return alertsender.Mail } + +func (s mail) composeMessage(from string, to []string, alert alertsender.Alert) *gomail.Message { msg := gomail.NewMessage() msg.SetHeader("From", from) msg.SetHeader("To", to...) msg.SetHeader("Subject", alert.Title) - msg.SetBody("text/plain", alert.Text) + msg.SetBody(s.c.ContentType, alert.Text) return msg } diff --git a/pkg/alertsender/renderer/renderer.go b/pkg/alertsender/renderer/renderer.go new file mode 100644 index 000000000..d71aa7f4d --- /dev/null +++ b/pkg/alertsender/renderer/renderer.go @@ -0,0 +1,69 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package renderer + +import ( + "bytes" + "github.com/Masterminds/sprig/v3" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/util/hostname" + "github.com/rabbitstack/fibratus/pkg/util/version" + "text/template" + "time" +) + +// RenderHTMLRuleAlert produces HTML template for rule alerts. This function generates +// inlined CSS to maximize the compatibility between email clients when the alert is +// transported via email sender or other senders that may render HTML content. +func RenderHTMLRuleAlert(ctx *config.ActionContext, alert alertsender.Alert) (string, error) { + data := struct { + *config.ActionContext + Alert alertsender.Alert + TriggeredAt time.Time + Hostname string + Version string + }{ + ctx, + alert, + time.Now(), + hostname.Get(), + version.Get(), + } + _ = data.Alert.MDToHTML() + funcmap := sprig.TxtFuncMap() + + // redefine hasKey to work on string map values + funcmap["hasKey"] = func(m map[string]string, key string) bool { + if _, ok := m[key]; ok { + return true + } + return false + } + tmpl, err := template.New("rule-alert").Funcs(funcmap).Parse(ruleAlertHTMLTemplate) + if err != nil { + return "", err + } + + var bb bytes.Buffer + if err := tmpl.Execute(&bb, data); err != nil { + return "", err + } + return bb.String(), nil +} diff --git a/pkg/alertsender/renderer/renderer_test.go b/pkg/alertsender/renderer/renderer_test.go new file mode 100644 index 000000000..90d318d1d --- /dev/null +++ b/pkg/alertsender/renderer/renderer_test.go @@ -0,0 +1,257 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package renderer + +import ( + "github.com/antchfx/htmlquery" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/fs" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pex "github.com/rabbitstack/fibratus/pkg/pe" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strings" + "testing" + "time" +) + +func TestHTMLFormatterRuleAlert(t *testing.T) { + out, err := RenderHTMLRuleAlert(&config.ActionContext{ + Group: config.FilterGroup{ + Description: "Identifies attempts from adversaries to acquire credentials from Vault files.", + Labels: map[string]string{ + "tactic.name": "Credential Access", + "tactic.ref": "https://attack.mitre.org/tactics/TA0006/", + "technique.name": "Credentials from Password Stores", + "technique.ref": "https://attack.mitre.org/techniques/T1555/", + "subtechnique.name": "Windows Credential Manager", + "subtechnique.ref": "https://attack.mitre.org/techniques/T1555/004/", + }, + }, + Events: []*kevent.Kevent{ + { + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: fs.FileDisposition(1)}, + }, + Metadata: map[kevent.MetadataKey]string{"foo": "bar", "fooz": "barzz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Parent: &pstypes.PS{ + PID: 2034, + Name: "explorer.exe", + Exe: `C:\Windows\System32\explorer.exe`, + Cwd: `C:\Windows\System32`, + SID: "admin\\SYSTEM", + Parent: &pstypes.PS{ + PID: 2345, + Name: "winlogon.exe", + }, + }, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit", "Path": "C:\\Program Files (x86)\\Common Files\\Oracle\\Java\\javapath;C:\\WINDOWS\\system32;C:\\WINDOWS;C:\\WINDOWS\\System32\\Wbem;C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\;C:\\Program Files\\Git\\cmd;C:\\msys64\\mingw64\\bin;C:\\WINDOWS\\System32\\OpenSSH\\;C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance Toolkit\\;C:\\Program Files\\nodejs\\;C:\\rubyinstaller-2.5.7-1-x64\\bin;C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;C:\\Program Files (x86)\\Windows Kits\\10\\App Certification Kit;C:\\Program Files (x86)\\Graphviz2.38\\bin;C:\\Program Files (x86)\\NSIS\\Bin;C:\\Program Files\\Jdk11\\bin;C:\\Python310;C:\\msys64\\usr\\bin;C:\\Program Files\\dotnet\\;C:\\Program Files\\Go\\bin;C:\\Program Files\\Fibratus\\Bin;C:\\Program Files\\AutoFirma\\AutoFirma;C:\\Users\\nedo\\AppData\\Local\\Programs\\Python\\Launcher\\;C:\\Scripts\\;C:\\;C:\\Users\\nedo\\AppData\\Local\\Programs\\Microsoft VS Code\\bin;C:\\Users\\nedo\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\nedo\\AppData\\Roaming\\npm;C:\\Users\\nedo\\AppData\\Local\\Programs\\oh-my-posh\\bin;C:\\Users\\nedo\\go\\bin"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Modules: []pstypes.Module{ + {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 1233405456}, + {Name: "C:\\Windows\\System32\\ntdll.dll", Size: 133405456}, + {Name: "C:\\Windows\\System32\\shell32.dll", Size: 33405456}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pex.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + }, + { + Type: ktypes.CreateProcess, + Tid: 2184, + PID: 1022, + CPU: 2, + Seq: 3, + Name: "CreateProcess", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates a new process", + Kparams: kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe"}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: "admin\\SYSTEM"}, + }, + Metadata: map[kevent.MetadataKey]string{"foo": "bar", "fooz": "barzz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Parent: &pstypes.PS{ + PID: 2034, + Name: "explorer.exe", + Exe: `C:\Windows\System32\explorer.exe`, + Cwd: `C:\Windows\System32`, + SID: "admin\\SYSTEM", + Parent: &pstypes.PS{ + PID: 2345, + Name: "winlogon.exe", + }, + }, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Modules: []pstypes.Module{ + {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 1233405456}, + {Name: "C:\\Windows\\System32\\ntdll.dll", Size: 133405456}, + {Name: "C:\\Windows\\System32\\shell32.dll", Size: 33405456}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pex.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + }, + }, + }, + alertsender.Alert{ + Title: "Suspicious access to Windows Vault files", + Text: "`cmd.exe` attempted to access Windows Vault files which was considered as a suspicious activity", + Severity: alertsender.Critical}) + require.NoError(t, err) + doc, err := htmlquery.Parse(strings.NewReader(out)) + require.NoError(t, err) + + alertTitle := htmlquery.FindOne(doc, "//h1") + + require.NotNil(t, alertTitle) + assert.Equal(t, "Suspicious access to Windows Vault files", htmlquery.InnerText(alertTitle)) +} diff --git a/pkg/alertsender/renderer/template.go b/pkg/alertsender/renderer/template.go new file mode 100644 index 000000000..2b0b34d85 --- /dev/null +++ b/pkg/alertsender/renderer/template.go @@ -0,0 +1,263 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package renderer + +var ruleAlertHTMLTemplate = ` + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + +
+

+ Triggered on {{ .TriggeredAt | date "Mon Jan 02 2006" }} at {{ .TriggeredAt | date "03:04:05 PM" }} in {{ .Hostname }} host +

+

{{ .Alert.Title }}

+ {{- if .Alert.Text }} +
+ {{- else }} +
+ {{- end }} + {{ $severityColor := "#fcd834" }} + {{- if eq .Alert.Severity.String "low" }} + {{ $severityColor := "#29b33e" }} + {{- else if eq .Alert.Severity.String "medium" }} + {{ $severityColor := "#fcd834" }} + {{- else }} + {{ $severityColor = "#fa7975" }} + {{- end }} + +

{{ .Alert.Severity.String | title }} Severity

+
+ {{- if .Alert.Text }} + {{ $text := (regexReplaceAll "" .Alert.Text "") }} +

{{ regexReplaceAll "\\s+" $text " " }}

+ {{- end }} + {{ if hasKey .Group.Labels "tactic.name" }} + + {{ end }} + {{ if hasKey .Group.Labels "technique.name" }} + + {{ end }} + {{ if hasKey .Group.Labels "subtechnique.name" }} + + {{ end }} +
+

+ {{- if .Filter -}} + {{- .Filter.Description | trimSuffix "." }} + {{- else }} + {{- .Group.Description | trimSuffix "." }} + {{- end -}} +

+
+
+ + + +
+

+ Security events involved in this incident +

+ {{- range $i, $evt := .Events }} + {{ with $evt }} + + + + +
+ + + + + + + + + + + + {{- if .PS }} + + + + {{- end }} +
+
+

# {{ $i | add1 }}

+
+
+

{{ .Name }}

+

{{ .Timestamp | date "03:04:05 PM" }}

+
+ {{ regexReplaceAll "" .Summary "" }} +
+ + {{- range $key, $par := .Kparams }} + + + + + {{- end }} +
+ {{ regexReplaceAll "_" $key " " | title }} + + {{ $paramValue := $par.String }} +

{{ $paramValue }}

+
+
+

Process

+
+ + + + + + + + + + + + + + {{- if gt (len .PS.Ancestors) 1 }} + + + + + {{- end }} + + + + + + + + + + + + + + + + + + + + +
+

Pid

+
+

{{ .PS.PID }}

+
+

Name

+
+

{{ .PS.Name }}

+
+

Parent

+
+ {{- if .PS.Parent -}} +

{{ .PS.Parent.Name }} ({{.PS.Parent.PID}})

+ {{- else -}} +

N/A {{ .PS.Ppid }}

+ {{- end -}} +
+

Ancestors

+
+

{{ .PS.Ancestors | join " ﹥ " }}

+
+

Exe

+
+

{{ .PS.Exe }}

+
+

Cmdline

+
+

{{ .PS.Comm }}

+
+

Cwd

+
+

{{ .PS.Cwd }}

+
+

User

+
+

{{ .PS.SID }}

+
+

Session ID

+
+

{{ .PS.SessionID }}

+
+
+
+
+ {{- end }} + {{- end }} +
+
+
+

+ This email was automatically generated by Fibratus {{ .Version }} +

+
+ + +` diff --git a/pkg/alertsender/sender.go b/pkg/alertsender/sender.go index 261f35bf3..ea2f15b43 100644 --- a/pkg/alertsender/sender.go +++ b/pkg/alertsender/sender.go @@ -61,6 +61,8 @@ func (s Type) String() string { type Sender interface { // Send emits an alert. Send(Alert) error + // Type returns the type that identifies a particular sender. + Type() Type } // ToType converts the string representation of the alert sender to its corresponding type. diff --git a/pkg/alertsender/slack/slack.go b/pkg/alertsender/slack/slack.go index 294720330..0a2a1665f 100644 --- a/pkg/alertsender/slack/slack.go +++ b/pkg/alertsender/slack/slack.go @@ -24,7 +24,7 @@ import ( "errors" "fmt" "github.com/rabbitstack/fibratus/pkg/alertsender" - "io/ioutil" + "io" "net" "net/http" "time" @@ -109,7 +109,7 @@ func (s slack) Send(alert alertsender.Alert) error { } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -126,3 +126,5 @@ func (s slack) Send(alert alertsender.Alert) error { } return nil } + +func (s slack) Type() alertsender.Type { return alertsender.Slack } diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 78ee2118b..933a0c70c 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -21,7 +21,6 @@ package config import ( "encoding/json" "fmt" - "io/ioutil" "time" "github.com/rabbitstack/fibratus/pkg/outputs/eventlog" @@ -275,11 +274,12 @@ func (c *Config) Validate() error { // we'll first validate the structure and values of the config file file := c.viper.GetString(configFile) var out interface{} - b, err := ioutil.ReadFile(file) + b, err := os.ReadFile(file) if err != nil { return err } switch filepath.Ext(file) { + //nolint:goconst case ".yaml", ".yml": err = yaml.Unmarshal(b, &out) case ".json": diff --git a/pkg/config/filters.go b/pkg/config/filters.go index 9f064d065..b1820cff7 100644 --- a/pkg/config/filters.go +++ b/pkg/config/filters.go @@ -22,7 +22,7 @@ import ( "bytes" "encoding/base64" "fmt" - "github.com/rabbitstack/fibratus/pkg/filter/funcmap" + "github.com/Masterminds/sprig/v3" "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/util/multierror" @@ -31,7 +31,6 @@ import ( "gopkg.in/yaml.v3" "hash/fnv" "io" - "io/ioutil" "net/http" u "net/url" "os" @@ -73,7 +72,7 @@ const ( UnknownRelation ) -// String yields human readable group policy. +// String yields a human-readable group policy. func (p FilterGroupPolicy) String() string { switch p { case IncludePolicy: @@ -87,7 +86,7 @@ func (p FilterGroupPolicy) String() string { } } -// String yields human readable group relation. +// String yields a human-readable group relation. func (r FilterGroupRelation) String() string { switch r { case OrRelation: @@ -147,14 +146,16 @@ func filterGroupRelationFromString(s string) FilterGroupRelation { // FilterConfig is the descriptor of a single filter. type FilterConfig struct { - Name string `json:"name" yaml:"name"` - Def string `json:"def" yaml:"def"` // deprecated in favor of `Condition` - Condition string `json:"condition" yaml:"condition"` - Action string `json:"action" yaml:"action"` - MaxSpan time.Duration `json:"max-span" yaml:"max-span"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Def string `json:"def" yaml:"def"` // deprecated in favor of `Condition` + Condition string `json:"condition" yaml:"condition"` + Action string `json:"action" yaml:"action"` + MaxSpan time.Duration `json:"max-span" yaml:"max-span"` + Labels map[string]string `json:"labels" yaml:"labels"` } -// parseTmpl ensures the correctness of the filter +// parseTmpl ensures the correctness of the rule // action template by trying to parse the template // string from the base64 payload. func (f FilterConfig) parseTmpl(resource string) error { @@ -165,7 +166,7 @@ func (f FilterConfig) parseTmpl(resource string) error { if err != nil { return err } - tmpl, err := template.New(f.Name).Funcs(funcmap.New()).Parse(string(decoded)) + tmpl, err := template.New(f.Name).Funcs(FilterFuncMap()).Parse(string(decoded)) if err != nil { return cleanupParseError(resource, err) } @@ -176,6 +177,7 @@ func (f FilterConfig) parseTmpl(resource string) error { // FilterGroup represents the container for filters. type FilterGroup struct { Name string `json:"group" yaml:"group"` + Description string `json:"description" yaml:"description"` Enabled *bool `json:"enabled" yaml:"enabled"` Selector FilterGroupSelector `json:"selector" yaml:"selector"` Policy FilterGroupPolicy `json:"policy" yaml:"policy"` @@ -183,6 +185,7 @@ type FilterGroup struct { Rules []*FilterConfig `json:"rules" yaml:"rules"` FromStrings []*FilterConfig `json:"from-strings" yaml:"from-strings"` // deprecated in favor or `Rules` Tags []string `json:"tags" yaml:"tags"` + Labels map[string]string `json:"labels" yaml:"labels"` Action string `json:"action" yaml:"action"` // only valid in sequence policies } @@ -197,7 +200,7 @@ func (g FilterGroup) validate(resource string) error { "Only groups with include policies can have rule actions", filter.Name, g.Name) } if filter.MaxSpan != 0 && g.Policy != SequencePolicy { - return fmt.Errorf("%q rule found has max span, but it is not in sequence policy " + + return fmt.Errorf("%q rule has max span, but it is not in sequence policy " + filter.Name) } if err := filter.parseTmpl(resource); err != nil { @@ -235,12 +238,19 @@ func (s FilterGroupSelector) Hash() uint32 { return s.Category.Hash() } -// Filters contains references to filter group definitions. -// Each filter group can contain multiple filter expressions. -// Filter expressions can reside in the filter group file or -// live in a separate file. +// Filters contains references to rule groups and macro definitions. +// Each filter group can contain multiple filter expressions whcih +// represent the rules. type Filters struct { - Rules Rules `json:"rules" yaml:"rules"` + Rules Rules `json:"rules" yaml:"rules"` + Macros Macros `json:"macros" yaml:"macros"` + macros map[string]*Macro +} + +// FiltersWithMacros builds the filter config with the map of +// predefined macros. Only used for testing purposes. +func FiltersWithMacros(macros map[string]*Macro) *Filters { + return &Filters{macros: macros} } // Rules contains attributes that describe the location of @@ -250,38 +260,124 @@ type Rules struct { FromURLs []string `json:"from-urls" yaml:"from-urls"` } -const rulesFromPaths = "filters.rules.from-paths" -const rulesFromURLs = "filters.rules.from-urls" +// Macros contains attributes that describe the location of +// macro resources. +type Macros struct { + FromPaths []string `json:"from-paths" yaml:"from-paths"` +} + +// Macro represents the state of the rule macro. Macros +// either expand to expressions or lists. +type Macro struct { + ID string `json:"macro" yaml:"macro"` + Description string `json:"description" yaml:"description"` + Expr string `json:"expr" yaml:"expr"` + List []string `json:"list" yaml:"list"` +} + +const ( + rulesFromPaths = "filters.rules.from-paths" + rulesFromURLs = "filters.rules.from-urls" + macrosFromPaths = "filters.macros.from-paths" +) func (f *Filters) initFromViper(v *viper.Viper) { f.Rules.FromPaths = v.GetStringSlice(rulesFromPaths) f.Rules.FromURLs = v.GetStringSlice(rulesFromURLs) + f.Macros.FromPaths = v.GetStringSlice(macrosFromPaths) } -// LoadGroups for each filter group file it decodes the -// groups and ensures the correctness of the yaml file. -func (f Filters) LoadGroups() ([]FilterGroup, error) { - allGroups := make([]FilterGroup, 0) - for _, path := range f.Rules.FromPaths { - log.Infof("loading rules from file %s", path) - file, err := os.Stat(path) +func (f Filters) HasMacros() bool { return len(f.macros) > 0 } +func (f Filters) GetMacro(id string) *Macro { return f.macros[id] } +func (f Filters) IsMacroList(id string) bool { + macro, ok := f.macros[id] + if !ok { + return false + } + return macro.List != nil +} + +// LoadMacros from the macro library. The Go templates are applied +// on each macro file before running the YAML decoder on them. +func (f *Filters) LoadMacros() error { + f.macros = make(map[string]*Macro) + for _, p := range f.Macros.FromPaths { + paths, err := filepath.Glob(p) if err != nil { - return nil, fmt.Errorf("couldn't open rule file %s: %v", path, err) + return err } - if file.IsDir() { - return nil, fmt.Errorf("expected yml file but got directory %s", path) - } - // read the file group yaml file and produce - // the corresponding filter groups from it - rawConfig, err := ioutil.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("couldn't load rule file: %v", err) + for _, path := range paths { + if filepath.Ext(path) != ".yml" && filepath.Ext(path) != ".yaml" { + continue + } + log.Infof("loading macros from file %s", path) + buf, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("couldn't load macros from file: %v", err) + } + // validate macro yaml structure + var out interface{} + err = yaml.Unmarshal(buf, &out) + if err != nil { + return fmt.Errorf("%q is invalid macro yaml file: %v", path, err) + } + valid, errs := validate(macrosSchema, out) + if !valid || len(errs) > 0 { + b, err := yaml.Marshal(&out) + if err == nil { + out = string(b) + } + return fmt.Errorf("invalid macro definition: \n\n"+ + "%v in %s: %v", out, path, multierror.Wrap(errs...)) + } + buf, err = renderTmpl(path, buf) + if err != nil { + return err + } + // unmarshal macros and transform to map + var macros []Macro + if err := yaml.Unmarshal(buf, ¯os); err != nil { + return err + } + for _, m := range macros { + f.macros[m.ID] = &Macro{ + ID: m.ID, + Description: m.Description, + Expr: m.Expr, + List: m.List, + } + } } - groups, err := decodeFilterGroups(path, rawConfig) + } + return nil +} + +// LoadGroups for each rule group file it decodes the +// groups and ensures the correctness of the yaml file. +func (f Filters) LoadGroups() ([]FilterGroup, error) { + allGroups := make([]FilterGroup, 0) + for _, p := range f.Rules.FromPaths { + paths, err := filepath.Glob(p) if err != nil { return nil, err } - allGroups = append(allGroups, groups...) + for _, path := range paths { + if filepath.Ext(path) != ".yml" && filepath.Ext(path) != ".yaml" { + continue + } + log.Infof("loading rules from file %s", path) + // read the file group yaml file and produce + // the corresponding filter groups from it + rawConfig, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("couldn't load rule file: %v", err) + } + groups, err := decodeFilterGroups(path, rawConfig) + if err != nil { + return nil, err + } + allGroups = append(allGroups, groups...) + } } for _, url := range f.Rules.FromURLs { log.Infof("loading rules from URL %s", url) @@ -329,7 +425,7 @@ func decodeFilterGroups(resource string, b []byte) ([]FilterGroup, error) { // apply validation to each group // declared in the yml config file for _, group := range rawGroups { - valid, errs := validate(filterGroupSchema, group) + valid, errs := validate(rulesSchema, group) if !valid || len(errs) > 0 { rawGroup := group b, err := yaml.Marshal(&rawGroup) @@ -373,41 +469,20 @@ func decodeFilterGroups(resource string, b []byte) ([]FilterGroup, error) { // file group yaml file. It returns the byte slice // with yaml content after template expansion. func renderTmpl(filename string, b []byte) ([]byte, error) { - rawValues, err := unmarshalValues(filename) - if err != nil { - return nil, err - } - tmpl, err := template.New(filename).Funcs(funcmap.New()).Parse(string(b)) + tmpl, err := template.New(filename).Funcs(FilterFuncMap()).Parse(string(b)) if err != nil { return nil, cleanupParseError(filename, err) } var w bytes.Buffer // force strict keys presence tmpl.Option("missingkey=error") - err = tmpl.Execute(&w, map[string]interface{}{"Values": rawValues}) + err = tmpl.Execute(&w, nil) if err != nil { return nil, cleanupParseError(filename, err) } return w.Bytes(), nil } -// unmarshalValues reads the values defined in -// the values.yml file is the file is present -// in the same directory as the filter group yaml file. -func unmarshalValues(filename string) (interface{}, error) { - path := filepath.Join(filepath.Dir(filename), "values.yml") - f, err := ioutil.ReadFile(path) - if err != nil { - return nil, nil - } - var rawValues interface{} - err = yaml.Unmarshal(f, &rawValues) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal yaml: %v", err) - } - return rawValues, nil -} - func cleanupParseError(filename string, err error) error { if err == nil { return nil @@ -434,21 +509,53 @@ func cleanupParseError(filename string, err error) error { return fmt.Errorf("syntax error in (%s) at %s: %s", location, key, errMsg) } -// TmplData is the template data object. Some -// fields of this structure represent empty -// values, since we have to satisfy the presence -// of certain keys when executing the template. -type TmplData struct { +// ActionContext is the convenient structure +// for grouping the event that resulted in +// matched filter along with filter group +// information. +type ActionContext struct { + Kevt *kevent.Kevent + // Kevts contains matched events for sequence group + // policies indexed by `k` + the slot number of the + // rule that produced a partial match + Kevts map[string]*kevent.Kevent + // Events contains a single element for non-sequence + // group policies or a list of ordered matched events + // for sequence group policies + Events []*kevent.Kevent Filter *FilterConfig - Group *FilterGroup - Kevt *kevent.Kevent + Group FilterGroup +} + +// FilterFuncMap returns the template func map +// populated with some useful template functions +// that can be used in rule actions. +func FilterFuncMap() template.FuncMap { + f := sprig.TxtFuncMap() + + extra := template.FuncMap{ + // This is a placeholder for the functions that might be + // late-bound to a template. By declaring them here, we + // can still execute the template associated with the + // filter action to ensure template syntax is correct + "emit": func(ctx *ActionContext, title string, text string, args ...string) string { return "" }, + "kill": func(pid uint32) string { return "" }, + } + + for k, v := range extra { + f[k] = v + } + + return f } -func tmplData() TmplData { - return TmplData{ +func tmplData() *ActionContext { + return &ActionContext{ Filter: &FilterConfig{}, - Group: &FilterGroup{}, + Group: FilterGroup{}, Kevt: kevent.Empty(), + Events: make([]*kevent.Kevent, 0), + Kevts: make(map[string]*kevent.Kevent), } } diff --git a/pkg/config/filters_test.go b/pkg/config/filters_test.go index 87f815efc..7001f2c7c 100644 --- a/pkg/config/filters_test.go +++ b/pkg/config/filters_test.go @@ -22,10 +22,10 @@ import ( "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "io/ioutil" "net" "net/http" "net/http/httptest" + "os" "testing" ) @@ -34,6 +34,8 @@ func newFilters(paths ...string) Filters { Rules{ FromPaths: paths, }, + Macros{FromPaths: nil}, + map[string]*Macro{}, } } @@ -44,6 +46,8 @@ func TestLoadGroupsFromPaths(t *testing.T) { "_fixtures/filters/default.yml", }, }, + Macros{FromPaths: nil}, + map[string]*Macro{}, } groups, err := filters.LoadGroups() require.NoError(t, err) @@ -78,6 +82,8 @@ func TestLoadGroupsFromPathsNewAttributes(t *testing.T) { "_fixtures/filters/default-new-attributes.yml", }, }, + Macros{FromPaths: nil}, + map[string]*Macro{}, } groups, err := filters.LoadGroups() require.NoError(t, err) @@ -109,7 +115,7 @@ func TestLoadGroupsFromPathsNewAttributes(t *testing.T) { func TestLoadGroupsFromURLs(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/default.yml", func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadFile("_fixtures/filters/default.yml") + b, err := os.ReadFile("_fixtures/filters/default.yml") if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -132,6 +138,8 @@ func TestLoadGroupsFromURLs(t *testing.T) { "http://localhost:3231/default.yml", }, }, + Macros{FromPaths: nil}, + map[string]*Macro{}, } groups, err := filters.LoadGroups() require.NoError(t, err) @@ -159,19 +167,3 @@ func TestLoadGroupsInvalidTemplates(t *testing.T) { } } } - -func TestLoadGroupsWithValues(t *testing.T) { - filters := Filters{ - Rules{ - FromPaths: []string{ - "_fixtures/filters/values/default.yml", - }, - }, - } - groups, err := filters.LoadGroups() - require.NoError(t, err) - require.Len(t, groups, 2) - - g2 := groups[1] - assert.Equal(t, "kevt.category = 'net' and ps.name in ('at.exe', 'java.exe', 'nc.exe')", g2.FromStrings[0].Def) -} diff --git a/pkg/config/output.go b/pkg/config/output.go index 6fdd55e2f..478ac4b2b 100644 --- a/pkg/config/output.go +++ b/pkg/config/output.go @@ -161,9 +161,9 @@ func findActiveOutputs(outputs map[string]interface{}) []string { // isWindowsService returns true if the process is running inside Windows Service. func isWindowsService() bool { - interactive, err := svc.IsAnInteractiveSession() + isWinService, err := svc.IsWindowsService() if err != nil { return false } - return !interactive + return isWinService } diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index 5d1e72d56..100752f70 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -51,7 +51,8 @@ var schema = ` "user": {"type": "string"}, "password": {"type": "string"}, "from": {"type": "string"}, - "to": {"type": "array", "items": {"type": "string", "format": "email"}} + "to": {"type": "array", "items": {"type": "string", "format": "email"}}, + "content-type": {"type": "string"} }, "if": { "properties": {"enabled": { "const": true }} @@ -129,7 +130,14 @@ var schema = ` "from-urls": {"type": ["array", "null"], "items": [{"type": "string", "minLength": 8}]} }, "additionalProperties": false - } + }, + "macros": { + "type": "object", + "properties": { + "from-paths": {"type": ["array", "null"], "items": [{"type": "string", "minLength": 4}]} + }, + "additionalProperties": false + } }, "additionalProperties": false }, @@ -472,7 +480,7 @@ var schema = ` } ` -var filterGroupSchema = ` +var rulesSchema = ` { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": {"rules": {"$id": "#rules", "type": "object", "type": "array", @@ -540,6 +548,30 @@ var filterGroupSchema = ` } ` +var macrosSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": + { + "type": "object", + "properties": { + "macro": {"type": "string", "minLength": 2, "pattern": "^[A-Za-z0-9_-]+$"}, + "description": {"type": "string"}, + "expr": {"type": "string", "minLength": 5}, + "list": {"type": "array", "items": [{"type": "string", "minLength": 1}]} + }, + "required": ["macro"], + "oneOf": [ + {"required": ["expr"]}, + {"required": ["list"]} + ], + "additionalProperties": false + }, + "additionalProperties": false +} +` + type schemaConfig struct { MaxBuffers uint32 MinBuffers uint32 diff --git a/pkg/filament/filament.go b/pkg/filament/filament.go index b189bc040..2e3da6957 100644 --- a/pkg/filament/filament.go +++ b/pkg/filament/filament.go @@ -36,7 +36,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/term" log "github.com/sirupsen/logrus" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -179,7 +178,7 @@ func New( if err != nil || !fstat.IsDir() { return nil, errFilamentsDir(path) } - filaments, err := ioutil.ReadDir(path) + filaments, err := os.ReadDir(path) if err != nil { return nil, err } diff --git a/pkg/filter/_fixtures/default/values.yml b/pkg/filter/_fixtures/default/values.yml deleted file mode 100644 index 6a4d43e4a..000000000 --- a/pkg/filter/_fixtures/default/values.yml +++ /dev/null @@ -1,46 +0,0 @@ -processes: - comm: - svchost: - - C:\\Windows\\system32\\svchost.exe -k appmodel -s StateRepository - - C:\\Windows\\system32\\svchost.exe -k appmodel -p -s camsvc - - C:\\Windows\\system32\\svchost.exe -k appmodel - - C:\\Windows\\system32\\svchost.exe -k appmodel -p -s tiledatamodelsvc - - C:\\Windows\\system32\\svchost.exe -k camera -s FrameServer - - C:\\Windows\\system32\\svchost.exe -k dcomlaunch -s LSM - - C:\\Windows\\system32\\svchost.exe -k dcomlaunch -s PlugPlay - # Windows defragmentation - - C:\\Windows\\system32\\svchost.exe -k defragsvc - - C:\\Windows\\system32\\svchost.exe -k devicesflow -s DevicesFlowUserSvc - # Microsoft: The Windows Image Acquisition Service - - C:\\Windows\\system32\\svchost.exe -k imgsvc - - C:\\Windows\\system32\\svchost.exe -k localService -s EventSystem - - C:\\Windows\\system32\\svchost.exe -k localService -s bthserv - - C:\\Windows\\system32\\svchost.exe -k LocalService -p -s BthAvctpSvc - - C:\\Windows\\system32\\svchost.exe -k localService -s nsi - - C:\\Windows\\system32\\svchost.exe -k localService -s w32Time - # Windows: Network services - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s Dhcp - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s EventLog - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s TimeBrokerSvc - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -s WFDSConMgrSvc - - C:\\Windows\\system32\\svchost.exe -k LocalServiceNetworkRestricted -s BTAGService - # Win10:1903: Network Connection Broker - - C:\\Windows\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation -s SensrSvc - # Windows: SSDP [ https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol ] - - C:\\Windows\\system32\\svchost.exe -k localServiceAndNoImpersonation -p -s SSDPSRV - - C:\\Windows\\system32\\svchost.exe -k localServiceNoNetwork - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s WPDBusEnum - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s fhsvc - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s DeviceAssociationService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s NcbService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s SensorService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s TabletInputService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s UmRdpService - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -s WPDBusEnum - # Microsoft: Passport - - C:\\Windows\\system32\\svchost.exe -k localSystemNetworkRestricted -p -s NgcSvc - # Microsoft: Passport Container - - C:\\Windows\\system32\\svchost.exe -k localServiceNetworkRestricted -p -s NgcCtnrSvc \ No newline at end of file diff --git a/pkg/filter/_fixtures/exclude_policy_or.yml b/pkg/filter/_fixtures/exclude_policy_or.yml index 218746220..c8b9c63ba 100644 --- a/pkg/filter/_fixtures/exclude_policy_or.yml +++ b/pkg/filter/_fixtures/exclude_policy_or.yml @@ -15,4 +15,4 @@ relation: or from-strings: - name: match http connections - def: net.dport = '{{ .Values.process.windows }}' \ No newline at end of file + def: net.dport = 80 diff --git a/pkg/filter/_fixtures/include_policy_emit_alert.yml b/pkg/filter/_fixtures/include_policy_emit_alert.yml index 0db8722a0..d63f78051 100644 --- a/pkg/filter/_fixtures/include_policy_emit_alert.yml +++ b/pkg/filter/_fixtures/include_policy_emit_alert.yml @@ -9,10 +9,10 @@ def: net.dport = 443 action: | {{ $text := cat .Kevt.PS.Name "process received data on port" .Kevt.Kparams.dport }} - {{ emit "Test alert" $text "critical" "tag1" "tag2" }} + {{ emit . "Test alert" $text "critical" "tag1" "tag2" }} - name: Windows error reporting/telemetry, WMI provider host def: ps.comm startswith ( ' \"C:\\Windows\\system32\\wermgr.exe\\" \"-queuereporting_svc\" ', 'C:\\Windows\\system32\\DllHost.exe /Processid' - ) \ No newline at end of file + ) diff --git a/pkg/filter/_fixtures/sequence_policy_complex_pattern_bindings.yml b/pkg/filter/_fixtures/sequence_policy_complex_pattern_bindings.yml index 98166f5ac..b74a3193b 100644 --- a/pkg/filter/_fixtures/sequence_policy_complex_pattern_bindings.yml +++ b/pkg/filter/_fixtures/sequence_policy_complex_pattern_bindings.yml @@ -22,6 +22,6 @@ condition: > kevt.name in ('Send', 'Connect') and ps.pid = $3.ps.sibling.pid action: > - {{ emit "Phishing dropper outbound communication" + {{ emit . "Phishing dropper outbound communication" (printf "%s process initiated outbound communication to %s" .Kevts.k3.Kparams.name .Kevts.k4.Kparams.dip) }} diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index 5140d12c2..196a68a12 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -20,7 +20,6 @@ package filter import ( "errors" - "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/kevent/kparams" @@ -31,9 +30,13 @@ var ( ErrPsNil = errors.New("process state is nil") ) -// kevtAccessor extracts kernel event specific values. +// kevtAccessor extracts generic event values. type kevtAccessor struct{} +func (kevtAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return filter.useKevtAccessor +} + func newKevtAccessor() accessor { return &kevtAccessor{} } diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index a66decd4b..c21e91aff 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -21,6 +21,7 @@ package filter import ( "errors" "fmt" + "github.com/rabbitstack/fibratus/pkg/util/cmdline" "path/filepath" "strconv" "strings" @@ -42,6 +43,11 @@ import ( type accessor interface { // get fetches the parameter value for the specified filter field. get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) + // canAccess indicates if the particular accessor is able to extract + // fields from the given event. The filter context is also provided to + // this method to determine whether the accessor should be visited depending + // on some condition derived from the filter expression. + canAccess(kevt *kevent.Kevent, filter *filter) bool } // getAccessors initializes and returns all available accessors. @@ -69,6 +75,8 @@ func getParentPs(kevt *kevent.Kevent) *pstypes.PS { // psAccessor extracts process's state or kevent specific values. type psAccessor struct{} +func (psAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { return filter.useProcAccessor } + func newPSAccessor() accessor { return &psAccessor{} } func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -127,6 +135,15 @@ func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, e return nil, ErrPsNil } return ps.Args, nil + case fields.PsSiblingArgs: + if kevt.Category != ktypes.Process { + return nil, nil + } + cmndline, err := kevt.Kparams.GetString(kparams.Comm) + if err != nil { + return nil, err + } + return cmdline.Split(cmndline), nil case fields.PsCwd: ps := kevt.PS if ps == nil { @@ -490,6 +507,10 @@ func ancestorFields(field string, kevt *kevent.Kevent) (kparams.Value, error) { // threadAccessor fetches thread parameters from thread kernel events. type threadAccessor struct{} +func (threadAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.Thread +} + func newThreadAccessor() accessor { return &threadAccessor{} } @@ -556,6 +577,10 @@ func (t *threadAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value // fileAccessor extracts file specific values. type fileAccessor struct{} +func (fileAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.File +} + func newFileAccessor() accessor { return &fileAccessor{} } @@ -622,9 +647,13 @@ func (l *fileAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, return nil, nil } -// imageAccessor extracts image (DLL) kevent values. +// imageAccessor extracts image (DLL) event values. type imageAccessor struct{} +func (imageAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.Image +} + func newImageAccessor() accessor { return &imageAccessor{} } @@ -658,6 +687,10 @@ func (i *imageAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, // registryAccessor extracts registry specific parameters. type registryAccessor struct{} +func (registryAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.Registry +} + func newRegistryAccessor() accessor { return ®istryAccessor{} } @@ -685,6 +718,10 @@ func (r *registryAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Val // networkAccessor deals with extracting the network specific kernel event parameters. type networkAccessor struct{} +func (networkAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.Net +} + func newNetworkAccessor() accessor { return &networkAccessor{} } func (n *networkAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -724,6 +761,10 @@ func (n *networkAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Valu // handleAccessor extracts handle event values. type handleAccessor struct{} +func (handleAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { + return kevt.Category == ktypes.Handle +} + func newHandleAccessor() accessor { return &handleAccessor{} } func (h *handleAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -747,6 +788,8 @@ func (h *handleAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value // peAccessor extracts PE specific values. type peAccessor struct{} +func (peAccessor) canAccess(kevt *kevent.Kevent, filter *filter) bool { return true } + func newPEAccessor() accessor { return &peAccessor{} } diff --git a/pkg/filter/action/emit.go b/pkg/filter/action/emit.go new file mode 100644 index 000000000..c1419dcce --- /dev/null +++ b/pkg/filter/action/emit.go @@ -0,0 +1,69 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package action + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/alertsender/renderer" + "github.com/rabbitstack/fibratus/pkg/config" + log "github.com/sirupsen/logrus" +) + +// Emit sends the rule alert via all configured alert senders. +func Emit(ctx *config.ActionContext, title string, text string, args ...string) error { + log.Debugf("sending alert: %s. Text: %s", title, text) + + senders := alertsender.FindAll() + if len(senders) == 0 { + return fmt.Errorf("no alertsenders registered. Alert won't be sent") + } + + severity := "medium" + tags := make([]string, 0) + if len(args) > 0 { + severity = args[0] + } + if len(args) > 1 { + tags = args[1:] + } + + for _, s := range senders { + alert := alertsender.NewAlert( + title, + text, + tags, + alertsender.ParseSeverityFromString(severity), + ) + // produce HTML rule alert text for email sender + if s.Type() == alertsender.Mail { + var err error + alert.Text, err = renderer.RenderHTMLRuleAlert(ctx, alert) + if err != nil { + log.Warn(err) + } + } + go func(s alertsender.Sender) { + if err := s.Send(alert); err != nil { + log.Warnf("unable to emit alert from rule: %v", err) + } + }(s) + } + return nil +} diff --git a/pkg/filter/action/kill_windows.go b/pkg/filter/action/kill_windows.go new file mode 100644 index 000000000..849c311fa --- /dev/null +++ b/pkg/filter/action/kill_windows.go @@ -0,0 +1,40 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package action + +import ( + "fmt" + "syscall" +) + +// Kill terminates a process with specified pid. +func Kill(pid uint32) error { + h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, pid) + if err != nil { + return fmt.Errorf("couldn't open pid %d for termination: %v", pid, err) + } + defer func() { + _ = syscall.CloseHandle(h) + }() + err = syscall.TerminateProcess(h, uint32(1)) + if err != nil { + return fmt.Errorf("fail to kill pid %d: %v", pid, err) + } + return nil +} diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 63629ff96..97e54f71c 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -110,6 +110,8 @@ const ( PsSiblingComm Field = "ps.sibling.comm" // PsSiblingExe represents the sibling process complete executable path field PsSiblingExe Field = "ps.sibling.exe" + // PsSiblingArgs represents the sibling process command line arguments path field + PsSiblingArgs Field = "ps.sibling.args" // PsSiblingSID represents the sibling processes security identifier field PsSiblingSID Field = "ps.sibling.sid" // PsSiblingSessionID represents the sibling process session id field @@ -304,6 +306,9 @@ const ( // String casts the field type to string. func (f Field) String() string { return string(f) } +func (f Field) IsPsField() bool { return strings.HasPrefix(string(f), "ps.") } +func (f Field) IsKevtField() bool { return strings.HasPrefix(string(f), "kevt.") } + // Segment represents the type alias for the segment. Segment // denotes the location of the value within an indexed field. type Segment string @@ -410,6 +415,7 @@ var fields = map[Field]FieldInfo{ PsSiblingPid: {PsSiblingPid, "created, terminated, or opened process id", kparams.PID, []string{"ps.sibling.pid = 320"}}, PsSiblingName: {PsSiblingName, "created, terminated, or opened process name", kparams.UnicodeString, []string{"ps.sibling.name = 'notepad.exe'"}}, PsSiblingComm: {PsSiblingComm, "created or terminated process command line", kparams.UnicodeString, []string{"ps.sibling.comm contains '\\k \\v'"}}, + PsSiblingArgs: {PsSiblingArgs, "created process command line arguments", kparams.Slice, []string{"ps.sibling.args in ('/cdir', '/-C')"}}, PsSiblingExe: {PsSiblingExe, "created, terminated, or opened process id", kparams.UnicodeString, []string{"ps.sibling.exe contains '\\Windows\\cmd.exe'"}}, PsSiblingSID: {PsSiblingSID, "created or terminated process security identifier", kparams.UnicodeString, []string{"ps.sibling.sid contains 'SERVICE'"}}, PsSiblingSessionID: {PsSiblingSessionID, "created or terminated process session identifier", kparams.Int16, []string{"ps.sibling.sessionid == 1"}}, diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 19d558aa1..8a8a0495d 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -26,6 +26,8 @@ import ( "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/filter/ql" "github.com/rabbitstack/fibratus/pkg/kevent" + "regexp" + "strconv" "strings" ) @@ -47,6 +49,8 @@ type Filter interface { // BindingIndex returns the binding index to which the filter is bound // or a zero value if there are no pattern bindings defined. BindingIndex() (uint16, bool) + // GetStringFields returns field names mapped to their string values + GetStringFields() map[fields.Field][]string } type filter struct { @@ -57,6 +61,12 @@ type filter struct { bindings map[uint16][]*ql.PatternBindingLiteral // useFuncValuer determines whether we should supply the function valuer useFuncValuer bool + // useProcAccessor indicates if the process accessor is called by this filter + useProcAccessor bool + // useKevtAccessor indicates if the event accessor is called by this filter + useKevtAccessor bool + // stringFields contains filter field names mapped to their string values + stringFields map[fields.Field][]string } // Compile parsers the filter expression and builds a binary expression tree @@ -77,10 +87,24 @@ func (f *filter) Compile() error { walk := func(n ql.Node) { if expr, ok := n.(*ql.BinaryExpr); ok { if lhs, ok := expr.LHS.(*ql.FieldLiteral); ok { - f.fields = append(f.fields, fields.Field(lhs.Value)) + field := fields.Field(lhs.Value) + f.fields = append(f.fields, field) + switch v := expr.RHS.(type) { + case *ql.StringLiteral: + f.stringFields[field] = append(f.stringFields[field], v.Value) + case *ql.ListLiteral: + f.stringFields[field] = append(f.stringFields[field], v.Values...) + } } if rhs, ok := expr.RHS.(*ql.FieldLiteral); ok { - f.fields = append(f.fields, fields.Field(rhs.Value)) + field := fields.Field(rhs.Value) + f.fields = append(f.fields, field) + switch v := expr.LHS.(type) { + case *ql.StringLiteral: + f.stringFields[field] = append(f.stringFields[field], v.Value) + case *ql.ListLiteral: + f.stringFields[field] = append(f.stringFields[field], v.Values...) + } } if rhs, ok := expr.RHS.(*ql.PatternBindingLiteral); ok { f.bindings[rhs.Index()] = append(f.bindings[rhs.Index()], rhs) @@ -101,6 +125,15 @@ func (f *filter) Compile() error { return errNoFields } + for _, field := range f.fields { + switch { + case field.IsKevtField(): + f.useKevtAccessor = true + case field.IsPsField(): + f.useProcAccessor = true + } + } + if len(f.bindings) > 1 { bindings := make([]string, 0) for _, b := range f.bindings { @@ -151,23 +184,28 @@ func (f *filter) BindingIndex() (uint16, bool) { return 0, false } +func (f filter) GetStringFields() map[fields.Field][]string { return f.stringFields } + // mapValuer for each field present in the AST, we run the -// accessors and extract the field vales that are +// accessors and extract the field values that are // supplied to the valuer. The valuer feeds the // expression with correct values. func (f *filter) mapValuer(kevt *kevent.Kevent) map[string]interface{} { - valuer := make(map[string]interface{}) + valuer := make(map[string]interface{}, len(f.fields)) for _, field := range f.fields { for _, accessor := range f.accessors { + if !accessor.canAccess(kevt, f) { + continue + } v, err := accessor.get(field, kevt) if err != nil && !kerrors.IsKparamNotFound(err) { accessorErrors.Add(err.Error(), 1) continue } - if v == nil { - continue + if v != nil { + valuer[field.String()] = v + break } - valuer[field.String()] = v } } return valuer @@ -179,16 +217,71 @@ func (f *filter) bindingValuer(kevt *kevent.Kevent, idx uint16) map[string]inter valuer := make(map[string]interface{}) for _, binding := range f.bindings[idx] { for _, accessor := range f.accessors { + if !accessor.canAccess(kevt, f) { + continue + } v, err := accessor.get(binding.Field(), kevt) if err != nil && !kerrors.IsKparamNotFound(err) { accessorErrors.Add(err.Error(), 1) continue } - if v == nil { - continue + if v != nil { + valuer[binding.Value] = v + break } - valuer[binding.Value] = v } } return valuer } + +// InterpolateFields replaces all occurrences of field modifiers in the given string +// with values extracted from the event. Field modifiers may contain a leading ordinal +// which refers to the event in particular sequence stage. Otherwise, the modifier is +// a well-known field name prepended with the `%` symbol. +func InterpolateFields(s string, evts []*kevent.Kevent) string { + var fieldsReplRegexp = regexp.MustCompile(`%([1-9]?)\.?([a-z0-9A-Z\[\].]+)`) + matches := fieldsReplRegexp.FindAllStringSubmatch(s, -1) + r := s + if len(matches) == 0 { + return s + } + for _, m := range matches { + switch { + case len(m) == 3: + // parse index if the field modifier + // refers to the event in the sequence + i := 1 + if m[1] != "" { + var err error + i, err = strconv.Atoi(m[1]) + if err != nil { + continue + } + } + if i-1 > len(evts)-1 { + continue + } + kevt := evts[i-1] + // extract field value from the event and replace in string + var val any + for _, accessor := range getAccessors() { + var err error + val, err = accessor.get(fields.Field(m[2]), kevt) + if err != nil { + continue + } + if val != nil { + break + } + } + if val != nil { + r = strings.ReplaceAll(r, m[0], fmt.Sprintf("%v", val)) + } else { + r = strings.ReplaceAll(r, m[0], "N/A") + } + default: + return r + } + } + return r +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index e9f738d76..1e9028a29 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -19,6 +19,8 @@ package filter import ( + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/stretchr/testify/assert" "net" "testing" "time" @@ -41,7 +43,8 @@ var cfg = &config.Config{ EnableImageKevents: true, EnableThreadKevents: true, }, - PE: pe.Config{Enabled: true}, + Filters: &config.Filters{}, + PE: pe.Config{Enabled: true}, } func TestFilterCompile(t *testing.T) { @@ -55,6 +58,14 @@ func TestFilterCompile(t *testing.T) { require.EqualError(t, f.Compile(), "ps.name =\n╭─────────^\n|\n|\n╰─────────────────── expected field, string, number, bool, ip, function, pattern binding") } +func TestStringFields(t *testing.T) { + f := New(`ps.name = 'cmd.exe' and kevt.name = 'CreateProcess' or kevt.name in ('TerminateProcess', 'CreateFile')`, cfg) + require.NoError(t, f.Compile()) + assert.Len(t, f.GetStringFields(), 2) + assert.Len(t, f.GetStringFields()[fields.KevtName], 3) + assert.Len(t, f.GetStringFields()[fields.PsName], 1) +} + func TestFilterRunProcessKevent(t *testing.T) { kpars := kevent.Kparams{ kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, @@ -128,6 +139,7 @@ func TestFilterRunProcessKevent(t *testing.T) { {`ps.name = 'svchost.exe'`, true}, {`ps.name = 'svchot.exe'`, false}, {`ps.name = 'mimikatz.exe' or ps.name contains 'svc'`, true}, + {`ps.name ~= 'SVCHOST.exe'`, true}, {`ps.username = 'tor'`, true}, {`ps.domain = 'LOCAL'`, true}, {`ps.pid = 1023`, true}, @@ -207,9 +219,10 @@ func TestFilterRunThreadKevent(t *testing.T) { } kevt := &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kpars, - Name: "CreateThread", + Type: ktypes.CreateThread, + Kparams: kpars, + Name: "CreateThread", + Category: ktypes.Thread, PS: &pstypes.PS{ Name: "svchost.exe", Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, @@ -379,6 +392,7 @@ func TestFilterRunNetKevent(t *testing.T) { PS: &pstypes.PS{ Name: "cmd.exe", }, + Category: ktypes.Net, Kparams: kevent.Kparams{ kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, @@ -429,9 +443,10 @@ func TestFilterRunNetKevent(t *testing.T) { func TestFilterRunRegistryKevent(t *testing.T) { kevt := &kevent.Kevent{ - Type: ktypes.RegSetValue, - Tid: 2484, - PID: 859, + Type: ktypes.RegSetValue, + Tid: 2484, + PID: 859, + Category: ktypes.Registry, Kparams: kevent.Kparams{ kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, kparams.RegValue: {Name: kparams.RegValue, Type: kparams.Uint32, Value: 10234}, @@ -514,6 +529,123 @@ func TestFilterRunPE(t *testing.T) { } } +func TestInterpolateFields(t *testing.T) { + var tests = []struct { + original string + interpolated string + evts []*kevent.Kevent + }{ + { + original: "Credential discovery via %ps.name and user %ps.sid", + interpolated: "Credential discovery via VaultCmd.exe and user LOCAL\\tor", + evts: []*kevent.Kevent{ + { + Type: ktypes.CreateProcess, + Category: ktypes.Process, + Name: "CreateProcess", + PID: 1023, + PS: &pstypes.PS{ + Name: "VaultCmd.exe", + Ppid: 345, + SID: "LOCAL\\tor", + }, + }, + }, + }, + { + original: "Credential discovery via %ps.name and pid %kevt.pid", + interpolated: "Credential discovery via N/A and pid 1023", + evts: []*kevent.Kevent{ + { + Type: ktypes.CreateProcess, + Category: ktypes.Process, + Name: "CreateProcess", + PID: 1023, + }, + }, + }, + { + original: `Detected an attempt by %1.ps.name process to access +and read the memory of the Local Security And Authority Subsystem Service +and subsequently write the %2.file.name dump file to the disk device`, + interpolated: `Detected an attempt by taskmgr.exe process to access +and read the memory of the Local Security And Authority Subsystem Service +and subsequently write the C:\Users +eo\Temp\lsass.dump dump file to the disk device`, + evts: []*kevent.Kevent{ + { + Type: ktypes.OpenProcess, + Category: ktypes.Process, + Name: "OpenProcess", + PID: 1023, + PS: &pstypes.PS{ + Name: "taskmgr.exe", + Ppid: 345, + SID: "LOCAL\\tor", + }, + }, + { + Type: ktypes.WriteFile, + Category: ktypes.File, + Name: "WriteFile", + PID: 1023, + Kparams: kevent.Kparams{ + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, + }, + PS: &pstypes.PS{ + Name: "taskmgr.exe", + Ppid: 345, + SID: "LOCAL\\tor", + }, + }, + }, + }, + { + original: `Detected an attempt by %ps.name process to access +and read the memory of the Local Security And Authority Subsystem Service +and subsequently write the %2.file.name dump file to the disk device`, + interpolated: `Detected an attempt by taskmgr.exe process to access +and read the memory of the Local Security And Authority Subsystem Service +and subsequently write the C:\Users +eo\Temp\lsass.dump dump file to the disk device`, + evts: []*kevent.Kevent{ + { + Type: ktypes.OpenProcess, + Category: ktypes.Process, + Name: "OpenProcess", + PID: 1023, + PS: &pstypes.PS{ + Name: "taskmgr.exe", + Ppid: 345, + SID: "LOCAL\\tor", + }, + }, + { + Type: ktypes.WriteFile, + Category: ktypes.File, + Name: "WriteFile", + PID: 1023, + Kparams: kevent.Kparams{ + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, + }, + PS: &pstypes.PS{ + Name: "taskmgr.exe", + Ppid: 345, + SID: "LOCAL\\tor", + }, + }, + }, + }, + } + + for _, tt := range tests { + s := InterpolateFields(tt.original, tt.evts) + if tt.interpolated != s { + t.Errorf("expected %s interpolated string but got %s", tt.interpolated, s) + } + } +} + func BenchmarkFilterRun(b *testing.B) { b.ReportAllocs() f := New(`ps.name = 'mimikatz.exe' or ps.name contains 'svc'`, cfg) diff --git a/pkg/filter/filter_windows.go b/pkg/filter/filter_windows.go index 76c940311..d71a201dd 100644 --- a/pkg/filter/filter_windows.go +++ b/pkg/filter/filter_windows.go @@ -28,7 +28,7 @@ import ( // New creates a new filter with the specified filter expression. The consumers must ensure // the expression is correctly parsed before executing the filter. This is achieved by calling the -// Compile` method after constructing the filter. +// `Compile` method after constructing the filter. func New(expr string, config *config.Config) Filter { accessors := []accessor{ // general event parameters @@ -37,6 +37,7 @@ func New(expr string, config *config.Config) Filter { newPSAccessor(), } kconfig := config.Kstream + fconfig := config.Filters if kconfig.EnableThreadKevents { accessors = append(accessors, newThreadAccessor()) @@ -60,11 +61,19 @@ func New(expr string, config *config.Config) Filter { accessors = append(accessors, newPEAccessor()) } + var parser *ql.Parser + if fconfig.HasMacros() { + parser = ql.NewParserWithConfig(expr, fconfig) + } else { + parser = ql.NewParser(expr) + } + return &filter{ - parser: ql.NewParser(expr), - accessors: accessors, - fields: make([]fields.Field, 0), - bindings: make(map[uint16][]*ql.PatternBindingLiteral), + parser: parser, + accessors: accessors, + fields: make([]fields.Field, 0), + stringFields: make(map[fields.Field][]string), + bindings: make(map[uint16][]*ql.PatternBindingLiteral), } } @@ -76,7 +85,7 @@ func NewFromCLI(args []string, config *config.Config) (Filter, error) { } filter := New(expr, config) if err := filter.Compile(); err != nil { - return nil, fmt.Errorf("bad filter:\n %v", err) + return nil, fmt.Errorf("bad filter:\n%v", err) } return filter, nil } @@ -88,10 +97,11 @@ func NewFromCLIWithAllAccessors(args []string) (Filter, error) { return nil, nil } filter := &filter{ - parser: ql.NewParser(expr), - accessors: getAccessors(), - fields: make([]fields.Field, 0), - bindings: make(map[uint16][]*ql.PatternBindingLiteral), + parser: ql.NewParser(expr), + accessors: getAccessors(), + fields: make([]fields.Field, 0), + stringFields: make(map[fields.Field][]string), + bindings: make(map[uint16][]*ql.PatternBindingLiteral), } if err := filter.Compile(); err != nil { return nil, fmt.Errorf("bad filter:\n %v", err) diff --git a/pkg/filter/funcmap.go b/pkg/filter/funcmap.go new file mode 100644 index 000000000..d2682fca0 --- /dev/null +++ b/pkg/filter/funcmap.go @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2021 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filter + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter/action" + "text/template" +) + +// NewFuncMap returns the template func map +// populated with some useful template functions +// that can be used in rule actions. +func NewFuncMap() template.FuncMap { + return config.FilterFuncMap() +} + +// InitFuncs assigns late-bound functions to the func map. +func InitFuncs(funcMap template.FuncMap) { + funcMap["emit"] = emit + funcMap["kill"] = kill +} + +// emit sends the rule alert via all configured alert senders. +func emit(ctx *config.ActionContext, title string, text string, args ...string) string { + err := action.Emit(ctx, InterpolateFields(title, ctx.Events), InterpolateFields(text, ctx.Events), args...) + if err != nil { + return err.Error() + } + return "" +} + +// kill terminates a process with specified pid. +func kill(pid uint32) string { + err := action.Kill(pid) + if err != nil { + return err.Error() + } + return "" +} diff --git a/pkg/filter/funcmap/funcmap_windows.go b/pkg/filter/funcmap/funcmap_windows.go deleted file mode 100644 index f42dcdcba..000000000 --- a/pkg/filter/funcmap/funcmap_windows.go +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020-2021 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package funcmap - -import ( - "fmt" - "github.com/Masterminds/sprig/v3" - "github.com/rabbitstack/fibratus/pkg/alertsender" - log "github.com/sirupsen/logrus" - "strings" - "syscall" - "text/template" -) - -// New returns the template func map -// populated with some useful template functions -// that can be used in filter actions. Some functions -// are late-bound, so we merely provide a declaration. -// The real function is attached when the filter action -// is triggered. -func New() template.FuncMap { - f := sprig.TxtFuncMap() - - extra := template.FuncMap{ - // This is a placeholder for the functions that might be - // late-bound to a template. By declaring them here, we - // can still execute the template associated with the - // filter action to ensure template syntax is correct - "emit": func(title string, text string, args ...string) string { return "" }, - "kill": func(pid uint32) string { return "" }, - "stringify": func(in []interface{}) string { - values := make([]string, 0) - for _, e := range in { - s, ok := e.(string) - if !ok { - continue - } - values = append(values, fmt.Sprintf("'%s'", s)) - } - return fmt.Sprintf("(%s)", strings.Join(values, ", ")) - }, - } - - for k, v := range extra { - f[k] = v - } - - return f -} - -// InitFuncs assigns late-bound functions to the func map. -func InitFuncs(funcMap template.FuncMap) { - funcMap["emit"] = emit - funcMap["kill"] = kill -} - -// emit sends an alert via all configured alert senders. -func emit(title string, text string, args ...string) string { - log.Debugf("sending alert: %s. Text: %s", title, text) - - senders := alertsender.FindAll() - if len(senders) == 0 { - return "no alertsenders registered. Alert won't be sent" - } - - severity := "normal" - tags := make([]string, 0) - if len(args) > 0 { - severity = args[0] - } - if len(args) > 1 { - tags = args[1:] - } - - for _, s := range senders { - alert := alertsender.NewAlert( - title, - text, - tags, - alertsender.ParseSeverityFromString(severity), - ) - if err := s.Send(alert); err != nil { - log.Warnf("unable to emit alert from rule: %v", err) - } - } - return "" -} - -// kill terminates a process with specified pid. -func kill(pid uint32) string { - h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, pid) - if err != nil { - return fmt.Sprintf("couldn't open pid %d for terminating: %v", pid, err) - } - defer func() { - _ = syscall.CloseHandle(h) - }() - err = syscall.TerminateProcess(h, uint32(1)) - if err != nil { - return fmt.Sprintf("fail to kill pid %d: %v", pid, err) - } - return "" -} diff --git a/pkg/filter/ql/ast.go b/pkg/filter/ql/ast.go index 67c21ee70..c6fcd1239 100644 --- a/pkg/filter/ql/ast.go +++ b/pkg/filter/ql/ast.go @@ -213,6 +213,12 @@ func (v *ValuerEval) Eval(expr Expr) interface{} { func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { lhs := v.Eval(expr.LHS) + // lazy evaluation for the AND operator + if lhs != nil && expr.Op == and { + if val, ok := lhs.(bool); ok && !val { + return false + } + } rhs := v.Eval(expr.RHS) if lhs == nil && rhs != nil { // when the LHS is nil and the RHS is a boolean, implicitly cast the @@ -709,6 +715,12 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { return false } return lhs == rhs + case ieq: + rhs, ok := rhs.(string) + if !ok { + return false + } + return strings.EqualFold(lhs, rhs) case neq: rhs, ok := rhs.(string) if !ok { @@ -924,12 +936,12 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { ips, ok := rhs.([]net.IP) if !ok { // keep backward compatibility with string lists - ips1, ok := rhs.([]string) + addrs, ok := rhs.([]string) if !ok { return false } - for _, ip := range ips1 { - if net.ParseIP(ip).Equal(lhs) { + for _, s := range addrs { + if net.ParseIP(s).Equal(lhs) { return true } } @@ -1004,6 +1016,19 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { } } return false + case iin: + rhs, ok := rhs.([]string) + if !ok { + return false + } + for _, i := range lhs { + for _, j := range rhs { + if strings.EqualFold(i, j) { + return true + } + } + } + return false case startswith: rhs, ok := rhs.([]string) if !ok { @@ -1088,7 +1113,7 @@ func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { // the types were not comparable. If our operation was an equality operation, // return false instead of true. switch expr.Op { - case eq, neq, lt, lte, gt, gte: + case eq, ieq, neq, lt, lte, gt, gte: return false } return nil diff --git a/pkg/filter/ql/error.go b/pkg/filter/ql/error.go index ace1475a2..773f7b437 100644 --- a/pkg/filter/ql/error.go +++ b/pkg/filter/ql/error.go @@ -61,10 +61,10 @@ func findPosInLine(expr string, pos int) (int, int) { break } } - return pos - j + 2, ln + return pos - j - 1, ln default: // single line expression - return pos + 1, 1 + return pos, 1 } } } @@ -126,7 +126,7 @@ func render(e *ParseError) string { // Error returns the string representation of the error. func (e *ParseError) Error() string { if e.Message != "" { - return fmt.Sprintf("%s at line %d, char %d", e.Message, e.Pos+1, e.Pos+1) + return fmt.Sprintf("%s at char %d", e.Message, e.Pos+1) } return render(e) } diff --git a/pkg/filter/ql/error_test.go b/pkg/filter/ql/error_test.go index 914fa8745..c88cff05f 100644 --- a/pkg/filter/ql/error_test.go +++ b/pkg/filter/ql/error_test.go @@ -20,6 +20,7 @@ package ql import ( "github.com/stretchr/testify/require" + "strings" "testing" ) @@ -87,17 +88,17 @@ func TestParseError(t *testing.T) { | ╰─────────────────── expected field, string, number, bool, ip, function, pattern binding` - e := newParseError("[", []string{"field, string, number, bool, ip, function, pattern binding"}, 142, expr) + e := newParseError("[", []string{"field, string, number, bool, ip, function, pattern binding"}, 145, expr) require.Equal(t, expected, e.Error()) expr = `ps.name = 'cmd.exe' aand ps.cmdline contains 'ss'` e = newParseError("[", []string{"operator"}, 20, expr) - expected1 := `ps.name = 'cmd.exe' aand ps.cmdline contains 'ss' -╭────────────────────^ + expected1 := ` +ps.name = 'cmd.exe' aand ps.cmdline contains 'ss' +╭───────────────────^ | | ╰─────────────────── expected operator` - - require.Equal(t, expected1, e.Error()) + require.Equal(t, strings.TrimSpace(expected1), e.Error()) } diff --git a/pkg/filter/ql/function.go b/pkg/filter/ql/function.go index 88d9cc5b3..0d9d21e06 100644 --- a/pkg/filter/ql/function.go +++ b/pkg/filter/ql/function.go @@ -60,6 +60,7 @@ var funcs = map[string]FunctionDef{ functions.SubstrFn.String(): &functions.Substr{}, functions.EntropyFn.String(): &functions.Entropy{}, functions.RegexFn.String(): functions.NewRegex(), + functions.IsMinidumpFn.String(): functions.IsMinidump{}, } // FunctionDef is the interface that all function definitions have to satisfy. diff --git a/pkg/filter/ql/functions/minidump.go b/pkg/filter/ql/functions/minidump.go new file mode 100644 index 000000000..e15dbc162 --- /dev/null +++ b/pkg/filter/ql/functions/minidump.go @@ -0,0 +1,64 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package functions + +import ( + "encoding/binary" + "io" + "os" +) + +// The 4-byte magic number at the start of a minidump file +const minidumpSignature = 1347241037 + +// IsMinidump determines if the specified file contains the minidump signature. +type IsMinidump struct{} + +func (f IsMinidump) Call(args []interface{}) (interface{}, bool) { + if len(args) < 1 { + return false, false + } + path := args[0].(string) + + file, err := os.Open(path) + if err != nil { + return false, true + } + defer file.Close() + + var header [4]byte + _, err = io.ReadFull(file, header[:]) + if err != nil { + return false, true + } + isMinidumpSignature := binary.LittleEndian.Uint32(header[:]) == minidumpSignature + return isMinidumpSignature, true +} + +func (f IsMinidump) Desc() FunctionDesc { + desc := FunctionDesc{ + Name: IsMinidumpFn, + Args: []FunctionArgDesc{ + {Keyword: "path", Types: []ArgType{String, Field, Func}, Required: true}, + }, + } + return desc +} + +func (f IsMinidump) Name() Fn { return IsMinidumpFn } diff --git a/pkg/filter/ql/functions/regex.go b/pkg/filter/ql/functions/regex.go index d834b46a7..d2f0e9f9a 100644 --- a/pkg/filter/ql/functions/regex.go +++ b/pkg/filter/ql/functions/regex.go @@ -19,9 +19,8 @@ package functions import ( - "regexp" - log "github.com/sirupsen/logrus" + "regexp" ) // Regex applies single/multiple regular expressions on the provided string arguments. @@ -34,41 +33,43 @@ func NewRegex() *Regex { return &Regex{rxs: make(map[string]*regexp.Regexp)} } -func (f Regex) Call(args []interface{}) (interface{}, bool) { +func (f *Regex) Call(args []interface{}) (interface{}, bool) { if len(args) < 2 { return false, false } s := parseString(0, args) + // match regular expressions for _, arg := range args[1:] { expr, ok := arg.(string) if !ok { continue } - rx, compiled := f.rxs[expr] - if compiled && rx == nil { - continue - } - if !compiled { + rx, ok := f.rxs[expr] + if !ok { var err error rx, err = regexp.Compile(expr) if err != nil { - log.Warnf("invalid regular expression pattern: %v", err) - // to avoid compiling the regex ad infinitum + log.Warnf( + "invalid %q pattern in "+ + "regex function: %v", expr, err) f.rxs[expr] = nil - continue + } else { + f.rxs[expr] = rx } - f.rxs[expr] = rx + } + if rx == nil { + continue } if rx.MatchString(s) { return true, true } } - return false, false + return false, true } -func (f Regex) Desc() FunctionDesc { +func (f *Regex) Desc() FunctionDesc { desc := FunctionDesc{ Name: RegexFn, Args: []FunctionArgDesc{ @@ -84,4 +85,4 @@ func (f Regex) Desc() FunctionDesc { return desc } -func (f Regex) Name() Fn { return RegexFn } +func (f *Regex) Name() Fn { return RegexFn } diff --git a/pkg/filter/ql/functions/regex_test.go b/pkg/filter/ql/functions/regex_test.go index cac6a93f4..5aa72c187 100644 --- a/pkg/filter/ql/functions/regex_test.go +++ b/pkg/filter/ql/functions/regex_test.go @@ -33,6 +33,9 @@ func TestRegex(t *testing.T) { res1, _ := call.Call([]interface{}{`powershell.exe`, `power.*(shell|hell).dll`, `.*hell.exe`}) assert.True(t, res1.(bool)) + res3, _ := call.Call([]interface{}{`powershell.exe`, "[`"}) + assert.False(t, res3.(bool)) + for i := 0; i < 10; i++ { res, _ := call.Call([]interface{}{`powershell.exe`, `power.*(shell|hell).dll`, `.*hell.exe`}) assert.True(t, res.(bool)) diff --git a/pkg/filter/ql/functions/types.go b/pkg/filter/ql/functions/types.go index b6346a411..e2b1f76d2 100644 --- a/pkg/filter/ql/functions/types.go +++ b/pkg/filter/ql/functions/types.go @@ -52,6 +52,8 @@ const ( EntropyFn // RegexFn represents the REGEX function RegexFn + // IsMinidumpFn represents the ISMINIDUMP function + IsMinidumpFn ) // ArgType is the type alias for the argument value type. @@ -166,6 +168,8 @@ func (f Fn) String() string { return "ENTROPY" case RegexFn: return "REGEX" + case IsMinidumpFn: + return "IS_MINIDUMP" default: return "UNDEFINED" } diff --git a/pkg/filter/ql/lexer.go b/pkg/filter/ql/lexer.go index 383a0c475..961dfa81d 100644 --- a/pkg/filter/ql/lexer.go +++ b/pkg/filter/ql/lexer.go @@ -77,6 +77,11 @@ func (s *scanner) scan() (tok token, pos int, lit string) { return dot, pos, "" case '=': return eq, pos, "" + case '~': + if ch1, _ := s.r.read(); ch1 == '=' { + return ieq, pos, "" + } + s.r.unread() case '!': if ch1, _ := s.r.read(); ch1 == '=' { return neq, pos, "" diff --git a/pkg/filter/ql/lexer_test.go b/pkg/filter/ql/lexer_test.go index a9db83c5e..59f457c2b 100644 --- a/pkg/filter/ql/lexer_test.go +++ b/pkg/filter/ql/lexer_test.go @@ -43,6 +43,7 @@ func TestScanner(t *testing.T) { {s: `or`, tok: or}, {s: `=`, tok: eq}, + {s: `~=`, tok: ieq}, {s: `<>`, tok: neq}, {s: `! `, tok: illegal, lit: "!"}, {s: `<`, tok: lt}, diff --git a/pkg/filter/ql/parser.go b/pkg/filter/ql/parser.go index 28d584cb6..d08740e20 100644 --- a/pkg/filter/ql/parser.go +++ b/pkg/filter/ql/parser.go @@ -21,6 +21,9 @@ package ql import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/util/multierror" "net" "strconv" "strings" @@ -29,6 +32,7 @@ import ( // Parser builds the binary expression tree from the filter string. type Parser struct { s *bufScanner + c *config.Filters expr string } @@ -37,11 +41,15 @@ func NewParser(expr string) *Parser { return &Parser{s: newBufScanner(strings.NewReader(expr)), expr: expr} } +// NewParserWithConfig builds a new parser instance with filters config. +func NewParserWithConfig(expr string, config *config.Filters) *Parser { + return &Parser{s: newBufScanner(strings.NewReader(expr)), expr: expr, c: config} +} + // ParseExpr parses an expression by building the binary expression tree. func (p *Parser) ParseExpr() (Expr, error) { var err error root := &BinaryExpr{} - // parse a non-binary expression type to start. This variable will always be // the root of the expression tree. root.RHS, err = p.parseUnaryExpr() @@ -65,7 +73,7 @@ func (p *Parser) ParseExpr() (Expr, error) { // expect LPAREN after in tok, pos, lit := p.scanIgnoreWhitespace() p.unscan() - if tok != lparen { + if tok != lparen && !p.c.IsMacroList(lit) { return nil, newParseError(tokstr(op, lit), []string{"'('"}, pos, p.expr) } } @@ -167,9 +175,26 @@ func (p *Parser) parseUnaryExpr() (Expr, error) { if tok0, _, _ := p.scan(); tok0 == lparen { return p.parseFunction(lit) } - // unscan lparen and ident tokens - p.unscan() + // unscan lparen token p.unscan() + + // expand macros + if p.c != nil { + macro := p.c.GetMacro(lit) + if macro != nil { + if macro.Expr != "" { + p := NewParserWithConfig(macro.Expr, p.c) + expr, err := p.ParseExpr() + if err != nil { + return nil, multierror.WrapWithSeparator("\n", fmt.Errorf("syntax error in %q macro", lit), err) + } + return expr, nil + } + return &ListLiteral{Values: macro.List}, nil + } + // unscan ident + p.unscan() + } case ip: return &IPLiteral{Value: net.ParseIP(lit)}, nil case str: diff --git a/pkg/filter/ql/parser_test.go b/pkg/filter/ql/parser_test.go index 2dce14d1c..840fbedc8 100644 --- a/pkg/filter/ql/parser_test.go +++ b/pkg/filter/ql/parser_test.go @@ -20,6 +20,7 @@ package ql import ( "errors" + "github.com/rabbitstack/fibratus/pkg/config" "testing" ) @@ -74,3 +75,89 @@ func TestParser(t *testing.T) { } } } + +func TestExpandMacros(t *testing.T) { + var tests = []struct { + c *config.Filters + expr string + expectedExpr string + err error + }{ + { + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.name = 'CreateProcess'"}}), + "spawn_process and ps.name in ('cmd.exe', 'powershell.exe')", + "kevt.name = CreateProcess AND ps.name IN (cmd.exe, powershell.exe)", + nil, + }, + { + config.FiltersWithMacros(map[string]*config.Macro{"span_process": {Expr: "kevt.name = 'CreateProcess'"}}), + "spawn_process and ps.name in ('cmd.exe', 'powershell.exe')", + "", + errors.New("expected field, string, number, bool, ip, function, pattern binding"), + }, + { + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.name = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), + "spawn_process and ps.name in command_clients", + "kevt.name = CreateProcess AND ps.name IN (cmd.exe, pwsh.exe)", + nil, + }, + { + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.nnname = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), + "spawn_process and ps.name in command_clients", + "", + errors.New("syntax error in \"spawn_process\" macro. expected field, string, number, bool, ip, function, pattern binding"), + }, + { + config.FiltersWithMacros(map[string]*config.Macro{ + "rename": {Expr: "kevt.name = 'RenameFile'"}, + "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "modify": {Expr: "rename or remove"}, + "wcm_files": {List: []string{"?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*"}}}), + "(modify) and file.name imatches wcm_files", + "(kevt.name = RenameFile OR kevt.name = DeleteFile) AND file.name IMATCHES (?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*)", + nil, + }, + { + config.FiltersWithMacros(map[string]*config.Macro{ + "rename": {Expr: "kevt.name = 'RenameFile'"}, + "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "modify": {Expr: "rename or remove"}}), + "entropy(file.name) > 0.22 and ren", + "", + errors.New("expected field, string, number, bool, ip, function, pattern binding"), + }, + { + config.FiltersWithMacros(map[string]*config.Macro{ + "rename": {Expr: "kevt.name = 'RenameFile'"}, + "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "modify": {Expr: "rename or remove"}}), + "entropy(file.name) > 0.22 and rename", + "entropy(file.name) > 2.2e-01 AND kevt.name = RenameFile", + nil, + }, + { + config.FiltersWithMacros(map[string]*config.Macro{ + "rename": {Expr: "kevt.name = 'RenameFile'"}, + "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "create": {Expr: "kevt.name = 'CreateFile' and file.operation = 'create'"}, + "modify": {Expr: "rename or remove"}, + "change_fs": {Expr: "modify or (create)"}}), + "change_fs", + "kevt.name = RenameFile OR kevt.name = DeleteFile OR (kevt.name = CreateFile AND file.operation = create)", + nil, + }, + } + + for i, tt := range tests { + p := NewParserWithConfig(tt.expr, tt.c) + expr, err := p.ParseExpr() + if err == nil && tt.err != nil { + t.Errorf("%d. exp=%s expected error=\n%v", i, tt.expr, tt.err) + } else if err != nil && tt.err == nil { + t.Errorf("%d. exp=%s got error=\n%v", i, tt.expr, err) + } + if tt.expectedExpr != "" && expr.String() != tt.expectedExpr { + t.Errorf("%d. exp=%s expected expr=%v", i, expr.String(), tt.expectedExpr) + } + } +} diff --git a/pkg/filter/ql/token.go b/pkg/filter/ql/token.go index be8c9a861..5783390ec 100644 --- a/pkg/filter/ql/token.go +++ b/pkg/filter/ql/token.go @@ -65,6 +65,7 @@ const ( fuzzynorm // fuzzynorm ifuzzynorm // ifuzzynorm eq // = + ieq // ~= neq // != lt // < lte // <= @@ -129,6 +130,7 @@ var tokens = [...]string{ ifuzzynorm: "IFUZZYNORM", eq: "=", + ieq: "~=", neq: "!=", lt: "<", lte: "<=", @@ -161,7 +163,7 @@ func (tok token) precedence() int { return 2 case not: return 3 - case eq, neq, lt, lte, gt, gte: + case eq, ieq, neq, lt, lte, gt, gte: return 4 case in, iin, contains, icontains, startswith, istartswith, endswith, iendswith, matches, imatches, fuzzy, ifuzzy, fuzzynorm, ifuzzynorm: diff --git a/pkg/filter/rules.go b/pkg/filter/rules.go index a22e1fad4..7fd30aef2 100644 --- a/pkg/filter/rules.go +++ b/pkg/filter/rules.go @@ -26,6 +26,9 @@ import ( "expvar" "fmt" fsm "github.com/qmuntal/stateless" + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "sort" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/util/atomic" @@ -35,7 +38,6 @@ import ( "time" "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/filter/funcmap" "github.com/rabbitstack/fibratus/pkg/kevent" log "github.com/sirupsen/logrus" ) @@ -57,7 +59,7 @@ var ( partialExpirations = expvar.NewMap("sequence.partial.expirations") ErrInvalidFilter = func(rule, group string, err error) error { - return fmt.Errorf("invalid filter %q in %q group: \n%v", rule, group, err) + return fmt.Errorf("syntax error in rule %q located in %q group: \n%v", rule, group, err) } ErrInvalidPatternBinding = func(rule string) error { return fmt.Errorf("%q is the initial sequence rule and can't contain pattern bindings", rule) @@ -103,8 +105,9 @@ type filterGroup struct { } type compiledFilter struct { - filter Filter - config *config.FilterConfig + filter Filter + buckets map[ktypes.Ktype]bool + config *config.FilterConfig } // sequenceState represents the state of the @@ -126,7 +129,7 @@ type sequenceState struct { fsm *fsm.StateMachine - // rule to rule index mapping + // rule to rule index mapping. Indices start at 1 idxs map[fsm.State]uint16 maxSpans map[fsm.State]time.Duration spanDeadlines map[fsm.State]*time.Timer @@ -230,15 +233,25 @@ func (s *sequenceState) currentState() fsm.State { } func (s *sequenceState) addPartial(rule string, kevt *kevent.Kevent) { - if len(s.partials[s.idxs[rule]]) > maxOutstandingPartials { - log.Warnf("max partials encountered in sequence %s index %d. "+ + i := s.idxs[rule] + if len(s.partials[i]) > maxOutstandingPartials { + log.Warnf("max partials encountered in sequence %s slot [%d]. "+ "Dropping incoming partial", s.name, s.idxs[rule]) return } if len(s.bindingIndexes) > 0 { - log.Debugf("adding partial to slot [%d] for rule %q: %s", s.idxs[rule], rule, kevt) + key := kevt.PartialKey() + if key != 0 { + for _, p := range s.partials[i] { + if key == p.PartialKey() { + log.Debugf("%s event tuple already in sequence state", kevt.Name) + return + } + } + } + log.Debugf("adding partial to slot [%d] for rule %q: %s", i, rule, kevt) partialsPerSequence.Add(s.name, 1) - s.partials[s.idxs[rule]] = append(s.partials[s.idxs[rule]], kevt) + s.partials[i] = append(s.partials[i], kevt) } } @@ -248,8 +261,7 @@ func (s *sequenceState) addMatch(idx uint16, kevt *kevent.Kevent) { func (s *sequenceState) getPartials(rule string) map[uint16][]*kevent.Kevent { i := s.idxs[rule] - 1 - // is this is the first rule in the sequence - // return no partials + // no partials for the first rule in the sequence if i == 0 { return nil } @@ -321,9 +333,8 @@ func (s *sequenceState) expire(e *kevent.Kevent) bool { return lhs.PID == rhs.PID } for _, idx := range s.idxs { - currentPartials := s.partials[idx] - for i, e1 := range currentPartials { - if !canExpire(e1, e) { + for i := len(s.partials[idx]) - 1; i >= 0; i-- { + if len(s.partials[idx]) > 0 && !canExpire(s.partials[idx][i], e) { continue } // if downstream rule didn't match, and it contains @@ -335,18 +346,19 @@ func (s *sequenceState) expire(e *kevent.Kevent) bool { matched := s.matchedRules[idx+1] bindingIndex := s.bindingIndexes[idx+1] if !matched && bindingIndex == idx { - log.Debugf("removing process %s (%d) "+ - "from partials pertaining to sequence [%s]", + log.Debugf("removing event originated from %s (%d) "+ + "in partials pertaining to sequence [%s]", e.Kparams.MustGetString(kparams.ProcessName), e.Kparams.MustGetPid(), s.name) + // remove partial event from the corresponding slot s.partials[idx] = append( s.partials[idx][:i], s.partials[idx][i+1:]...) partialsPerSequence.Add(s.name, -1) if len(s.partials[idx]) == 0 { - log.Infof("%q sequence expired. All partials terminated", s.name) + log.Infof("%q sequence expired. All partials retracted", s.name) partialExpirations.Add(s.name, 1) s.inExpired = true err := s.expireTransition() @@ -371,8 +383,22 @@ func newFilterGroup(g config.FilterGroup, filters []compiledFilter) *filterGroup return &filterGroup{group: g, filters: filters} } -func newCompiledFilter(f Filter, filterConfig *config.FilterConfig) compiledFilter { - return compiledFilter{config: filterConfig, filter: f} +func newCompiledFilter(f Filter, filterConfig *config.FilterConfig, groupConfig config.FilterGroup) compiledFilter { + buckets := make(map[ktypes.Ktype]bool) + for name, values := range f.GetStringFields() { + if name == fields.KevtName { + for _, v := range values { + buckets[ktypes.KeventNameToKtype(v)] = true + } + } + } + if len(buckets) == 0 && groupConfig.Policy == config.SequencePolicy { + log.Warnf("%q rule in %q group doesn't have event type condition! "+ + "This could cause runtime performance degradation. Please consider "+ + "narrowing the scope of this rule by including the `kevt.name` condition", + filterConfig.Name, groupConfig.Name) + } + return compiledFilter{config: filterConfig, filter: f, buckets: buckets} } // run execute the filter and returns the matching partial index along with @@ -384,6 +410,16 @@ func (f compiledFilter) run(kevt *kevent.Kevent, i uint16, partials map[uint16][ return f.filter.Run(kevt), i, kevt } +// isEligible determines if the filter should be evaluated by inspecting +// the event type filter fields defined in the expression. We allow an event +// being eligible for evaluation even when the filter doesn't contain the kevt.name +// binary expression. However, this is considered a warning which could potentially +// cause runtime performance degradation if the filter is evaluated against every +// event. +func (f compiledFilter) isEligible(ktype ktypes.Ktype) bool { + return len(f.buckets) == 0 || f.buckets[ktype] +} + type filterGroups []*filterGroup func (groups filterGroups) hasIncludePolicy(kevt *kevent.Kevent) bool { @@ -418,9 +454,14 @@ func expr(c *config.FilterConfig) string { return c.Def } -// Compile loads the filter groups from all files -// and creates the filters for each filter group. +// Compile loads macros and rule groups from all +// indicated resources and creates the rules for +// each filter group. It also sets up the state +// machine transitions for sequence rule group policies. func (r *Rules) Compile() error { + if err := r.config.Filters.LoadMacros(); err != nil { + return err + } groups, err := r.config.Filters.LoadGroups() if err != nil { return err @@ -430,7 +471,7 @@ func (r *Rules) Compile() error { log.Warnf("rule group [%s] disabled", group.Name) continue } - log.Infof("loading rule group [%s]", group.Name) + log.Infof("loading rule group [%s] with %q policy", group.Name, group.Policy) rules := append(group.Rules, group.FromStrings...) if group.Policy != config.SequencePolicy && group.Action != "" { @@ -488,7 +529,7 @@ func (r *Rules) Compile() error { Permit(expireTransition, sequenceExpiredState) } } - filters = append(filters, newCompiledFilter(f, filterConfig)) + filters = append(filters, newCompiledFilter(f, filterConfig, group)) filtersCount.Add(1) } @@ -564,6 +605,10 @@ func (r *Rules) Fire(kevt *kevent.Kevent) bool { // the groups with exclude policies got matched ok = r.runRules(groups, config.IncludePolicy, kevt) if ok { + // transition state machine. In this case + // both include/sequence groups could produce + // a match + r.runRules(groups, config.SequencePolicy, kevt) return true } @@ -587,9 +632,12 @@ nextGroup: } // if the sequence expired we'll not keep evaluating if seqState.expire(kevt) { - return false + continue } for i, f := range g.filters { + if !f.isEligible(kevt.Type) { + continue + } if !seqState.next(i) { continue } @@ -600,7 +648,7 @@ nextGroup: err := seqState.matchTransition(rule, kevt) if err != nil { matchTransitionErrors.Add(1) - log.Warnf("match transition: %v", err) + log.Warnf("match transition failure: %v", err) } seqState.addMatch(uint16(i+1), kevt) seqState.addMatch(idx, e) @@ -611,13 +659,16 @@ nextGroup: log.Debugf("rule group [%s] matched", g.group.Name) // this is the event that triggered the group match kevt.AddMeta(kevent.RuleGroupKey, g.group.Name) + for k, v := range g.group.Labels { + kevt.AddMeta(kevent.MetadataKey(k), v) + } err := runFilterAction(nil, seqState.matches, g.group, nil) if err != nil { log.Warnf("unable to execute %q sequence action: %v", g.group.Name, err) } seqState.clear() + return done } - return done } var andMatched bool // process include/exclude filter groups. Each of them @@ -648,13 +699,16 @@ nextGroup: if ok { includeOrFilterMatches.Add(f.config.Name, 1) log.Debugf("rule [%s] in group [%s] matched", f.config.Name, g.group.Name) + // attach rule and group meta + kevt.AddMeta(kevent.RuleNameKey, f.config.Name) + kevt.AddMeta(kevent.RuleGroupKey, g.group.Name) + for k, v := range g.group.Labels { + kevt.AddMeta(kevent.MetadataKey(k), v) + } err := runFilterAction(kevt, nil, g.group, f.config) if err != nil { log.Warnf("unable to execute %q rule action: %v", f.config.Name, err) } - // attach rule and group meta - kevt.AddMeta(kevent.RuleNameKey, f.config.Name) - kevt.AddMeta(kevent.RuleGroupKey, g.group.Name) return true } case config.AndRelation: @@ -690,17 +744,6 @@ nextGroup: return false } -// ActionContext is the convenient structure -// for grouping the event that resulted in -// matched filter along with filter group -// information. -type ActionContext struct { - Kevt *kevent.Kevent - Kevts map[string]*kevent.Kevent - Filter *config.FilterConfig - Group config.FilterGroup -} - // runFilterAction executes the template associated with the filter // that has produced a match in one of the include groups. func runFilterAction( @@ -712,35 +755,43 @@ func runFilterAction( if (filter != nil && filter.Action == "") && group.Action == "" { return nil } - var action []byte + var actionBlock []byte var err error if group.Policy == config.SequencePolicy { - action, err = base64.StdEncoding.DecodeString(group.Action) + actionBlock, err = base64.StdEncoding.DecodeString(group.Action) } else { if filter == nil { panic("filter shouldn't be nil") } - action, err = base64.StdEncoding.DecodeString(filter.Action) + actionBlock, err = base64.StdEncoding.DecodeString(filter.Action) } if err != nil { return fmt.Errorf("corrupted filter/group action: %v", err) } - fmap := funcmap.New() - funcmap.InitFuncs(fmap) - tmpl, err := template.New(group.Name).Funcs(fmap).Parse(string(action)) - if err != nil { - return err + events := make([]*kevent.Kevent, 0) + matches := make(map[string]*kevent.Kevent, len(kevts)) + if kevt != nil { + events = append(events, kevt) + } else { + for k, kevt := range kevts { + events = append(events, kevt) + matches["k"+strconv.Itoa(int(k))] = kevt + } + sort.Slice(events, func(i, j int) bool { return events[i].Timestamp.Before(events[j].Timestamp) }) } - matches := make(map[string]*kevent.Kevent, len(kevts)) - for k, v := range kevts { - matches["k"+strconv.Itoa(int(k))] = v + fmap := NewFuncMap() + InitFuncs(fmap) + tmpl, err := template.New(group.Name).Funcs(fmap).Parse(string(actionBlock)) + if err != nil { + return err } - ctx := &ActionContext{ + ctx := &config.ActionContext{ Kevt: kevt, Kevts: matches, + Events: events, Filter: filter, Group: group, } diff --git a/pkg/filter/rules_test.go b/pkg/filter/rules_test.go index 3f3e3f9b1..94470213e 100644 --- a/pkg/filter/rules_test.go +++ b/pkg/filter/rules_test.go @@ -22,6 +22,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/fs" log "github.com/sirupsen/logrus" "net" + "sync" "testing" "time" @@ -37,6 +38,7 @@ import ( type mockSender struct{} +var mu sync.Mutex var emitAlert *alertsender.Alert func (s *mockSender) Send(a alertsender.Alert) error { @@ -44,6 +46,10 @@ func (s *mockSender) Send(a alertsender.Alert) error { return nil } +func (s *mockSender) Type() alertsender.Type { + return alertsender.None +} + func makeSender(config alertsender.Config) (alertsender.Sender, error) { return &mockSender{}, nil } @@ -56,10 +62,11 @@ func fireRules(t *testing.T, c *config.Config) bool { rules := NewRules(c) kevt := &kevent.Kevent{ - Type: ktypes.Recv, - Name: "Recv", - Tid: 2484, - PID: 859, + Type: ktypes.Recv, + Name: "Recv", + Tid: 2484, + PID: 859, + Category: ktypes.Net, Kparams: kevent.Kparams{ kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, @@ -284,10 +291,11 @@ func TestSimpleSequencePolicy(t *testing.T) { } kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, - Name: "CreateFile", - Tid: 2484, - PID: 859, + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 859, + Category: ktypes.File, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", @@ -320,10 +328,11 @@ func TestSimpleSequencePolicyWithMaxSpanReached(t *testing.T) { } kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, - Name: "CreateFile", - Tid: 2484, - PID: 859, + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 859, + Category: ktypes.File, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", @@ -365,10 +374,11 @@ func TestSimpleSequencePolicyWithMaxSpanNotReached(t *testing.T) { } kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, - Name: "CreateFile", - Tid: 2484, - PID: 859, + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 859, + Category: ktypes.File, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", @@ -402,10 +412,11 @@ func TestSimpleSequencePolicyPatternBindings(t *testing.T) { } kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, - Name: "CreateFile", - Tid: 2484, - PID: 859, + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 859, + Category: ktypes.File, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", @@ -441,11 +452,12 @@ func TestSequenceComplexPatternBindings(t *testing.T) { } kevt2 := &kevent.Kevent{ - Seq: 2, - Type: ktypes.CreateFile, - Name: "CreateFile", - Tid: 2484, - PID: 2243, + Seq: 2, + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 2243, + Category: ktypes.File, PS: &types.PS{ Name: "firefox.exe", Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", @@ -511,8 +523,12 @@ func TestSequenceComplexPatternBindings(t *testing.T) { // register alert sender require.NoError(t, alertsender.LoadAll([]alertsender.Config{{Type: alertsender.Noop}})) + mu.Lock() + defer mu.Unlock() require.True(t, rules.Fire(kevt4)) + time.Sleep(time.Millisecond * 25) + // check the format of the generated alert require.NotNil(t, emitAlert) assert.Equal(t, "Phishing dropper outbound communication", emitAlert.Title) @@ -536,10 +552,11 @@ func TestFilterActionEmitAlert(t *testing.T) { require.NoError(t, rules.Compile()) kevt := &kevent.Kevent{ - Type: ktypes.Recv, - Name: "Recv", - Tid: 2484, - PID: 859, + Type: ktypes.Recv, + Name: "Recv", + Tid: 2484, + PID: 859, + Category: ktypes.Net, PS: &types.PS{ Name: "cmd.exe", }, @@ -551,9 +568,10 @@ func TestFilterActionEmitAlert(t *testing.T) { }, Metadata: make(map[kevent.MetadataKey]string), } - + mu.Lock() + defer mu.Unlock() require.True(t, rules.Fire(kevt)) - + time.Sleep(time.Millisecond * 25) require.NotNil(t, emitAlert) assert.Equal(t, "Test alert", emitAlert.Title) assert.Equal(t, "cmd.exe process received data on port 443", emitAlert.Text) @@ -562,6 +580,52 @@ func TestFilterActionEmitAlert(t *testing.T) { emitAlert = nil } +func TestIsKtypeEligible(t *testing.T) { + rules := NewRules(newConfig("_fixtures/sequence_policy_simple_pattern_bindings.yml")) + require.NoError(t, rules.Compile()) + log.SetLevel(log.DebugLevel) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Name: "CreateProcess", + Tid: 2484, + PID: 859, + PS: &types.PS{ + Name: "cmd.exe", + Exe: "C:\\Windows\\system32\\svchost.exe", + }, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + }, + Metadata: map[kevent.MetadataKey]string{"foo": "bar", "fooz": "barzz"}, + } + + kevt2 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Name: "CreateFile", + Tid: 2484, + PID: 859, + PS: &types.PS{ + Name: "cmd.exe", + Exe: "C:\\Windows\\system32\\svchost.exe", + }, + Kparams: kevent.Kparams{ + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper"}, + }, + Metadata: map[kevent.MetadataKey]string{"foo": "bar", "fooz": "barzz"}, + } + + groups := rules.sequenceGroups + for _, g := range groups { + for _, f := range g.filters { + if f.config.Name == "spawn command shell" { + assert.False(t, f.isEligible(kevt2.Type)) + assert.True(t, f.isEligible(kevt1.Type)) + } + } + } +} + func BenchmarkChainRun(b *testing.B) { b.ReportAllocs() @@ -570,10 +634,11 @@ func BenchmarkChainRun(b *testing.B) { kevts := []*kevent.Kevent{ { - Type: ktypes.Connect, - Name: "Recv", - Tid: 2484, - PID: 859, + Type: ktypes.Connect, + Name: "Recv", + Tid: 2484, + PID: 859, + Category: ktypes.Net, PS: &types.PS{ Name: "cmd.exe", }, diff --git a/pkg/handle/key.go b/pkg/handle/key.go index b88704c02..5e5b7853a 100644 --- a/pkg/handle/key.go +++ b/pkg/handle/key.go @@ -30,14 +30,15 @@ import ( "github.com/rabbitstack/fibratus/pkg/syscall/security" ) +var ( + hklmPrefixes = []string{"\\REGISTRY\\MACHINE", "\\Registry\\Machine", "\\Registry\\MACHINE"} + hkcrPrefixes = []string{"\\REGISTRY\\MACHINE\\SOFTWARE\\CLASSES", "\\Registry\\Machine\\Software\\Classes"} + hkuPrefixes = []string{"\\REGISTRY\\USER", "\\Registry\\User"} +) + const ( - hklmPrefixUppercase = "\\REGISTRY\\MACHINE" - hklmPrefixCapitalized = "\\Registry\\Machine" - hkcrPrefixUppercase = "\\REGISTRY\\MACHINE\\SOFTWARE\\CLASSES" - hkcrPrefixCapitalized = "\\Registry\\Machine\\Software\\Classes" - hkuPrefixUppercase = "\\REGISTRY\\USER" - hkuPrefixCapitalized = "\\Registry\\User" - // hive represents an application hive. Application hives are loaded by user-mode processes via RegLoadAppKey to store application-specific state data. + // hive represents an application hive. Application hives are loaded by user-mode processes + // via RegLoadAppKey to store application-specific state data. hive = "\\REGISTRY\\A" ) @@ -52,12 +53,15 @@ var ( // FormatKey produces a root,key tuple from registry native key name. func FormatKey(key string) (registry.Key, string) { - if strings.HasPrefix(key, hklmPrefixUppercase) || strings.HasPrefix(key, hklmPrefixCapitalized) { - return registry.LocalMachine, subkey(key, hklmPrefixUppercase) + for _, p := range hklmPrefixes { + if strings.HasPrefix(key, p) { + return registry.LocalMachine, subkey(key, p) + } } - - if strings.HasPrefix(key, hkcrPrefixUppercase) || strings.HasPrefix(key, hkcrPrefixCapitalized) { - return registry.ClassesRoot, subkey(key, hkcrPrefixUppercase) + for _, p := range hkcrPrefixes { + if strings.HasPrefix(key, p) { + return registry.ClassesRoot, subkey(key, p) + } } once.Do(func() { initKeys() }) @@ -65,9 +69,10 @@ func FormatKey(key string) (registry.Key, string) { if root, k := findSIDKey(key); root != registry.InvalidKey { return root, k } - - if strings.HasPrefix(key, hkuPrefixUppercase) || strings.HasPrefix(key, hkuPrefixCapitalized) { - return registry.Users, subkey(key, hkuPrefixUppercase) + for _, p := range hkuPrefixes { + if strings.HasPrefix(key, p) { + return registry.Users, subkey(key, p) + } } if strings.HasPrefix(key, hive) { @@ -88,7 +93,7 @@ func initKeys() { mux.Lock() defer mux.Unlock() for _, sid := range sids { - user := hkuPrefixUppercase + "\\" + sid + user := "\\REGISTRY\\USER\\" + sid keys = append(keys, user, user+"\\_Classes") } } diff --git a/pkg/handle/object_test.go b/pkg/handle/object_test.go index 4312d5fe9..db2926e44 100644 --- a/pkg/handle/object_test.go +++ b/pkg/handle/object_test.go @@ -40,7 +40,7 @@ var ( ) func createNamedPipe(name *uint16, openMode uint32, pipeMode uint32, maxInstances uint32, outBufSize uint32, inBufSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) { - r0, _, e1 := syscall.Syscall9(procCreateNamedPipeW.Addr(), 8, uintptr(unsafe.Pointer(name)), uintptr(openMode), uintptr(pipeMode), uintptr(maxInstances), uintptr(outBufSize), uintptr(inBufSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)), 0) + r0, _, e1 := syscall.SyscallN(procCreateNamedPipeW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(openMode), uintptr(pipeMode), uintptr(maxInstances), uintptr(outBufSize), uintptr(inBufSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)), 0) handle = syscall.Handle(r0) if handle == syscall.InvalidHandle { if e1 != 0 { diff --git a/pkg/handle/snapshotter.go b/pkg/handle/snapshotter.go index 5e433eb7f..518a83dfc 100644 --- a/pkg/handle/snapshotter.go +++ b/pkg/handle/snapshotter.go @@ -54,6 +54,13 @@ var ( currentPid = uint32(os.Getpid()) ) +const ( + // maxProcHandles determines the maximum number of handles the handle snapshotter can store + maxProcHandles = 70000 + // maxHandlesPerProc determines the maximum number of handles a particular process state can store + maxHandlesPerProc = 800 +) + // CreateCallback defines the function that is triggered when new handle is conceived type CreateCallback func(pid uint32, handle htypes.Handle) @@ -195,6 +202,11 @@ func (s *snapshotter) FindHandles(pid uint32) ([]htypes.Handle, error) { // the type and the name of each allocated handle handles := make([]htypes.Handle, 0) count := snapshot.NumberOfHandles + if count > maxHandlesPerProc { + log.Warnf("maximum handle table size reached for %d pid. "+ + "Shrinking table size from %d to %d handles", pid, count, maxHandlesPerProc) + count = maxHandlesPerProc + } sysHandles := (*[1 << 30]object.ProcessHandleTableEntryInfo)(unsafe.Pointer(&snapshot.Handles[0]))[:count:count] for _, sh := range sysHandles { @@ -224,6 +236,10 @@ func (s *snapshotter) initSnapshot() { } else if err == nil { sysHandleInfo := (*object.SystemHandleInformationEx)(unsafe.Pointer(&buf[0])) count := int(sysHandleInfo.NumberOfHandles) + if count > maxProcHandles { + log.Warnf("handle snapshotter size exceeded. Shrinking from %d to %d handles", count, maxProcHandles) + count = maxProcHandles + } sysHandles := (*[1 << 30]object.SystemHandleTableEntryInfoEx)(unsafe.Pointer(&sysHandleInfo.Handles[0]))[:count:count] // iterate through available handles to get extended info @@ -344,6 +360,10 @@ func (s *snapshotter) housekeeping() { } else if err == nil { sysHandleInfo := (*object.SystemHandleInformationEx)(unsafe.Pointer(&buf[0])) count := int(sysHandleInfo.NumberOfHandles) + if count > maxProcHandles { + log.Warnf("handle snapshotter size exceeded. Shrinking from %d to %d handles", count, maxProcHandles) + count = maxProcHandles + } sysHandles := (*[1 << 30]object.SystemHandleTableEntryInfoEx)(unsafe.Pointer(&sysHandleInfo.Handles[0]))[:count:count] s.Lock() diff --git a/pkg/kevent/formatter_windows.go b/pkg/kevent/formatter_windows.go index b4358a905..4c2c465b9 100644 --- a/pkg/kevent/formatter_windows.go +++ b/pkg/kevent/formatter_windows.go @@ -20,7 +20,6 @@ package kevent import ( "strconv" - "strings" ) // Format applies the template on the provided kernel event. @@ -66,7 +65,7 @@ func (f *Formatter) Format(kevt *Kevent) []byte { // expand all parameters into the map so we can ask // for specific parameter names in the template for _, kpar := range kevt.Kparams { - values[".Kparams."+strings.Title(kpar.Name)] = kpar.String() + values[".Kparams."+caser.String(kpar.Name)] = kpar.String() } } diff --git a/pkg/kevent/kevent_windows.go b/pkg/kevent/kevent_windows.go index b4b1b3966..1c2b68813 100644 --- a/pkg/kevent/kevent_windows.go +++ b/pkg/kevent/kevent_windows.go @@ -18,14 +18,257 @@ package kevent -import "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" +import ( + "encoding/binary" + "fmt" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "hash/fnv" + "strings" +) // IsNetworkTCP determines whether the kevent pertains to network TCP events. func (kevt Kevent) IsNetworkTCP() bool { - return kevt.Type != ktypes.RecvUDPv4 && kevt.Type != ktypes.RecvUDPv6 && kevt.Type != ktypes.SendUDPv4 && kevt.Type != ktypes.SendUDPv6 + return kevt.Category == ktypes.Net && kevt.Type != ktypes.RecvUDPv4 && kevt.Type != ktypes.RecvUDPv6 && kevt.Type != ktypes.SendUDPv4 && kevt.Type != ktypes.SendUDPv6 } // IsNetworkUDP determines whether the kevent pertains to network UDP events. func (kevt Kevent) IsNetworkUDP() bool { return kevt.Type == ktypes.RecvUDPv4 || kevt.Type == ktypes.RecvUDPv6 || kevt.Type == ktypes.SendUDPv4 || kevt.Type == ktypes.SendUDPv6 } + +// PartialKey computes the unique hash of the event +// that can be employed to determine if the event +// from the given process and source has been processed +// in the rule sequences. +func (kevt Kevent) PartialKey() uint64 { + switch kevt.Type { + case ktypes.WriteFile, ktypes.ReadFile: + b := make([]byte, 12) + object, _ := kevt.Kparams.GetUint64(kparams.FileObject) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint64(b, object) + + return fnvHash(b) + case ktypes.CreateFile: + file, _ := kevt.Kparams.GetString(kparams.FileName) + b := make([]byte, 4+len(file)) + + binary.LittleEndian.PutUint32(b, kevt.PID) + b = append(b, []byte(file)...) + + return fnvHash(b) + case ktypes.OpenProcess: + b := make([]byte, 8) + pid, _ := kevt.Kparams.GetUint32(kparams.ProcessID) + access, _ := kevt.Kparams.GetUint32(kparams.DesiredAccess) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint32(b, pid) + binary.LittleEndian.PutUint32(b, access) + return fnvHash(b) + case ktypes.OpenThread: + b := make([]byte, 8) + tid, _ := kevt.Kparams.GetUint32(kparams.ThreadID) + access, _ := kevt.Kparams.GetUint32(kparams.DesiredAccess) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint32(b, tid) + binary.LittleEndian.PutUint32(b, access) + return fnvHash(b) + case ktypes.AcceptTCPv4, ktypes.RecvTCPv4, ktypes.RecvUDPv4: + b := make([]byte, 10) + + ip, _ := kevt.Kparams.GetIP(kparams.NetSIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetSport) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint32(b, binary.BigEndian.Uint32(ip.To4())) + binary.LittleEndian.PutUint16(b, port) + return fnvHash(b) + case ktypes.AcceptTCPv6, ktypes.RecvTCPv6, ktypes.RecvUDPv6: + b := make([]byte, 22) + + ip, _ := kevt.Kparams.GetIP(kparams.NetSIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetSport) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[0:8])) + binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[8:16])) + binary.LittleEndian.PutUint16(b, port) + return fnvHash(b) + case ktypes.ConnectTCPv4, ktypes.SendTCPv4, ktypes.SendUDPv4: + b := make([]byte, 10) + + ip, _ := kevt.Kparams.GetIP(kparams.NetDIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetDport) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint32(b, binary.BigEndian.Uint32(ip.To4())) + binary.LittleEndian.PutUint16(b, port) + return fnvHash(b) + case ktypes.ConnectTCPv6, ktypes.SendTCPv6, ktypes.SendUDPv6: + b := make([]byte, 22) + + ip, _ := kevt.Kparams.GetIP(kparams.NetDIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetDport) + + binary.LittleEndian.PutUint32(b, kevt.PID) + binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[0:8])) + binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[8:16])) + binary.LittleEndian.PutUint16(b, port) + return fnvHash(b) + case ktypes.RegOpenKey, ktypes.RegQueryKey, ktypes.RegQueryValue, + ktypes.RegDeleteKey, ktypes.RegDeleteValue, ktypes.RegSetValue: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + b := make([]byte, 4+len(key)) + + binary.LittleEndian.PutUint32(b, kevt.PID) + b = append(b, key...) + return fnvHash(b) + } + return 0 +} + +// Summary returns a brief summary of this event. Various important substrings +// in the summary text are highlighted by surrounding them inside HTML tags. +func (kevt *Kevent) Summary() string { + switch kevt.Type { + case ktypes.CreateProcess: + exe := kevt.Kparams.MustGetString(kparams.Exe) + sid := kevt.Kparams.MustGetString(kparams.UserSID) + return printSummary(kevt, fmt.Sprintf("spawned %s process as %s user", exe, sid)) + case ktypes.TerminateProcess: + exe := kevt.Kparams.MustGetString(kparams.Exe) + sid := kevt.Kparams.MustGetString(kparams.UserSID) + return printSummary(kevt, fmt.Sprintf("terminated %s process as %s user", exe, sid)) + case ktypes.OpenProcess: + access, _ := kevt.Kparams.GetStringSlice(kparams.DesiredAccessNames) + exe, _ := kevt.Kparams.GetString(kparams.Exe) + return printSummary(kevt, fmt.Sprintf("opened %s process object with %s access right(s)", + exe, strings.Join(access, "|"))) + case ktypes.CreateThread: + tid, _ := kevt.Kparams.GetTid() + addr, _ := kevt.Kparams.GetHex(kparams.ThreadEntrypoint) + return printSummary(kevt, fmt.Sprintf("spawned a new thread with %d id at %s address", + tid, addr)) + case ktypes.TerminateThread: + tid, _ := kevt.Kparams.GetTid() + addr, _ := kevt.Kparams.GetHex(kparams.ThreadEntrypoint) + return printSummary(kevt, fmt.Sprintf("terminated a thread with %d id at %s address", + tid, addr)) + case ktypes.OpenThread: + access, _ := kevt.Kparams.GetStringSlice(kparams.DesiredAccessNames) + exe, _ := kevt.Kparams.GetString(kparams.Exe) + return printSummary(kevt, fmt.Sprintf("opened %s process' thread object with %s access right(s)", + exe, strings.Join(access, "|"))) + case ktypes.LoadImage: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("loaded %s module", filename)) + case ktypes.UnloadImage: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("unloaded %s module", filename)) + case ktypes.CreateFile: + op := kevt.Kparams.MustGetFileOperation() + filename := kevt.Kparams.MustGetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("%sed a file %s", op, filename)) + case ktypes.ReadFile: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + size, _ := kevt.Kparams.GetUint32(kparams.FileIoSize) + return printSummary(kevt, fmt.Sprintf("read %d bytes from %s file", size, filename)) + case ktypes.WriteFile: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + size, _ := kevt.Kparams.GetUint32(kparams.FileIoSize) + return printSummary(kevt, fmt.Sprintf("wrote %d bytes to %s file", size, filename)) + case ktypes.SetFileInformation: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + class, _ := kevt.Kparams.GetString(kparams.FileInfoClass) + return printSummary(kevt, fmt.Sprintf("set %s information class on %s file", class, filename)) + case ktypes.DeleteFile: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("deleted %s file", filename)) + case ktypes.RenameFile: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("renamed %s file", filename)) + case ktypes.CloseFile: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("closed %s file", filename)) + case ktypes.EnumDirectory: + filename, _ := kevt.Kparams.GetString(kparams.FileName) + return printSummary(kevt, fmt.Sprintf("enumerated %s directory", filename)) + case ktypes.RegCreateKey: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("created %s key", key)) + case ktypes.RegOpenKey: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("opened %s key", key)) + case ktypes.RegDeleteKey: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("deleted %s key", key)) + case ktypes.RegQueryKey: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("queried %s key", key)) + case ktypes.RegSetValue: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + val, err := kevt.Kparams.GetString(kparams.RegValue) + if err != nil { + return printSummary(kevt, fmt.Sprintf("set %s value", key)) + } + return printSummary(kevt, fmt.Sprintf("set %s payload in %s value", val, key)) + case ktypes.RegDeleteValue: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("deleted %s value", key)) + case ktypes.RegQueryValue: + key, _ := kevt.Kparams.GetString(kparams.RegKeyName) + return printSummary(kevt, fmt.Sprintf("queried %s value", key)) + case ktypes.AcceptTCPv4, ktypes.AcceptTCPv6: + ip, _ := kevt.Kparams.GetIP(kparams.NetSIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetSport) + return printSummary(kevt, fmt.Sprintf("accepted connection from %v and %d port", ip, port)) + case ktypes.ConnectTCPv4, ktypes.ConnectTCPv6: + ip, _ := kevt.Kparams.GetIP(kparams.NetDIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetDport) + return printSummary(kevt, fmt.Sprintf("connected to %v and %d port", ip, port)) + case ktypes.SendTCPv4, ktypes.SendTCPv6, ktypes.SendUDPv4, ktypes.SendUDPv6: + ip, _ := kevt.Kparams.GetIP(kparams.NetDIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetDport) + size, _ := kevt.Kparams.GetUint32(kparams.NetSize) + return printSummary(kevt, fmt.Sprintf("sent %d bytes to %v and %d port", + size, ip, port)) + case ktypes.RecvTCPv4, ktypes.RecvTCPv6, ktypes.RecvUDPv4, ktypes.RecvUDPv6: + ip, _ := kevt.Kparams.GetIP(kparams.NetSIP) + port, _ := kevt.Kparams.GetUint16(kparams.NetSport) + size, _ := kevt.Kparams.GetUint32(kparams.NetSize) + return printSummary(kevt, fmt.Sprintf("received %d bytes from %v and %d port", + size, ip, port)) + case ktypes.CreateHandle: + handleType, _ := kevt.Kparams.GetString(kparams.HandleObjectTypeName) + handleName, _ := kevt.Kparams.GetString(kparams.HandleObjectName) + return printSummary(kevt, fmt.Sprintf("created %s handle of %s type", + handleName, handleType)) + case ktypes.CloseHandle: + handleType, _ := kevt.Kparams.GetString(kparams.HandleObjectTypeName) + handleName, _ := kevt.Kparams.GetString(kparams.HandleObjectName) + return printSummary(kevt, fmt.Sprintf("closed %s handle of %s type", + handleName, handleType)) + case ktypes.LoadDriver: + driver, _ := kevt.Kparams.GetString(kparams.ImageFilename) + return printSummary(kevt, fmt.Sprintf("loaded %s driver", driver)) + } + return "" +} + +func printSummary(kevt *Kevent, text string) string { + ps := kevt.PS + if ps != nil { + return fmt.Sprintf("%s %s", ps.Name, text) + } + return fmt.Sprintf("process with %d id %s", kevt.PID, text) +} + +func fnvHash(b []byte) uint64 { + h := fnv.New64() + _, _ = h.Write(b) + return h.Sum64() +} diff --git a/pkg/kevent/kevent_windows_test.go b/pkg/kevent/kevent_windows_test.go index ae2031d7d..bb2d9cf96 100644 --- a/pkg/kevent/kevent_windows_test.go +++ b/pkg/kevent/kevent_windows_test.go @@ -19,17 +19,69 @@ package kevent import ( + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" + "time" ) func TestKeventIsNetworkTCP(t *testing.T) { - assert.True(t, Kevent{Type: ktypes.AcceptTCPv4}.IsNetworkTCP()) - assert.False(t, Kevent{Type: ktypes.SendUDPv6}.IsNetworkTCP()) + assert.True(t, Kevent{Type: ktypes.AcceptTCPv4, Category: ktypes.Net}.IsNetworkTCP()) + assert.False(t, Kevent{Type: ktypes.SendUDPv6, Category: ktypes.Net}.IsNetworkTCP()) } func TestKeventIsNetworkUDP(t *testing.T) { assert.True(t, Kevent{Type: ktypes.RecvUDPv4}.IsNetworkUDP()) assert.False(t, Kevent{Type: ktypes.SendTCPv6}.IsNetworkUDP()) } + +func TestKeventSummary(t *testing.T) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: fs.FileDisposition(1)}, + }, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Parent: &pstypes.PS{ + PID: 2034, + Name: "explorer.exe", + Exe: `C:\Windows\System32\explorer.exe`, + Cwd: `C:\Windows\System32`, + SID: "admin\\SYSTEM", + Parent: &pstypes.PS{ + PID: 2345, + Name: "winlogon.exe", + }, + }, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + }, + } + + require.Equal(t, "firefox.exe opened a file C:\\Windows\\system32\\user32.dll", kevt.Summary()) + kevt.PS = nil + require.Equal(t, "process with 859 id opened a file C:\\Windows\\system32\\user32.dll", kevt.Summary()) +} diff --git a/pkg/kevent/kparam.go b/pkg/kevent/kparam.go index d5a22eaf9..1eecb9709 100644 --- a/pkg/kevent/kparam.go +++ b/pkg/kevent/kparam.go @@ -20,6 +20,8 @@ package kevent import ( "fmt" + "golang.org/x/text/cases" + "golang.org/x/text/language" "net" "reflect" "sort" @@ -30,6 +32,8 @@ import ( "github.com/rabbitstack/fibratus/pkg/kevent/kparams" ) +var caser = cases.Title(language.English) + // ParamCaseStyle is the type definition for parameter name case style type ParamCaseStyle uint8 @@ -545,7 +549,7 @@ func (kpars Kparams) String() string { case DotCase: sb.WriteString(strings.Replace(kpar.Name, "_", ".", -1) + ParamKVDelimiter + kpar.String()) case PascalCase: - sb.WriteString(strings.Replace(strings.Title(strings.Replace(kpar.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + kpar.String()) + sb.WriteString(strings.Replace(caser.String(strings.Replace(kpar.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + kpar.String()) case CamelCase: } if i != len(pars)-1 { diff --git a/pkg/kevent/kparam_windows.go b/pkg/kevent/kparam_windows.go index bbee9b742..2e25be32c 100644 --- a/pkg/kevent/kparam_windows.go +++ b/pkg/kevent/kparam_windows.go @@ -128,4 +128,17 @@ func (k Kparam) String() string { } } +// MustGetFileOperation returns the file operation involved in the I/O request. +func (kpars Kparams) MustGetFileOperation() string { + op, err := kpars.Get(kparams.FileOperation) + if err != nil { + panic(err) + } + disposition, ok := op.(fs.FileDisposition) + if !ok { + panic("couldn't type assert to file operation enum") + } + return disposition.String() +} + func joinSID(account, domain string) string { return fmt.Sprintf("%s\\%s", domain, account) } diff --git a/pkg/kevent/ktypes/ktypes_windows.go b/pkg/kevent/ktypes/ktypes_windows.go index c2be6b8f1..6f9cae79a 100644 --- a/pkg/kevent/ktypes/ktypes_windows.go +++ b/pkg/kevent/ktypes/ktypes_windows.go @@ -247,7 +247,7 @@ func (k Ktype) String() string { case LoadDriver: return "LoadDriver" default: - return string(k[:]) + return "" } } diff --git a/pkg/kevent/marshaller_test.go b/pkg/kevent/marshaller_test.go index 46eee6d86..ef724b749 100644 --- a/pkg/kevent/marshaller_test.go +++ b/pkg/kevent/marshaller_test.go @@ -20,7 +20,7 @@ package kevent import ( "encoding/json" - "io/ioutil" + "os" "testing" "time" @@ -215,7 +215,7 @@ func TestKeventMarshalJSON(t *testing.T) { } func TestUnmarshalHugeHandles(t *testing.T) { - b, err := ioutil.ReadFile("_fixtures\\handles.json") + b, err := os.ReadFile("_fixtures\\handles.json") require.NoError(t, err) handles := make([]htypes.Handle, 0) err = json.Unmarshal(b, &handles) diff --git a/pkg/outputs/eventlog/template.go b/pkg/kevent/template.go similarity index 74% rename from pkg/outputs/eventlog/template.go rename to pkg/kevent/template.go index c9e8b6436..81c2ed1f6 100644 --- a/pkg/outputs/eventlog/template.go +++ b/pkg/kevent/template.go @@ -16,23 +16,21 @@ * limitations under the License. */ -package eventlog +package kevent import ( "bytes" "fmt" - - "github.com/rabbitstack/fibratus/pkg/kevent" + "text/template" ) -// Template is the default Go template used for producing the eventlog messages. +// Template is the default Go template used for formatting events in textual format. var Template = `Name: {{ .Kevt.Name }} Sequence: {{ .Kevt.Seq }} +Description: {{ .Kevt.Description }} Process ID: {{ .Kevt.PID }} Thread ID: {{ .Kevt.Tid }} -Cpu: {{ .Kevt.CPU }} Params: {{ .Kevt.Kparams }} -Category: {{ .Kevt.Category }} {{- if .Kevt.PS }} @@ -119,10 +117,26 @@ Resources: {{- end }} ` -func (e *evtlog) renderTemplate(kevt *kevent.Kevent) ([]byte, error) { +// RenderDefaultTemplate returns the event string representation +// after applying the default Go template. +func (kevt *Kevent) RenderDefaultTemplate() ([]byte, error) { + tmpl, err := template.New("event").Parse(Template) + if err != nil { + return nil, err + } + return renderTemplate(kevt, tmpl) +} + +// RenderCustomTemplate returns the event string representation +// after applying the given Go template. +func (kevt *Kevent) RenderCustomTemplate(tmpl *template.Template) ([]byte, error) { + return renderTemplate(kevt, tmpl) +} + +func renderTemplate(kevt *Kevent, tmpl *template.Template) ([]byte, error) { var writer bytes.Buffer data := struct { - Kevt *kevent.Kevent + Kevt *Kevent SerializeHandles bool SerializeThreads bool SerializeImages bool @@ -130,15 +144,15 @@ func (e *evtlog) renderTemplate(kevt *kevent.Kevent) ([]byte, error) { SerializePE bool }{ kevt, - kevent.SerializeHandles, - kevent.SerializeThreads, - kevent.SerializeImages, - kevent.SerializeEnvs, - kevent.SerializePE, + SerializeHandles, + SerializeThreads, + SerializeImages, + SerializeEnvs, + SerializePE, } - err := e.tmpl.Execute(&writer, data) + err := tmpl.Execute(&writer, data) if err != nil { - return nil, fmt.Errorf("unable to render eventlog template: %v", err) + return nil, fmt.Errorf("unable to render event template: %v", err) } return writer.Bytes(), nil } diff --git a/pkg/kstream/interceptors/ps_windows.go b/pkg/kstream/interceptors/ps_windows.go index c3eab695a..df2a655be 100644 --- a/pkg/kstream/interceptors/ps_windows.go +++ b/pkg/kstream/interceptors/ps_windows.go @@ -20,6 +20,7 @@ package interceptors import ( "expvar" + "github.com/rabbitstack/fibratus/pkg/util/cmdline" "os" "path/filepath" "regexp" @@ -78,34 +79,35 @@ func (ps psInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, er case ktypes.CreateProcess, ktypes.TerminateProcess, ktypes.EnumProcess: - cmdline, err := kevt.Kparams.GetString(kparams.Comm) + cmndline, err := kevt.Kparams.GetString(kparams.Comm) if err != nil { return kevt, true, err } - // if leading/trailing quotes are found, get rid of them - if len(cmdline) > 0 && cmdline[0] == '"' && cmdline[len(cmdline)-1] == '"' { - cmdline = cmdline[1 : len(cmdline)-1] + // if leading/trailing quotes are found in the executable path, get rid of them + args := cmdline.Split(cmndline) + if len(args) > 0 { + cmndline = cmdline.CleanExe(args) } // expand all variations of the SystemRoot env variable - if systemRootRegexp.MatchString(cmdline) { - cmdline = systemRootRegexp.ReplaceAllString(cmdline, os.Getenv("SystemRoot")) + if systemRootRegexp.MatchString(cmndline) { + cmndline = systemRootRegexp.ReplaceAllString(cmndline, os.Getenv("SystemRoot")) } // some system processes are reported without the path in the command line, // but we can expand the path from the SystemRoot environment variable - if !driveRegexp.MatchString(cmdline) { + if !driveRegexp.MatchString(cmndline) { proc, _ := kevt.Kparams.GetString(kparams.ProcessName) _, ok := sysProcs[proc] if ok { - cmdline = filepath.Join(os.Getenv("SystemRoot"), "System32", cmdline) + cmndline = filepath.Join(os.Getenv("SystemRoot"), "System32", cmndline) } } // append executable path parameter - i := strings.Index(strings.ToLower(cmdline), ".exe") + i := strings.Index(strings.ToLower(cmndline), ".exe") if i > 0 { - exe := cmdline[0 : i+4] + exe := cmndline[0 : i+4] kevt.Kparams.Append(kparams.Exe, kparams.UnicodeString, exe) } - _ = kevt.Kparams.SetValue(kparams.Comm, cmdline) + _ = kevt.Kparams.SetValue(kparams.Comm, cmndline) // convert hexadecimal PID values to integers pid, err := kevt.Kparams.GetHexAsUint32(kparams.ProcessID) diff --git a/pkg/kstream/kstreamc_windows.go b/pkg/kstream/kstreamc_windows.go index 0d0e2c577..64ad420a2 100644 --- a/pkg/kstream/kstreamc_windows.go +++ b/pkg/kstream/kstreamc_windows.go @@ -403,16 +403,16 @@ func (k *kstreamConsumer) processKevent(evt *etw.EventRecord) error { kevt.Release() return nil } + // increment sequence + if !kevt.Type.Dropped(false) { + k.sequencer.Increment() + } // run rules. In case of rule groups with sequence policy // the last event matching the group is forwarded to the // outputs if rulesFired := k.rules.Fire(kevt); !rulesFired { return nil } - // increment sequence - if !kevt.Type.Dropped(false) { - k.sequencer.Increment() - } if k.eventCallback != nil { return k.eventCallback(kevt) } diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go index ae563d707..5d801beea 100644 --- a/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go @@ -4458,12 +4458,11 @@ ReadMethod reads method from frame's payload Method frames carry the high-level protocol commands (which we call "methods"). One method frame carries one command. The method frame payload has this format: - 0 2 4 - +----------+-----------+-------------- - - - | class-id | method-id | arguments... - +----------+-----------+-------------- - - - short short ... - + 0 2 4 + +----------+-----------+-------------- - - + | class-id | method-id | arguments... + +----------+-----------+-------------- - - + short short ... */ func ReadMethod(reader io.Reader, protoVersion string) (Method, error) { classID, err := ReadShort(reader) diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go index 22e8b2ddd..a81962f64 100644 --- a/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go @@ -36,11 +36,11 @@ ReadFrame reads and parses raw data from conn reader and returns amqp frame All frames consist of a header (7 octets), a payload of arbitrary size, and a 'frame-end' octet that detects malformed frames: - 0 1 3 7 size+7 size+8 - +------+---------+-------------+ +------------+ +-----------+ - | type | channel | size | | payload | | frame-end | - +------+---------+-------------+ +------------+ +-----------+ - octet short long size octets octet + 0 1 3 7 size+7 size+8 + +------+---------+-------------+ +------------+ +-----------+ + | type | channel | size | | payload | | frame-end | + +------+---------+-------------+ +------------+ +-----------+ + octet short long size octets octet To read a frame, we: 1. Read the header and check the frame type and channel. @@ -807,11 +807,11 @@ follows it with a content header and zero or more content body frames. A content header frame has this format: - 0 2 4 12 14 - +----------+--------+-----------+----------------+------------- - - - | class-id | weight | body size | property flags | property list... - +----------+--------+-----------+----------------+------------- - - - short short long long short remainder... + 0 2 4 12 14 + +----------+--------+-----------+----------------+------------- - - + | class-id | weight | body size | property flags | property list... + +----------+--------+-----------+----------------+------------- - - + short short long long short remainder... */ func ReadContentHeader(r io.Reader, protoVersion string) (*ContentHeader, error) { var err error diff --git a/pkg/outputs/elasticsearch/elasticsearch_test.go b/pkg/outputs/elasticsearch/elasticsearch_test.go index c3105b501..935b6e4c7 100644 --- a/pkg/outputs/elasticsearch/elasticsearch_test.go +++ b/pkg/outputs/elasticsearch/elasticsearch_test.go @@ -21,7 +21,7 @@ package elasticsearch import ( "bytes" "encoding/json" - "io/ioutil" + "io" "net/http" "net/http/httptest" "strings" @@ -88,7 +88,7 @@ func TestElasticsearchConnectUnsupportedVersion(t *testing.T) { func TestElasticsearchPublish(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "_bulk") { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/outputs/eventlog/config.go b/pkg/outputs/eventlog/config.go index b1e62b90b..418e088fb 100644 --- a/pkg/outputs/eventlog/config.go +++ b/pkg/outputs/eventlog/config.go @@ -20,6 +20,7 @@ package eventlog import ( "fmt" + "github.com/rabbitstack/fibratus/pkg/kevent" "text/template" "github.com/spf13/pflag" @@ -72,7 +73,7 @@ type Config struct { func (c Config) parseTemplate() (*template.Template, error) { if c.Template == "" { // use built-in template - return template.New("evtlog").Parse(Template) + return template.New("evtlog").Parse(kevent.Template) } return template.New("evtlog").Parse(c.Template) } diff --git a/pkg/outputs/eventlog/eventlog.go b/pkg/outputs/eventlog/eventlog.go index f60ae9ef0..6f27656b1 100644 --- a/pkg/outputs/eventlog/eventlog.go +++ b/pkg/outputs/eventlog/eventlog.go @@ -112,7 +112,7 @@ func (e *evtlog) Publish(batch *kevent.Batch) error { } func (e *evtlog) publish(kevt *kevent.Kevent) error { - buf, err := e.renderTemplate(kevt) + buf, err := kevt.RenderCustomTemplate(e.tmpl) if err != nil { return err } diff --git a/pkg/outputs/http/http.go b/pkg/outputs/http/http.go index fb4a4ae49..c4a47d473 100644 --- a/pkg/outputs/http/http.go +++ b/pkg/outputs/http/http.go @@ -23,7 +23,7 @@ import ( "compress/gzip" "context" "fmt" - "io/ioutil" + "io" "net/http" "net/url" @@ -116,7 +116,7 @@ func (h *_http) Publish(batch *kevent.Batch) error { defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return err } diff --git a/pkg/outputs/http/http_test.go b/pkg/outputs/http/http_test.go index 0328f3137..57bb7c73c 100644 --- a/pkg/outputs/http/http_test.go +++ b/pkg/outputs/http/http_test.go @@ -22,7 +22,7 @@ import ( "compress/gzip" "encoding/json" "github.com/rabbitstack/fibratus/pkg/outputs" - "io/ioutil" + "io" "log" "net" "net/http" @@ -49,7 +49,7 @@ func TestHttpPublish(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/intake", func(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -109,7 +109,7 @@ func TestHttpGzipPublish(t *testing.T) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - body, err := ioutil.ReadAll(gr) + body, err := io.ReadAll(gr) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 485838cec..858e26937 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -63,7 +63,7 @@ func TestSnapshotterWrite(t *testing.T) { assert.Equal(t, `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`, ps.Comm) assert.Equal(t, `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`, ps.Exe) assert.Equal(t, `admin\SYSTEM`, ps.SID) - assert.Len(t, ps.Args, 14) + assert.Len(t, ps.Args, 13) assert.Equal(t, "--type=crashpad-handler", ps.Args[1]) assert.Equal(t, "ps", filepath.Base(ps.Cwd)) assert.True(t, len(ps.Envs) > 0) diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index 2b4f4dd10..6a41ab440 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -20,8 +20,8 @@ package types import ( "fmt" + "github.com/rabbitstack/fibratus/pkg/util/cmdline" "path/filepath" - "strings" "sync" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" @@ -74,7 +74,7 @@ func (ps *PS) String() string { Pid: %d Ppid: %d Name: %s - Comm: %s + Cmdline: %s Exe: %s Cwd: %s SID: %s @@ -95,7 +95,18 @@ func (ps *PS) String() string { ) } -// Thread stores several metadata about a thread that's executing in process's address space. +// Ancestors returns all ancestors of this process. The string slice contains +// the process image name followed by the process id. +func (ps *PS) Ancestors() []string { + ancestors := make([]string, 0) + walk := func(proc *PS) { + ancestors = append(ancestors, fmt.Sprintf("%s (%d)", proc.Name, proc.PID)) + } + Walk(walk, ps) + return ancestors +} + +// Thread stores metadata about a thread that's executing in process's address space. type Thread struct { // Tid is the unique identifier of thread inside the process. Tid uint32 @@ -151,7 +162,7 @@ func FromKevent(pid, ppid uint32, name, comm, exe, sid string, sessionID uint8) Name: name, Comm: comm, Exe: exe, - Args: splitArgs(comm), + Args: cmdline.Split(comm), SID: sid, SessionID: sessionID, Threads: make(map[uint32]Thread), @@ -196,7 +207,7 @@ func NewPS(pid, ppid uint32, exe, cwd, comm string, thread Thread, envs map[stri Exe: exe, Comm: comm, Cwd: cwd, - Args: splitArgs(comm), + Args: cmdline.Split(comm), Threads: map[uint32]Thread{thread.Tid: thread}, Modules: make([]Module, 0), Handles: make([]htypes.Handle, 0), @@ -219,8 +230,6 @@ func NewFromKcap(buf []byte) (*PS, error) { return &ps, nil } -func splitArgs(cmdline string) []string { return strings.Fields(cmdline) } - // AddThread adds a thread to process's state descriptor. func (ps *PS) AddThread(thread Thread) { ps.mu.Lock() diff --git a/pkg/ps/types/types_windows_test.go b/pkg/ps/types/types_windows_test.go index 4c0796980..94f47b06f 100644 --- a/pkg/ps/types/types_windows_test.go +++ b/pkg/ps/types/types_windows_test.go @@ -69,6 +69,6 @@ func TestPSArgs(t *testing.T) { "", "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler \"--metrics-dir=C:\\Users\\admin\\AppData\\Local\\Spotify\\User Data\" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify", Thread{}, nil) - require.Len(t, ps.Args, 12) + require.Len(t, ps.Args, 11) require.Equal(t, "/prefetch:7", ps.Args[2]) } diff --git a/pkg/syscall/security/privileges.go b/pkg/syscall/security/privileges.go index a249222f5..f5ff06aef 100644 --- a/pkg/syscall/security/privileges.go +++ b/pkg/syscall/security/privileges.go @@ -73,7 +73,7 @@ func lookupPrivilegeValue(systemName string, name string, luid *int64) (err erro } func lookupPrivilegeValueW(systemName *uint16, name *uint16, luid *int64) (err error) { - r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) + r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) if r1 == 0 { if e1 != 0 { err = error(e1) @@ -91,7 +91,7 @@ func adjustTokenPrivileges(token syscall.Token, releaseAll bool, input *byte, ou } else { _p0 = 0 } - r0, _, e1 := syscall.Syscall6(procAdjustTokenPrivileges.Addr(), 6, uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) + r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) success = r0 != 0 if true { if e1 != 0 { diff --git a/pkg/util/cmdline/cmdline.go b/pkg/util/cmdline/cmdline.go new file mode 100644 index 000000000..b4000012a --- /dev/null +++ b/pkg/util/cmdline/cmdline.go @@ -0,0 +1,42 @@ +/* + * Copyright 2022-2023 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmdline + +import ( + "regexp" + "strings" +) + +// splitRegexp declares the regular expression for splitting the string +// by white spaces if the string is not inside a double quote. +var splitRegexp = regexp.MustCompile(`("[^"]+?"\S*|\S+)`) + +// Split returns a slice of strings where each element is +// a single argument in the process command line. +func Split(cmdline string) []string { return splitRegexp.FindAllString(cmdline, -1) } + +// CleanExe removes the quotes from the executable path and rejoins +// the rest of the command line arguments. +func CleanExe(args []string) string { + exe := args[0] + if exe[0] == '"' && exe[len(exe)-1] == '"' { + return strings.Join(append([]string{exe[1 : len(exe)-1]}, args[1:]...), " ") + } + return strings.Join(args, " ") +} diff --git a/pkg/util/cmdline/cmdline_test.go b/pkg/util/cmdline/cmdline_test.go new file mode 100644 index 000000000..a74e4ead4 --- /dev/null +++ b/pkg/util/cmdline/cmdline_test.go @@ -0,0 +1,33 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmdline + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSplit(t *testing.T) { + cmdline := `svchost.exe "-k host" arg1 arg2` + args := Split(cmdline) + require.Len(t, args, 4) + assert.Equal(t, "svchost.exe", args[0]) + assert.Equal(t, `"-k host"`, args[1]) +} diff --git a/pkg/util/fasttemplate/template.go b/pkg/util/fasttemplate/template.go index f02024db5..e215c84be 100644 --- a/pkg/util/fasttemplate/template.go +++ b/pkg/util/fasttemplate/template.go @@ -151,9 +151,9 @@ func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { // values from the map m and writes the result to the given writer w. // // Substitution map m may contain values with the following types: -// * []byte - the fastest value type -// * string - convenient value type -// * TagFunc - flexible value type +// - []byte - the fastest value type +// - string - convenient value type +// - TagFunc - flexible value type // // Returns the number of bytes written to w. func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { @@ -182,9 +182,9 @@ func (t *Template) ExecuteFuncString(f TagFunc) []byte { // values from the map m and returns the result. // // Substitution map m may contain values with the following types: -// * []byte - the fastest value type -// * string - convenient value type -// * TagFunc - flexible value type +// - []byte - the fastest value type +// - string - convenient value type +// - TagFunc - flexible value type // // This function is optimized for frozen templates. // Use ExecuteString for constantly changing templates. diff --git a/pkg/util/log/logger.go b/pkg/util/log/logger.go index e34bb3d84..faefc6cd2 100644 --- a/pkg/util/log/logger.go +++ b/pkg/util/log/logger.go @@ -25,7 +25,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/log/rotate" fs "github.com/rifflock/lfshook" "github.com/sirupsen/logrus" - "io/ioutil" + "io" "os" "path/filepath" ) @@ -82,7 +82,7 @@ func InitFromConfig(c Config) error { // disable writing to stdout if !c.LogStdout { - logrus.SetOutput(ioutil.Discard) + logrus.SetOutput(io.Discard) } // initialize log rotate hook diff --git a/pkg/util/multierror/multierror.go b/pkg/util/multierror/multierror.go index 308cf10b7..f53565a2b 100644 --- a/pkg/util/multierror/multierror.go +++ b/pkg/util/multierror/multierror.go @@ -18,6 +18,8 @@ import ( "strings" ) +var sep = ", " + // Wrap takes a slice of errors and returns a single error that encapsulates // those underlying errors. If the slice is nil or empty it returns nil. // If the slice only contains a single element, that error is returned directly. @@ -27,6 +29,13 @@ func Wrap(errs ...error) error { return multiError(errs).flatten() } +// WrapWithSeparator same as Wrap but uses a custom separator when joining +// error messages. +func WrapWithSeparator(s string, errs ...error) error { + sep = s + return multiError(errs).flatten() +} + // multiError bundles several errors together into a single error. type multiError []error @@ -53,5 +62,5 @@ func (errors multiError) Error() string { } parts = append(parts, err.Error()) } - return strings.Join(parts, ", ") + return strings.Join(parts, sep) } diff --git a/pkg/util/rest/rest.go b/pkg/util/rest/rest.go index c712326c0..507d35478 100644 --- a/pkg/util/rest/rest.go +++ b/pkg/util/rest/rest.go @@ -22,7 +22,7 @@ import ( "context" "errors" "github.com/rabbitstack/fibratus/pkg/api" - "io/ioutil" + "io" "net" "net/http" "path" @@ -103,10 +103,7 @@ func request(method string, options ...Option) ([]byte, error) { } scheme := "http://" - addr := opts.addr - if strings.HasPrefix(addr, `npipe:///`) { - addr = strings.TrimPrefix(addr, `npipe:///`) - } + addr := strings.TrimPrefix(opts.addr, `npipe:///`) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -121,7 +118,7 @@ func request(method string, options ...Option) ([]byte, error) { } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/pkg/util/tls/tls.go b/pkg/util/tls/tls.go index fb878a30c..3ec823842 100644 --- a/pkg/util/tls/tls.go +++ b/pkg/util/tls/tls.go @@ -22,7 +22,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" + "os" ) // MakeConfig builds a TLS config from the certificate, private/public key and the CA cert files. @@ -48,7 +48,7 @@ func MakeConfig(certFile, keyFile, caFile string, insecureSkipVerify bool) (*tls // load certificate issuing authority if caFile != "" { cpool := x509.NewCertPool() - caCert, err := ioutil.ReadFile(caFile) + caCert, err := os.ReadFile(caFile) if err != nil { return nil, err } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index dfa533d66..d1e2e4cb1 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -46,6 +46,9 @@ var version string // Set initializes the version string as global variable. func Set(v string) { version = v } +// Get returns the version string. +func Get() string { return version } + // ProductToken returns a tag to be poked in User Agent headers. func ProductToken() string { return fmt.Sprintf("fibratus/%s", version) } diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go index 55493408c..c8788d023 100644 --- a/pkg/yara/scanner_test.go +++ b/pkg/yara/scanner_test.go @@ -22,13 +22,13 @@ package yara import ( + "github.com/hillu/go-yara/v4" "os" "path/filepath" "syscall" "testing" "time" - "github.com/hillu/go-yara/v4" "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" @@ -55,6 +55,10 @@ func (s *mockSender) Send(a alertsender.Alert) error { return nil } +func (s *mockSender) Type() alertsender.Type { + return alertsender.Noop +} + func makeSender(config alertsender.Config) (alertsender.Sender, error) { return &mockSender{}, nil } diff --git a/rules/README.md b/rules/README.md new file mode 100644 index 000000000..feaace27b --- /dev/null +++ b/rules/README.md @@ -0,0 +1,40 @@ +# Detection Rules + +This directory contains a catalog of detection rules modelled around the prominent [MITRE ATT&CK](https://attack.mitre.org/) framework. The goal is to provide a direct mapping of tactics, techniques, and subtechniques for each rule. The following sections introduce the general structure, design guidelines, and best practices to keep in mind when creating new rules. + +## Structure + +Detection rules are organized as `yaml` files whose names adhere to the `tactic-name_technique-name.yml` nomenclature. Every `yaml` file may contain a number of different rules grouped under a specific [rule policy](https://www.fibratus.io/#/filters/rules). Each rule in the group represents a particular adversary subtechnique. If the technique doesn't break down into individual subtechniques, it is expressed as a sole rule in the group. Let's suppose we want to detect unusual accesses to Windows Credentials history which is backed by the [Windows Credentials Manager](https://attack.mitre.org/techniques/T1555/004/) MITRE subtechnique. Since the subtechnique pertains to `Credentials from Password Stores` technique living under the `Credential Access` tactic, we would have created the `credential_access_credentials_from_password_stores.yml` file to store the rule definitions. + +Next, we declare the `Credentials access from Windows Credential Manager` group using the [rule specification](https://www.fibratus.io/#/filters/rules?id=defining-rules) idiom, and define the rule that fires when an unusual process accesses a file matching the `?:\\Users\\*\\AppData\\*\\Microsoft\\Protect` wildcard expression. Similarly, we can append additional rules to supervise Windows Credential Manager or Windows Vault file accesses. All of the above rules can be expressed with the `include` rule policy and the `CreateFile` selector. Check [here](credential_access_credentials_from_password_stores.yml) for the end result. + +## Guidelines + +### Stick to naming nomenclature + +It is highly recommended to name the rule files after the pattern explained in the above section. This facilitates the organization and searching through the detection rules catalog. + +### Include descriptions and labels + +Rule groups should have a meaningful description. Individual rules inside the group should ideally have a description too. +For example, the `Spearphishing attachment execution of files written by Microsoft Office processes` group has the following description that has been borrowed verbatim from the MITRE knowledge base. + +> Adversaries may send spearphishing emails with a malicious attachment in an +attempt to gain access to victim systems. Spearphishing attachment is a specific +variant of spearphishing. Spearphishing attachment is different from other forms +of spearphishing in that it employs the use of malware attached to an email. + +Additionally, there should exist labels attached to every rule group describing the MITRE tactic, technique, and subtechnique. This information is used when rendering email rule alert templates. + +### Sequence policies should have the event type condition + +Sequence rule groups may have multiple rules each targeting different event types. If the rule definition lacks the event type condition, the rule engine needs to execute every single rule for the incoming event. To alleviate the pressure on the rule engine, all rules should +have the event type condition. In fact, if a rule is declared without the event type condition, you'll get a warning message in Fibratus logs informing about unwanted side effects. + +### Sequence policies with early binding index condition + +When writing detections that employ various event types or event multiple data sources, relationships between events are connected via [binding patterns](https://www.fibratus.io/#/filters/rules?id=stateful-event-tracking). The rule engine can lazily evaluate binary expressions comprising a rule. If the binding patterns are the first condition in downstream sequence rule groups, the rule engine will not keep on evaluating every single binary expression in the rule and thus will benefit the overall runtime performance. + +### Prefer macros over raw conditions + +Fibratus comes with a macro library to promote the reusability and modularization of rule conditions and lists. Before trying to spell out a raw rule condition, explore the library to check if there's already a macro you can pull into the rule. If there's no such macro, you can consider creating it. Future detection engineers and rule writers could profit from those macros. \ No newline at end of file diff --git a/rules/credential_access_credentials_from_password_stores.yml b/rules/credential_access_credentials_from_password_stores.yml new file mode 100644 index 000000000..ec87331d3 --- /dev/null +++ b/rules/credential_access_credentials_from_password_stores.yml @@ -0,0 +1,151 @@ +- group: Credentials access from Windows Credential Manager + description: | + Adversaries may acquire credentials from the Windows Credential Manager. + The Credential Manager stores credentials for signing into websites, + applications, and/or devices that request authentication through NTLM + or Kerberos in Credential Lockers. + labels: + tactic.id: TA0006 + tactic.name: Credential Access + tactic.ref: https://attack.mitre.org/tactics/TA0006/ + technique.id: T1555 + technique.name: Credentials from Password Stores + technique.ref: https://attack.mitre.org/techniques/T1555/ + subtechnique.id: T1555.004 + subtechnique.name: Windows Credential Manager + subtechnique.ref: https://attack.mitre.org/techniques/T1555/004/ + selector: + type: CreateFile + rules: + - name: Unusual process accessing Windows Credential history + description: | + Detects unusual accesses to the Windows Credential history file. + The CREDHIST file contains all previous password-linked master key hashes used by + DPAPI to protect secrets on the device. Adversaries may obtain credentials + from the Windows Credentials Manager. + condition: > + file.operation = 'open' + and + file.name imatches '?:\\Users\\*\\AppData\\*\\Microsoft\\Protect' + and + not + ps.exe imatches + ( + '?:\\Program Files\\*', + '?:\\Windows\\System32\\lsass.exe', + '?:\\Windows\\System32\\svchost.exe', + '?:\\Windows\\ccmcache\\*.exe' + ) + action: > + {{ + emit . "Unusual access to Windows Credential history files" "" + }} + - name: Suspicious access to Windows Credential Manager files + description: | + Identifies suspicious processes trying to acquire credentials from the Windows Credential Manager. + condition: > + file.operation = 'open' + and + file.name imatches + ( + '?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*', + '?:\\Windows\\System32\\config\\systemprofile\\AppData\\*\\Microsoft\\Credentials\\*' + ) + and + not + ps.exe imatches + ( + '?:\\Program Files\\*', + '?:\\Program Files(x86)\\*' + ) + action: > + {{ + emit . "Suspicious access to Windows Credential Manager files" "" + }} + - name: Suspicious access to Windows Vault files + description: | + Identifies attempts from adversaries to acquire credentials from Vault files. + condition: > + file.operation = 'open' + and + file.name imatches + ( + '?:\\Users\\*\\AppData\\*\\Microsoft\\Vault\\*\\*', + '?:\\ProgramData\\Microsoft\\Vault\\*' + ) + and + file.extension in + ( + '.vcrd', + '.vpol' + ) + and + not + ps.exe imatches + ( + '?:\\Program Files\\*', + '?:\\Program Files(x86)\\*', + '?:\\Windows\\System32\\lsass.exe', + '?:\\Windows\\System32\\svchost.exe' + ) + action: > + {{ + emit . "Suspicious access to Windows Vault files" "" + }} + +- group: Credentials discovery from Windows Credential Manager through built-in tools + description: | + Adversaries may acquire credentials from the Windows Credential Manager + by employing external tools such as VaultCmd that can be used to enumerate + credentials stored in the Credential Locker through a command-line interface. + The Credential Manager stores credentials for signing into websites, + applications, and/or devices that request authentication through NTLM + or Kerberos in Credential Lockers. dversaries may also obtain credentials from credential backups. + Credential backups and restorations may be performed by running rundll32.exe builtin binary. + labels: + tactic.id: TA0006 + tactic.name: Credential Access + tactic.ref: https://attack.mitre.org/tactics/TA0006/ + technique.id: T1555 + technique.name: Credentials from Password Stores + technique.ref: https://attack.mitre.org/techniques/T1555/ + subtechnique.id: T1555.004 + subtechnique.name: Windows Credential Manager + subtechnique.ref: https://attack.mitre.org/techniques/T1555/004/ + selector: + type: CreateProcess + rules: + - name: Enumerate credentials from Windows Credentials Manager via VaultCmd.exe + description: | + Detects the usage of the VaultCmd tool to list Windows Credentials. + VaultCmd creates, displays and deletes stored credentials. + condition: > + ps.sibling.name ~= 'VaultCmd.exe' + and + ps.sibling.args + in + ( + '"/listcreds:Windows Credentials"', + '"/listcreds:Web Credentials"' + ) + action: > + {{ + emit + . + "Credential discovery via VaultCmd.exe" + "`%ps.exe` executed the `VaultCmd` tool to enumerate Windows Credentials" + }} + - name: Credentials access from credential backups + description: | + Detects an attempt to obtain credentials from credential backups. + condition: > + ps.sibling.name ~= 'rundll32.exe' + and + (ps.sibling.args iin ('keymgr.dll') and ps.sibling.args iin ('KRShowKeyMgr')) + action: > + {{ + emit + . + "Credential access from credential backups" + "`%ps.exe` executed the `rundll32.exe` binary to obtain credentials from credentials backups" + }} diff --git a/rules/credential_access_os_credential_dumping.yml b/rules/credential_access_os_credential_dumping.yml new file mode 100644 index 000000000..aa6ba2da8 --- /dev/null +++ b/rules/credential_access_os_credential_dumping.yml @@ -0,0 +1,127 @@ +- group: Access to Security Account Manager database file + description: | + Adversaries may attempt to extract credential material from + the Security Account Manager (SAM) database. The SAM is a database + file that contains local accounts for the host. + labels: + tactic.id: TA0006 + tactic.name: Credential Access + tactic.ref: https://attack.mitre.org/tactics/TA0006/ + technique.id: T1003 + technique.name: OS Credential Dumping + technique.ref: https://attack.mitre.org/techniques/T1003/ + subtechnique.id: T1003.002 + subtechnique.name: Security Account Manager + subtechnique.ref: https://attack.mitre.org/techniques/T1003/002/ + selector: + type: CreateFile + rules: + - name: File access to SAM database + condition: > + file.name imatches + ( + '?:\\WINDOWS\\SYSTEM32\\CONFIG\\SAM', + '\\Device\\HarddiskVolumeShadowCopy*\\WINDOWS\\SYSTEM32\\CONFIG\\SAM', + '\\??\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy*\\WINDOWS\\SYSTEM32\\CONFIG\\SAM' + ) + and + not + ps.exe imatches + ( + '?:\\Program Files\\*', + '?:\\Program Files (x86)\\*', + '?:\\Windows\\System32\\lsass.exe' + ) + action: > + {{ + emit . "File access to SAM database" "" + }} + +- group: Access to Security Account Manager database through registry + description: | + Adversaries may attempt to extract credential material from the + Security Account Manager (SAM) database through registry. + labels: + tactic.id: TA0006 + tactic.name: Credential Access + tactic.ref: https://attack.mitre.org/tactics/TA0006/ + technique.id: T1003 + technique.name: OS Credential Dumping + technique.ref: https://attack.mitre.org/techniques/T1003/ + subtechnique.id: T1003.002 + subtechnique.name: Security Account Manager + subtechnique.ref: https://attack.mitre.org/techniques/T1003/002/ + selector: + category: registry + rules: + - name: Potential SAM database dump through registry + condition: > + (query_registry or open_registry) + and + registry.key.name imatches + ( + 'HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account\\*', + 'HKEY_LOCAL_MACHINE\\SAM\\*', + 'HKEY_LOCAL_MACHINE\\SAM' + ) + and + not + ps.exe imatches + ( + '?:\\Windows\\System32\\lsass.exe' + ) + action: > + {{ + emit . "Potential SAM database dump through registry" "" + }} + +- group: LSASS memory dumping via legitimate or offensive tools + description: | + Adversaries may attempt to access credential material stored in the + process memory of the Local Security Authority Subsystem Service (LSASS). + After a user logs on, the system generates and stores a variety of credential + materials in LSASS process memory. These credential materials can be harvested + by an administrative user or SYSTEM and used to conduct Lateral Movement. + This rule detects attempts to dump the LSAAS memory to the disk by employing legitimate + tools such as procdump, Task Manager, Process Explorer or built-in Windows tools such + as comsvcs.dll. + labels: + tactic.id: TA0006 + tactic.name: Credential Access + tactic.ref: https://attack.mitre.org/tactics/TA0006/ + technique.id: T1003 + technique.name: OS Credential Dumping + technique.ref: https://attack.mitre.org/techniques/T1003/ + subtechnique.id: T1003.001 + subtechnique.name: LSASS Memory + subtechnique.ref: https://attack.mitre.org/techniques/T1003/001/ + policy: sequence + rules: + - name: LSASS local process object acquired + condition: > + open_process and ps.access.mask.names in ('ALL_ACCESS', 'CREATE_PROCESS') + and + ps.sibling.name ~= 'lsass.exe' + and + not + ps.exe imatches + ( + '?:\\Windows\\System32\\svchost.exe', + '?:\\ProgramData\\Microsoft\\Windows Defender\\*\\MsMpEng.exe' + ) + - name: LSASS dump written to the file system + condition: > + ps.exe = $1.ps.exe + and + write_minidump_file + max-span: 2m + action: > + {{ + emit + . + "LSASS memory dumping" + `Detected an attempt by %1.ps.name process to access + and read the memory of the **Local Security And Authority Subsystem Service** + and subsequently write the %2.file.name dump file to the disk device` + "critical" + }} diff --git a/rules/defense_evasion_system_binary_proxy_execution.yml b/rules/defense_evasion_system_binary_proxy_execution.yml new file mode 100644 index 000000000..4a91dc64a --- /dev/null +++ b/rules/defense_evasion_system_binary_proxy_execution.yml @@ -0,0 +1,86 @@ +- group: System Binary Proxy Execution via Rundll32 + description: | + Adversaries may abuse rundll32.exe to proxy execution of malicious code. + Using rundll32.exe, vice executing directly (i.e. Shared Modules), + may avoid triggering security tools that may not monitor execution of the + rundll32.exe process because of allowlists or false positives from normal operations. + Rundll32.exe is commonly associated with executing DLL payloads. + labels: + tactic.id: TA0005 + tactic.name: Defense Evasion + tactic.ref: https://attack.mitre.org/tactics/TA0005/ + technique.id: T1218 + technique.name: System Binary Proxy Execution + technique.ref: https://attack.mitre.org/techniques/T1218/ + subtechnique.id: T1218.011 + subtechnique.name: Rundll32 + subtechnique.ref: https://attack.mitre.org/techniques/T1218/011/ + policy: sequence + rules: + - name: Rundll32 process executed with suspicious command line + condition: > + spawn_process + and + ps.sibling.name ~= 'rundll32.exe' + and + ps.sibling.comm imatches + ( + '*javascript:*', + '*shell32.dll*ShellExec_RunDLL*', + '*-sta*' + ) + - name: Rundll32 child process executed + condition: > + ps.pid = $1.ps.sibling.pid + and + spawn_process + max-span: 45s + action: > + {{ + emit . "System Binary Proxy Execution via Rundll32" "" + }} + +- group: System Binary Proxy Execution via Regsvr32 + description: | + Adversaries may abuse Regsvr32.exe to proxy execution of malicious code. + Regsvr32.exe is a command-line program used to register and unregister object + linking and embedding controls, including dynamic link libraries (DLLs), on Windows systems. + labels: + tactic.id: TA0005 + tactic.name: Defense Evasion + tactic.ref: https://attack.mitre.org/tactics/TA0005/ + technique.id: T1218 + technique.name: System Binary Proxy Execution + technique.ref: https://attack.mitre.org/techniques/T1218/ + subtechnique.id: T1218.010 + subtechnique.name: Regsvr32 + subtechnique.ref: https://attack.mitre.org/techniques/T1218/010/ + selector: + type: CreateProcess + rules: + - name: Regsvr32 scriptlet execution + description: | + Identifies the exection of a scriptlet file by regsvr32.exe process. Regsvr32 + is usually abused by adversaries to execute malicious payloads without triggering + AV product alerts. + condition: > + ps.sibling.name ~= 'regsvr32.exe' + and + ( + ps.sibling.comm imatches + ( + '*scrobj*' + ) + and + ps.sibling.comm imatches + ( + '*/i:*', + '*-i:*', + '*.sct*' + ) + ) + action: > + {{ + emit . "Regsvr32 scriptlet execution" "" + }} + diff --git a/rules/initial_access_phishing.yml b/rules/initial_access_phishing.yml new file mode 100644 index 000000000..87b30c8f2 --- /dev/null +++ b/rules/initial_access_phishing.yml @@ -0,0 +1,85 @@ +- group: Spearphishing attachment execution of files written by Microsoft Office processes + description: | + Adversaries may send spearphishing emails with a malicious attachment in an + attempt to gain access to victim systems. Spearphishing attachment is a specific + variant of spearphishing. Spearphishing attachment is different from other forms + of spearphishing in that it employs the use of malware attached to an email. + labels: + tactic.id: TA0001 + tactic.name: Initial Access + tactic.ref: https://attack.mitre.org/tactics/TA0001/ + technique.id: T1566 + technique.name: Phishing + technique.ref: https://attack.mitre.org/techniques/T1566/ + subtechnique.id: T1566.001 + subtechnique.name: Spearphishing Attachment + subtechnique.ref: https://attack.mitre.org/techniques/T1566/001/ + policy: sequence + rules: + - name: Binary file written by Microsoft Office process + condition: > + write_file + and + file.extension iin + ( + '.exe', + '.com', + '.scr' + ) + and + ps.name iin msoffice_binaries + - name: Binary executed by Microsoft Office process + condition: > + ps.sibling.exe = $1.file.name + and + spawn_process + and + ps.name iin msoffice_binaries + max-span: 1h + action: > + {{ + emit . "File execution via Microsoft Office process" "" + }} + +- group: Spearphishing DLL attachment loaded by Microsoft Office processes + description: | + Adversaries may send spearphishing emails with a malicious attachment in an + attempt to gain access to victim systems. Identifes the creation of the DLL + which is loaded by Microsoft Office process afterwards. May indicate loading of + a possibly malicious module. + labels: + tactic.id: TA0001 + tactic.name: Initial Access + tactic.ref: https://attack.mitre.org/tactics/TA0001/ + technique.id: T1566 + technique.name: Phishing + technique.ref: https://attack.mitre.org/techniques/T1566/ + subtechnique.id: T1566.001 + subtechnique.name: Spearphishing Attachment + subtechnique.ref: https://attack.mitre.org/techniques/T1566/001/ + policy: sequence + rules: + - name: Module file written by Microsoft Office process + condition: > + write_file + and + file.extension iin + ( + '.dll', + '.ocx', + '.cpl' + ) + and + ps.name iin msoffice_binaries + - name: Module loaded by Microsoft Office process + condition: > + image.name = $1.file.name + and + load_module + and + ps.name iin msoffice_binaries + max-span: 1h + action: > + {{ + emit . "Potentially malicious module loaded by Microsoft Office process" "" + }} diff --git a/rules/macros/macros.yml b/rules/macros/macros.yml new file mode 100644 index 000000000..933ddfc50 --- /dev/null +++ b/rules/macros/macros.yml @@ -0,0 +1,51 @@ +- macro: spawn_process + expr: kevt.name = 'CreateProcess' + +- macro: open_process + expr: kevt.name = 'OpenProcess' and ps.access.status = 'success' + description: Acquires the local process object + +- macro: open_process_all_access + expr: open_process and ps.access.mask.names in ('ALL_ACCESS') + description: Acquires the local process object with all possible rights for the process object + +- macro: spawn_msoffice_process + expr: spawn_process and ps.sibling.exe iin msoffice_binaries + description: Identifies the execution of the MS Office process + +- macro: write_file + expr: kevt.name = 'WriteFile' + +- macro: open_file + expr: kevt.name = 'CreateFile' and file.operation = 'open' + +- macro: create_file + expr: kevt.name = 'CreateFile' and file.operation = 'create' + +- macro: query_registry + expr: kevt.name in ('RegQueryKey', 'RegQueryValue') + +- macro: open_registry + expr: kevt.name = 'RegOpenKey' + +- macro: load_module + expr: kevt.name = 'LoadImage' + +- macro: write_minidump_file + expr: > + write_file + and + ( + file.extension iin + ( + '.dmp', + '.mdmp', + '.dump' + ) + or + is_minidump(file.name) + ) + description: Detects when a process writes a minidump file + +- macro: msoffice_binaries + list: [EXCEL.EXE, WINWORD.EXE, MSACCESS.EXE, POWERPNT.EXE, WORDPAD.EXE]