use crate::container::{Container, Format, SymbolKind};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct DetectionMatches(u16);
impl DetectionMatches {
pub const EMPTY: Self = Self(0);
pub const NIMMAIN_SYMBOL: Self = Self(1 << 0);
pub const NIMMAIN_STRING: Self = Self(1 << 1);
pub const FATAL_NIM: Self = Self(1 << 2);
pub const SYS_FATAL: Self = Self(1 << 3);
pub const NIM_ERROR_STRINGS: Self = Self(1 << 4);
pub const INIT000_SYMBOL: Self = Self(1 << 5);
pub const NTIV2_SYMBOL: Self = Self(1 << 6);
pub const NTI_LEGACY_SYMBOL: Self = Self(1 << 7);
pub const STT_FILE_DOT_NIM: Self = Self(1 << 8);
pub const NIMBLE_PATH_LEAK: Self = Self(1 << 9);
pub const NIM_SIGNAL_STRINGS: Self = Self(1 << 10);
pub fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
pub fn count(self) -> u32 {
self.0.count_ones()
}
pub fn iter(self) -> impl Iterator<Item = (&'static str, Self)> {
const ALL: &[(&str, DetectionMatches)] = &[
("NIMMAIN_SYMBOL", DetectionMatches::NIMMAIN_SYMBOL),
("NIMMAIN_STRING", DetectionMatches::NIMMAIN_STRING),
("FATAL_NIM", DetectionMatches::FATAL_NIM),
("SYS_FATAL", DetectionMatches::SYS_FATAL),
("NIM_ERROR_STRINGS", DetectionMatches::NIM_ERROR_STRINGS),
("INIT000_SYMBOL", DetectionMatches::INIT000_SYMBOL),
("NTIV2_SYMBOL", DetectionMatches::NTIV2_SYMBOL),
("NTI_LEGACY_SYMBOL", DetectionMatches::NTI_LEGACY_SYMBOL),
("STT_FILE_DOT_NIM", DetectionMatches::STT_FILE_DOT_NIM),
("NIMBLE_PATH_LEAK", DetectionMatches::NIMBLE_PATH_LEAK),
("NIM_SIGNAL_STRINGS", DetectionMatches::NIM_SIGNAL_STRINGS),
];
ALL.iter()
.copied()
.filter(move |(_, flag)| self.contains(*flag))
}
}
impl core::ops::BitOr for DetectionMatches {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl core::ops::BitOrAssign for DetectionMatches {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone)]
pub struct DetectionReport {
pub matches: DetectionMatches,
pub is_nim: bool,
}
impl DetectionReport {
pub fn run(container: &Container<'_>) -> Self {
let mut matches = DetectionMatches::EMPTY;
matches |= probe_symbols(container);
matches |= probe_rodata(container);
let is_nim = !matches.is_empty();
Self { matches, is_nim }
}
}
const ENTRY_SHIM_NAMES: &[&str] = &[
"NimMain",
"NimMainInner",
"NimMainModule",
"PreMain",
"PreMainInner",
];
const SYMBOL_FLAGS_COMMON: DetectionMatches = DetectionMatches(
DetectionMatches::NIMMAIN_SYMBOL.0
| DetectionMatches::INIT000_SYMBOL.0
| DetectionMatches::NTIV2_SYMBOL.0
| DetectionMatches::NTI_LEGACY_SYMBOL.0,
);
fn probe_symbols(c: &Container<'_>) -> DetectionMatches {
let mut m = DetectionMatches::EMPTY;
let check_file = c.format() == Format::Elf || c.format() == Format::Pe;
let ceiling = if check_file {
DetectionMatches(SYMBOL_FLAGS_COMMON.0 | DetectionMatches::STT_FILE_DOT_NIM.0)
} else {
SYMBOL_FLAGS_COMMON
};
for sym in c.symbols() {
let name = sym.name.as_ref();
if !m.contains(DetectionMatches::NIMMAIN_SYMBOL) {
let stripped = name.strip_prefix('_').unwrap_or(name);
for &probe in ENTRY_SHIM_NAMES {
if stripped == probe {
m |= DetectionMatches::NIMMAIN_SYMBOL;
break;
}
}
}
if !m.contains(DetectionMatches::INIT000_SYMBOL)
&& (name.ends_with("Init000") || name.ends_with("DatInit000"))
{
m |= DetectionMatches::INIT000_SYMBOL;
}
if name.ends_with('_') {
if !m.contains(DetectionMatches::NTIV2_SYMBOL) && name.starts_with("NTIv2") {
m |= DetectionMatches::NTIV2_SYMBOL;
} else if !m.contains(DetectionMatches::NTI_LEGACY_SYMBOL)
&& name.starts_with("NTI")
&& !name.starts_with("NTIv2")
{
m |= DetectionMatches::NTI_LEGACY_SYMBOL;
}
}
if check_file
&& !m.contains(DetectionMatches::STT_FILE_DOT_NIM)
&& sym.kind == SymbolKind::File
{
if name.starts_with("@m")
|| name.ends_with(".nim")
|| name.ends_with(".nim.c")
|| name.ends_with(".nim.cpp")
|| name.ends_with(".nim.m")
{
m |= DetectionMatches::STT_FILE_DOT_NIM;
}
}
if m.contains(ceiling) {
break;
}
}
m
}
const RODATA_NEEDLES: &[(&[u8], DetectionMatches)] = &[
(b"fatal.nim", DetectionMatches::FATAL_NIM),
(b"sysFatal", DetectionMatches::SYS_FATAL),
(b"NimMain", DetectionMatches::NIMMAIN_STRING),
(b"PreMainInner", DetectionMatches::NIMMAIN_STRING),
(b".nimble/pkgs", DetectionMatches::NIMBLE_PATH_LEAK),
(
b"Attempt to read from nil?",
DetectionMatches::NIM_SIGNAL_STRINGS,
),
(b"@[[reraised from:", DetectionMatches::NIM_SIGNAL_STRINGS),
];
const ERROR_NEEDLES: &[&[u8]] = &[
b"@value out of range",
b"@division by zero",
b"@over- or underflow",
b"@index out of bounds",
];
const ALL_RODATA_FLAGS: DetectionMatches = DetectionMatches(
DetectionMatches::FATAL_NIM.0
| DetectionMatches::SYS_FATAL.0
| DetectionMatches::NIMMAIN_STRING.0
| DetectionMatches::NIM_ERROR_STRINGS.0
| DetectionMatches::NIMBLE_PATH_LEAK.0
| DetectionMatches::NIM_SIGNAL_STRINGS.0,
);
fn probe_rodata(c: &Container<'_>) -> DetectionMatches {
let mut m = DetectionMatches::EMPTY;
let mut error_hits: u8 = 0;
for section in c.rodata_sections() {
if section.data.is_empty() {
continue;
}
for &(needle, flag) in RODATA_NEEDLES {
if !m.contains(flag) && memchr::memmem::find(section.data, needle).is_some() {
m |= flag;
}
}
if error_hits < 2 {
for needle in ERROR_NEEDLES {
if memchr::memmem::find(section.data, needle).is_some() {
error_hits = error_hits.saturating_add(1);
if error_hits >= 2 {
m |= DetectionMatches::NIM_ERROR_STRINGS;
break;
}
}
}
}
if m.contains(ALL_RODATA_FLAGS) {
break;
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_default_is_empty() {
let m = DetectionMatches::default();
assert!(m.is_empty());
assert_eq!(m.count(), 0);
assert!(!m.contains(DetectionMatches::NIMMAIN_SYMBOL));
}
#[test]
fn matches_bitor() {
let m = DetectionMatches::NIMMAIN_SYMBOL | DetectionMatches::FATAL_NIM;
assert!(m.contains(DetectionMatches::NIMMAIN_SYMBOL));
assert!(m.contains(DetectionMatches::FATAL_NIM));
assert!(!m.contains(DetectionMatches::SYS_FATAL));
assert_eq!(m.count(), 2);
}
#[test]
fn matches_iter_names() {
let m = DetectionMatches::NIMMAIN_SYMBOL | DetectionMatches::STT_FILE_DOT_NIM;
let names: Vec<_> = m.iter().map(|(n, _)| n).collect();
assert!(names.contains(&"NIMMAIN_SYMBOL"));
assert!(names.contains(&"STT_FILE_DOT_NIM"));
assert_eq!(names.len(), 2);
}
}