#![cfg_attr(
test,
allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
clippy::indexing_slicing
)
)]
pub mod detection;
pub mod formats;
pub mod metadata;
pub mod structures;
use crate::{
detection::{
Confidence, ConfidenceReport, ConfidenceSignal, ParseError, VersionSource, find_bytes,
heuristic_hits,
},
formats::{BinaryContext, BinaryFormat},
metadata::{BuildInfo, Compiler, FipsInfo, FunctionIter, InitFunc, InitTask, ObfuscationKind},
structures::{
Arch, PclntabVersion, buildid, buildinfo, embed, gcprog,
goslice::{GoSlice, GoStr},
inittask, inline, itab,
locate::ModuledataLocator,
moduledata::{LayoutHints, ModuleHash, Moduledata, ModuledataVersion, PtabEntry, TextSect},
name::decode_name,
pclntab::{self, FuncData, ParsedPclntab, PclntabMeta},
strings as gostrings, types,
util::read_i32,
},
};
pub struct GoBinary<'a> {
report: ConfidenceReport,
ctx: BinaryContext<'a>,
build_id: Option<&'a str>,
build_info: Option<BuildInfo<'a>>,
pclntab_meta: Option<PclntabMeta>,
go_version: Option<&'a str>,
moduledata: Option<Moduledata>,
}
impl<'a> GoBinary<'a> {
pub fn parse(data: &'a [u8]) -> Option<Self> {
Self::try_parse(data).ok()
}
pub fn try_parse(data: &'a [u8]) -> Result<Self, ParseError> {
let ctx = BinaryContext::new(data);
let mut report = ConfidenceReport::empty();
let sections = ctx.sections();
if sections.has_gopclntab {
report.push(ConfidenceSignal::GopclntabSectionPresent);
report.raise_to(Confidence::High);
}
if sections.has_go_buildinfo {
report.push(ConfidenceSignal::BuildinfoSectionPresent);
report.raise_to(Confidence::High);
}
if sections.has_go_buildid_note {
report.push(ConfidenceSignal::BuildidNotePresent);
report.raise_to(Confidence::High);
}
for (present, section) in [
(sections.typelink.is_some(), ".typelink"),
(sections.itablink.is_some(), ".itablink"),
(sections.go_type.is_some(), ".go.type"),
(sections.go_func.is_some(), ".go.func"),
] {
if present {
report.push(ConfidenceSignal::TypeSectionPresent { section });
report.raise_to(Confidence::High);
}
}
let build_id = buildid::extract(&ctx);
if build_id.is_some() {
report.push(ConfidenceSignal::BuildIdMarkerFound);
report.raise_to(Confidence::High);
}
let build_info_result = buildinfo::extract(&ctx);
match build_info_result.as_ref() {
Some(_) => {
report.push(ConfidenceSignal::BuildinfoParsed);
report.raise_to(Confidence::Medium);
}
None => {
report.push(ConfidenceSignal::BuildinfoMissing {
reason: "no buildinfo magic header found",
});
}
}
let pclntab_meta = pclntab::parse(&ctx).as_ref().map(ParsedPclntab::meta);
match pclntab_meta {
Some(m) => {
report.push(ConfidenceSignal::PclntabParsed {
version: m.version,
nfunc: m.nfunc,
});
report.raise_to(Confidence::High);
}
None => {
report.push(ConfidenceSignal::PclntabMissing {
reason: "no pclntab magic / structural pattern matched",
});
}
}
let (go_version, version_source): (Option<&'a str>, VersionSource) =
match build_info_result.as_ref().and_then(|bi| bi.go_version) {
Some(v) => (Some(v), VersionSource::BuildInfoBlob),
None => (
buildinfo::find_version_string(data),
VersionSource::StringScan,
),
};
if let Some(v) = go_version {
report.push(ConfidenceSignal::GoVersionString {
version: v.to_string(),
source: version_source,
});
report.raise_to(Confidence::Medium);
}
if report.tier == Confidence::None {
let hits = heuristic_hits(data);
if hits >= 3 {
report.push(ConfidenceSignal::HeuristicStringsMatched { hits });
report.raise_to(Confidence::Low);
}
}
if report.tier == Confidence::None {
return Err(ParseError::NotAGoBinary { report });
}
let moduledata = pclntab_meta.and_then(|meta| {
let pcl = meta.attach(ctx.structure_search_data())?;
find_moduledata(&ctx, &pcl, go_version)
});
Ok(GoBinary {
report,
ctx,
build_id,
build_info: build_info_result,
pclntab_meta,
go_version,
moduledata,
})
}
pub fn data(&self) -> &'a [u8] {
self.ctx.data()
}
pub fn context(&self) -> &BinaryContext<'a> {
&self.ctx
}
pub fn confidence(&self) -> Confidence {
self.report.tier
}
pub fn report(&self) -> &ConfidenceReport {
&self.report
}
pub fn go_version(&self) -> Option<&'a str> {
self.go_version
}
pub fn build_id(&self) -> Option<&'a str> {
self.build_id
}
pub fn build_info(&self) -> Option<&BuildInfo<'a>> {
self.build_info.as_ref()
}
pub fn pclntab(&self) -> Option<ParsedPclntab<'_>> {
self.pclntab_meta?.attach(self.ctx.structure_search_data())
}
pub fn functions(&self) -> FunctionIter<'_> {
FunctionIter::new(self.pclntab())
}
pub fn inline_tree(&self, func: &FuncData) -> inline::InlineTreeIter<'_> {
let pcl = match self.pclntab() {
Some(p) => p,
None => return inline::InlineTreeIter::empty(),
};
inline::extract_iter(&self.ctx, pcl, self.moduledata.as_ref(), func)
}
pub fn moduledata(&self) -> Option<&Moduledata> {
self.moduledata.as_ref()
}
pub fn modules(&self) -> Vec<Moduledata> {
const MAX_MODULES: usize = 1024;
let mut out = Vec::new();
let first = match self.moduledata.clone() {
Some(m) => m,
None => return out,
};
let mut next_va = first.next;
let mut seen: Vec<u64> = vec![first.pc_header];
out.push(first);
while next_va != 0 && out.len() < MAX_MODULES {
if seen.contains(&next_va) {
break; }
seen.push(next_va);
let md = match self.parse_moduledata_at(next_va) {
Some(m) => m,
None => break,
};
next_va = md.next;
out.push(md);
}
out
}
fn parse_moduledata_at(&self, va: u64) -> Option<Moduledata> {
let meta = self.pclntab_meta?;
let bytes = self.ctx.slice_at_va(va)?;
let hints = layout_hints(&self.ctx, meta.version, self.go_version);
Moduledata::parse(bytes, meta.ptr_size, hints)
}
pub fn text_va(&self) -> Option<u64> {
if let Some(m) = self.moduledata.as_ref() {
return Some(m.text);
}
if let Some(pcl) = self.pclntab() {
if let Some(va) = pcl.header_text_start {
return Some(va);
}
if pcl.text_start != 0 {
return Some(pcl.text_start);
}
}
self.ctx
.sections()
.text_section
.map(|s| s.va)
.filter(|&va| va != 0)
}
pub fn entry_va(&self, func: &FuncData) -> Option<u64> {
self.text_va()?.checked_add(u64::from(func.entry_off))
}
pub fn entry_rva(&self, func: &FuncData) -> Option<u64> {
self.entry_va(func)?.checked_sub(self.ctx.image_base())
}
pub fn etext_va(&self) -> Option<u64> {
self.moduledata.as_ref().map(|m| m.etext)
}
pub fn is_main_module(&self) -> Option<bool> {
self.moduledata.as_ref().map(|m| m.has_main)
}
pub fn data_pointer_map(&self) -> Option<gcprog::PointerMap> {
let md = self.moduledata.as_ref()?;
self.pointer_map(md.gcdata, md.data)
}
pub fn bss_pointer_map(&self) -> Option<gcprog::PointerMap> {
let md = self.moduledata.as_ref()?;
self.pointer_map(md.gcbss, md.bss)
}
fn pointer_map(
&self,
prog_va: u64,
segment: structures::moduledata::VaRange,
) -> Option<gcprog::PointerMap> {
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
if ptr_size == 0 || prog_va == 0 || segment.is_empty() {
return None;
}
let max_words = usize::try_from(segment.size().checked_div(u64::from(ptr_size))?).ok()?;
let prog = self.ctx.slice_at_va(prog_va)?;
let words = gcprog::run_gc_prog(prog, max_words);
Some(gcprog::PointerMap {
base_va: segment.start,
ptr_size,
words,
})
}
pub fn module_name(&self) -> Option<&str> {
self.resolve_go_str(self.moduledata.as_ref()?.modulename)
}
pub fn plugin_path(&self) -> Option<&str> {
self.resolve_go_str(self.moduledata.as_ref()?.pluginpath)
}
pub fn is_coverage_build(&self) -> bool {
self.moduledata
.as_ref()
.and_then(|m| m.covctrs)
.is_some_and(|r| !r.is_empty())
}
fn resolve_go_str(&self, s: GoStr) -> Option<&str> {
if s.is_empty() {
return None;
}
let bytes = self.ctx.slice_at_va(s.ptr)?.get(..s.len as usize)?;
std::str::from_utf8(bytes).ok()
}
pub fn text_sections(&self) -> Vec<TextSect> {
let md = match self.moduledata.as_ref() {
Some(m) => m,
None => return Vec::new(),
};
let ps = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
decode_slice(&self.ctx, &md.textsectmap, ps, TextSect::size(ps), |buf| {
TextSect::parse(buf, 0, ps)
})
}
pub fn plugin_exports(&self) -> Vec<PtabEntry<'_>> {
let md = match self.moduledata.as_ref() {
Some(m) => m,
None => return Vec::new(),
};
let ps = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
let types_base = md.types;
let legacy = self.legacy_names();
decode_slice(&self.ctx, &md.ptab, ps, 8, |buf| {
let name_off = read_i32(buf, 0)?;
let type_offset = read_i32(buf, 4)?;
let name = self.resolve_name_off(types_base, name_off, legacy);
Some(PtabEntry { name, type_offset })
})
}
pub fn package_hashes(&self) -> Vec<ModuleHash<'_>> {
let md = match self.moduledata.as_ref() {
Some(m) => m,
None => return Vec::new(),
};
self.decode_module_hashes(&md.pkghashes)
}
pub fn module_hashes(&self) -> Vec<ModuleHash<'_>> {
let md = match self.moduledata.as_ref() {
Some(m) => m,
None => return Vec::new(),
};
self.decode_module_hashes(&md.modulehashes)
}
fn decode_module_hashes(&self, slice: &GoSlice) -> Vec<ModuleHash<'_>> {
let ps = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
if ps == 0 {
return Vec::new();
}
let entry_size = (ps as usize).saturating_mul(5);
let p = ps as usize;
decode_slice(&self.ctx, slice, ps, entry_size, |buf| {
let module_name = GoStr::parse(buf, 0, ps).and_then(|s| self.resolve_go_str(s));
let linktime_hash =
GoStr::parse(buf, p.checked_mul(2)?, ps).and_then(|s| self.resolve_go_str(s));
Some(ModuleHash {
module_name,
linktime_hash,
})
})
}
fn legacy_names(&self) -> bool {
self.pclntab_meta
.map(|m| matches!(m.version, PclntabVersion::Go116 | PclntabVersion::Go12))
.unwrap_or(false)
}
fn resolve_name_off(&self, types_base: u64, name_off: i32, legacy: bool) -> Option<&str> {
if name_off == 0 {
return None;
}
let name_va = (types_base as i64).saturating_add(name_off as i64) as u64;
let bytes = self.ctx.slice_at_va(name_va)?;
decode_name(bytes, legacy).filter(|s| !s.is_empty())
}
pub fn arch(&self) -> Arch {
if self.ctx.format() == BinaryFormat::Wasm {
return Arch::Wasm;
}
if let Some(p) = self.pclntab() {
return p.arch();
}
match self.build_info.as_ref().and_then(BuildInfo::goarch) {
Some("386") => Arch::X86,
Some("amd64") => Arch::X86_64,
Some("arm") => Arch::Arm,
Some("arm64") => Arch::Arm64,
Some("mips") | Some("mipsle") => Arch::Mips32,
Some("mips64") | Some("mips64le") => Arch::Mips64,
Some("ppc64") | Some("ppc64le") => Arch::Ppc64,
Some("riscv64") => Arch::RiscV,
Some("s390x") => Arch::S390x,
Some("wasm") => Arch::Wasm,
_ => Arch::Unknown,
}
}
pub fn compiler(&self) -> Compiler {
if let Some(info) = self.build_info.as_ref() {
match info.setting("-compiler") {
Some("gc") => return Compiler::Gc,
Some("gccgo") => return Compiler::Gccgo,
Some("tinygo") => return Compiler::TinyGo,
_ => {}
}
}
if self
.go_version
.map(|v| v.to_ascii_lowercase().contains("tinygo"))
.unwrap_or(false)
{
return Compiler::TinyGo;
}
if self.pclntab_meta.is_some() {
return Compiler::Gc;
}
Compiler::Unknown
}
pub fn obfuscation(&self) -> ObfuscationKind {
if self.pclntab_meta.is_none() {
return ObfuscationKind::None;
}
let mut user_pkgs = std::collections::BTreeSet::new();
for f in self.functions() {
if f.is_runtime() || f.is_internal() {
continue;
}
if let Some(pkg) = f.package() {
user_pkgs.insert(pkg.to_string());
}
}
if user_pkgs.is_empty() {
return ObfuscationKind::None;
}
let total = user_pkgs.len();
let obfuscated = user_pkgs.iter().filter(|p| is_garble_token(p)).count();
let ratio = obfuscated as f32 / total as f32;
let deps_scrubbed = self
.build_info
.as_ref()
.map(|i| i.deps.is_empty())
.unwrap_or(true);
let confidence = if ratio >= 0.5 && deps_scrubbed {
Confidence::High
} else if ratio >= 0.5 {
Confidence::Medium
} else if ratio >= 0.2 {
Confidence::Low
} else {
return ObfuscationKind::None;
};
ObfuscationKind::Garble { confidence }
}
pub fn is_likely_garbled(&self) -> bool {
matches!(self.obfuscation(), ObfuscationKind::Garble { .. })
}
pub fn runtime_commit(&self) -> Option<&str> {
let v = self.go_version?;
let bytes = v.as_bytes();
let mut i: usize = 0;
while let Some(&byte) = bytes.get(i) {
if byte == b'-' {
let start = i.checked_add(1)?;
let mut end = start;
while bytes.get(end).is_some_and(|b| b.is_ascii_hexdigit()) {
end = end.checked_add(1)?;
}
if end.checked_sub(start)? >= 7 {
return v.get(start..end);
}
i = end;
} else {
i = i.checked_add(1)?;
}
}
None
}
pub fn fips_info(&self) -> Option<FipsInfo<'a>> {
let info = self.build_info.as_ref()?;
let version = info.setting("GOFIPS140")?;
let enforced_by_default = info
.setting("DefaultGODEBUG")
.is_some_and(|v| v.split(',').any(|kv| kv == "fips140=on"));
let module_sum = self.fips_module_sum();
Some(FipsInfo {
version,
enforced_by_default,
module_sum,
})
}
fn fips_module_sum(&self) -> Option<[u8; 32]> {
const EXPECTED_MAGIC: &[u8; 16] = b"\xff Go fipsinfo \xff\x00";
let range = self.ctx.sections().fipsinfo.as_ref()?;
let data = self.ctx.section_data(range)?;
if data.get(0..16)? != EXPECTED_MAGIC.as_slice() {
return None;
}
let mut sum = [0u8; 32];
sum.copy_from_slice(data.get(16..48)?);
Some(sum)
}
pub fn embedded_assets(&self) -> Vec<embed::EmbeddedAsset<'_>> {
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
if ptr_size == 0 {
return Vec::new();
}
embed::extract(&self.ctx, ptr_size)
}
pub fn init_order(&self) -> Vec<InitTask<'_>> {
let md = match self.moduledata.as_ref() {
Some(m) => m,
None => return Vec::new(),
};
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
let raw = inittask::decode(&self.ctx, md, ptr_size);
if raw.is_empty() {
return Vec::new();
}
let text_va = self.text_va();
let entry_off_of =
|pc_va: u64| -> Option<u32> { u32::try_from(pc_va.checked_sub(text_va?)?).ok() };
let mut wanted: Vec<u32> = raw
.iter()
.flatten()
.filter_map(|pc| entry_off_of(*pc))
.collect();
wanted.sort_unstable();
wanted.dedup();
let mut names: Vec<(u32, &str)> = Vec::with_capacity(wanted.len());
if !wanted.is_empty()
&& let Some(pcl) = self.pclntab()
{
for (entry_off, func_off) in pcl.func_entries() {
if wanted.binary_search(&entry_off).is_err() {
continue;
}
if let Some(func) = pcl.parse_func(func_off)
&& let Some(name) = pcl.func_name(func.name_off as u32)
{
names.push((entry_off, name));
}
}
names.sort_unstable_by_key(|(off, _)| *off);
}
let resolve = |pc_va: u64| -> Option<&str> {
let off = entry_off_of(pc_va)?;
names
.binary_search_by_key(&off, |(o, _)| *o)
.ok()
.and_then(|i| names.get(i))
.map(|(_, n)| *n)
};
raw.into_iter()
.map(|pcs| {
let functions: Vec<InitFunc<'_>> = pcs
.into_iter()
.map(|pc| InitFunc {
entry_va: pc,
name: resolve(pc),
})
.collect();
let package = functions
.iter()
.find_map(|f| f.name)
.and_then(metadata::package_of);
InitTask { package, functions }
})
.collect()
}
pub fn itab_pairs(&self) -> itab::ItabIter<'_> {
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
itab::extract_iter(&self.ctx, ptr_size, self.moduledata.as_ref())
}
pub fn itab_methods(&self, pair: &itab::ItabPair) -> Vec<u64> {
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
itab::itab_methods(&self.ctx, pair, ptr_size)
}
pub fn has_cgo(&self) -> bool {
self.functions().any(|f| is_cgo_runtime_fn(f.name))
}
pub fn uses_concurrency(&self) -> bool {
self.functions().any(|f| is_concurrency_runtime_fn(f.name))
}
pub fn types(&self) -> types::TypeIter<'_> {
let (Some(meta), Some(md)) = (self.pclntab_meta, self.moduledata.as_ref()) else {
return types::TypeIter::empty(&self.ctx);
};
types::extract_types_iter(&self.ctx, md, self.type_abi(meta))
}
pub fn type_at(&self, va: u64) -> Option<types::GoType<'_>> {
let meta = self.pclntab_meta?;
let types_base = self.moduledata.as_ref()?.types;
types::type_at_va(&self.ctx, va, types_base, self.type_abi(meta))
}
pub fn all_types(&self) -> Vec<types::GoType<'_>> {
let meta = match self.pclntab_meta {
Some(m) => m,
None => return Vec::new(),
};
let (types_base, etypes) = match self.moduledata.as_ref() {
Some(m) => (m.types, m.etypes),
None => return Vec::new(),
};
types::extract_all_types(
&self.ctx,
self.types().collect(),
types_base,
etypes,
self.type_abi(meta),
)
}
fn type_abi(&self, meta: PclntabMeta) -> types::TypeAbi {
let md_version = self.moduledata.as_ref().map(|m| m.version);
types::TypeAbi {
ps: meta.ptr_size,
legacy_names: self.legacy_names(),
map_layout: types::MapLayout::infer(
self.go_version().and_then(parse_go_minor_version),
meta.version == PclntabVersion::Go120,
md_version == Some(ModuledataVersion::V5),
md_version == Some(ModuledataVersion::V4),
),
}
}
pub fn strings(&self) -> gostrings::GoStringIter<'_> {
let ptr_size = self.pclntab_meta.map(|m| m.ptr_size).unwrap_or(0);
gostrings::extract_iter(&self.ctx, self.moduledata.as_ref(), ptr_size)
}
}
pub fn detect(data: &[u8]) -> bool {
if find_bytes(data, b"\xff Go buildinf:").is_some() {
return true;
}
if find_bytes(data, b"\xff Go build ID:").is_some() {
return true;
}
const PCLNTAB_MAGICS: &[[u8; 4]] = &[
[0xf1, 0xff, 0xff, 0xff],
[0xf0, 0xff, 0xff, 0xff],
[0xfa, 0xff, 0xff, 0xff],
[0xfb, 0xff, 0xff, 0xff],
];
let limit = data.len().saturating_sub(4);
let mut offset: usize = 0;
while offset <= limit {
let end = match offset.checked_add(4) {
Some(e) => e,
None => break,
};
let m = match data.get(offset..end) {
Some(s) => s,
None => break,
};
for magic in PCLNTAB_MAGICS {
if m == magic {
return true;
}
}
offset = match offset.checked_add(4) {
Some(o) => o,
None => break,
};
}
false
}
const CGO_RUNTIME_FNS: &[&str] = &[
"runtime.cgocall",
"runtime.cgocallback",
"runtime.cgocall_native",
"runtime.asmcgocall",
"runtime.cgoCheckPointer",
"runtime._cgo_panic",
];
const CONCURRENCY_RUNTIME_FNS: &[&str] = &[
"runtime.newproc",
"runtime.chansend",
"runtime.chansend1",
"runtime.chanrecv",
"runtime.chanrecv1",
"runtime.chanrecv2",
"runtime.closechan",
"runtime.selectgo",
];
fn is_cgo_runtime_fn(name: &str) -> bool {
CGO_RUNTIME_FNS.contains(&name)
}
fn is_concurrency_runtime_fn(name: &str) -> bool {
CONCURRENCY_RUNTIME_FNS.contains(&name)
}
fn is_garble_token(pkg: &str) -> bool {
if pkg.contains('/') || pkg.contains('.') {
return false;
}
let len = pkg.len();
if !(8..=16).contains(&len) {
return false;
}
let mut has_digit = false;
for b in pkg.bytes() {
if !(b.is_ascii_alphanumeric() || b == b'_') {
return false;
}
if b.is_ascii_digit() {
has_digit = true;
}
}
has_digit
}
fn decode_slice<T>(
ctx: &BinaryContext<'_>,
slice: &structures::goslice::GoSlice,
ps: u8,
entry_size: usize,
mut parse: impl FnMut(&[u8]) -> Option<T>,
) -> Vec<T> {
const MAX_ENTRIES: u64 = 10_000_000;
if ps == 0 || entry_size == 0 || slice.len == 0 || slice.len > MAX_ENTRIES {
return Vec::new();
}
let arr = match ctx.slice_at_va(slice.ptr) {
Some(a) => a,
None => return Vec::new(),
};
let n = slice.len as usize;
let mut out = Vec::with_capacity(n.min(1024));
for i in 0..n {
let off = match i.checked_mul(entry_size) {
Some(o) => o,
None => break,
};
let end = match off.checked_add(entry_size) {
Some(e) => e,
None => break,
};
let buf = match arr.get(off..end) {
Some(b) => b,
None => break,
};
if let Some(item) = parse(buf) {
out.push(item);
}
}
out
}
fn parse_go_minor_version(version: &str) -> Option<u32> {
let rest = version.strip_prefix("go1.")?;
let minor_str = rest
.split(|c: char| !c.is_ascii_digit())
.next()
.filter(|s| !s.is_empty())?;
minor_str.parse().ok()
}
fn find_moduledata(
ctx: &BinaryContext<'_>,
pclntab: &ParsedPclntab<'_>,
go_version: Option<&str>,
) -> Option<Moduledata> {
let hints = layout_hints(ctx, pclntab.version, go_version);
ModuledataLocator::new(ctx, pclntab.ptr_size, pclntab.offset, hints).locate()
}
fn layout_hints(
ctx: &BinaryContext<'_>,
pclntab_version: PclntabVersion,
go_version: Option<&str>,
) -> LayoutHints {
let sections = ctx.sections();
LayoutHints {
pclntab_version,
go_minor: go_version.and_then(parse_go_minor_version),
has_typelink_section: sections.typelink.is_some(),
has_go_type_section: sections.go_type.is_some(),
}
}