Files

335 lines
10 KiB
Go

// Package report formats extraction and verification results as text or JSON.
package report
import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"strings"
"ringer/extract"
"ringer/verify"
)
// Report aggregates everything the tool produces for one driver.
type Report struct {
File string
Machine string
Is64 bool
Entropy float64
Ioctls []extract.IoctlCode
Strings []extract.StringInfo
IoctlResults []verify.IoctlResult
SectionResults []verify.SectionResult
RuntimeDevices []string // live device names discovered from the object manager
RuntimeSections []string // live shared-section names discovered from the object manager
}
// SharedRingBufferStrings returns the extracted strings classified as shared
// ring buffer strings (section names + ring-buffer keywords), deduplicated.
func (r *Report) SharedRingBufferStrings() []extract.StringInfo {
seen := map[string]bool{}
var out []extract.StringInfo
for _, s := range r.Strings {
if !extract.IsSharedRingBuffer(s) {
continue
}
key := s.Encoding + ":" + s.Value
if seen[key] {
continue
}
seen[key] = true
out = append(out, s)
}
return out
}
// DeviceStrings returns device names and symbolic links, deduplicated.
func (r *Report) DeviceStrings() []extract.StringInfo {
seen := map[string]bool{}
var out []extract.StringInfo
for _, s := range r.Strings {
if s.Category != "device" && s.Category != "symlink" {
continue
}
key := s.Encoding + ":" + s.Value
if seen[key] {
continue
}
seen[key] = true
out = append(out, s)
}
return out
}
// WriteText renders a human-readable report.
func (r *Report) WriteText(w io.Writer) {
fmt.Fprintf(w, "Ringer - driver IOCTL & shared ring buffer extractor\n")
fmt.Fprintf(w, "====================================================\n")
fmt.Fprintf(w, "File: %s\n", r.File)
fmt.Fprintf(w, "Machine: %s\n", r.Machine)
packed := ""
if r.Entropy > 7.2 {
packed = " [WARNING: high entropy, may be packed/encrypted - results unreliable]"
}
fmt.Fprintf(w, "Entropy: %.2f bits/byte%s\n", r.Entropy, packed)
fmt.Fprintf(w, "\n--- IOCTL Codes (%d found) ---\n", len(r.Ioctls))
if len(r.Ioctls) == 0 {
fmt.Fprintf(w, " (none)\n")
} else {
fmt.Fprintf(w, "%-12s %-24s %-6s %-18s %-28s %-6s %-8s %s\n",
"Code", "DeviceType", "Func", "Method", "Access", "Conf", "Source", "RVA")
for _, c := range r.Ioctls {
dt := extract.DeviceTypeName(c.DeviceType)
if dt == "" {
dt = fmt.Sprintf("0x%04X", c.DeviceType)
}
note := ""
if c.Note != "" {
note = " [" + c.Note + "]"
}
fmt.Fprintf(w, "0x%08X %-24s 0x%03X %-18s %-28s %-6s %-8s 0x%X%s\n",
c.Code, dt, c.Function, extract.MethodName(c.Method),
extract.AccessName(c.Access), c.Confidence, c.Source, c.RVA, note)
}
}
rb := r.SharedRingBufferStrings()
fmt.Fprintf(w, "\n--- Shared Ring Buffer Strings (%d found) ---\n", len(rb))
if len(rb) == 0 {
fmt.Fprintf(w, " (none)\n")
} else {
for _, s := range rb {
fmt.Fprintf(w, " [%-10s] %-40s (%s @ 0x%X)\n", s.Category, s.Value, s.Encoding, s.Offset)
}
}
dev := r.DeviceStrings()
fmt.Fprintf(w, "\n--- Device Names / Symbolic Links (%d found) ---\n", len(dev))
if len(dev) == 0 {
fmt.Fprintf(w, " (none)\n")
} else {
for _, s := range dev {
fmt.Fprintf(w, " [%-8s] %s (0x%X)\n", s.Category, s.Value, s.Offset)
}
}
if len(r.RuntimeDevices) > 0 || len(r.RuntimeSections) > 0 {
fmt.Fprintf(w, "\n--- Runtime Discovery (live object manager) ---\n")
if len(r.RuntimeDevices) > 0 {
fmt.Fprintf(w, " Devices (%d):\n", len(r.RuntimeDevices))
for _, d := range r.RuntimeDevices {
fmt.Fprintf(w, " %s\n", d)
}
}
if len(r.RuntimeSections) > 0 {
fmt.Fprintf(w, " Shared sections (%d):\n", len(r.RuntimeSections))
for _, s := range r.RuntimeSections {
fmt.Fprintf(w, " %s\n", s)
}
}
}
if len(r.IoctlResults) > 0 {
fmt.Fprintf(w, "\n--- Live Verification: IOCTL ---\n")
writeIoctlResults(w, r.IoctlResults)
}
if len(r.SectionResults) > 0 {
fmt.Fprintf(w, "\n--- Live Verification: Shared Sections ---\n")
writeSectionResults(w, r.SectionResults)
}
}
func writeIoctlResults(w io.Writer, results []verify.IoctlResult) {
cur := ""
for _, res := range results {
if res.Device != cur {
cur = res.Device
fmt.Fprintf(w, "Device %s:\n", cur)
}
if res.Code == 0 {
// Device-open failure placeholder.
fmt.Fprintf(w, " open failed: %s (%d)\n", res.Error, res.ErrorCode)
continue
}
if res.TimedOut {
fmt.Fprintf(w, " 0x%08X TIMEOUT\n", res.Code)
continue
}
if res.Success {
fmt.Fprintf(w, " 0x%08X SUCCESS (%d bytes)\n", res.Code, res.BytesReturned)
if len(res.Output) > 0 {
fmt.Fprintf(w, " %s\n", hexDump(res.Output, 64))
}
} else {
fmt.Fprintf(w, " 0x%08X %s (%d)\n", res.Code, res.Error, res.ErrorCode)
}
}
}
func writeSectionResults(w io.Writer, results []verify.SectionResult) {
for _, res := range results {
if !res.Success {
fmt.Fprintf(w, "%s: open/map failed: %s\n", res.Name, res.Error)
continue
}
fmt.Fprintf(w, "%s: mapped %d bytes, first dword (possible write index) = %d\n",
res.Name, res.RegionSize, res.FirstDword)
if len(res.Data) > 0 {
fmt.Fprintf(w, "%s\n", hexDump(res.Data, 256))
}
}
}
// hexDump renders up to maxLen bytes as offset + hex + ASCII.
func hexDump(b []byte, maxLen int) string {
if len(b) > maxLen {
b = b[:maxLen]
}
var sb strings.Builder
for off := 0; off < len(b); off += 16 {
end := off + 16
if end > len(b) {
end = len(b)
}
row := b[off:end]
sb.WriteString(fmt.Sprintf(" %08X ", off))
hexPart := make([]string, 0, 16)
asciiPart := make([]byte, 0, 16)
for _, c := range row {
hexPart = append(hexPart, fmt.Sprintf("%02X", c))
if c >= 0x20 && c <= 0x7E {
asciiPart = append(asciiPart, c)
} else {
asciiPart = append(asciiPart, '.')
}
}
sb.WriteString(strings.Join(hexPart, " "))
if len(row) < 16 {
sb.WriteString(strings.Repeat(" ", 16-len(row)))
}
sb.WriteString(" |")
sb.Write(asciiPart)
sb.WriteString("|\n")
}
return strings.TrimRight(sb.String(), "\n")
}
// WriteJSON renders a machine-readable report.
func (r *Report) WriteJSON(w io.Writer) error {
type ioctlJSON struct {
Code uint32 `json:"code"`
DeviceType uint16 `json:"device_type"`
DeviceTypeName string `json:"device_type_name"`
Function uint16 `json:"function"`
Method uint8 `json:"method"`
MethodName string `json:"method_name"`
Access uint8 `json:"access"`
AccessName string `json:"access_name"`
Confidence string `json:"confidence"`
Source string `json:"source"`
RVA uint32 `json:"rva"`
Note string `json:"note,omitempty"`
}
type stringJSON struct {
Value string `json:"value"`
Encoding string `json:"encoding"`
Offset uint32 `json:"offset"`
Category string `json:"category"`
}
type ioctlResultJSON struct {
Code uint32 `json:"code"`
Device string `json:"device"`
Success bool `json:"success"`
ErrorCode uint32 `json:"error_code,omitempty"`
Error string `json:"error,omitempty"`
BytesReturned uint32 `json:"bytes_returned,omitempty"`
Output string `json:"output_hex,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
}
type sectionResultJSON struct {
Name string `json:"name"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
RegionSize uint64 `json:"region_size,omitempty"`
FirstDword uint32 `json:"first_dword,omitempty"`
DataHex string `json:"data_hex,omitempty"`
}
out := struct {
File string `json:"file"`
Machine string `json:"machine"`
Is64 bool `json:"is_64bit"`
Entropy float64 `json:"entropy"`
Ioctls []ioctlJSON `json:"ioctls"`
RingBuffer []stringJSON `json:"shared_ring_buffer_strings"`
Devices []stringJSON `json:"device_strings"`
RuntimeDevices []string `json:"runtime_devices"`
RuntimeSections []string `json:"runtime_sections"`
IoctlResults []ioctlResultJSON `json:"ioctl_verification"`
SectionResults []sectionResultJSON `json:"section_verification"`
}{
File: r.File,
Machine: r.Machine,
Is64: r.Is64,
Entropy: r.Entropy,
RuntimeDevices: r.RuntimeDevices,
RuntimeSections: r.RuntimeSections,
}
for _, c := range r.Ioctls {
dt := extract.DeviceTypeName(c.DeviceType)
if dt == "" {
dt = fmt.Sprintf("0x%04X", c.DeviceType)
}
out.Ioctls = append(out.Ioctls, ioctlJSON{
Code: c.Code,
DeviceType: c.DeviceType,
DeviceTypeName: dt,
Function: c.Function,
Method: c.Method,
MethodName: extract.MethodName(c.Method),
Access: c.Access,
AccessName: extract.AccessName(c.Access),
Confidence: c.Confidence,
Source: c.Source,
RVA: c.RVA,
Note: c.Note,
})
}
for _, s := range r.SharedRingBufferStrings() {
out.RingBuffer = append(out.RingBuffer, stringJSON{s.Value, s.Encoding, s.Offset, s.Category})
}
for _, s := range r.DeviceStrings() {
out.Devices = append(out.Devices, stringJSON{s.Value, s.Encoding, s.Offset, s.Category})
}
for _, res := range r.IoctlResults {
out.IoctlResults = append(out.IoctlResults, ioctlResultJSON{
Code: res.Code,
Device: res.Device,
Success: res.Success,
ErrorCode: res.ErrorCode,
Error: res.Error,
BytesReturned: res.BytesReturned,
Output: hex.EncodeToString(res.Output),
TimedOut: res.TimedOut,
})
}
for _, res := range r.SectionResults {
out.SectionResults = append(out.SectionResults, sectionResultJSON{
Name: res.Name,
Success: res.Success,
Error: res.Error,
RegionSize: uint64(res.RegionSize),
FirstDword: res.FirstDword,
DataHex: hex.EncodeToString(res.Data),
})
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(out)
}