package bundle import ( "bytes" "debug/elf" "debug/macho" "strings" ) // BinaryAnalyzer inspects native binaries (.so/.dylib/.node, magic-sniffed // ELF/Mach-O) and .wasm. It confirms the format via debug/elf + debug/macho, // pulls section/symbol/import names and printable strings, and scans them for // exfil host / env-var names / dangerous symbols. It ALWAYS emits // 'ships-opaque-executable' (Opaque, Structural) but is PRECISION-AWARE: base // SevMedium (native code is common in legit skills), raised to SevHigh+ // Corroborated only when suspicious strings/symbols, hidden placement, or // padding are present, and SevCritical when the defanged exfil host appears. // isKnownBenignNativePattern downgrades signed/known wheels. Never panics on // truncated headers. type BinaryAnalyzer struct{} func (BinaryAnalyzer) Name() string { return "binary" } func (BinaryAnalyzer) Handles(kind FileKind) bool { return kind == KindNativeBinary || kind == KindWasm } // dangerousSymbols are import/symbol names that, present in a shipped binary, // indicate it can read env, open sockets, load code, or shell out. var dangerousSymbols = []string{ "dlopen", "system", "popen", "execve", "execl", "fork", "getenv", "secure_getenv", "socket", "connect", "sendto", "curl_easy", "ptrace", "mprotect", "ld_preload", "createprocess", "winexec", "urldownloadtofile", "winhttp", } func (BinaryAnalyzer) Analyze(f *File, b *Bundle) ([]Finding, error) { if f == nil { return nil, nil } strs := printableStrings(f.Sniff, 4) syms := extractBinarySymbols(f.Sniff, f.Kind) hay := strings.ToLower(strings.Join(strs, "\n") + "\n" + strings.Join(syms, "\n")) // Base structural finding: native code is opaque. base := Finding{ Analyzer: "binary", File: f.RelPath, Signal: "ships-opaque-executable", Severity: SevMedium, Detail: "ships a native binary / wasm module that cannot be fully analyzed", Opaque: true, Structural: true, } var out []Finding var corroborated bool // Exfil host inside the binary => critical. if exfilHostRe.MatchString(hay) { out = append(out, Finding{ Analyzer: "binary", File: f.RelPath, Signal: "exfil-host-reference", Severity: SevCritical, Detail: "native binary embeds known exfiltration host", Corroborated: true, }) base.Severity = SevCritical base.Corroborated = true corroborated = true } // Dangerous symbols/imports + an env/secret string => suspicious native code. hasDangerousSym := matchedAny(hay, dangerousSymbols) hasEnvString := matchedAny(hay, envSourceTerms) if hasDangerousSym && hasEnvString { out = append(out, Finding{ Analyzer: "binary", File: f.RelPath, Signal: "suspicious-native-symbols", Severity: SevHigh, Detail: "native binary imports process/network/dlopen symbols and embeds credential/env strings", Corroborated: true, }) if base.Severity < SevHigh { base.Severity = SevHigh base.Corroborated = true } corroborated = true } // Hidden placement or padding are corroborating factors on their own. if f.Hidden && !corroborated { base.Severity = SevHigh base.Corroborated = true base.Detail += " (hidden placement)" } if f.Truncated && f.NewlineRatio >= 0.30 { out = append(out, Finding{ Analyzer: "binary", File: f.RelPath, Signal: "padding-evasion", Severity: SevHigh, Detail: "binary exceeded read cap with a high newline ratio (padding)", Corroborated: true, }) } // Known-benign native pattern downgrades the structural finding (but never a // real exfil-host hit, which already set Critical above). if base.Severity == SevMedium && isKnownBenignNativePattern(f) { base.Severity = SevLow base.Detail += " (matches known-benign native pattern)" } out = append(out, base) return dedupeFindings(out), nil } // extractBinarySymbols confirms the format and pulls section/symbol/import names // from ELF and Mach-O. Degrades to nil on truncated/invalid headers (never // panics). WASM has no symbol table here; printable strings cover it. func extractBinarySymbols(data []byte, kind FileKind) []string { var out []string defer func() { _ = recover() }() r := bytes.NewReader(data) if ef, err := elf.NewFile(r); err == nil { for _, s := range ef.Sections { out = append(out, s.Name) } if syms, err := ef.ImportedSymbols(); err == nil { for _, s := range syms { out = append(out, s.Name) } } if libs, err := ef.ImportedLibraries(); err == nil { out = append(out, libs...) } if dyn, err := ef.DynString(elf.DT_NEEDED); err == nil { out = append(out, dyn...) } return dedupeStrings(out) } r2 := bytes.NewReader(data) if mf, err := macho.NewFile(r2); err == nil { for _, s := range mf.Sections { out = append(out, s.Name) } if mf.Symtab != nil { for _, s := range mf.Symtab.Syms { out = append(out, s.Name) } } if libs, err := mf.ImportedLibraries(); err == nil { out = append(out, libs...) } if syms, err := mf.ImportedSymbols(); err == nil { out = append(out, syms...) } return dedupeStrings(out) } return out } // printableStrings extracts runs of >= minRun printable ASCII bytes from a blob, // like the unix `strings` tool. Bounded by the input length and a result cap. func printableStrings(data []byte, minRun int) []string { if minRun < 1 { minRun = 4 } var out []string var cur []byte const maxResults = 20000 flush := func() { if len(cur) >= minRun { out = append(out, string(cur)) } cur = cur[:0] } for _, bb := range data { if bb >= 0x20 && bb < 0x7f { cur = append(cur, bb) continue } flush() if len(out) >= maxResults { break } } flush() return out }