Files
nexus/internal/leviathan/binsnoop/binsnoop.go
T
2026-06-16 11:51:07 +01:00

178 lines
4.8 KiB
Go

// Package binsnoop identifies and optionally extracts files embedded inside binary data.
//
// It is a Go port of the binwalk scanning engine, implementing all magic byte signatures,
// structure parsers, and the Aho-Corasick based multi-pattern matching algorithm.
//
// # Quick Start
//
// scanner := binsnoop.New()
// data, _ := os.ReadFile("firmware.bin")
// results := scanner.Scan(data)
// for _, r := range results {
// fmt.Printf("0x%08X %s\n", r.Offset, r.Description)
// }
//
// # Extraction
//
// // Extract a specific result
// err := binsnoop.CarveResult(data, results[0], "/tmp/output")
//
// # Filtering
//
// // Only scan for specific signatures
// scanner := binsnoop.NewWithOptions(Options{Include: []string{"gzip", "squashfs"}})
package binsnoop
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Options configures the Scanner.
type Options struct {
// Include restricts scanning to only these signature names.
Include []string
// Exclude removes these signature names from scanning.
Exclude []string
// FullSearch makes short signatures match at any offset, not just offset 0.
FullSearch bool
// AdditionalSignatures adds user-defined signatures to the scanner.
AdditionalSignatures []Signature
}
// New creates a Scanner with all default signatures enabled.
func New() *Scanner {
s, _ := NewWithOptions(Options{})
return s
}
// NewWithOptions creates a Scanner with the specified options.
func NewWithOptions(opts Options) (*Scanner, error) {
s := &Scanner{
patternSigTable: make(map[int]Signature),
extractorLookup: make(map[string]*Extractor),
}
allSigs := AllSignatures()
if opts.AdditionalSignatures != nil {
allSigs = append(allSigs, opts.AdditionalSignatures...)
}
for _, sig := range allSigs {
if !includeSig(&sig, opts.Include, opts.Exclude) {
continue
}
s.signatureCount++
s.patternCount += len(sig.Magic)
// Register extractor
var ext *Extractor
if sig.Extractor != nil {
extCopy := *sig.Extractor
ext = &extCopy
}
s.extractorLookup[sig.Name] = ext
for _, pattern := range sig.Magic {
if sig.Short && !opts.FullSearch {
s.shortSignatures = append(s.shortSignatures, sig)
break
}
s.patternSigTable[len(s.patterns)] = sig
s.patterns = append(s.patterns, pattern)
}
}
s.compile()
return s, nil
}
// ScanBytes scans raw bytes for signatures and returns all matches.
func (s *Scanner) ScanBytes(data []byte) []SignatureResult {
return s.Scan(data)
}
// ScanFile reads a file from disk and scans it for signatures.
func (s *Scanner) ScanFile(path string) ([]SignatureResult, error) {
data, err := ReadFile(path)
if err != nil {
return nil, fmt.Errorf("binsnoop: read file: %w", err)
}
return s.Scan(data), nil
}
// AnalyzeBytes scans and optionally extracts from raw bytes.
func (s *Scanner) AnalyzeBytes(data []byte, filePath string, extract bool) *AnalysisResults {
results := &AnalysisResults{
FilePath: filePath,
Extractions: make(map[string]*ExtractionResult),
}
results.FileMap = s.Scan(data)
if extract && len(results.FileMap) > 0 {
results.Extractions = s.Extract(data, filePath, results.FileMap)
}
return results
}
// AnalyzeFile reads a file from disk, scans, and optionally extracts.
func (s *Scanner) AnalyzeFile(path string, extract bool) (*AnalysisResults, error) {
data, err := ReadFile(path)
if err != nil {
return nil, fmt.Errorf("binsnoop: read file: %w", err)
}
return s.AnalyzeBytes(data, path, extract), nil
}
// CarveResult extracts raw bytes for a specific signature result to a file.
func CarveResult(data []byte, result SignatureResult, outputDir string) error {
if result.Offset < 0 || result.Offset+result.Size > len(data) {
return fmt.Errorf("binsnoop: result offset/size exceeds data bounds")
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("binsnoop: mkdir: %w", err)
}
outPath := filepath.Join(outputDir, fmt.Sprintf("%x_%s.bin", result.Offset, result.Name))
return os.WriteFile(outPath, data[result.Offset:result.Offset+result.Size], 0644)
}
// SignatureCount returns the number of loaded signatures.
func (s *Scanner) SignatureCount() int { return s.signatureCount }
// PatternCount returns the total number of magic byte patterns.
func (s *Scanner) PatternCount() int { return s.patternCount }
// ListSignatures returns the names of all available signatures.
func ListSignatures() []string {
sigs := AllSignatures()
names := make([]string, len(sigs))
for i, s := range sigs {
names[i] = s.Name
}
return names
}
func includeSig(sig *Signature, include, exclude []string) bool {
if len(include) > 0 {
for _, inc := range include {
if strings.EqualFold(sig.Name, inc) {
return true
}
}
return false
}
if len(exclude) > 0 {
for _, exc := range exclude {
if strings.EqualFold(sig.Name, exc) {
return false
}
}
}
return true
}