反虚拟机和沙箱检测
反虚拟机和沙箱检测
本文适合
已经理解 反调试基础、动态调试基础 和基本系统 API 的逆向学习者。学完你能:从程序异常退出、假逻辑和环境判断代码中定位反虚拟机/沙箱检测,并用 patch、hook 或环境模拟进入真实校验路径
一句话判断
程序在本机、虚拟机、调试器或在线沙箱里表现不一致,尤其是一运行就退出、输出假 flag、延迟很久、或者只在特定环境触发校验,就要怀疑反虚拟机和沙箱检测。
反虚拟机检测问的是“我是不是在分析环境里”,反调试问的是“我是不是正被调试”。CTF 题里二者经常连在一起出现。
题目中常见信号
静态字符串信号:
VMware
VirtualBox
VBoxGuest
VBoxService
QEMU
KVM
Hyper-V
SbieDll
Sandboxie
Wine
vmtools
VBOX HARDDISKWindows API 信号:
RegOpenKeyEx
GetSystemInfo
GetComputerName
GetUserName
CreateToolhelp32Snapshot
Process32First
GetTickCount
QueryPerformanceCounter
IsDebuggerPresent
CheckRemoteDebuggerPresentLinux 文件和命令信号:
/proc/cpuinfo
/proc/self/status
/sys/class/dmi/id/product_name
/sys/class/dmi/id/sys_vendor
/proc/scsi/scsi
uname
lspci
dmidecode行为信号:
- 不调试时有输出,调试时直接退出。
- 在物理机和虚拟机输出不同。
- 程序睡眠很久,跳过 sleep 后逻辑改变。
- 输入正确但始终失败,patch 环境检测后才进入真正校验。
- 字符串里有多个看似无关的假 flag。
核心概念
反虚拟机和沙箱检测通常不是最终算法,而是题目前置门禁。
常见结构是:
check_env()
|
+-- 检测虚拟机厂商
+-- 检测系统资源
+-- 检测进程/服务
+-- 检测时间流逝
+-- 检测用户行为
|
v
if bad_env:
fake_logic()
else:
real_check()逆向时不要把所有检测都“修到完美”。目标是进入真实校验路径,拿到关键算法或 flag。
最小分析流程
1. 先记录差异
至少跑两次:
./challenge
strace -f ./challenge记录:
- 是否立刻退出。
- 是否访问
/proc、/sys、注册表或特定文件。 - 是否有
sleep、时间查询、进程枚举。 - 输出是否随环境变化。
Windows 下用 x64dbg / Process Monitor 观察:
- 进程启动后调用了哪些 API。
- 是否打开注册表虚拟机键值。
- 是否枚举进程、服务、驱动。
2. 搜字符串和导入表
Linux:
strings -a ./challenge | rg -i "vmware|virtual|qemu|vbox|sandbox|proc|cpuinfo|dmi|debug"
objdump -T ./challenge | rg "getenv|open|read|sleep|ptrace|clock|gettimeofday"Windows:
用 Detect It Easy / PE-bear 看导入表
在 IDA/Ghidra 里搜索 RegOpenKeyEx、GetSystemInfo、QueryPerformanceCounter3. 找汇聚变量
很多检测最后会汇总成一个变量:
score += check_vm_vendor();
score += check_small_memory();
score += check_sandbox_process();
if (score > 0) {
exit(0);
}优先找这种 score、bad_env、is_vm、sandbox、check_failed,比逐个 patch 检测函数更稳。
4. 确认真实分支
不要只 patch 退出点。要看两个分支分别做什么:
- 哪个分支读取用户输入。
- 哪个分支生成比对数组。
- 哪个分支调用加密/校验函数。
- 哪个分支出现成功字符串。
如果 patch 后进入假分支,说明判断方向反了。
最小验证示例
Linux:确认 DMI 检测
假设 strings 看到:
/sys/class/dmi/id/product_name
VirtualBox用 strace 验证:
strace -f ./challenge 2>&1 | rg "dmi|product_name|cpuinfo|proc"如果输出类似:
openat(AT_FDCWD, "/sys/class/dmi/id/product_name", O_RDONLY) = 3
read(3, "VirtualBox\n", 4096) = 11说明程序确实读取了虚拟机特征。
下一步在 GDB 里断 open 或相关检测函数,找到读取结果如何影响分支:
gdb ./challenge
break open
run如果目标是快速解题,可以在关键比较处 patch 条件跳转,或让检测函数返回 0。
Windows:确认注册表检测
如果程序查 VMware 注册表键:
HARDWARE\\ACPI\\DSDT\\VBOX__
SYSTEM\\ControlSet001\\Services\\VBoxGuest用 x64dbg:
- 在
RegOpenKeyExA/W下断点。 - 运行到断点,查看参数里的 key。
- Step out,看返回值如何被判断。
- 把
jnz/jzpatch 到非虚拟机分支。
常见利用 / 解题路线
路线总览:
路线一:patch 检测函数返回值
适合检测集中、函数边界清晰的题。
目标:
int check_vm() {
...
return 1;
}patch 成:
return 0;优点是快。缺点是如果函数里还初始化了后续校验所需状态,直接返回可能破坏流程。
路线二:patch 最终条件跳转
适合多个检测汇总到一个判断:
test eax, eax
jnz bad_env可以改成:
jz bad_env或 NOP 掉跳转。
优点是保留前面初始化逻辑。缺点是要确认哪个分支是真逻辑。
路线三:hook API 返回值
适合不想改文件,只想动态跑通。
例子:
- Hook
GetSystemInfo,返回 8 核 16G。 - Hook
RegOpenKeyEx,让虚拟机键不存在。 - Hook
QueryPerformanceCounter,让时间差正常。 - Hook
open/read,把/sys/class/dmi/id/product_name改成物理机名称。
路线四:环境模拟
适合检测很多且散乱的题:
- 增大内存和 CPU。
- 改主机名、用户名。
- 删除 Guest Additions 痕迹。
- 用物理机或更真实的 VM 镜像跑。
CTF 中通常优先 patch 或 hook,环境模拟作为兜底。
常见失败原因
- patch 太早:检测函数顺手初始化了 key、表或全局变量,直接 return 会导致后续校验错误。
- patch 方向反了:
0不一定代表正常,先看调用点怎么判断。 - 只绕过反调试:程序仍然因为虚拟机检测走假逻辑。
- 跳过 sleep 后逻辑变化:有的题用时间差参与计算,不能粗暴 NOP。
- 误把假 flag 当真:反沙箱分支常输出诱饵字符串。
- 没有记录环境:复现时换一台机器,行为又不一样。
迷你案例
题目是一个 Linux ELF。直接运行:
byestrings 发现:
/sys/class/dmi/id/product_name
VirtualBox
wrong env
correct!strace 验证:
strace -f ./rev 2>&1 | rg "dmi|product"得到:
openat(AT_FDCWD, "/sys/class/dmi/id/product_name", O_RDONLY) = 3
read(3, "VirtualBox\n", 64) = 11在 Ghidra 里找到:
if (strstr(product_name, "VirtualBox") != 0) {
puts("bye");
return 0;
}
real_check();patch 方法:
把 strstr 之后的 jnz bad_env 改成 jz bad_env重新运行后程序开始要求输入。继续用 字符串、交叉引用与控制流 找 correct! 的交叉引用,再进入真实校验函数。
WP 里要写清楚:
- 程序不是输入错,而是环境门禁挡住。
- 用
strace证明它读取 DMI。 - patch 条件跳转进入真实分支。
- 后续才是普通逆向校验。