diff --git a/Makefile b/Makefile index 7ff65ae..94a5111 100644 --- a/Makefile +++ b/Makefile @@ -6,3 +6,6 @@ test: windows: GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o dumpevtx.exe cmd/*.go + +staticcheck: + staticcheck ./... diff --git a/cmd/extract_windows.go b/cmd/extract_windows.go index 150b554..8dca9eb 100644 --- a/cmd/extract_windows.go +++ b/cmd/extract_windows.go @@ -78,7 +78,7 @@ func walkProvider(cb func(provider string, message_table string) error) error { continue } - for _, message_file := range resolver.ExpandLocations( + for _, message_file := range resolver.ExpandMessageFileLocation( message_files) { err = cb(provider_name, message_file) if err != nil { diff --git a/evtx.go b/evtx.go index a13289e..7bdfc94 100644 --- a/evtx.go +++ b/evtx.go @@ -300,9 +300,6 @@ type ParseContext struct { // XML Attributes are written to this template. stack []*TemplateNode - // Remember the attribute we are currently parsing. - current_keys []string - attribute_mode bool // A diff --git a/go.mod b/go.mod index 5143b95..abaca3a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sebdah/goldie v1.0.0 github.com/stretchr/testify v1.9.0 - golang.org/x/sys v0.37.0 + golang.org/x/sys v0.46.0 www.velocidex.com/golang/binparsergen v0.1.1-0.20240404114946-8f66c7cf586e www.velocidex.com/golang/go-pe v0.1.1-0.20251107001057-f93001158cd9 ) @@ -26,11 +26,11 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/text v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) // replace www.velocidex.com/golang/go-pe => /home/mic/projects/go-pe/ // replace github.com/Velocidex/ordereddict => /home/mic/projects/ordereddict -go 1.24.0 +go 1.25.0 diff --git a/go.sum b/go.sum index 3b036c6..e5a642d 100644 --- a/go.sum +++ b/go.sum @@ -56,10 +56,10 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/message_sets.go b/message_sets.go index 2c97f19..2c2e14b 100644 --- a/message_sets.go +++ b/message_sets.go @@ -81,8 +81,7 @@ func (self *MessageSet) GetParameter(id int) string { self.mu.Lock() defer self.mu.Unlock() - res, _ := self.Parameters[id] - return res + return self.Parameters[id] } // Calculates the largest expansion number from the message string. diff --git a/messages.go b/messages.go index 3c76517..262f890 100644 --- a/messages.go +++ b/messages.go @@ -9,12 +9,8 @@ import ( ) var ( - expansion_re = regexp.MustCompile(`\%[0-9ntr]+`) - parameter_re = regexp.MustCompile(`^\%\%([0-9]+)`) - system_root_re = regexp.MustCompile("(?i)%?SystemRoot%?") - windir_re = regexp.MustCompile("(?i)%windir%") - programfiles_re = regexp.MustCompile("(?i)%programfiles%") - system32_re = regexp.MustCompile(`(?i)\\System32\\`) + expansion_re = regexp.MustCompile(`\%[0-9ntr]+`) + parameter_re = regexp.MustCompile(`^\%\%([0-9]+)`) ) type MessageResolver interface { diff --git a/messages_windows.go b/messages_windows.go index df75ce8..1442408 100644 --- a/messages_windows.go +++ b/messages_windows.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" lru "github.com/hashicorp/golang-lru" errors "github.com/pkg/errors" @@ -18,12 +19,21 @@ import ( ) var ( - mui_dir_regex = regexp.MustCompile("^[a-z]{2}-[a-z]{2}$") - invalidGUID = errors.New("invalidGUID") + // Search for potential MUI files - these are typically found in + // directories names like " en-US cz-CZ + mui_dir_regex = regexp.MustCompile("^[a-z]{2}-[a-z]{2}$") + system_root_re = regexp.MustCompile("(?i)%?SystemRoot%?") + windir_re = regexp.MustCompile("(?i)%windir%") + programfiles_re = regexp.MustCompile("(?i)%programfiles%") + system32_re = regexp.MustCompile(`(?i)\\System32\\`) + + invalidGUID = errors.New("invalidGUID") mui_debug = 0 ) +// NewWindowsMessageResolver is the constructor for the +// WindowsMessageResolver. func NewWindowsMessageResolver( opts MessageResolverOpts) (*WindowsMessageResolver, error) { lru_size := opts.LRUSize @@ -35,7 +45,8 @@ func NewWindowsMessageResolver( if err != nil { return nil, err } - res := &WindowsMessageResolver{ + + self := &WindowsMessageResolver{ // string->MessageSet cache: cache, @@ -43,25 +54,55 @@ func NewWindowsMessageResolver( // periodically. mui_cache: make(map[string][]string), checked_mui_dirs: make(map[string]bool), + opts: opts, + } + + self.system_root = os.Getenv("SystemRoot") + if self.system_root == "" { + self.system_root = "C:/Windows/" } - res.buildMUIcache() + filter := opts.LangPreferenceRegeExp + if filter == "" { + filter = "en-(US|AU|GB)" + } + + filter_re, err := regexp.Compile("(?i)" + filter) + if err != nil { + return nil, err + } + + self.lang_filter_re = filter_re + + // Get MUI files from e.g. C:\Windows\System32\en-US\*.mui + self.buildMUIcacheFromDir( + filepath.Join(self.system_root, "System32"), mui_dir_regex) - return res, res.sortMRUWithRegexp(opts.LangPreferenceRegeExp) + // Get MUI files from e.g. C:\Windows\WinSxS\*\*.mui + // This seems to slow things down a lot because there are many SxS dlls typically. + //self.buildMUIcacheFromDir( + // filepath.Join(system_root, "WinSxS"), nil) + + return self, nil } type WindowsMessageResolver struct { cache *lru.Cache + // Protect the below + mu sync.Mutex mui_cache map[string][]string checked_mui_dirs map[string]bool - - location_expander func([]string) []string + system_root string + lang_filter_re *regexp.Regexp + opts MessageResolverOpts } +// Discover possible MUI files in the directory specified. func (self *WindowsMessageResolver) buildMUIcacheFromDir( directory string, dir_regex *regexp.Regexp) { + // Only process directory once. _, pres := self.checked_mui_dirs[directory] if pres { return @@ -95,8 +136,10 @@ func (self *WindowsMessageResolver) buildMUIcacheFromDir( basename := strings.TrimSuffix(filepath.Base(filename), ".mui") locations, _ := self.mui_cache[basename] - locations = append(locations, fullpath) - self.mui_cache[basename] = locations + + // Make sure the mui_cache is always properly sorted. + self.mui_cache[basename] = self.sortListWithPreference( + append(locations, fullpath)) if mui_debug > 0 { fmt.Printf("buildMUIcacheFromDir: adding %v to %v\n", fullpath, basename) @@ -105,22 +148,20 @@ func (self *WindowsMessageResolver) buildMUIcacheFromDir( } } -func (self *WindowsMessageResolver) buildMUIcache() { - self.mui_cache = make(map[string][]string) - - system_root := os.Getenv("SystemRoot") - if system_root == "" { - system_root = "C:/Windows/" - } - - // Get MUI files from e.g. C:\Windows\System32\en-US\*.mui - self.buildMUIcacheFromDir( - filepath.Join(system_root, "System32"), mui_dir_regex) - - // Get MUI files from e.g. C:\Windows\WinSxS\*\*.mui - // This seems to slow things down a lot because there are many SxS dlls typically. - //self.buildMUIcacheFromDir( - // filepath.Join(system_root, "WinSxS"), nil) +// ExpandMessageFileLocation Produces a list of possible locations the +// message file may be. We process all of them because sometimes event +// messages are split across multiple dlls. For example, a generic +// message table may exist in C:\Windows\System32\XXX.dll but a +// localized message table also exists in +// C:\Windows\System32\en-us\XXX.dll.mui +// +// NOTE: This function will effectively be called once per provider +// since there is a higher level provider LRU cache. So it is probably +// not worth memoizing it. +func (self *WindowsMessageResolver) ExpandMessageFileLocation( + message_file string) []string { + self.mu.Lock() + defer self.mu.Unlock() windir := os.Getenv("WinDir") programfiles := os.Getenv("programfiles") @@ -130,7 +171,8 @@ func (self *WindowsMessageResolver) buildMUIcache() { replace_env_vars := func(paths []string) []string { result := []string{} for _, path := range paths { - path = system_root_re.ReplaceAllLiteralString(path, system_root) + path = system_root_re.ReplaceAllLiteralString( + path, self.system_root) path = windir_re.ReplaceAllLiteralString(path, windir) @@ -161,11 +203,14 @@ func (self *WindowsMessageResolver) buildMUIcache() { path, "\\SysWow64\\")) } } - return result } - // On international systems messages may be stored in MUI files. + // On international systems messages may be stored in MUI + // files. This appends possible MUI files **after** the provided + // list. The search order looks at the provided list first + // (usually System32) and then only if the message is not found + // consults the MUI files. include_muis := func(paths []string) []string { result := []string{} seen := make(map[string]bool) @@ -175,18 +220,24 @@ func (self *WindowsMessageResolver) buildMUIcache() { dirname := filepath.Dir(path) - // Make sure we checked this directory for MUI files. + // Make sure we checked this directory for MUI + // files. Sometimes an application will distribute MUI + // files inside its own path. self.buildMUIcacheFromDir(dirname, mui_dir_regex) dll_name := strings.ToLower(filepath.Base(path)) + + // process each dll only once to avoid recursion. _, pres := seen[dll_name] if pres { continue } seen[dll_name] = true + // Append all muis in search order. muis, pres := self.mui_cache[dll_name] if pres { + // muis list is always properly sorted in search order. result = append(result, muis...) } } @@ -206,17 +257,12 @@ func (self *WindowsMessageResolver) buildMUIcache() { return result } - self.location_expander = func(in []string) []string { - res := filter_files( - include_muis( - split_system32( - replace_env_vars(in)))) - - return res - } + locations := strings.Split(message_file, ";") + return filter_files( + include_muis(split_system32(replace_env_vars(locations)))) } -func (self *WindowsMessageResolver) getMessageSets( +func (self *WindowsMessageResolver) GetMessageSets( provider, channel string) (*MessageSet, error) { // Get provider from cache - the cache key is both provider and @@ -249,7 +295,7 @@ func (self *WindowsMessageResolver) getMessageSets( func (self *WindowsMessageResolver) GetMessage( provider, channel string, event_id, number_of_expansions int) string { - message_set, err := self.getMessageSets(provider, channel) + message_set, err := self.GetMessageSets(provider, channel) if err != nil { return "" } @@ -265,7 +311,7 @@ func (self *WindowsMessageResolver) GetMessage( func (self *WindowsMessageResolver) GetParameter( provider, channel string, parameter_id int) string { - message_set, err := self.getMessageSets(provider, channel) + message_set, err := self.GetMessageSets(provider, channel) if err != nil { return "" } @@ -279,23 +325,6 @@ func (self *WindowsMessageResolver) GetParameter( func (self *WindowsMessageResolver) Close() {} -// ExpandLocations Produces a list of possible locations the message -// file may be. We process all of them because sometimes event -// messages are split across multiple dlls. For example, a generic -// message table may exist in C:\Windows\System32\XXX.dll but a -// localized message table also exists in -// C:\Windows\System32\en-us\XXX.dll.mui -func (self *WindowsMessageResolver) ExpandLocations( - message_file string) []string { - - // Message file values may be separated by ; - res := self.location_expander(strings.Split(message_file, ";")) - if mui_debug > 0 { - fmt.Printf("ExpandLocations: %v %v\n", message_file, res) - } - return res -} - func (self *WindowsMessageResolver) GetMessagesByGUID( provider_guid, channel string) (*MessageSet, error) { @@ -338,11 +367,11 @@ func (self *WindowsMessageResolver) GetMessagesByGUID( provider = provider_guid } - return self.expandLocations( + return self.GetMessageSetsForProvider( message_files, parameter_files, provider, channel) } -func (self *WindowsMessageResolver) expandLocations( +func (self *WindowsMessageResolver) GetMessageSetsForProvider( message_files, parameter_files, provider, channel string) (*MessageSet, error) { msg_set := &MessageSet{ @@ -353,18 +382,19 @@ func (self *WindowsMessageResolver) expandLocations( Filenames: make(map[string]int), } - self.populateMessages(message_files, msg_set.AddMessage) + self.PopulateMessages(message_files, msg_set.AddMessage) if parameter_files != "" { - self.populateMessages(parameter_files, msg_set.AddParameter) + self.PopulateMessages(parameter_files, msg_set.AddParameter) } return msg_set, nil } -func (self *WindowsMessageResolver) populateMessages( +func (self *WindowsMessageResolver) PopulateMessages( message_files string, adder func(event_id int, message string, filename string)) { - for _, message_file := range self.ExpandLocations(message_files) { + for _, message_file := range self.ExpandMessageFileLocation( + message_files) { fd, err := os.Open(message_file) if err != nil { continue @@ -430,5 +460,5 @@ func (self *WindowsMessageResolver) GetMessages( if mui_debug > 1 { fmt.Printf("GetMessages %v %v: %v\n", provider, channel, message_files) } - return self.expandLocations(message_files, "", provider, channel) + return self.GetMessageSetsForProvider(message_files, "", provider, channel) } diff --git a/prefs.go b/prefs.go index 9e3797f..925b820 100644 --- a/prefs.go +++ b/prefs.go @@ -3,37 +3,20 @@ package evtx -import "regexp" - -func (self *WindowsMessageResolver) sortMRUWithRegexp(filter string) error { - if filter == "" { - filter = "en-(US|AU|GB)" - } - - filter_re, err := regexp.Compile("(?i)" + filter) - if err != nil { - return err +// Reorder the list so files matching the language preference regex are +// before ones that do not. +func (self *WindowsMessageResolver) sortListWithPreference(list []string) []string { + if self.lang_filter_re == nil { + return list } - // Resort the MUI cache to prefer a certain language (by default - // English) - new_mui_cache := make(map[string][]string) - for k, list := range self.mui_cache { - var new_list []string - for _, item := range list { - // If we match we push it to the front, otherwise we - // append at the end. The result is that files matching - // the regex are before ones that do not. - if filter_re.MatchString(item) { - new_list = append([]string{item}, new_list...) - } else { - new_list = append(new_list, item) - } + var preferred, rest []string + for _, item := range list { + if self.lang_filter_re.MatchString(item) { + preferred = append(preferred, item) + } else { + rest = append(rest, item) } - new_mui_cache[k] = new_list } - - self.mui_cache = new_mui_cache - - return nil + return append(preferred, rest...) } diff --git a/prefs_test.go b/prefs_test.go new file mode 100644 index 0000000..ce9c782 --- /dev/null +++ b/prefs_test.go @@ -0,0 +1,77 @@ +//go:build windows +// +build windows + +package evtx + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSortListWithPreference(t *testing.T) { + resolver, err := NewWindowsMessageResolver(MessageResolverOpts{}) + if err != nil { + t.Fatal(err) + } + + list := []string{ + `C:\Test\cs-CZ\MpEvMsg.dll.mui`, + `C:\Test\en-US\MpEvMsg.dll.mui`, + `C:\Test\zh-TW\MpEvMsg.dll.mui`, + } + + sorted := resolver.sortListWithPreference(list) + if len(sorted) != 3 { + t.Fatalf("expected 3 entries, got %v", sorted) + } + if !strings.Contains(sorted[0], "en-US") { + t.Fatalf("expected en-US first, got %v", sorted) + } +} + +// MUI files for message DLLs outside System32 are discovered after the +// constructor's sorting pass - they must still come back in preference +// order. +func TestExpandLocationsPrefersLanguageForLazyMUIDirs(t *testing.T) { + tmp := t.TempDir() + + dll := filepath.Join(tmp, "MpEvMsg.dll") + if err := os.WriteFile(dll, []byte("x"), 0600); err != nil { + t.Fatal(err) + } + + // en-US deliberately surrounded alphabetically on both sides. + for _, lang := range []string{"cs-CZ", "en-US", "zh-TW"} { + mui_dir := filepath.Join(tmp, lang) + if err := os.MkdirAll(mui_dir, 0700); err != nil { + t.Fatal(err) + } + mui := filepath.Join(mui_dir, "MpEvMsg.dll.mui") + if err := os.WriteFile(mui, []byte("x"), 0600); err != nil { + t.Fatal(err) + } + } + + resolver, err := NewWindowsMessageResolver(MessageResolverOpts{}) + if err != nil { + t.Fatal(err) + } + + locations := resolver.ExpandMessageFileLocation(dll) + + var muis []string + for _, location := range locations { + if strings.HasSuffix(location, ".mui") { + muis = append(muis, location) + } + } + + if len(muis) != 3 { + t.Fatalf("expected 3 mui files, got %v", locations) + } + if !strings.Contains(muis[0], "en-US") { + t.Fatalf("expected en-US mui first, got %v", muis) + } +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..9c6aed6 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,27 @@ +checks = ["all", + # error strings should not be capitalized (ST1005) + "-ST1005", + # receiver name should be a reflection of its identity + "-ST1006", + + # should not use underscores in Go names + "-ST1003", + # at least one file in a package should have a package comment + "-ST1000", + # the surrounding loop is unconditionally terminated + "-SA4004", + + # Dont tell me how to write comments. + # The documentation of an exported function should start with the function’s name + "-ST1020", + # The documentation of an exported type should start with type’s name + "-ST1021", + # comment on exported const .. should be of the form + "-ST1022", + + # Poorly chosen name for error variable + "-ST1012", + + # Using a deprecated function, variable, constant or field + "-SA1019", + ] \ No newline at end of file