time.Sleep():因为想摸鱼,所以写了一个免杀框架

有趣的 Sleep

闲聊几句

项目发布也半年了,反沙箱模块已经有段时间没有更新了,在更新的时候发现了一些有趣的东西

time.Sleep()

首先是Golang time.Sleep() 函数,会被微步沙箱的时间流逝跳过,先来看下函数原理

time.Sleep() 是 Go 语言中用于暂停当前 goroutine 执行的函数,其原理涉及到操作系统级别的调度和时间管理。

在调用 time.Sleep(d) 时,程序会暂停当前 goroutine 的执行,让出 CPU 给其他可以运行的 goroutine。参数 d 是一个 Duration 类型,表示暂停的时间长度。

具体原理如下:

  1. 系统调用: 当 time.Sleep() 被调用时,Go 运行时会发起一个系统调用,以请求操作系统在一定时间后恢复当前 goroutine 的执行。

  2. 时钟管理: 操作系统会将当前 goroutine 标记为休眠状态,并设置一个定时器(timer),用来计时暂停的时间。定时器可以基于硬件时钟或操作系统内部的时间管理机制。

  3. 进入休眠: 一旦定时器到达设定的时间(d),操作系统会唤醒当前 goroutine,使其重新加入到可运行状态的队列中,等待调度器(scheduler)再次分配 CPU 时间片给它。

  4. 精度和误差: time.Sleep() 的精度取决于操作系统的实现和硬件。一般来说,精度可以达到毫秒级别,但具体取决于操作系统的调度策略和实现细节。在某些操作系统上,可能存在睡眠时间的小偏差。

  5. Goroutine 调度: Go 语言的运行时会负责管理所有的 goroutine,包括它们的创建、销毁和调度。当一个 goroutine 调用了 time.Sleep() 后,它会让出当前的执行权,让其他的 goroutine 有机会执行,这种调度是由 Go 运行时管理的。

影响

神奇的是,这个函数在一些情况下会出现特殊情况

先看第一组代码

package main

import (
    "fmt"
    "time"
)

func main() {
    startTime := time.Now()
    for i := 0; i < 1000; i++ {
        time.Sleep(1 * time.Millisecond)
    }
    fmt.Printf("%d\n", time.Since(startTime).Milliseconds())
}

输出得到 

15467

第二组代码

package main

import (
    "fmt"
    "time"
)

func main() {
    startTime := time.Now()
    for i := 0; i < 1000; i++ {
        time.Sleep(10 * time.Millisecond)
    }
    fmt.Printf("%d\n", time.Since(startTime).Milliseconds())
}

输出得到

15611

差距较大的睡眠时间,居然得出差不多的结果,有兴趣可以看看Github

https://github.com/golang/go/issues/44343

和反沙箱有什么关系

通常,网上的文章会说睡眠一段时间有概率跳过沙箱的检测,但是现在沙箱基本都会有不同于现实世界的时间流速,所有也有反其道行之的检测时间流速来判断是否是沙箱。沙箱的开发肯定也会考虑到这个问题,可能会对睡眠函数或者进程的某些地方进行检测。

d2b5ca33bd20240704111756

NtDelayExecution

NtDelayExecution 是一个未公开的 Win32 API

d2b5ca33bd20240704111907

不过我把它转为了Go原生可以调用的API

func NtDelayExecution(DelayInterval int64) (err error) {
	delay := -(DelayInterval * 1000 * 10000)

	r1, _, e1 := syscall.SyscallN(
		procNtDelayExecution.Addr(),
		uintptr(0),
		uintptr(unsafe.Pointer(&delay)),
	)
	if r1 != 0 {
		err = errnoErr(e1)
	}
	return
}

它有两个参数

  • Alertable:无需关注,设置为 0 即可
  • DelayInterval:延迟间隔,但需要接收负数作为参数来表示延迟时间,负数也可以理解为未来时间

使用这个API进行延迟执行,沙箱会直接标记

d2b5ca33bd20240704112423

Beep

Beep 函数 (utilapiset.h)

在扬声器上生成简单的音调。 函数是同步的;它执行可警报等待,在声音完成之前不会将控制权返回到其调用方。

BOOL Beep(
  [in] DWORD dwFreq,
  [in] DWORD dwDuration
);

参数
[in] dwFreq

声音的频率,以Hz为单位。 此参数的范围必须介于 37 到 32,767 (0x25 到 0x7FFF) 。

[in] dwDuration

声音的持续时间(以毫秒为单位)。

返回值

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。

Doc:https://learn.microsoft.com/zh-cn/windows/win32/api/utilapiset/nf-utilapiset-beep

Go Beep()

freq: 人类的听觉范围大约在 20Hz – 20000Hz 之间
duration : 设置时间,毫秒为单位

func BeepSleep(duration uint32) {
	freq := uint32(30000)
	startTime := time.Now()

	r1, _, _ := wdll.Beep().Call(uintptr(freq), uintptr(duration*1000))
	if r1 == 0 {
		os.Exit(0)
	}

	durationElapsed := time.Since(startTime).Seconds()

	if uint32(durationElapsed) != duration {
		os.Exit(0)
	}
}

有趣的事

Link:https://securityliterate.com/beeeeeeeeep-how-malware-uses-the-beep-winapi-function-for-anti-analysis/

当程序调用 Beep 函数时,它最终会调用一个名为NtDelayExecution的函数,该函数的作用正如其名称所示:它延迟调用程序正在运行的线程的执行。

所以,沙箱检测了NtDelayExecution函数,但是没有检测Beep???

Beep() 的Plus版本 MessageBeep()

进一步探索,还发现了 MessageBeep 函数 (winuser.h)

Doc:https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-messagebeep

注意 

在 Windows Server 2022 上,已禁用任务计划程序中的 Microsoft\Windows\Multimedia\SystemSoundsService 任务。 需要启用此任务才能使 MessageBeep 正常运行。

总结

延迟执行的函数可能还不止这些,有趣的事情是:无论是直接使用函数还是使用函数最终调用的API结果居然是不同。沙箱还真的是有点意思。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 共2条

请登录后发表评论

    请登录后查看评论内容