package extract import ( "strings" "unicode/utf16" "ringer/pefile" ) // StringInfo is a single extracted string with its encoding, file offset, and // semantic category. type StringInfo struct { Value string Encoding string // "ascii", "utf16le", or "utf16be" Offset uint32 // file offset of the first byte Category string // "device", "symlink", "section", "ringbuffer", or "other" } // ExtractStrings scans the whole file for ASCII and UTF-16 strings of at least // minLen characters and classifies each one. func ExtractStrings(f *pefile.File, minLen int) []StringInfo { if minLen < 1 { minLen = 1 } var out []StringInfo out = append(out, extractASCII(f.Raw, minLen)...) out = append(out, extractUTF16(f.Raw, minLen)...) for i := range out { out[i].Category = classify(out[i].Value) } return out } // IsSharedRingBuffer reports whether a string is a "Shared Ring Buffer" string: // a named-object section name or a ring-buffer/shared-memory keyword. func IsSharedRingBuffer(s StringInfo) bool { return s.Category == "section" || s.Category == "ringbuffer" } // extractASCII finds runs of printable ASCII (0x20-0x7E). func extractASCII(data []byte, minLen int) []StringInfo { var out []StringInfo var cur []byte var start int flush := func(end int) { if len(cur) >= minLen { out = append(out, StringInfo{Value: string(cur), Encoding: "ascii", Offset: uint32(start)}) } cur = nil } for i := 0; i < len(data); i++ { b := data[i] if b >= 0x20 && b <= 0x7E { if cur == nil { start = i } cur = append(cur, b) } else { flush(i) } } flush(len(data)) return out } // extractUTF16 finds runs of printable UTF-16LE code units (ASCII range only, // to avoid CJK false positives). Windows drivers are overwhelmingly UTF-16LE; // UTF-16BE is not scanned because it is essentially nonexistent in PE images // and produces misaligned false positives. func extractUTF16(data []byte, minLen int) []StringInfo { var out []StringInfo var cur []uint16 var start int flush := func() { if len(cur) >= minLen { out = append(out, StringInfo{Value: string(utf16.Decode(cur)), Encoding: "utf16le", Offset: uint32(start)}) } cur = nil } for i := 0; i+1 < len(data); { u := uint16(data[i]) | uint16(data[i+1])<<8 if u >= 0x20 && u <= 0x7E { if cur == nil { start = i } cur = append(cur, u) i += 2 } else { flush() i++ } } flush() return out } // classify assigns a semantic category to a string. func classify(s string) string { // A printf-style format string (e.g. "\??\%ls", "\Device\%ls") is a template // the driver fills in at runtime, not a literal device/symlink/section name. // Reporting it as a concrete name would be wrong, so it is left as "other". if isFormatString(s) { return "other" } switch { case strings.HasPrefix(s, `\Device\`): return "device" case strings.HasPrefix(s, `\DosDevices\`), strings.HasPrefix(s, `\??\`), strings.HasPrefix(s, `\\.\`): return "symlink" case strings.HasPrefix(s, `\BaseNamedObjects\`), strings.HasPrefix(s, `\REGISTRY\`), strings.HasPrefix(s, `Global\`), strings.HasPrefix(s, `\KnownDlls\`): return "section" } lower := strings.ToLower(s) // Match ring-buffer / shared-memory phrases specifically. "ring" alone // would false-positive on "string", and "buffer"/"log"/"map" are too broad. for _, kw := range []string{"ring buffer", "ringbuffer", "ring_buffer", "ringbuf", "circular", "shared"} { if strings.Contains(lower, kw) { return "ringbuffer" } } return "other" } // isFormatString reports whether s contains a printf-style format specifier // (e.g. "%ls", "%S", "%d", "%08X"). "%%" is a literal percent and is ignored. func isFormatString(s string) bool { for i := 0; i < len(s); i++ { if s[i] != '%' { continue } if i+1 < len(s) && s[i+1] == '%' { i++ continue } return true } return false }