| package bundle |
|
|
| import ( |
| "bytes" |
| "debug/elf" |
| "debug/macho" |
| "strings" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| type BinaryAnalyzer struct{} |
|
|
| func (BinaryAnalyzer) Name() string { return "binary" } |
|
|
| func (BinaryAnalyzer) Handles(kind FileKind) bool { |
| return kind == KindNativeBinary || kind == KindWasm |
| } |
|
|
| |
| |
| 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 := 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 |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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, |
| }) |
| } |
|
|
| |
| |
| if base.Severity == SevMedium && isKnownBenignNativePattern(f) { |
| base.Severity = SevLow |
| base.Detail += " (matches known-benign native pattern)" |
| } |
|
|
| out = append(out, base) |
| return dedupeFindings(out), nil |
| } |
|
|
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| 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 |
| } |
|
|