Skip to content

Commit

Permalink
Initial commit, version 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
qjerome committed Feb 1, 2018
1 parent 2e6c9f8 commit 3f5e481
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 1 deletion.
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,51 @@
# whids
# WHIDS

Very flexible Host IDS designed for Windows. We are making
use of a previously developped rule engine [Gene](https://github.com/0xrawsec/gene)
designed to match Windows events according to custom rules. The
rules are simple to write and easy to understand so that everyone can
understand why a rule has triggered.

With the democratisation of Sysmon, this tools is perfect to quickly build
hunting rules or rules to monitoring things of interest happening on your
machine(s). With WHIDS you don't have to bother with an over
complicated Sysmon configuration which often turns to the nightmare when you want
to be very specific.The simplest thing is just to enable all the logging
capabilites of Sysmon and let WHIDS do his job, grab a coffee and wait
for the juicy stuff to happen. The tool has a very low overhead for the system,
according to our current benchmarks.

This tool can be used on any Windows machine so you might install it easer on
regular workstations or on Windows Event Collectors where you are receiving
all the logs of your infrastructure. The output format is nothing else than
JSON so it is very easy to handle the alerts generated by the HIDS in whatever
tool you want to use for this purpose like ELK, Splunk or simply your favourite
SIEM.

# Example

Here is an example of a rule designed to catch suspicious access to *lsass.exe*
as it is done by the well known Mimikatz credential dump tool. You can find a
bunch of other rules on our [repository](https://github.com/0xrawsec/gene-rules).

```json
{
"Name": "MaliciousLsassAccess",
"Tags": ["Mimikatz", "Credentials", "Lsass"],
"Meta": {
"EventIDs": [10],
"Channels": ["Microsoft-Windows-Sysmon/Operational"],
"Computers": [],
"Traces": [],
"Criticality": 10,
"Author": "0xrawsec"
},
"Matches": [
"$ct: CallTrace ~= 'UNKNOWN'",
"$lsass: TargetImage ~= '(?i:\\\\lsass\\.exe$)'"
],
"Condition": "$lsass and $ct"
}
```

![WHIDS Mimikatz Demo](https://rawsec.lu/img/gifs/whids.gif)
32 changes: 32 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
RELEASE=release
MAIN_BASEN_SRC=whids
VERSION=v1.0

# Strips symbols and dwarf to make binary smaller
OPTS=-ldflags "-s -w"
ifdef DEBUG
OPTS=
endif

all:
$(MAKE) clean
$(MAKE) init
$(MAKE) compile

init:
mkdir -p $(RELEASE)
mkdir -p $(RELEASE)/windows

install:
go install $(OPTS) $(MAIN_BASEN_SRC).go

compile:windows

windows:
GOARCH=386 GOOS=windows go build $(OPTS) -o $(RELEASE)/windows/$(MAIN_BASEN_SRC)-386.exe $(MAIN_BASEN_SRC).go
GOARCH=amd64 GOOS=windows go build $(OPTS) -o $(RELEASE)/windows/$(MAIN_BASEN_SRC)-amd64.exe $(MAIN_BASEN_SRC).go
cd $(RELEASE)/windows; shasum -a 256 * > sha256.txt
cd $(RELEASE)/windows; tar -cvzf ../windows-$(VERSION).tar.gz *

clean:
rm -rf $(RELEASE)/*
212 changes: 212 additions & 0 deletions whids.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"time"

"github.com/0xrawsec/gene/engine"
"github.com/0xrawsec/golang-evtx/evtx"
"github.com/0xrawsec/golang-utils/args"
"github.com/0xrawsec/golang-utils/datastructs"
"github.com/0xrawsec/golang-utils/fsutil"
"github.com/0xrawsec/golang-utils/fsutil/fswalker"
"github.com/0xrawsec/golang-utils/log"
"github.com/0xrawsec/golang-win32/win32/wevtapi"
)

/////////////////////////////////// Main ///////////////////////////////////////

// XMLEventToGoEvtxMap converts an XMLEvent as returned by wevtapi to a GoEvtxMap
// object that Gene can use
// TODO: Improve for more perf
func XMLEventToGoEvtxMap(xe *wevtapi.XMLEvent) (*evtx.GoEvtxMap, error) {
ge := make(evtx.GoEvtxMap)
bytes, err := json.Marshal(xe.ToJSONEvent())
if err != nil {
return &ge, err
}
err = json.Unmarshal(bytes, &ge)
if err != nil {
return &ge, err
}
return &ge, nil
}

/////////////////////////////////// Main ///////////////////////////////////////

const (
exitFail = 1
exitSuccess = 0
banner = `
██╗ ██╗██╗ ██╗██╗██████╗ ███████╗
██║ ██║██║ ██║██║██╔══██╗██╔════╝
██║ █╗ ██║███████║██║██║ ██║███████╗
██║███╗██║██╔══██║██║██║ ██║╚════██║
╚███╔███╔╝██║ ██║██║██████╔╝███████║
╚══╝╚══╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝
Windows Host IDS
`
version = "1.0"
copyright = "WHIDS Copyright (C) 2017 RawSec SARL (@0xrawsec)"
license = `License Apache 2.0: This program comes with ABSOLUTELY NO WARRANTY.`
)

var (
debug bool
trace bool
versionFlag bool
rulesPath string
criticalityThresh int
tags []string
names []string
tagsVar args.ListVar
namesVar args.ListVar
windowsChannels args.ListVar
timeout args.DurationVar
channelAliases = map[string]string{
"sysmon": "Microsoft-Windows-Sysmon/Operational"}
ruleExts = args.ListVar{".gen", ".gene"}
)

func printInfo(writer io.Writer) {
fmt.Fprintf(writer, "%s\nVersion: %s\nCopyright: %s\nLicense: %s\n\n", banner, version, copyright, license)

}

func main() {
flag.Var(&windowsChannels, "c", "Windows channels to monitor")
flag.Var(&timeout, "timeout", "Stop working after timeout (format: 1s, 1m, 1h, 1d ...)")
flag.BoolVar(&trace, "trace", trace, "Tells the engine to use the trace function of the rules")
flag.BoolVar(&debug, "d", debug, "Enable debugging messages")
flag.BoolVar(&versionFlag, "v", versionFlag, "Print version information and exit")
flag.StringVar(&rulesPath, "r", rulesPath, "Rule file or directory")
flag.IntVar(&criticalityThresh, "t", criticalityThresh, "Criticality treshold. Prints only if criticality above threshold")

flag.Usage = func() {
printInfo(os.Stderr)
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", filepath.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(exitSuccess)
}

flag.Parse()

// Print version information and exit
if versionFlag {
printInfo(os.Stderr)
os.Exit(exitSuccess)
}

// Enabling debug if needed
if debug {
log.InitLogger(log.LDebug)
}

// Control parameters
if rulesPath == "" {
log.LogErrorAndExit(fmt.Errorf("No rule file to load"), exitFail)
}

// Initialization
e := engine.NewEngine(trace)
setRuleExts := datastructs.NewSyncedSet()
tags = []string(tagsVar)
names = []string(namesVar)

// Validation
if len(tags) > 0 && len(names) > 0 {
log.LogErrorAndExit(fmt.Errorf("Cannot search by tags and names at the same time"), exitFail)
}
e.SetFilters(names, tags)

// Initializes the set of rule extensions
for _, e := range ruleExts {
setRuleExts.Add(e)
}

// Loading the rules
realPath, err := fsutil.ResolveLink(rulesPath)
if err != nil {
log.LogErrorAndExit(err, exitFail)
}

// Handle both rules argument as file or directory
switch {
case fsutil.IsFile(realPath):
err := e.Load(realPath)
if err != nil {
log.Error(err)
}
case fsutil.IsDir(realPath):
for wi := range fswalker.Walk(realPath) {
for _, fi := range wi.Files {
ext := filepath.Ext(fi.Name())
rulefile := filepath.Join(wi.Dirpath, fi.Name())
log.Debug(ext)
if setRuleExts.Contains(ext) {
err := e.Load(rulefile)
if err != nil {
log.Errorf("Error loading %s: %s", rulefile, err)
}
}
}
}
default:
log.LogErrorAndExit(fmt.Errorf("Cannot resolve %s to file or dir", rulesPath), exitFail)
}

log.Infof("Loaded %d rules", e.Count())

// Register a timeout if specified in Command line
signals := make(chan bool)
eventCnt, alertsCnt := 0, 0
start := time.Now()
if timeout > 0 {
go func() {
time.Sleep(time.Duration(timeout))
signals <- true
}()
}

// Registering handler for interrupt signal
osSignals := make(chan os.Signal)
signal.Notify(osSignals, os.Interrupt)
go func() {
<-osSignals
signals <- true
}()

for _, winChan := range []string(windowsChannels) {
// Try to find an alias to the channel
if c, ok := channelAliases[strings.ToLower(winChan)]; ok {
winChan = c
}
ec := wevtapi.GetAllEventsFromChannel(winChan, wevtapi.EvtSubscribeToFutureEvents, signals)
for xe := range ec {
event, err := XMLEventToGoEvtxMap(xe)
if err != nil {
log.Errorf("Failed to convert event: %s", err)
log.Debugf("Error data: %v", xe)
}
if n, crit := e.Match(event); len(n) > 0 {
if crit >= criticalityThresh {
fmt.Println(string(evtx.ToJSON(event)))
alertsCnt++
}
}
eventCnt++
}
stop := time.Now()
log.Infof("Count Event Scanned: %d", eventCnt)
log.Infof("Average Event Rate: %.2f EPS", float64(eventCnt)/(stop.Sub(start).Seconds()))
log.Infof("Alerts Reported: %d", alertsCnt)
log.Infof("Count Rules Used (loaded + generated): %d", e.Count())
}
}

0 comments on commit 3f5e481

Please sign in to comment.