Files
NomanNasirMinhas 28f02e2fd2 init
2026-08-16 19:28:23 +03:00

140 lines
3.8 KiB
Go

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
}