浅谈恶意样本の反沙箱分析

举报
亿人安全 发表于 2025/02/28 23:56:37 2025/02/28
【摘要】 原文首发在:奇安信攻防社区https://forum.butian.net/share/4050说实话单纯的静态免杀其实不是很难,只要通过足够新颖的加壳手段就能够成功将木马加载到内存中,但是抵御不了蓝队(比如微步云沙箱)使用沙箱的动态分析,所以通常只能够免杀小一天就上传了病毒库,从而免杀失效了。本文就是来介绍几种反沙箱的思路来帮助红队搞出耐得住沙箱考验的payload说实话单纯的静态免杀其实...

原文首发在:奇安信攻防社区

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: %v\n", 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: %s\n", 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°C\n", 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: %v\n", 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: %v\n", 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: %v\n", target, err)
        return
    }

    fmt.Printf("Pinging %s (%s) with ICMP packets:\n\n", target, ipAddr.String())

    // 创建原始套接字
    conn, err := net.Dial("ip4:icmp", ipAddr.String())
    if err != nil {
        fmt.Printf("Failed to create connection: %v\n", 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: %v\n", err)
            continue
        }

        // 记录发送时间
        startTime := time.Now()

        // 发送 ICMP 请求
        _, err = conn.Write(messageBytes)
        if err != nil {
            fmt.Printf("Failed to send ICMP packet: %v\n", 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=%d\n", i)
            continue
        }

        // 记录接收时间
        duration := time.Since(startTime)

        // 解析响应
        parsedMessage, err := icmp.ParseMessage(1, reply[:n])
        if err != nil {
            fmt.Printf("Failed to parse ICMP response: %v\n", err)
            continue
        }

        // 检查是否是 Echo Reply
        if parsedMessage.Type == ipv4.ICMPTypeEchoReply {
            echoReply, ok := parsedMessage.Body.(*icmp.Echo)
            if !ok {
                fmt.Printf("Invalid ICMP echo reply format\n")
                continue
            }

            if echoReply.ID == 1 && echoReply.Seq == i {
                fmt.Printf("Reply from %s: seq=%d time=%v\n", ipAddr.String(), i, duration)
                successCount++
            } else {
                fmt.Printf("Mismatched reply: ID=%d, Seq=%d\n", echoReply.ID, echoReply.Seq)
            }
        } else {
            fmt.Printf("Received non-echo reply: %+v\n", 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)
}

结语

还~有~一~件~事~(老爹音)

反沙箱的前提是被用作动态分析,如果你的恶意软件直接在静态分析那里就被查杀了,那反沙箱措施就没有作用了,所以一定要该混淆的混淆,该加壳的加壳,再去考虑什么反沙箱就是了

亿人安全
知其黑,守其白。手握利剑,心系安全。主要研究方向包括:Web、内网、红蓝对抗、代码审计、安卓逆向、CTF。
187篇原创内容
公众号

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。