diff --git a/README.md b/README.md index a1a5c17..f9e7505 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ Required system commands: - [ ] [System Prefetch Files](https://forensics.wiki/prefetch/) - [x] [System Event Logs](https://forensics.wiki/windows_event_log_%28evt%29/) - [ ] [System AmCache](https://forensics.wiki/amcache/) -- [ ] [User ShellBags](https://forensics.wiki/shell_item/) - [x] [User JumpLists](https://forensics.wiki/jump_lists/) +- [x] [User ShellBags](https://forensics.wiki/shell_item/) - [ ] [User Browser Histories](https://forensics.wiki/google_chrome/) ## License diff --git a/internal/flog/ez.go b/internal/flog/cmd.go similarity index 53% rename from internal/flog/ez.go rename to internal/flog/cmd.go index e1ff7c1..6496ae5 100644 --- a/internal/flog/ez.go +++ b/internal/flog/cmd.go @@ -2,13 +2,15 @@ package flog import ( + "fmt" + "os" "path/filepath" "github.com/cuhsat/fact/internal/fact/ez" "github.com/cuhsat/fact/internal/sys" ) -func EvtxeCmd(src, dir string) (log string, err error) { +func Evtxe(src, dir string) (log string, err error) { cmd, err := ez.Path("EvtxECmd.dll") if err != nil { @@ -27,7 +29,7 @@ func EvtxeCmd(src, dir string) (log string, err error) { return } -func JleCmd(src, dir string) (log string, err error) { +func Jle(src, dir string) (log string, err error) { cmd, err := ez.Path("JLECmd.dll") if err != nil { @@ -52,3 +54,43 @@ func JleCmd(src, dir string) (log string, err error) { return } + +func Sbe(src, dir string) (log string, err error) { + cmd, err := ez.Path("SBECmd.dll") + + if err != nil { + return + } + + if len(dir) == 0 { + dir = filepath.Dir(src) + } + + b := BaseFile(filepath.Base(src)) + + dst := "out.csv" + tmp := filepath.Join(dir, "tmp") + log = filepath.Join(dir, fmt.Sprintf("%s_%s", b, dst)) + + if err = os.MkdirAll(tmp, sys.MODE_DIR); err != nil { + return + } + + if err = Copy(tmp, src); err != nil { + return + } + + _, err = sys.StdCall("dotnet", cmd, "-d", tmp, "--csv", dir, "--csvf", dst) + + if err != nil { + return + } + + if err = os.Remove(filepath.Join(dir, "!SBECmd_Messages.txt")); err != nil { + return + } + + err = os.RemoveAll(tmp) + + return +} diff --git a/internal/flog/file.go b/internal/flog/file.go index 3f98408..4ea0118 100644 --- a/internal/flog/file.go +++ b/internal/flog/file.go @@ -18,6 +18,24 @@ func BaseFile(name string) string { return strings.TrimSuffix(b, filepath.Ext(b)) } +func Copy(dir, src string) (err error) { + dst := filepath.Join(dir, filepath.Base(src)) + + b, err := os.ReadFile(src) + + if err != nil { + return + } + + err = os.WriteFile(dst, b, sys.MODE_FILE) + + if os.IsNotExist(err) { + err = nil + } + + return +} + func ConsumeJson(name string) (lines []string, err error) { f, err := os.Open(name) diff --git a/internal/testdata/windows/ms.zip b/internal/testdata/windows/ms.zip deleted file mode 100644 index 1202f76..0000000 Binary files a/internal/testdata/windows/ms.zip and /dev/null differ diff --git a/internal/testdata/windows/evtx.zip b/internal/testdata/windows/system.zip similarity index 100% rename from internal/testdata/windows/evtx.zip rename to internal/testdata/windows/system.zip diff --git a/internal/testdata/windows/user.zip b/internal/testdata/windows/user.zip new file mode 100644 index 0000000..b8942e2 Binary files /dev/null and b/internal/testdata/windows/user.zip differ diff --git a/pkg/ecs/event.go b/pkg/ecs/event.go index 6f2b4d5..1517a0d 100644 --- a/pkg/ecs/event.go +++ b/pkg/ecs/event.go @@ -2,8 +2,6 @@ package ecs import ( - "time" - "github.com/cuhsat/fact/internal/flog" ) @@ -14,48 +12,39 @@ func MapEvent(s, src string) (log *Log, err error) { return } - channel := m.GetString("Event/System/Channel") - - timestamp := m.GetTime("Event/System/TimeCreated/@SystemTime") - timezone, _ := timestamp.Zone() - message := m.GetString("Event/EventData/Data/#text") - - return NewLog( - src, - Base{ - Timestamp: timestamp, - Message: message, - Tags: "EventLog", - Labels: map[string]interface{}{ - "Channel": channel, - "Level": m.GetInt64("Event/System/Level"), - "Task": m.GetInt64("Event/System/Task"), - }, - }, - Evt{ - Kind: "event", - Module: "EventLog", - Dataset: "EventLog." + channel, - Severity: m.GetInt64("Event/System/Level"), - ID: m.GetString("Event/System/EventRecordID"), - Code: m.GetString("Event/System/EventID/#text"), - Provider: m.GetString("Event/System/Provider/@Name"), - Timezone: timezone, - Created: timestamp, - Ingested: time.Now().UTC(), - Original: s, - Hash: GetHash(s), - }, - Host{ - Hostname: m.GetString("Event/System/Computer"), - Name: m.GetString("Event/System/Computer"), + log = NewLog(s, src, &Base{ + Timestamp: m.GetTime("Event/System/TimeCreated/@SystemTime"), + Message: m.GetString("Event/EventData/Data/#text"), + Tags: "EventLog", + Labels: map[string]interface{}{ + "Channel": m.GetString("Event/System/Channel"), + "Level": m.GetInt64("Event/System/Level"), + "Task": m.GetInt64("Event/System/Task"), }, - User{ - ID: m.GetString("Event/System/Security/@UserID"), - }, - Process{ - PID: m.GetInt64("Event/System/Execution/@ProcessID"), - ThreadID: m.GetInt64("Event/System/Execution/@ThreadID"), + }) + + log.Event.Kind = "event" + log.Event.Module = "EventLog" + log.Event.Dataset = "EventLog." + log.Labels["Channel"].(string) + log.Event.Severity = m.GetInt64("Event/System/Level") + log.Event.ID = m.GetString("Event/System/EventRecordID") + log.Event.Code = m.GetString("Event/System/EventID/#text") + log.Event.Provider = m.GetString("Event/System/Provider/@Name") + + log.Host = &Host{ + Hostname: m.GetString("Event/System/Computer"), + } + + log.User = &User{ + ID: m.GetString("Event/System/Security/@UserID"), + } + + log.Process = &Process{ + PID: m.GetInt64("Event/System/Execution/@ProcessID"), + Thread: &Thread{ + ID: m.GetInt64("Event/System/Execution/@ThreadID"), }, - ), nil + } + + return } diff --git a/pkg/ecs/jumplist.go b/pkg/ecs/jumplist.go index 286232f..2672e1f 100644 --- a/pkg/ecs/jumplist.go +++ b/pkg/ecs/jumplist.go @@ -4,7 +4,6 @@ package ecs import ( "path/filepath" "strings" - "time" "github.com/cuhsat/fact/internal/flog" ) @@ -16,60 +15,43 @@ func MapJumpList(s, src string) (log *Log, err error) { return } - exec := m.GetString("LocalPath", "Path") + exe := m.GetString("LocalPath", "Path") arg := m.GetString("Arguments") + log = NewLog(s, src, &Base{ + Timestamp: m.GetTime("LastModified", "TargetAccessed"), + Message: strings.TrimSpace(exe + " " + arg), + Tags: "JumpList", + Labels: make(map[string]interface{}, 1), + }) + + if strings.Contains(s, "DestListVersion") { + log.Labels["Destination"] = "automatic" + } else { + log.Labels["Destination"] = "custom" + } + + log.Host = &Host{ + Hostname: m.GetString("Hostname", "MachineID"), + MAC: m.GetString("MacAddress", "MachineMACAddress"), + } + var args []string if len(arg) > 0 { args = strings.Split(arg, " ") } - timestamp := m.GetTime("LastModified", "TargetAccessed") - timezone, _ := timestamp.Zone() - message := strings.TrimSpace(exec + " " + m.GetString("Arguments")) - - return NewLog( - src, - Base{ - Timestamp: timestamp, - Message: message, - Tags: "JumpList", - Labels: map[string]interface{}{ - "Destination": target(s), - }, - }, - Evt{ - Timezone: timezone, - Created: timestamp, - Ingested: time.Now().UTC(), - Original: s, - Hash: GetHash(s), - }, - Host{ - Hostname: m.GetString("Hostname", "MachineID"), - Name: m.GetString("Hostname", "MachineID"), - MAC: m.GetString("MacAddress", "MachineMACAddress"), - }, - User{}, - Process{ - EntityID: m.GetString("AppId"), - Start: timestamp, - Name: filepath.Base(exec), - Title: m.GetString("AppIdDescription"), - Executable: exec, - Args: args, - ArgsCount: int64(len(args)), - CommandLine: message, - WorkingDirectory: m.GetString("WorkingDirectory"), - }, - ), nil -} - -func target(log string) string { - if strings.Contains(log, "DestListVersion") { - return "automatic" - } else { - return "custom" + log.Process = &Process{ + EntityID: m.GetString("AppId"), + Name: filepath.Base(exe), + Title: m.GetString("AppIdDescription"), + Executable: exe, + Args: args, + ArgsCount: int64(len(args)), + CommandLine: log.Message, + WorkingDirectory: m.GetString("WorkingDirectory"), } + + return } diff --git a/pkg/ecs/shellbag.go b/pkg/ecs/shellbag.go new file mode 100644 index 0000000..cab3ed8 --- /dev/null +++ b/pkg/ecs/shellbag.go @@ -0,0 +1,26 @@ +// ECS shellbag mapping functions. +package ecs + +import ( + "github.com/cuhsat/fact/internal/flog" +) + +func MapShellBag(s, src string) (log *Log, err error) { + m, err := flog.NewMap(s) + + if err != nil { + return + } + + log = NewLog(s, src, &Base{ + Timestamp: m.GetTime("LastInteracted", "LastWriteTime"), + Message: m.GetString("AbsolutePath"), + Tags: "ShellBag", + }) + + log.Registry = &Registry{ + Hive: "HKU", + } + + return +} diff --git a/pkg/ecs/spec.go b/pkg/ecs/spec.go index a46fdee..281c123 100644 --- a/pkg/ecs/spec.go +++ b/pkg/ecs/spec.go @@ -16,23 +16,16 @@ const ( ) type Log struct { - Ecs Ecs `json:"ecs"` - Agent Agent `json:"agent"` - Base Base `json:"base"` - File File `json:"file,omitempty"` - Event Evt `json:"event,omitempty"` - Host Host `json:"host,omitempty"` - User User `json:"user,omitempty"` - Process Process `json:"process,omitempty"` -} - -type Ecs struct { - Version string `json:"version"` -} - -type Agent struct { - Type string `json:"type"` - Version string `json:"version"` + Base + + Ecs *Ecs `json:"ecs"` + Agent *Agent `json:"agent"` + Event *Evt `json:"event"` + File *File `json:"file"` + Host *Host `json:"host,omitempty"` + User *User `json:"user,omitempty"` + Process *Process `json:"process,omitempty"` + Registry *Registry `json:"registry,omitempty"` } type Base struct { @@ -42,13 +35,13 @@ type Base struct { Labels map[string]interface{} `json:"labels,omitempty"` } -type File struct { - Name string `json:"name,omitempty"` - Directory string `json:"directory,omitempty"` - Extension string `json:"extension,omitempty"` - DriveLetter string `json:"drive_letter,omitempty"` - Path string `json:"path,omitempty"` - Type string `json:"type,omitempty"` +type Ecs struct { + Version string `json:"version"` +} + +type Agent struct { + Type string `json:"type"` + Version string `json:"version"` } type Evt struct { @@ -59,16 +52,22 @@ type Evt struct { ID string `json:"id,omitempty"` Code string `json:"code,omitempty"` Provider string `json:"provider,omitempty"` - Timezone string `json:"timezone,omitempty"` - Created time.Time `json:"created,omitempty"` Ingested time.Time `json:"ingested,omitempty"` Original string `json:"original,omitempty"` Hash string `json:"hash,omitempty"` } +type File struct { + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Extension string `json:"extension,omitempty"` + Directory string `json:"directory,omitempty"` + DriveLetter string `json:"drive_letter,omitempty"` + Path string `json:"path,omitempty"` +} + type Host struct { Hostname string `json:"hostname,omitempty"` - Name string `json:"name,omitempty"` MAC string `json:"mac,omitempty"` } @@ -77,51 +76,65 @@ type User struct { } type Process struct { - PID int64 `json:"pid,omitempty"` - ThreadID int64 `json:"thread.id,omitempty"` - EntityID string `json:"entity_id,omitempty"` - Start time.Time `json:"start,omitempty"` - Name string `json:"name,omitempty"` - Title string `json:"title,omitempty"` - Args []string `json:"args,omitempty"` - ArgsCount int64 `json:"args_count,omitempty"` - Executable string `json:"executable,omitempty"` - CommandLine string `json:"command_line,omitempty"` - WorkingDirectory string `json:"working_directory,omitempty"` + PID int64 `json:"pid,omitempty"` + Thread *Thread `json:"thread,omitempty"` + EntityID string `json:"entity_id,omitempty"` + Name string `json:"name,omitempty"` + Title string `json:"title,omitempty"` + Args []string `json:"args,omitempty"` + ArgsCount int64 `json:"args_count,omitempty"` + Executable string `json:"executable,omitempty"` + CommandLine string `json:"command_line,omitempty"` + WorkingDirectory string `json:"working_directory,omitempty"` +} + +type Thread struct { + ID int64 `json:"id,omitempty"` +} + +type Registry struct { + Path string `json:"path,omitempty"` + Hive string `json:"hive,omitempty"` + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` } -func NewLog(src string, base Base, event Evt, host Host, user User, process Process) *Log { +func NewLog(s, src string, base *Base) *Log { return &Log{ - Ecs: Ecs{ + Base: *base, + Ecs: &Ecs{ Version: Version, }, - Agent: Agent{ + Agent: &Agent{ Type: fact.Product, Version: fact.Version, }, - Base: base, - File: File{ - Name: filepath.Base(src), - Directory: r(filepath.Abs(filepath.Dir(src))), - Extension: strings.Replace(filepath.Ext(src), ".", "", 1), - DriveLetter: strings.Replace(filepath.VolumeName(src), ":", "", 1), - Path: r(filepath.Abs(src)), - Type: "file", + Event: &Evt{ + Ingested: time.Now().UTC(), + Original: s, + Hash: hash(s), }, - Event: event, - Host: host, - User: user, - Process: process, + File: file(src), } } -func GetHash(s string) string { +func file(f string) *File { + dir, _ := filepath.Abs(filepath.Dir(f)) + abs, _ := filepath.Abs(f) + + return &File{ + Type: "file", + Name: filepath.Base(f), + Extension: strings.Replace(filepath.Ext(f), ".", "", 1), + DriveLetter: strings.Replace(filepath.VolumeName(f), ":", "", 1), + Directory: dir, + Path: abs, + } +} + +func hash(s string) string { h := sha1.New() h.Write([]byte(s)) return hex.EncodeToString(h.Sum(nil)) } - -func r(a string, _ error) string { - return a -} diff --git a/pkg/flog/flog.go b/pkg/flog/flog.go index 3cafd3a..600b62c 100644 --- a/pkg/flog/flog.go +++ b/pkg/flog/flog.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "github.com/cuhsat/fact/internal/fact" @@ -18,19 +19,26 @@ import ( type fnlog func(string, string, bool) ([]string, error) func Log(files []string, dir string, jp bool) error { + usrHives := []string{ + "ntuser.dat", + "usrclass.dat", + } + g := new(errgroup.Group) for _, f := range files { var fn fnlog + name := strings.ToLower(filepath.Base(f)) ext := strings.ToLower(filepath.Ext(f)) - if ext == "evtx" { + if ext == ".evtx" { fn = LogEvent } else if strings.HasSuffix(ext, "destinations-ms") { fn = LogJumpList + } else if slices.Contains(usrHives, name) { + fn = LogShellBag } else { - sys.Error("ignored", f) continue } @@ -44,7 +52,7 @@ func Log(files []string, dir string, jp bool) error { } func LogEvent(src, dir string, jp bool) (logs []string, err error) { - log, err := flog.EvtxeCmd(src, dir) + log, err := flog.Evtxe(src, dir) if err != nil { return @@ -82,7 +90,7 @@ func LogEvent(src, dir string, jp bool) (logs []string, err error) { } func LogJumpList(src, dir string, jp bool) (logs []string, err error) { - log, err := flog.JleCmd(src, dir) + log, err := flog.Jle(src, dir) if err != nil { return @@ -123,6 +131,48 @@ func LogJumpList(src, dir string, jp bool) (logs []string, err error) { return logs, nil } +func LogShellBag(src, dir string, jp bool) (logs []string, err error) { + log, err := flog.Sbe(src, dir) + + if err != nil { + return + } + + if _, err = os.Stat(log); os.IsNotExist(err) { + return logs, nil + } + + ll, err := flog.ConsumeCsv(log) + + if err != nil { + return + } + + f := flog.BaseFile(src) + + for i, l := range ll { + dst := filepath.Join(dir, fmt.Sprintf("%s_%08d.json", f, i)) + + m, err := ecs.MapShellBag(l, src) + + if err != nil { + sys.Error(err) + continue + } + + log, err = write(m, dst, jp) + + if err != nil { + sys.Error(err) + continue + } + + logs = append(logs, log) + } + + return logs, nil +} + func StripHash(in []string) (out []string) { if len(in) == 0 { return in diff --git a/pkg/flog/flog_test.go b/pkg/flog/flog_test.go index 27205d8..351a465 100644 --- a/pkg/flog/flog_test.go +++ b/pkg/flog/flog_test.go @@ -26,7 +26,7 @@ func TestLogEvent(t *testing.T) { }{ { name: "Test log event", - data: test.Testdata("windows", "evtx.zip"), + data: test.Testdata("windows", "system.zip"), file: "System.evtx", }, } @@ -70,12 +70,12 @@ func TestLogJumpList(t *testing.T) { }{ { name: "Test log automatic jumplist", - data: test.Testdata("windows", "ms.zip"), + data: test.Testdata("windows", "user.zip"), file: "0.automaticDestinations-ms", }, { name: "Test log custom jumplist", - data: test.Testdata("windows", "ms.zip"), + data: test.Testdata("windows", "user.zip"), file: "0.customDestinations-ms", }, } @@ -113,6 +113,50 @@ func TestLogJumpList(t *testing.T) { } } +func TestLogShellbag(t *testing.T) { + cases := []struct { + name, data, file string + }{ + { + name: "Test log usrclass.dat", + data: test.Testdata("windows", "user.zip"), + file: "UsrClass.dat", + }, + } + + for _, tt := range cases { + tmp, _ := os.MkdirTemp(os.TempDir(), "log") + + err := zip.Unzip(tt.data, tmp) + + if err != nil { + t.Fatal(err) + } + + t.Run(tt.name, func(t *testing.T) { + l, err := LogShellBag(filepath.Join(tmp, tt.file), tmp, true) + + if err != nil { + t.Fatal(err) + } + + if len(l) == 0 { + t.Fatal("file count zero") + } + + b, err := os.ReadFile(l[0]) + + if err != nil { + t.Fatal(err) + } + + if !json.Valid(b) { + t.Fatal("invalid json") + } + }) + } +} + func TestStripHash(t *testing.T) { cases := []struct { name, file, hash string diff --git a/scripts/eztools.sh b/scripts/eztools.sh index 5b3eaa0..d47450f 100755 --- a/scripts/eztools.sh +++ b/scripts/eztools.sh @@ -28,6 +28,7 @@ mkdir -p $BIN $TMP download "EvtxECmd" && installMap "EvtxECmd" "EvtxeCmd" download "JLECmd" && install "JLECmd" +download "SBECmd" && install "SBECmd" rm -rf $TMP