原文首发在:奇安信攻防社区
https://forum.butian.net/share/4050
说实话单纯的静态免杀其实不是很难,只要通过足够新颖的加壳手段就能够成功将木马加载到内存中,但是抵御不了蓝队(比如微步云沙箱)使用沙箱的动态分析,所以通常只能够免杀小一天就上传了病毒库,从而免杀失效了。本文就是来介绍几种反沙箱的思路来帮助红队搞出耐得住沙箱考验的payload
说实话单纯的静态免杀其实不是很难,只要通过足够新颖的加壳手段就能够成功将木马加载到内存中,但是抵御不了蓝队(比如微步云沙箱)使用沙箱的动态分析,所以通常只能够免杀小一天就上传了病毒库,从而免杀失效了。
本文就是来介绍几种反沙箱的思路来帮助红队搞出耐得住沙箱考验的payload
硬件检测法
BIOS检测法
win下可以直接通过wql来实现检测BIOS相关的信息
package main
import (
"fmt"
"strings"
"github.com/StackExchange/wmi"
)
// 定义用于存储 WMI 查询结果的结构体
type Win32_BIOS struct {
SMBIOSBIOSVersion string
Manufacturer string
Name string
SerialNumber string
}
func containsVMware(output string) bool {
return strings.Contains(strings.ToLower(output), "vmware")
}
func getBIOSInfoWindows() (string, error) {
var biosInfo []Win32_BIOS
// 使用 WMI 查询 BIOS 信息
err := wmi.Query("SELECT SMBIOSBIOSVersion, Manufacturer, Name, SerialNumber FROM Win32_BIOS", &biosInfo)
if err != nil {
return "", err
}
// 拼接所有 BIOS 信息字段
var result []string
for _, bios := range biosInfo {
result = append(result, bios.SMBIOSBIOSVersion)
result = append(result, bios.Manufacturer)
result = append(result, bios.Name)
result = append(result, bios.SerialNumber)
}
return strings.Join(result, " "), nil
}
func main() {
biosInfo, err := getBIOSInfoWindows()
if err != nil {
fmt.Println("Error fetching BIOS information:", err)
return
}
fmt.Println("BIOS Information:")
fmt.Println(biosInfo)
if containsVMware(biosInfo) {
fmt.Println("VMware detected in BIOS information.")
} else {
fmt.Println("No VMware detected in BIOS information.")
}
}
在Linux下可以通过读取虚拟路径下的文件配置/sys来实现获取bios信息。
package main
import (
"fmt"
"io/ioutil"
"strings"
)
// 检查字符串是否包含特定关键字(不区分大小写)
func containsIgnoreCase(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// 从指定文件路径读取内容
func readFile(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
// 检测是否运行在 WSL 环境中
func isWSL() (bool, error) {
// 检查 /proc/version 文件
version, err := readFile("/proc/version")
if err == nil && (containsIgnoreCase(version, "Microsoft") || containsIgnoreCase(version, "WSL")) {
return true, nil
}
// 检查 /proc/sys/kernel/osrelease 文件
osrelease, err := readFile("/proc/sys/kernel/osrelease")
if err == nil && (containsIgnoreCase(osrelease, "Microsoft") || containsIgnoreCase(osrelease, "microsoft-standard")) {
return true, nil
}
// 检查 /proc/self/mounts 文件
mounts, err := readFile("/proc/self/mounts")
if err == nil && (containsIgnoreCase(mounts, "lxfs") || containsIgnoreCase(mounts, "wslfs")) {
return true, nil
}
return false, nil
}
// 检测是否运行在 Hyper-V 环境中
func isHyperV() (bool, error) {
// 检查 /sys/class/dmi/id/bios_vendor 文件
biosVendor, err := readFile("/sys/class/dmi/id/bios_vendor")
if err == nil && containsIgnoreCase(biosVendor, "Microsoft") {
return true, nil
}
// 检查 /sys/class/dmi/id/product_name 文件
productName, err := readFile("/sys/class/dmi/id/product_name")
if err == nil && (containsIgnoreCase(productName, "Virtual Machine") || containsIgnoreCase(productName, "Hyper-V")) {
return true, nil
}
// 检查 /proc/cpuinfo 文件
cpuInfo, err := readFile("/proc/cpuinfo")
if err == nil && containsIgnoreCase(cpuInfo, "hypervisor") {
return true, nil
}
return false, nil
}
// 检测是否运行在其他虚拟机中
func isVirtualMachine() (bool, error) {
checkFiles := map[string][]string{
"/sys/class/dmi/id/bios_vendor": {"VMware", "QEMU", "VirtualBox", "Microsoft", "Xen", "Alibaba Cloud", "OVMF"},
"/sys/class/dmi/id/bios_version": {"VMware", "QEMU", "VirtualBox", "Hyper-V", "OVMF"},
"/sys/class/dmi/id/product_name": {"VirtualBox", "VMware", "KVM", "Alibaba Cloud ECS", "OpenStack"},
"/sys/class/dmi/id/sys_vendor": {"VMware", "QEMU", "VirtualBox", "Microsoft", "Xen", "Alibaba Cloud"},
}
for file, keywords := range checkFiles {
content, err := readFile(file)
if err != nil {
// 如果文件不存在或无法读取,跳过
continue
}
for _, keyword := range keywords {
if containsIgnoreCase(content, keyword) {
return true, nil
}
}
}
return false, nil
}
func main() {
// 检测 WSL 环境
if isWSL, _ := isWSL(); isWSL {
fmt.Println("The system is running in WSL (Windows Subsystem for Linux).")
return
}
// 检测 Hyper-V 环境
if isHyperV, _ := isHyperV(); isHyperV {
fmt.Println("The system is running in a Hyper-V virtual machine.")
return
}
// 检测其他虚拟机环境
if isVM, _ := isVirtualMachine(); isVM {
fmt.Println("The system is running in a virtual machine.")
return
}
// 如果都不是,则认为是物理机
fmt.Println("The system is running on a physical machine.")
}
检测MAC地址
一般来说VM的MAC地址会以特殊数据开头:例如00,这里收集了一些常见的数据用于检测(兼容win/linux)
package main
import (
"fmt"
"net"
"strings"
)
// 常见虚拟机的 MAC 地址前缀
var vmMacPrefixes = []string{
"00:05:69", "00:0C:29", "00:1C:14", "00:50:56", // VMware
"08:00:27", // VirtualBox
"00:03:FF", "00:15:5D", // Hyper-V
"00:1C:42", // Parallels
"00:16:3E", // Xen
"52:54:00", // QEMU/KVM
}
// 检查 MAC 地址是否属于虚拟机
func isVirtualMachine(mac string) bool {
mac = strings.ToUpper(mac)
for _, prefix := range vmMacPrefixes {
if strings.HasPrefix(mac, prefix) {
return true
}
}
return false
}
// 获取系统的所有 MAC 地址
func getMacAddresses() ([]string, error) {
var macAddresses []string
// 获取所有网络接口
interfaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to get network interfaces: %v", err)
}
// 遍历所有接口,提取 MAC 地址
for _, iface := range interfaces {
// 检查接口是否有有效的硬件地址(MAC 地址)
if iface.HardwareAddr != nil && len(iface.HardwareAddr) > 0 {
macAddresses = append(macAddresses, iface.HardwareAddr.String())
}
}
// 如果没有找到任何 MAC 地址,返回错误
if len(macAddresses) == 0 {
return nil, fmt.Errorf("no MAC addresses found")
}
return macAddresses, nil
}
func main() {
// 获取所有 MAC 地址
macAddresses, err := getMacAddresses()
if err != nil {
fmt.Printf("Error: %vn", err)
return
}
if len(macAddresses) == 0 {
fmt.Println("No MAC addresses found.")
return
}
// 检查是否为虚拟机
// isVM := false
for _, mac := range macAddresses {
if isVirtualMachine(mac) {
fmt.Printf("VM MAC Address detected! This maybe a Virtual Machine or Host Machine MAC Address: %sn", mac)
// isVM = true
break
}
}
}
但是有个问题,一般来说宿主机也会创建对应的虚拟网卡用于做NAT桥接,所以这个代码会把部分宿主机器当作虚拟机
检测CPU温度
例如在windows下我们能够通过别人写好的第三方库获取CPU温度,本质上还是在调用wmi查询,需要admin权限。
package main
import (
"fmt"
"strings"
"github.com/shirou/gopsutil/host"
)
func main() {
// 获取传感器温度信息
sensors, err := host.SensorsTemperatures()
if err != nil {
fmt.Println("Error retrieving sensor data:", err)
return
}
// 如果没有传感器数据,可能是虚拟机
if len(sensors) == 0 {
fmt.Println("No temperature sensors detected. This might be a virtual machine.")
return
}
// 遍历传感器数据并输出
isVirtualMachine := false
for _, sensor := range sensors {
fmt.Printf("Sensor: %s, Temperature: %.2f°Cn", sensor.SensorKey, sensor.Temperature)
// 检查传感器名称是否包含虚拟化相关信息
if strings.Contains(strings.ToLower(sensor.SensorKey), "virtual") ||
strings.Contains(strings.ToLower(sensor.SensorKey), "vmware") ||
strings.Contains(strings.ToLower(sensor.SensorKey), "hyperv") {
isVirtualMachine = true
}
}
// 判断是否为虚拟机
if isVirtualMachine {
fmt.Println("Virtualization-related sensors detected. This is likely a virtual machine.")
} else {
fmt.Println("No virtualization-related sensors detected. This is likely a physical machine.")
}
}
同理,Linux下就是去/sys查询就是了。这个代码也兼容Linux
进程判断法
进程黑白名单
例如在虚拟机器中会有些用于管理的进程例如vmtools.exe
这类,可以被标记为黑名单,还有就是普通的办公软件,例如微信,飞书这一类的,可以视为白名单
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
)
// 常用办公软件进程列表
var commonProcesses = []string{
"WINWORD.EXE", "EXCEL.EXE", "WeChat.exe", "QQ.exe", "chrome.exe",
"firefox.exe", "msedge.exe", "soffice.bin", "Pages", "Numbers",
"Safari", "Google Chrome",
}
// 虚拟机相关的特殊进程列表
var vmProcesses = []string{
"vmtoolsd.exe", "vmwaretray.exe", "vmwareuser.exe", // VMware
"VBoxService.exe", "VBoxTray.exe", // VirtualBox
"vmcompute.exe", "vmms.exe", // Hyper-V
"prl_toolsd", "prl_cc.exe", // Parallels
}
// 获取系统的进程列表
func getProcessList() ([]string, error) {
var cmd *exec.Cmd
// 根据操作系统选择合适的命令
switch runtime.GOOS {
case "windows":
cmd = exec.Command("tasklist") // Windows 使用 tasklist 获取进程列表
case "linux", "darwin": // Linux 和 macOS 使用 ps 获取进程列表
cmd = exec.Command("ps", "-e")
default:
return nil, fmt.Errorf("unsupported platform")
}
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get process list: %v", err)
}
// 将输出按行分割
lines := strings.Split(string(output), "n")
return lines, nil
}
// 检测是否存在目标进程
func detectProcesses(processList []string, targets []string) bool {
for _, process := range processList {
for _, target := range targets {
if strings.Contains(strings.ToLower(process), strings.ToLower(target)) {
return true
}
}
}
return false
}
func main() {
// 获取进程列表
processList, err := getProcessList()
if err != nil {
fmt.Printf("Error: %vn", err)
return
}
// 检测常用办公软件进程
if detectProcesses(processList, commonProcesses) {
fmt.Println("Common office software detected. Likely a normal user environment.")
} else {
fmt.Println("No common office software detected.")
}
// 检测虚拟机相关的特殊进程
if detectProcesses(processList, vmProcesses) {
fmt.Println("Virtual machine-related processes detected! Likely running in a virtual machine.")
} else {
fmt.Println("No virtual machine-related processes detected.")
}
}
父进程检测法
一般来说,我们反沙箱恶意样本使用场景之一是钓鱼,也有可能遭到研究员用物理机器暴力分析。这个时候我的打开进程一般是IDA,或者是其他的程序,而在实战环境下来说,一般是使用GUI的explorer打开所有可以通过检测自己的进程是否是处于被研究的环境下
package main
import (
"fmt"
"os"
"strings"
"syscall"
"github.com/shirou/gopsutil/v3/process"
"golang.org/x/sys/windows"
)
// 检测自身父进程是否为 explorer.exe
func isParentProcessExplorer() (bool, string, error) {
// 获取当前进程的 PID
currentPID := int32(os.Getpid())
// 获取当前进程对象
currentProcess, err := process.NewProcess(currentPID)
if err != nil {
return false, "", fmt.Errorf("failed to get current process: %v", err)
}
// 获取父进程的 PID
parentPID, err := currentProcess.Ppid()
if err != nil {
return false, "", fmt.Errorf("failed to get parent process PID: %v", err)
}
// 获取父进程对象
parentProcess, err := process.NewProcess(parentPID)
if err != nil {
return false, "", fmt.Errorf("failed to get parent process: %v", err)
}
// 获取父进程的名字
parentName, err := parentProcess.Name()
if err != nil {
return false, "", fmt.Errorf("failed to get parent process name: %v", err)
}
// 检查父进程是否为 explorer.exe
isExplorer := strings.ToLower(parentName) == "explorer.exe"
return isExplorer, parentName, nil
}
// 调用 Windows API 显示一个弹窗
func showMessageBox(title, message string) {
// 转换字符串为 UTF-16 格式
titlePtr, _ := syscall.UTF16PtrFromString(title)
messagePtr, _ := syscall.UTF16PtrFromString(message)
// 调用 MessageBoxW 函数
windows.MessageBox(
0, // HWND (窗口句柄),0 表示当前窗口
messagePtr, // 弹窗内容
titlePtr, // 弹窗标题
windows.MB_OK|windows.MB_ICONINFORMATION, // 弹窗类型
)
}
func main() {
// 检测父进程是否为 explorer
isExplorer, parentName, err := isParentProcessExplorer()
if err != nil {
fmt.Printf("Error: %vn", err)
return
}
// 根据检测结果执行不同逻辑
if isExplorer {
showMessageBox("Parent Process Check", "Parent process is explorer.exe. Proceeding...")
} else {
fmt.Printf("Parent process is not explorer.exe (Detected: %s). Exiting...n", parentName)
}
}
时间延时方法
例如在微步云沙箱中默认检测时限是三分钟,我们只要睡眠5分种就能够绕过云沙箱的检测,其他杀软的沙箱也是一样,因为不可能一直耗着占用计算资源就是。但是一般的直接调用Sleep函数的方法已经失效了。得看看有无其他的办法?
原生API大法
例如我们可以直接调用Win32API来实现我们的延时,在逆向的视角中我们只是在正常的调用dll
package main
import (
"fmt"
"math/rand"
"syscall"
"time"
)
func main() {
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
// 加载 kernel32.dll 的 Sleep 函数
kernel32 := syscall.NewLazyDLL("kernel32.dll")
sleepProc := kernel32.NewProc("Sleep")
delay := rand.Intn(900) + 100
fmt.Printf("Sleeping for %d milliseconds...n", delay)
// 调用 Windows 的 Sleep 函数
sleepProc.Call(uintptr(delay))
fmt.Println("Woke up!")
}
ping延时法
例如我们可以通过ping特殊的网站特定的次数来实现延时,这是正常软件也会做的事情,用于判断当前的网络环境
以Ping百度50次为例子:
package main
import (
"fmt"
"net"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
func main() {
target := "www.baidu.com"
count := 50
timeout := time.Second * 2
// 解析目标地址
ipAddr, err := net.ResolveIPAddr("ip4", target)
if err != nil {
fmt.Printf("Failed to resolve target %s: %vn", target, err)
return
}
fmt.Printf("Pinging %s (%s) with ICMP packets:nn", target, ipAddr.String())
// 创建原始套接字
conn, err := net.Dial("ip4:icmp", ipAddr.String())
if err != nil {
fmt.Printf("Failed to create connection: %vn", err)
return
}
defer conn.Close()
var successCount int
for i := 0; i < count; i++ {
// 构造 ICMP Echo 请求
icmpMessage := icmp.Message{
Type: ipv4.ICMPTypeEcho, Code: 0,
Body: &icmp.Echo{
ID: 1, // 标识符(可以随意设置)
Seq: i, // 序列号
Data: []byte("PING"),
},
}
// 序列化 ICMP 消息
messageBytes, err := icmpMessage.Marshal(nil)
if err != nil {
fmt.Printf("Failed to marshal ICMP message: %vn", err)
continue
}
// 记录发送时间
startTime := time.Now()
// 发送 ICMP 请求
_, err = conn.Write(messageBytes)
if err != nil {
fmt.Printf("Failed to send ICMP packet: %vn", err)
continue
}
// 设置读取超时时间
conn.SetReadDeadline(time.Now().Add(timeout))
// 接收 ICMP 响应
reply := make([]byte, 1500)
n, err := conn.Read(reply)
if err != nil {
fmt.Printf("Request timeout for seq=%dn", i)
continue
}
// 记录接收时间
duration := time.Since(startTime)
// 解析响应
parsedMessage, err := icmp.ParseMessage(1, reply[:n])
if err != nil {
fmt.Printf("Failed to parse ICMP response: %vn", err)
continue
}
// 检查是否是 Echo Reply
if parsedMessage.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := parsedMessage.Body.(*icmp.Echo)
if !ok {
fmt.Printf("Invalid ICMP echo reply formatn")
continue
}
if echoReply.ID == 1 && echoReply.Seq == i {
fmt.Printf("Reply from %s: seq=%d time=%vn", ipAddr.String(), i, duration)
successCount++
} else {
fmt.Printf("Mismatched reply: ID=%d, Seq=%dn", echoReply.ID, echoReply.Seq)
}
} else {
fmt.Printf("Received non-echo reply: %+vn", parsedMessage)
}
// 等待一段时间再发送下一次请求
time.Sleep(time.Second)
}
fmt.Printf("nPing statistics for %s:n", target)
fmt.Printf(" Packets: Sent = %d, Received = %d, Lost = %d (%.2f%% loss)n",
count, successCount, count-successCount, float64(count-successCount)/float64(count)*100)
}
结语
还~有~一~件~事~(老爹音)
反沙箱的前提是被用作动态分析,如果你的恶意软件直接在静态分析那里就被查杀了,那反沙箱措施就没有作用了,所以一定要该混淆的混淆,该加壳的加壳,再去考虑什么反沙箱就是了
原文始发于微信公众号(亿人安全):浅谈恶意样本の反沙箱分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论