use std::path::Path;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct AntiDebugFinding {
pub technique: AntiDebugTechnique,
pub addr: u64,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiDebugTechnique {
PtraceTraceme,
ProcSelfStatus,
TimingCheck,
Int3SelfCheck,
PrctlNondumpable,
}
impl std::fmt::Display for AntiDebugTechnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PtraceTraceme => write!(f, "ptrace(TRACEME)"),
Self::ProcSelfStatus => write!(f, "/proc/self/status check"),
Self::TimingCheck => write!(f, "timing check"),
Self::Int3SelfCheck => write!(f, "INT3/self-checksum"),
Self::PrctlNondumpable => write!(f, "prctl(PR_SET_DUMPABLE, 0)"),
}
}
}
pub fn scan(path: &Path) -> Result<Vec<AntiDebugFinding>> {
let data = std::fs::read(path).map_err(|e| Error::Other(format!("read: {}", e)))?;
scan_bytes(&data)
}
pub fn scan_bytes(data: &[u8]) -> Result<Vec<AntiDebugFinding>> {
let elf =
goblin::elf::Elf::parse(data).map_err(|e| Error::Other(format!("parse ELF: {}", e)))?;
let mut findings = Vec::new();
for sym in elf.dynsyms.iter() {
if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
if name == "ptrace" {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::PtraceTraceme,
addr: sym.st_value,
description: "Dynamic symbol 'ptrace' imported — likely self-trace check"
.into(),
});
}
if name == "prctl" {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::PrctlNondumpable,
addr: sym.st_value,
description: "Dynamic symbol 'prctl' imported — may set PR_SET_DUMPABLE(0)"
.into(),
});
}
}
}
scan_strings_for_antidebug(data, &elf, &mut findings);
for sym in elf.dynsyms.iter() {
if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
if name == "clock_gettime" || name == "gettimeofday" || name == "time" {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::TimingCheck,
addr: sym.st_value,
description: format!(
"Timing function '{}' imported — potential timing-based anti-debug",
name
),
});
}
}
}
scan_for_rdtsc(data, &elf, &mut findings);
scan_for_int3_checks(data, &elf, &mut findings);
Ok(findings)
}
fn scan_strings_for_antidebug(
data: &[u8],
elf: &goblin::elf::Elf,
findings: &mut Vec<AntiDebugFinding>,
) {
let patterns: &[(&[u8], &str)] = &[
(
b"/proc/self/status",
"Reads /proc/self/status — TracerPid check",
),
(
b"/proc/self/maps",
"Reads /proc/self/maps — memory layout inspection",
),
(
b"TracerPid",
"References TracerPid string — debugger detection",
),
(
b"/proc/self/exe",
"Reads /proc/self/exe — binary integrity check",
),
];
for sh in &elf.section_headers {
let offset = sh.sh_offset as usize;
let size = sh.sh_size as usize;
if offset + size > data.len() {
continue;
}
let section_data = &data[offset..offset + size];
for (pattern, desc) in patterns {
if let Some(pos) = find_bytes(section_data, pattern) {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::ProcSelfStatus,
addr: sh.sh_addr + pos as u64,
description: desc.to_string(),
});
}
}
}
}
fn scan_for_rdtsc(data: &[u8], elf: &goblin::elf::Elf, findings: &mut Vec<AntiDebugFinding>) {
for sh in &elf.section_headers {
if sh.sh_flags & u64::from(goblin::elf::section_header::SHF_EXECINSTR) == 0 {
continue;
}
let offset = sh.sh_offset as usize;
let size = sh.sh_size as usize;
if offset + size > data.len() {
continue;
}
let section_data = &data[offset..offset + size];
for (i, window) in section_data.windows(2).enumerate() {
if window == [0x0F, 0x31] {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::TimingCheck,
addr: sh.sh_addr + i as u64,
description: "RDTSC instruction found — hardware timing check".into(),
});
}
}
}
}
fn scan_for_int3_checks(data: &[u8], elf: &goblin::elf::Elf, findings: &mut Vec<AntiDebugFinding>) {
for sh in &elf.section_headers {
if sh.sh_flags & u64::from(goblin::elf::section_header::SHF_EXECINSTR) == 0 {
continue;
}
let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
if name.contains(".plt") {
continue;
}
let offset = sh.sh_offset as usize;
let size = sh.sh_size as usize;
if offset + size > data.len() {
continue;
}
let section_data = &data[offset..offset + size];
let mut count = 0u32;
for (i, &byte) in section_data.iter().enumerate() {
if byte == 0xCC {
count += 1;
} else {
if count >= 3 {
let start = i - count as usize;
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::Int3SelfCheck,
addr: sh.sh_addr + start as u64,
description: format!(
"INT3 cluster ({} bytes) in {} — potential trap/checksum region",
count, name
),
});
}
count = 0;
}
}
}
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > haystack.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
#[derive(Debug, Clone, Default)]
pub struct BypassConfig {
pub bypass_ptrace: bool,
pub skip_int3_traps: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_bytes_basic() {
let data = b"hello world";
assert_eq!(find_bytes(data, b"world"), Some(6));
assert_eq!(find_bytes(data, b"xyz"), None);
assert_eq!(find_bytes(data, b""), None);
}
#[test]
fn technique_display() {
assert_eq!(
format!("{}", AntiDebugTechnique::PtraceTraceme),
"ptrace(TRACEME)"
);
assert_eq!(
format!("{}", AntiDebugTechnique::TimingCheck),
"timing check"
);
}
#[test]
fn bypass_config_default() {
let cfg = BypassConfig::default();
assert!(!cfg.bypass_ptrace);
assert!(!cfg.skip_int3_traps);
}
#[test]
fn scan_finds_rdtsc() {
let rdtsc_bytes = [0x90, 0x0F, 0x31, 0x90]; let mut findings = Vec::new();
for (i, window) in rdtsc_bytes.windows(2).enumerate() {
if window == [0x0F, 0x31] {
findings.push(AntiDebugFinding {
technique: AntiDebugTechnique::TimingCheck,
addr: 0x1000 + i as u64,
description: "RDTSC".into(),
});
}
}
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].addr, 0x1001);
}
#[test]
fn scan_finds_proc_self_status() {
let data = b"\x00/proc/self/status\x00";
let pos = find_bytes(data, b"/proc/self/status");
assert_eq!(pos, Some(1));
}
}