From d4ba4c0544233ff7571244edc39d6f8f3c5d1d15 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Mon, 27 Apr 2026 17:09:17 +1000 Subject: [PATCH] Bugfix: Bounds checks in parser Fixed checks that could lead to a crash in parser. Also allow MUI files to be sorted in preference to a given language. This allows callers to receive messages in their preferred language while still falling back if necessary. --- cmd/extract_windows.go | 12 +++- cmd/parse.go | 7 ++- debug.go | 10 ++- evtx.go | 135 +++++++++++++++-------------------------- message_sets.go | 8 +++ messages_windows.go | 14 +++-- opts.go | 13 ++++ prefs.go | 39 ++++++++++++ resolver_windows.go | 5 +- 9 files changed, 147 insertions(+), 96 deletions(-) create mode 100644 opts.go create mode 100644 prefs.go diff --git a/cmd/extract_windows.go b/cmd/extract_windows.go index 8356218..150b554 100644 --- a/cmd/extract_windows.go +++ b/cmd/extract_windows.go @@ -22,13 +22,23 @@ var ( extract = app.Command("extract", "Extract all log messages from all providers.") extract_file = extract.Arg("file", "File to write all messages").Required(). String() + + // https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/available-language-packs-for-windows + extract_lang = extract.Flag("lang", "A preferred language for messages (e.g. jp)").String() ) // Walk over all the providers in the registry and call the callback // with potential message files. The message_table paths are not // guaranteed to exists. func walkProvider(cb func(provider string, message_table string) error) error { - resolver := evtx.NewWindowsMessageResolver() + resolver, err := evtx.NewWindowsMessageResolver( + evtx.MessageResolverOpts{ + LangPreferenceRegeExp: *extract_lang, + }) + if err != nil { + return err + } + channels_key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Services\EventLog`, registry.READ|registry.ENUMERATE_SUB_KEYS|registry.WOW64_64KEY) diff --git a/cmd/parse.go b/cmd/parse.go index 380e257..58ad67e 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -29,6 +29,9 @@ var ( Default("99999999").Int() event_id_filter = parse.Flag("event_id", "Only show these event IDs").Int() + + // https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/available-language-packs-for-windows + parse_lang = parse.Flag("lang", "A preferred language for messages (e.g. ja-jp (default is en-US ) )").String() ) type parsingContext struct { @@ -92,7 +95,9 @@ func NewParsingContext() *parsingContext { } // Otherwise use the native resolver - resolver, err := evtx.GetNativeResolver() + resolver, err := evtx.GetNativeResolver(evtx.MessageResolverOpts{ + LangPreferenceRegeExp: *parse_lang, + }) kingpin.FatalIfError(err, " %v", err) return &parsingContext{resolver} diff --git a/debug.go b/debug.go index 574839f..41941b1 100644 --- a/debug.go +++ b/debug.go @@ -1,6 +1,9 @@ package evtx -import "fmt" +import ( + "encoding/json" + "fmt" +) const ( debug_enabled = false @@ -13,3 +16,8 @@ func debug(format string, args ...interface{}) { } func DlvBreak() {} + +func Dump(x interface{}) { + serialized, _ := json.MarshalIndent(x, " ", " ") + fmt.Println(string(serialized)) +} diff --git a/evtx.go b/evtx.go index b442da7..a13289e 100644 --- a/evtx.go +++ b/evtx.go @@ -18,6 +18,7 @@ package evtx import ( "bytes" "encoding/binary" + "math" "strings" "time" @@ -364,7 +365,7 @@ func NewParseContext(chunk *Chunk) *ParseContext { } func (self *ParseContext) ConsumeUint8() uint8 { - if len(self.buff) < self.offset+1 { + if self.offset > len(self.buff) { return 0 } result := self.buff[self.offset] @@ -373,31 +374,34 @@ func (self *ParseContext) ConsumeUint8() uint8 { } func (self *ParseContext) ConsumeUint16() uint16 { - if len(self.buff) < self.offset+2 { + if self.offset+2 > len(self.buff) { return 0 } - result := binary.LittleEndian.Uint16(self.buff[self.offset:]) + result := binary.LittleEndian.Uint16( + self.buff[self.offset : self.offset+2]) self.offset += 2 return result } func (self *ParseContext) ConsumeUint32() uint32 { - if len(self.buff) < self.offset+4 { + if self.offset+4 > len(self.buff) { return 0 } - result := binary.LittleEndian.Uint32(self.buff[self.offset:]) + result := binary.LittleEndian.Uint32( + self.buff[self.offset : self.offset+4]) self.offset += 4 return result } func (self *ParseContext) ConsumeUint64() uint64 { - if len(self.buff) < self.offset+8 { + if self.offset+8 > len(self.buff) { return 0 } - result := binary.LittleEndian.Uint64(self.buff[self.offset:]) + result := binary.LittleEndian.Uint64( + self.buff[self.offset : self.offset+8]) self.offset += 8 return result } @@ -413,70 +417,56 @@ func (self *ParseContext) ConsumeBytes(size int) []byte { } func (self *ParseContext) ConsumeInt64() (ret int64) { - - if len(self.buff) < self.offset+8 { + if self.offset+8 > len(self.buff) { return 0 } - buf := bytes.NewReader(self.buff[self.offset:]) - err := binary.Read(buf, binary.LittleEndian, &ret) - if err != nil { - return 0 - } + ret = int64(binary.LittleEndian.Uint64( + self.buff[self.offset : self.offset+8])) self.offset += 8 - return + return ret } func (self *ParseContext) ConsumeInt32() (ret int32) { - if len(self.buff) < self.offset+4 { + if self.offset+4 > len(self.buff) { return 0 } - buf := bytes.NewReader(self.buff[self.offset:]) - err := binary.Read(buf, binary.LittleEndian, &ret) - if err != nil { - return 0 - } + ret = int32(binary.LittleEndian.Uint32( + self.buff[self.offset : self.offset+4])) self.offset += 4 - return - + return ret } func (self *ParseContext) ConsumeReal32() (ret float32) { - if len(self.buff) < self.offset+4 { - return 0 - } - - buf := bytes.NewReader(self.buff[self.offset:]) - err := binary.Read(buf, binary.LittleEndian, &ret) - if err != nil { + if self.offset+4 > len(self.buff) { return 0 } + ret = math.Float32frombits( + binary.LittleEndian.Uint32(self.buff[self.offset : self.offset+4])) self.offset += 4 - return + return ret } func (self *ParseContext) ConsumeReal64() (ret float64) { - if len(self.buff) < self.offset+8 { + if self.offset+8 > len(self.buff) { return 0 } - buf := bytes.NewReader(self.buff[self.offset:]) - err := binary.Read(buf, binary.LittleEndian, &ret) - if err != nil { - return 0 - } + ret = math.Float64frombits( + binary.LittleEndian.Uint64(self.buff[self.offset : self.offset+8])) + self.offset += 8 - return + return ret } func (self *ParseContext) ConsumeSysTime(size int) string { - if len(self.buff) < self.offset+16 { + if self.offset+16 > len(self.buff) { return "SysTimeParsingError" } @@ -491,81 +481,52 @@ func (self *ParseContext) ConsumeSysTime(size int) string { sec := binary.LittleEndian.Uint16(buffer[12:14]) msec := binary.LittleEndian.Uint16(buffer[14:16]) - result := time.Date(int(year), time.Month(month), int(day), int(hour), int(min), int(sec), int(msec), time.UTC) + result := time.Date(int(year), time.Month(month), + int(day), int(hour), int(min), int(sec), int(msec), time.UTC) return result.String() } func (self *ParseContext) ConsumeUnit16Array(size int) []uint16 { - uint16array := []uint16{} - if self.offset+size >= len(self.buff) { - size = len(self.buff) - self.offset - 1 - } - if self.offset > len(self.buff) { - return nil - } - - buffer := self.buff[self.offset : self.offset+size] - self.offset += size - index := 0 - for index < len(buffer) { - value := binary.LittleEndian.Uint16((buffer[index:])) + index := self.offset + for index+2 < len(self.buff) { + value := binary.LittleEndian.Uint16((self.buff[index : index+2])) uint16array = append(uint16array, value) index += 2 - } + // Still advance the offset as required to maintain alignment. + self.offset += size + return uint16array } func (self *ParseContext) ConsumeUnit64Array(size int) []uint64 { - uint64array := []uint64{} - if self.offset+size >= len(self.buff) { - size = len(self.buff) - self.offset - 1 - } - if self.offset > len(self.buff) { - return nil + index := self.offset + for index+8 < len(self.buff) { + value := binary.LittleEndian.Uint64((self.buff[index : index+8])) + uint64array = append(uint64array, value) + index += 8 } - buffer := self.buff[self.offset : self.offset+size] self.offset += size - index := 0 - for index < len(buffer) { - value := binary.LittleEndian.Uint64((buffer[index:])) - uint64array = append(uint64array, value) - index += 8 - - } return uint64array } func (self *ParseContext) ConsumeInt64hexArray(size int) []string { - if self.offset+size >= len(self.buff) { - size = len(self.buff) - self.offset - 1 - } + result := []string{} - if self.offset > len(self.buff) { - return nil + index := self.offset + for index+8 < len(self.buff) { + value := binary.LittleEndian.Uint64((self.buff[index : index+8])) + result = append(result, "0x"+fmt.Sprintf("%x", value)) + index += 8 } - - buffer := self.buff[self.offset : self.offset+size] self.offset += size - result := []string{} - - for i := 0; i < len(buffer); i = i + 8 { - - var ret int64 - buf := bytes.NewReader(buffer[i : i+8]) - err := binary.Read(buf, binary.LittleEndian, &ret) - if err != nil { - return result - } - result = append(result, "0x"+fmt.Sprintf("%x", ret)) - } return result } diff --git a/message_sets.go b/message_sets.go index f708dba..2c97f19 100644 --- a/message_sets.go +++ b/message_sets.go @@ -58,6 +58,14 @@ func (self *MessageSet) AddMessage( number_of_expansions := self.getLargestExpansion(message) key := event_id<<16 | number_of_expansions + // Only add the message if we do not already have it. This means + // messages n files earlies in the search sequence will be found + // instead of files later. + _, pres := self.Messages[key] + if pres { + return + } + self.Messages[key] = message self.Filenames[filename] = 1 } diff --git a/messages_windows.go b/messages_windows.go index c08143f..df75ce8 100644 --- a/messages_windows.go +++ b/messages_windows.go @@ -24,10 +24,16 @@ var ( mui_debug = 0 ) -func NewWindowsMessageResolver() *WindowsMessageResolver { - cache, err := lru.New(100) +func NewWindowsMessageResolver( + opts MessageResolverOpts) (*WindowsMessageResolver, error) { + lru_size := opts.LRUSize + if lru_size <= 0 { + lru_size = 100 + } + + cache, err := lru.New(lru_size) if err != nil { - panic(err) + return nil, err } res := &WindowsMessageResolver{ // string->MessageSet @@ -41,7 +47,7 @@ func NewWindowsMessageResolver() *WindowsMessageResolver { res.buildMUIcache() - return res + return res, res.sortMRUWithRegexp(opts.LangPreferenceRegeExp) } type WindowsMessageResolver struct { diff --git a/opts.go b/opts.go new file mode 100644 index 0000000..4d5c5da --- /dev/null +++ b/opts.go @@ -0,0 +1,13 @@ +package evtx + +type MessageResolverOpts struct { + // A regular expression that if matched, will place the language + // first in the list of languages. + // This defaults to "en-(US|AU|GB)" + + // https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/available-language-packs-for-windows?view=windows-11 + LangPreferenceRegeExp string + + // Size of Message LRU - defaults to 100 + LRUSize int +} diff --git a/prefs.go b/prefs.go new file mode 100644 index 0000000..9e3797f --- /dev/null +++ b/prefs.go @@ -0,0 +1,39 @@ +//go:build windows +// +build windows + +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 + } + + // 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) + } + } + new_mui_cache[k] = new_list + } + + self.mui_cache = new_mui_cache + + return nil +} diff --git a/resolver_windows.go b/resolver_windows.go index f743fcc..b58372f 100644 --- a/resolver_windows.go +++ b/resolver_windows.go @@ -1,7 +1,8 @@ +//go:build windows // +build windows package evtx -func GetNativeResolver() (MessageResolver, error) { - return NewWindowsMessageResolver(), nil +func GetNativeResolver(opts MessageResolverOpts) (MessageResolver, error) { + return NewWindowsMessageResolver(opts) }