File size: 5,793 Bytes
d2507b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | 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
}
|