ONNX
security
malware-detection
Vigil / source /pkg /bundle /analyzer_binary.go
turentomer's picture
Publish self-contained Vigil distribution
d2507b5 verified
Raw
History Blame Contribute Delete
5.79 kB
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
}