diff --git a/plugins/workflow_go/Cargo.toml b/plugins/workflow_go/Cargo.toml new file mode 100644 index 000000000..385b949da --- /dev/null +++ b/plugins/workflow_go/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "binja_go" +version = "0.1.0" +edition = "2024" +license = "BSD-3-Clause" +publish = false + +[dependencies] +binaryninja = { workspace = true } +binaryninjacore-sys.workspace = true +anyhow = "1.0.103" +tracing = "0.1.44" + +[lib] +crate-type = ["cdylib"] + diff --git a/plugins/workflow_go/build.rs b/plugins/workflow_go/build.rs new file mode 100644 index 000000000..9006f16a6 --- /dev/null +++ b/plugins/workflow_go/build.rs @@ -0,0 +1,25 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(target_os = "linux")] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + #[cfg(target_os = "macos")] + { + let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set"); + let lib_name = crate_name.replace('-', "_"); + println!( + "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib", + lib_name + ); + } +} diff --git a/plugins/workflow_go/src/activities/calling_convention.rs b/plugins/workflow_go/src/activities/calling_convention.rs new file mode 100644 index 000000000..822ace796 --- /dev/null +++ b/plugins/workflow_go/src/activities/calling_convention.rs @@ -0,0 +1,164 @@ +use binaryninja::architecture::{Architecture, ArchitectureExt, CoreRegister, Register}; +use binaryninja::binary_view::AnalysisContext; +use binaryninja::low_level_il::VisitorAction; +use binaryninja::low_level_il::expression::LowLevelILExpressionKind::Load; +use binaryninja::low_level_il::expression::{ + ExpressionHandler, LowLevelILExpression, LowLevelILExpressionKind, ValueExpr, +}; +use binaryninja::low_level_il::function::{Mutable, NonSSA}; +use binaryninja::low_level_il::instruction::InstructionHandler; +use binaryninja::low_level_il::instruction::LowLevelILInstructionKind; + +/// The workflow runs over the functions of a golang binary and applies the correct +/// Go calling convention, so that register-passed arguments are recovered as +/// parameters instead of appearing as spurious register reads. +/// +/// Go uses two ABIs: the register-based `ABIInternal` (go1.17+) and the older +/// stack-based `ABI0`. Binary Ninja ships neither, so we register both as custom +/// conventions (`go-abiinternal` / `go-stack`, per architecture) and select one +/// per function. +/// +/// The selection works in this way: thunks are skipped (they are just trampolines +/// with no convention of their own); if the function name ends with ".abi0" we +/// apply `go-stack`; otherwise we default to the register-based `go-abiinternal`. +pub struct GoCallingConventionWorkflow {} + +impl GoCallingConventionWorkflow { + /// Apply the workflow + pub fn apply(ctx: &AnalysisContext) { + let view = ctx.view(); + + // runs this workflow only under golang binaries + let is_go = view + .query_metadata("go_workflow.is_go") + .and_then(|m| m.get_boolean()) + .unwrap_or(false); + + if !is_go { + return; + } + + let func = ctx.function(); + + unsafe { + // thunk functions are trampolines for the external functions and for that we + // do not require to set a specific golang calling convention + if Self::is_thunk(ctx) { + return; + } + } + + // 1) if the function has a symbol and ends with ".abi0", then we apply the go-stack + // convention + let name = func.symbol().full_name(); + let name = name.to_str().unwrap_or_default().to_ascii_lowercase(); + + let cc_name = match name.as_str() { + // explicit ABI0 wrapper + n if n.ends_with(".abi0") => "go-stack", + // stripped / no usable name: fall back to the stack-read heuristic + n if n.is_empty() || n.starts_with("sub_") => { + match unsafe { Self::reads_args_from_stack(ctx) } { + true => "go-stack", + false => "go-abiinternal", + } + } + // named non-.abi0 function: register-based + _ => "go-abiinternal", + }; + + let arch = func.arch(); + let Some(cc) = arch.calling_convention_by_name(cc_name) else { + tracing::warn!( + "go: CC '{cc_name}' not registered on architecture {}", + arch.name() + ); + return; + }; + + func.set_auto_calling_convention(Some(&cc)); + } + + /// Heuristic: does the first basic block read an incoming argument from + /// the stack? A stack-based ABI0 function loads its arguments from `[sp + off]`; + /// a register-based ABIInternal one receives them in registers. + /// + /// Fallback for when the ".abi0" name suffix is missing (stripped binary). + unsafe fn reads_args_from_stack(ctx: &AnalysisContext) -> bool { + unsafe { + let Some(llil) = ctx.llil_function() else { + return false; + }; + let blocks = llil.basic_blocks(); + let Some(first) = blocks.iter().next() else { + return false; + }; + + let arch = ctx.function().arch(); + let Some(sp) = arch.stack_pointer_reg() else { + return false; + }; + + first.iter().any(|instr| { + let mut found = false; + instr.visit_tree(&mut |e| { + if let Load(op) = e.kind() + && Self::addr_is_stack(&op.source_expr(), sp) + { + found = true; + return VisitorAction::Halt; + }; + VisitorAction::Descend + }); + found + }) + } + } + + /// True if an address expression is `sp` or `sp +/- const`. + fn addr_is_stack( + addr: &LowLevelILExpression, + sp: CoreRegister, + ) -> bool { + match addr.kind() { + LowLevelILExpressionKind::Reg(op) => op.source_reg().id() == sp.id(), + LowLevelILExpressionKind::Add(op) | LowLevelILExpressionKind::Sub(op) => { + Self::side_is_sp(&op.left(), sp) || Self::side_is_sp(&op.right(), sp) + } + _ => false, + } + } + + fn side_is_sp(e: &LowLevelILExpression, sp: CoreRegister) -> bool { + matches!(e.kind(), LowLevelILExpressionKind::Reg(op) if op.source_reg().id() == sp.id()) + } + + /// Check if a specified function at low level is a thunk function. From python source [1], we + /// define a thunk as a function that has a single basic block and ends with a tail call. + /// + /// Returns true if the llil function from the analysis context is a thunk. + /// + /// [1]: https://github.com/Vector35/binaryninja-api/blob/5b9a08ec4c061718097d9eb0a40e8a6b9774391d/python/lowlevelil.py#L3667 + unsafe fn is_thunk(ctx: &AnalysisContext) -> bool { + unsafe { + let Some(llil) = ctx.llil_function() else { + return false; + }; + + let blocks = llil.basic_blocks(); + let mut it = blocks.iter(); + + // only one basic block + let (Some(block), None) = (it.next(), it.next()) else { + return false; + }; + + // grep the last instruction of the basic block + let Some(last) = block.iter().last() else { + return false; + }; + + matches!(last.kind(), LowLevelILInstructionKind::TailCall(_)) + } + } +} diff --git a/plugins/workflow_go/src/activities/mod.rs b/plugins/workflow_go/src/activities/mod.rs new file mode 100644 index 000000000..28cefa014 --- /dev/null +++ b/plugins/workflow_go/src/activities/mod.rs @@ -0,0 +1,5 @@ +/// Exports the workflow that applies the calling conventions under x86 and arm +pub mod calling_convention; + +/// Exports the workflow for adjusting the string parameter on the arguments +pub mod string_argument; diff --git a/plugins/workflow_go/src/activities/string_argument.rs b/plugins/workflow_go/src/activities/string_argument.rs new file mode 100644 index 000000000..8ba15aaa7 --- /dev/null +++ b/plugins/workflow_go/src/activities/string_argument.rs @@ -0,0 +1,81 @@ +use binaryninja::binary_view::{AnalysisContext, BinaryView, BinaryViewBase}; +use binaryninja::high_level_il::{ + HighLevelILLiftedInstruction as LInstr, HighLevelILLiftedInstructionKind as LK, + HighLevelILLiftedOperand as LOp, +}; +use binaryninja::types::Type; + +/// Runs over the HLIL of a golang binary and caps the length of string literals +/// passed as call arguments. +/// +/// When a call passes a (const pointer, immediate length) pair, that is a Go +/// string header, so the pointed-to data is redefined as `char[len]` to stop it +/// spilling into the concatenated rodata blob. +pub struct NarrowStringsAction {} + +impl NarrowStringsAction { + /// Apply the workflow + pub fn apply(ctx: &AnalysisContext) { + let view = ctx.view(); + + let is_go = view + .query_metadata("go_workflow.is_go") + .and_then(|m| m.get_boolean()) + .unwrap_or(false); + if !is_go { + return; + } + + let Some(hlil) = ctx.hlil_function(true) else { + return; + }; + + for block in &hlil.basic_blocks() { + for instr in block.iter() { + Self::visit(&instr.lift(), &view); + } + } + } + + /// Recursively find calls and narrow their string arguments. + fn visit(instr: &LInstr, view: &BinaryView) { + if let LK::Call(op) | LK::Tailcall(op) = &instr.kind { + Self::narrow_call(&op.params, view); + } + for (_, operand) in instr.operands() { + match operand { + LOp::Expr(e) => Self::visit(&e, view), + LOp::ExprList(list) => list.iter().for_each(|e| Self::visit(e, view)), + _ => {} + } + } + } + + /// Pair adjacent (ptr, len) constants in a call's params and redefine the + /// pointed-to data as `char[len]`. + fn narrow_call(params: &[LInstr], view: &BinaryView) { + let consts: Vec> = params + .iter() + .map(|p| match &p.kind { + LK::Const(c) | LK::ConstPtr(c) => Some(c.constant), + _ => None, + }) + .collect(); + + for w in consts.windows(2) { + let (Some(ptr), Some(len)) = (w[0], w[1]) else { + continue; + }; + if len == 0 || len > 0x8000 || !view.offset_valid(ptr) { + continue; + } + let want = Type::array(&Type::char(), len); + if let Some(dv) = view.data_variable_at_address(ptr) + && dv.ty.contents == want + { + continue; + } + view.define_auto_data_var(ptr, &want); + } + } +} diff --git a/plugins/workflow_go/src/go/cc.rs b/plugins/workflow_go/src/go/cc.rs new file mode 100644 index 000000000..bb0474d78 --- /dev/null +++ b/plugins/workflow_go/src/go/cc.rs @@ -0,0 +1,287 @@ +use binaryninja::architecture::register::RegisterId; +use binaryninja::architecture::{ArchitectureExt, CoreArchitecture, Register}; +use binaryninja::calling_convention::{ + CallingConvention, CoreCallingConvention, register_calling_convention, +}; + +/// Registers the Go calling conventions (`go-abiinternal` and `go-stack`) on the +/// supported architectures. +/// +/// TODO: decide whether these belong on the architecture itself instead of here. +pub struct GoCallingConventions {} + +impl GoCallingConventions { + /// Resolves register names to ids; returns None if any name is missing. + fn ids(arch: &CoreArchitecture, names: &[&str]) -> Option> { + names.iter().map(|n| Self::reg_id(arch, n)).collect() + } + + fn reg_id(arch: &CoreArchitecture, name: &str) -> Option { + let r = arch.register_by_name(name).map(|r| r.id()); + if r.is_none() { + tracing::warn!("go-cc: register '{name}' not found on {}", arch.name()); + } + r + } + + /// Register the calling convention for x86 + pub fn for_x86() -> Option<()> { + let arch = CoreArchitecture::by_name("x86_64")?; + + let int_args = Self::ids( + &arch, + &["rax", "rbx", "rcx", "rdi", "rsi", "r8", "r9", "r10", "r11"], + )?; + let float_args: Vec = (0..15) + .map(|i| Self::reg_id(&arch, &format!("xmm{i}"))) + .collect::>()?; + let callee = Self::ids(&arch, &["rbp", "r14"])?; + let caller = Self::ids( + &arch, + &[ + "rax", "rbx", "rcx", "rdx", "rdi", "rsi", "r8", "r9", "r10", "r11", "r12", "r13", + "r15", + ], + )?; + let ret_int = Self::reg_id(&arch, "rax")?; + let ret_hi = Self::reg_id(&arch, "rbx")?; + let ret_float = Self::reg_id(&arch, "xmm0")?; + + register_calling_convention(&arch, "go-abiinternal", move |core| GoAbiInternal { + core, + int_args, + float_args, + callee, + caller, + ret_int, + ret_hi, + ret_float, + }); + + let s_callee = Self::ids(&arch, &["rbp", "r14"])?; + let s_caller = Self::ids( + &arch, + &[ + "rax", "rbx", "rcx", "rdx", "rdi", "rsi", "r8", "r9", "r10", "r11", "r12", "r13", + "r15", + ], + )?; + let s_ret = Self::reg_id(&arch, "rax")?; + register_calling_convention(&arch, "go-stack", move |core| GoStack { + core, + callee: s_callee, + caller: s_caller, + ret_int: s_ret, + }); + + Some(()) + } + + /// Register the calling convention for arm64 + pub fn for_arm64() -> Option<()> { + let arch = CoreArchitecture::by_name("aarch64")?; + + let int_args: Vec = (0..16) + .map(|i| Self::reg_id(&arch, &format!("x{i}"))) + .collect::>()?; + let float_args: Vec = (0..16) + .map(|i| Self::reg_id(&arch, &format!("v{i}"))) + .collect::>()?; + let callee = Self::ids(&arch, &["fp", "x28"])?; + + let mut caller_names: Vec = Vec::new(); + caller_names.extend((0..=17).map(|i| format!("x{i}"))); + caller_names.extend((19..=27).map(|i| format!("x{i}"))); + caller_names.push("lr".into()); + caller_names.extend((0..32).map(|i| format!("v{i}"))); + let caller: Vec = caller_names + .iter() + .map(|n| Self::reg_id(&arch, n)) + .collect::>()?; + + let ret_int = Self::reg_id(&arch, "x0")?; + let ret_hi = Self::reg_id(&arch, "x1")?; + let ret_float = Self::reg_id(&arch, "v0")?; + + register_calling_convention(&arch, "go-abiinternal", move |core| GoAbiInternal { + core, + int_args, + float_args, + callee, + caller, + ret_int, + ret_hi, + ret_float, + }); + + let s_callee = Self::ids(&arch, &["fp", "x28"])?; + let mut s_caller_names: Vec = Vec::new(); + s_caller_names.extend((0..=17).map(|i| format!("x{i}"))); + s_caller_names.extend((19..=27).map(|i| format!("x{i}"))); + s_caller_names.push("lr".into()); + let s_caller: Vec = s_caller_names + .iter() + .map(|n| Self::reg_id(&arch, n)) + .collect::>()?; + let s_ret = Self::reg_id(&arch, "x0")?; + + register_calling_convention(&arch, "go-stack", move |core| GoStack { + core, + callee: s_callee, + caller: s_caller, + ret_int: s_ret, + }); + + Some(()) + } +} + +/// Go `ABIInternal` calling convention (go1.17+), register-based. +/// +/// Go uses a common register-based ABI across all architectures: integer and +/// floating-point arguments and results are passed in fixed register sequences, +/// which do not share an index. There are no callee-save registers except those +/// with a fixed runtime meaning (frame pointer and the goroutine pointer); a call +/// may clobber anything else. +/// +/// Source: +pub struct GoAbiInternal { + /// Handle to the registered core convention, returned via `AsRef`. + core: CoreCallingConvention, + /// Integer argument/result registers. + int_args: Vec, + /// Floating-point argument/result registers. + float_args: Vec, + /// Callee-saved (fixed-meaning) registers. + callee: Vec, + /// Caller-saved (clobbered) registers. + caller: Vec, + /// First integer return register. + ret_int: RegisterId, + /// Second integer return register. + ret_hi: RegisterId, + /// Floating-point return register. + ret_float: RegisterId, +} + +impl AsRef for GoAbiInternal { + fn as_ref(&self) -> &CoreCallingConvention { + &self.core + } +} + +impl CallingConvention for GoAbiInternal { + fn caller_saved_registers(&self) -> Vec { + self.caller.clone() + } + fn callee_saved_registers(&self) -> Vec { + self.callee.clone() + } + fn int_arg_registers(&self) -> Vec { + self.int_args.clone() + } + fn float_arg_registers(&self) -> Vec { + self.float_args.clone() + } + fn arg_registers_shared_index(&self) -> bool { + false + } + fn reserved_stack_space_for_arg_registers(&self) -> bool { + false + } + fn stack_adjusted_on_return(&self) -> bool { + false + } + fn is_eligible_for_heuristics(&self) -> bool { + true + } + fn return_int_reg(&self) -> Option { + Some(self.ret_int) + } + fn return_hi_int_reg(&self) -> Option { + Some(self.ret_hi) + } + fn return_float_reg(&self) -> Option { + Some(self.ret_float) + } + fn global_pointer_reg(&self) -> Option { + None + } + fn implicitly_defined_registers(&self) -> Vec { + vec![] + } + fn are_argument_registers_used_for_var_args(&self) -> bool { + false + } +} + +/// Go `ABI0` calling convention, stack-based. +/// +/// The stable ABI used by assembly functions and `.abi0` wrappers. All arguments +/// and results are passed on the stack, so there are no argument registers. It is +/// equivalent to ABIInternal with zero available argument registers. The +/// fixed-meaning registers (frame pointer, goroutine pointer) stay callee-saved, +/// everything else is caller-saved, and the stack is caller-cleaned. +/// +/// Source: +pub struct GoStack { + /// Handle to the registered core convention, returned via `AsRef` + core: CoreCallingConvention, + /// Callee-saved (fixed-meaning) registers + callee: Vec, + /// Caller-saved (clobbered) registers + caller: Vec, + /// Integer return register, but ABI0 returns on the stack + ret_int: RegisterId, +} + +impl AsRef for GoStack { + fn as_ref(&self) -> &CoreCallingConvention { + &self.core + } +} + +impl CallingConvention for GoStack { + fn caller_saved_registers(&self) -> Vec { + self.caller.clone() + } + fn callee_saved_registers(&self) -> Vec { + self.callee.clone() + } + fn int_arg_registers(&self) -> Vec { + vec![] + } + fn float_arg_registers(&self) -> Vec { + vec![] + } + fn arg_registers_shared_index(&self) -> bool { + false + } + fn reserved_stack_space_for_arg_registers(&self) -> bool { + false + } + fn stack_adjusted_on_return(&self) -> bool { + false + } + fn is_eligible_for_heuristics(&self) -> bool { + false + } + fn return_int_reg(&self) -> Option { + Some(self.ret_int) + } + fn return_hi_int_reg(&self) -> Option { + None + } + fn return_float_reg(&self) -> Option { + None + } + fn global_pointer_reg(&self) -> Option { + None + } + fn implicitly_defined_registers(&self) -> Vec { + vec![] + } + fn are_argument_registers_used_for_var_args(&self) -> bool { + false + } +} diff --git a/plugins/workflow_go/src/go/detect.rs b/plugins/workflow_go/src/go/detect.rs new file mode 100644 index 000000000..f8049ec9a --- /dev/null +++ b/plugins/workflow_go/src/go/detect.rs @@ -0,0 +1,254 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewBase}; + +/// Magic that marks the start of the buildinfo blob, embedded by the Go linker +/// since go1.13. Followed by the pointer size, flags, and the version string. +/// Source: +const BUILDINFO_MAGIC: &[u8; 14] = b"\xff Go buildinf:"; + +/// Section names that hold the pclntab, by object format: `.gopclntab` on ELF, +/// `__gopclntab` on Mach-O. The `.data.rel.ro` variant appears on some +/// position-independent ELF builds. PE has no dedicated section (see the scan). +const PCLNTAB_SECTIONS: &[&str] = &[".gopclntab", "__gopclntab", ".data.rel.ro.gopclntab"]; + +/// Section names that hold the buildinfo blob: `.go.buildinfo` on ELF, +/// `__go_buildinfo` on Mach-O. As with the pclntab, PE embeds it without a +/// dedicated section. +const BUILDINFO_SECTIONS: &[&str] = &[".go.buildinfo", "__go_buildinfo"]; + +const FLAGS_VERSION_MASK: u8 = 0x2; + +/// pclntab header magics, mapped to the Go version range that produces them. +const MAGICS: [(u32, &str); 4] = [ + (0xFFFF_FFFB, "go1.2 -- 1.15"), + (0xFFFF_FFFA, "go1.16 -- 1.17"), + (0xFFFF_FFF0, "go1.18 -- 1.19"), + (0xFFFF_FFF1, "go1.20+"), +]; + +/// Sanity cap on the decoded version string length (e.g. "go1.22.2"). +const MAX_VERSION_LEN: u64 = 128; + +/// A uvarint encodes at most 10 bytes for a u64; the 10th holds a single bit. +const UVARINT_MAX_BYTES: usize = 9; + +/// Outcome of Go detection for a binary. +pub struct GoInfo { + /// Whether the binary is a Go binary. + pub is_go: bool, + /// Whether a valid pclntab was located. + pub has_pclntab: bool, + /// Exact Go version (from buildinfo) or a version range (from the pclntab magic). + pub version: String, +} + +/// Detects Go binaries and extracts version information from their embedded +/// metadata: the pclntab header and the buildinfo blob. +/// +/// The pclntab is located by section name on ELF/Mach-O, and by scanning section +/// contents on PE (where it is embedded in .rdata or .text with no dedicated +/// section). +pub struct GoDetector {} + +impl GoDetector { + /// Apply the identification of golang binary + pub fn analyze(bv: &BinaryView) -> GoInfo { + let pclntab_magic = Self::find_pclntab_magic(bv); + let has_pclntab = pclntab_magic.is_some(); + let build_version = Self::read_buildinfo_version(bv); + + // Prefer the exact version from buildinfo; fall back to the pclntab range. + let version = build_version + .clone() + .or_else(|| pclntab_magic.map(Self::version_bucket)) + .unwrap_or_else(|| "unknown".to_string()); + + // A valid pclntab or a buildinfo blob is proof the binary is Go. + let is_go = has_pclntab || build_version.is_some(); + + GoInfo { + is_go, + has_pclntab, + version, + } + } + + /// Locates the pclntab header magic: known section names first, then a + /// content scan of every section (covers PE, and renamed sections). + fn find_pclntab_magic(bv: &BinaryView) -> Option { + for name in PCLNTAB_SECTIONS { + if let Some(sec) = bv.section_by_name(*name) + && let Some(m) = Self::check_magic_at(bv, sec.start()) + { + return Some(m); + } + } + for sec in &bv.sections() { + if let Some(m) = Self::scan_section(bv, sec.start(), sec.end()) { + return Some(m); + } + } + None + } + + /// Validates a pclntab header at `addr`: known magic (either endianness), + /// two zero padding bytes, and a sane pointer size. + fn check_magic_at(bv: &BinaryView, addr: u64) -> Option { + let mut buf = [0u8; 8]; + if bv.read(&mut buf, addr) != 8 || buf[4] != 0 || buf[5] != 0 { + return None; + } + if buf[7] != 4 && buf[7] != 8 { + return None; + } + let word = [buf[0], buf[1], buf[2], buf[3]]; + [u32::from_le_bytes(word), u32::from_be_bytes(word)] + .into_iter() + .find(|m| MAGICS.iter().any(|&(x, _)| x == *m)) + } + + /// Scans a section byte-by-byte for a valid pclntab header. A cheap + /// in-buffer pre-filter avoids a `read` call per offset. + fn scan_section(bv: &BinaryView, start: u64, end: u64) -> Option { + const CHUNK: usize = 0x10000; // read window size + const HEADER_LEN: usize = 8; // magic(4) + pad(2) + quantum(1) + ptr_size(1) + + // magic bytes to look for, derived from MAGICS (single source of truth) + let needles: [[u8; 4]; 4] = MAGICS.map(|(magic, _)| u32::to_le_bytes(magic)); + + let mut buf = vec![0u8; CHUNK]; + let mut addr = start; + + while addr < end { + let want = ((end - addr) as usize).min(CHUNK); + let n = bv.read(&mut buf[..want], addr); + if n < HEADER_LEN { + break; + } + + // the header may be unaligned, so check every offset in the chunk + for i in 0..=n - HEADER_LEN { + let magic = &buf[i..i + 4]; + let padding = buf[i + 4] == 0 && buf[i + 5] == 0; // two zero pad bytes + let ptr_size_ok = buf[i + 7] == 4 || buf[i + 7] == 8; // 32- or 64-bit + + if !(padding && ptr_size_ok && needles.iter().any(|m| m == magic)) { + continue; + } + + // cheap pre-filter passed: confirm with a clean re-read (endianness) + if let Some(mg) = Self::check_magic_at(bv, addr + i as u64) { + return Some(mg); + } + } + + // advance, overlapping by HEADER_LEN-1 so a header split across chunks isn't missed + addr += (n - (HEADER_LEN - 1)) as u64; + } + None + } + + fn version_bucket(m: u32) -> String { + MAGICS + .iter() + .find(|&&(x, _)| x == m) + .map(|&(_, s)| s.to_string()) + .unwrap_or_else(|| "go (unknown version)".to_string()) + } + + fn read_buildinfo_version(bv: &BinaryView) -> Option { + for name in BUILDINFO_SECTIONS { + if let Some(sec) = bv.section_by_name(*name) + && let Some(v) = Self::parse_buildinfo(bv, sec.start()) + { + return Some(v); + } + } + None + } + + /// Parses the buildinfo blob. Two formats exist: inline (go1.18+), where the + /// version is a varint-length-prefixed string at offset 32, and the older + /// pointer format, where offset 16 holds a pointer to a Go string header. + fn parse_buildinfo(bv: &BinaryView, addr: u64) -> Option { + let mut hdr = [0u8; 64]; + if bv.read(&mut hdr, addr) < 32 || &hdr[..14] != BUILDINFO_MAGIC { + return None; + } + + let ptr_size = hdr[14] as usize; + let flags = hdr[15]; + + if flags & FLAGS_VERSION_MASK != 0 { + let (len, consumed) = Self::uvarint(&hdr[32..])?; + let start = 32 + consumed; + let mut buf = vec![0u8; start + len as usize]; + if (bv.read(&mut buf, addr) as u64) < start as u64 + len { + return None; + } + String::from_utf8(buf[start..start + len as usize].to_vec()).ok() + } else { + let big_endian = flags & 0x1 != 0; + let ver_hdr = Self::read_ptr(bv, addr + 16, ptr_size, big_endian)?; + Self::read_go_string(bv, ver_hdr, ptr_size, big_endian) + } + } + + fn read_ptr(bv: &BinaryView, addr: u64, ptr_size: usize, big_endian: bool) -> Option { + let mut buf = [0u8; 8]; + if bv.read(&mut buf[..ptr_size], addr) != ptr_size { + return None; + } + let mut val = 0u64; + for i in 0..ptr_size { + let idx = if big_endian { ptr_size - 1 - i } else { i }; + val |= u64::from(buf[idx]) << (8 * i); + } + Some(val) + } + + /// Reads a Go string ({data ptr, len}) at the given header address. + fn read_go_string( + bv: &BinaryView, + hdr: u64, + ptr_size: usize, + big_endian: bool, + ) -> Option { + let data = Self::read_ptr(bv, hdr, ptr_size, big_endian)?; + let len = Self::read_ptr(bv, hdr + ptr_size as u64, ptr_size, big_endian)?; + if len == 0 || len > MAX_VERSION_LEN { + return None; + } + let mut buf = vec![0u8; len as usize]; + if bv.read(&mut buf, data) != len as usize { + return None; + } + String::from_utf8(buf).ok() + } + + /// Decodes an unsigned LEB128 varint, returning (value, bytes consumed). + /// + /// Each byte carries 7 value bits in its low bits; the high bit (0x80) means + /// "more bytes follow". The final byte has the high bit clear. A u64 value needs at + /// most 10 bytes, and on the 10th only a single bit is valid, so anything past + /// that is rejected as overflow. + fn uvarint(buf: &[u8]) -> Option<(u64, usize)> { + let mut value = 0u64; + let mut shift = 0u32; + + for (i, &byte) in buf.iter().enumerate() { + let is_last = byte < 0x80; + if is_last { + // overflow: value wider than an u64 + if i > UVARINT_MAX_BYTES || (i == UVARINT_MAX_BYTES && byte > 1) { + return None; + } + return Some((value | (u64::from(byte) << shift), i + 1)); + } + // strip the continuation bit, accumulate the 7 payload bits + value |= u64::from(byte & 0x7f) << shift; + shift += 7; + } + + None + } +} diff --git a/plugins/workflow_go/src/go/mod.rs b/plugins/workflow_go/src/go/mod.rs new file mode 100644 index 000000000..dc27891cc --- /dev/null +++ b/plugins/workflow_go/src/go/mod.rs @@ -0,0 +1,4 @@ +/// Definition of the calling conventions under x86 and arm +pub mod cc; +/// Exports the identification of golang binaries +pub mod detect; diff --git a/plugins/workflow_go/src/lib.rs b/plugins/workflow_go/src/lib.rs new file mode 100644 index 000000000..6b9ff479e --- /dev/null +++ b/plugins/workflow_go/src/lib.rs @@ -0,0 +1,64 @@ +#![warn(clippy::all)] +#![warn(missing_docs)] + +//! Binary Ninja workflow plugin for analyzing Go binaries. +//! +//! Given golang binaries use some wild calling conventions, and have some interesting +//! data structures (pcIntTab) we may parse, we can add details to the default analysis of +//! binja to improve precision, and readability. +//! +//! # Workflows +//! +//! A module workflow detects whether the binary is Go, and records the result +//! (Go-ness, pclntab presence, version) as view metadata that the per-function +//! activities gate on. +//! +//! Then, two function workflows then run only on Go binaries: +//! - *Calling convention*: applies the Go ABI to each function -- the register-based +//! `go-abiinternal`, or the stack-based `go-stack` for `.abi0` functions. +//! - *String narrowing*: where a call passes a (const pointer, length) pair, the +//! pointed-to data is redefined as `char[len]` so literals have the correct length. + +/// Export the analyses +pub mod activities; +/// Export the internals of golang +pub mod go; +/// Export the module that registers the workflows +pub mod workflow; + +use crate::workflow::register_activities; +use binaryninja::add_optional_plugin_dependency; +use go::cc::GoCallingConventions; + +/// Entrypoint of our plugin +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn CorePluginInit() -> bool { + binaryninja::tracing_init!("Plugin.Golang"); + + if GoCallingConventions::for_x86().is_none() { + tracing::warn!("go: x86_64 calling conventions not registered"); + return false; + } + + if GoCallingConventions::for_arm64().is_none() { + tracing::warn!("go: aarch64 calling conventions not registered"); + return false; + } + + if register_activities().is_err() { + tracing::warn!("go: workflow registration failed"); + return false; + } + + true +} + +/// We mark the main architectures that have a calling convention defined as dependencies +#[unsafe(no_mangle)] +#[allow(non_snake_case)] +pub extern "C" fn CorePluginDependencies() { + add_optional_plugin_dependency("arch_x86"); + add_optional_plugin_dependency("arch_armv7"); + add_optional_plugin_dependency("arch_arm64"); +} diff --git a/plugins/workflow_go/src/workflow.rs b/plugins/workflow_go/src/workflow.rs new file mode 100644 index 000000000..c71b4c1b2 --- /dev/null +++ b/plugins/workflow_go/src/workflow.rs @@ -0,0 +1,91 @@ +use crate::activities::calling_convention::GoCallingConventionWorkflow; +use crate::activities::string_argument::NarrowStringsAction; +use crate::go::detect::GoDetector; +use anyhow::{Result, anyhow}; +use binaryninja::workflow::{Activity, AnalysisContext, Workflow, activity}; + +const FUNCTION_WF: &str = "core.function.metaAnalysis"; +const MODULE_WF: &str = "core.module.metaAnalysis"; +const MLIL_ANCHOR: &str = "core.function.generateMediumLevelIL"; +const MODULE_ANCHOR: &str = "core.module.loadDebugInfo"; + +const ACT_APPLY_CC: &str = "analysis.plugins.goWorkflow.applyCallingConvention"; +const ACT_NARROW: &str = "analysis.plugins.goWorkflow.narrowStrings"; +const HLIL_ANCHOR: &str = "core.function.commitAnalysisData"; + +/// Registers the necessary workflow for golang analyses +pub fn register_activities() -> Result<()> { + register_module_workflow()?; + register_function_workflow()?; + Ok(()) +} + +fn register_function_workflow() -> Result<()> { + let apply_cc = Activity::new_with_action( + activity::Config::action( + ACT_APPLY_CC, + "Go ABIInternal: Apply Calling Convention", + "Set the correct calling convention for golang binaries.", + ) + .eligibility(activity::Eligibility::auto()), + GoCallingConventionWorkflow::apply, + ); + + let narrow = Activity::new_with_action( + activity::Config::action( + ACT_NARROW, + "Go: Narrow String Literals", + "Adjust the size of the strings passed as arguments", + ) + .eligibility(activity::Eligibility::auto()), + NarrowStringsAction::apply, + ); + + Workflow::cloned(FUNCTION_WF) + .ok_or_else(|| anyhow!("failed to clone {FUNCTION_WF}"))? + .activity_before(&apply_cc, MLIL_ANCHOR) + .map_err(|_| anyhow!("activity_before failed: anchor '{MLIL_ANCHOR}' missing?"))? + .activity_before(&narrow, HLIL_ANCHOR) + .map_err(|_| anyhow!("activity_after failed: anchor '{HLIL_ANCHOR}'"))? + .register() + .map_err(|_| anyhow!("register failed: {FUNCTION_WF}"))?; + + Ok(()) +} + +fn register_module_workflow() -> Result<()> { + const ACT_ANNOTATE: &str = "analysis.plugins.goWorkflow.annotate"; + + let annotate = Activity::new_with_action( + activity::Config::action( + ACT_ANNOTATE, + "Go: identify golang binary", + "Identify if the binary is golang, pclntab presence and version.", + ) + .eligibility(activity::Eligibility::auto()), + annotate_action, + ); + + Workflow::cloned(MODULE_WF) + .ok_or_else(|| anyhow!("failed clone for : {MODULE_WF}"))? + .activity_after(&annotate, MODULE_ANCHOR) + .map_err(|_| anyhow!("activity_after annotate: anchor '{MODULE_ANCHOR}'"))? + .register() + .map_err(|_| anyhow!("failed workflow registration: {MODULE_WF}"))?; + + Ok(()) +} + +fn annotate_action(ctx: &AnalysisContext) { + let view = ctx.view(); + let info = GoDetector::analyze(&view); + + if !info.is_go { + tracing::info!("golang: non-go binary detected, workflows for golang will be skipped."); + return; + } + + view.store_metadata("go_workflow.is_go", info.is_go, true); + view.store_metadata("go_workflow.has_pclntab", info.has_pclntab, true); + view.store_metadata("go_workflow.go_version", info.version.as_str(), true); +}