这次我们来聊聊反沙箱,众所周知。很多前人大佬都对反沙箱有自己的思路理解和实现方法,也开源了很多,但是安全厂商的AV沙箱也一直在进步。
啥是沙箱呢?沙箱能够模拟出软件执行所需的运行环境,通过进程hook技术等来对软件执行过程中的行为进行分析,判断是否有敏感的操作行为。或者更高级的检测手法,将获取到的程序API调用序列以及其他的一些行为特征输入到智能分析引擎中进行检测。
特别进入2024年,大数据,LLM大模型智能结合起来的沙箱行为识别,具备更强的检测能力。如果我们的程序没有做好反沙箱,很容易就被检测出来。所以做好反沙箱,来增加程序持久性是非常有必要的。
那沙箱的判断,个人认为,要有准确性、兼容性、通用性,如何通过这些去实现判断的准确性和兼容性呢,这就是比较繁琐的一个操作,网上开源的有很多方法以及代码,但是大多是从简单的直接方法去判断,比如内存大小、硬盘大小、硬件设备 、桌面壁纸的哈希值 ,效果不是十分理想。
一方面,很多杀软也增加了反反沙箱的规则,不加反沙箱还好,加了反沙箱反倒被反反沙箱检测出可疑行为,得不偿失。
另一方面,很多的判断方法是伤兵一千,自损八百,有时确实反了沙箱,但是也会有很多环境下的系统运行不起来,这就是我所说的兼容性 ,要尽可能适配所有操作系统,还要准确的判断出沙箱环境和虚拟机环境。
下面主要说利用时间流速来进行沙箱识别的方法。
沙箱为了防止恶意代码长时间Sleep而不进行恶意行为,大部分沙箱都会选择进行时间加速。
但是问题就出现在这里,如果进行了时间加速,那Sleep函数中的时间流速是必然不同于正常值的,如果我们可以选择一个不会被修改的时间作为基准,就很容易识别出其中的差异。
┌────────────────┐ ┌──────────────────┐
│ │compare │ │
│ time_base ├───────►│ Sleep(xxx) │
└────────────────┘ └──────────────────┘
下面介绍的都是当前市面沙箱的通杀技能。
使用NTP服务器获取的时间作为基准。NTP服务的全称是 Network Time Protocol,基于UDP的时间同步协议,网络的时间可以同步到标准UTC时间。提供NTP对时服务的服务器有很多,比如微软的 time.windows.com.
关于NTP协议的报文以及怎么使用UDP协议获取标准时间相关的内容请参考网络,这里仅介绍实现思路:
在执行Sleep之前利用NTP服务器来获取一下时间戳,执行Sleep之后再获取一下NTP时间,对比两次的差异,伪代码如下:
#define DURATION 10*1000__int64 start = GetNtpTime();
Sleep(DURATION);
BOOL is_sandbox=FALSE;
if( GetNtpTime() - start < DURATION ){
is_sandbox = TRUE;
}
利用线程同步的机制来实现。主要思路如下,我们设置一个Event,然后将这个事件传入一个线程中,线程中会Sleep X秒后对Event进行激活,主线程使用 WaitForSingleObject 在规定时间内等待这个事件,如果在规定时间内出现超时,则不是沙箱,否则是沙箱。
伪代码如下:
void ThreadFunc(PHANDLE pevent) {
Sleep(10000);
SetEvent(*pevent);
}int main() {
BOOL is_sandbox = FALSE;
HANDLE eventHandle = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动重置事件,初始状态为非信号状态
if (eventHandle == NULL) {
std::cerr << "CreateEvent failed with " << GetLastError() << std::endl;
}
// 设置超时为8000毫秒(8秒)
DWORD timeout = 9000; // 9秒的等待时间
// 等待事件或超时
HANDLE hThread = CreateThread(
NULL,
0,
(LPTHREAD_START_ROUTINE)ThreadFunc,
&eventHandle,
0,
NULL
);
DWORD waitResult = WaitForSingleObject(eventHandle, timeout);
if (waitResult != WAIT_TIMEOUT) {
is_sandbox = TRUE;
}
return 0;
}
GetTickCount64 函数获取系统自启动以来处于工作状态的时间, 函数的分辨率限制为系统计时器的分辨率,通常介于 10 毫秒到 16 毫秒之间。
虽然微软文档说获取的时间并不是非常准确,作为时间基准已经足够用了。但是我们不可以直接使用这个函数,因为这个函数大概率已经被沙箱hook了,那我们要怎么获取到相同的效果呢?
我们这里逆向分析一下 GetTickCount64
的函数实现:
经过我的验证,函数中两个全局变量的偏移在不同的windows NT版本上都是一样的,所以我们可以直接使用如下汇编来完成自己的 GetTickCount64 函数,代码如下:
调试一下,看到和标准函数的输出相同,说明实现的没有问题:
接下来就比较简单了,与思路一相同,伪代码如下:
#define DURATION 10*1000 __int64 start = MyGetTickCount64();
Sleep(DURATION);
BOOL is_sandbox = FALSE;
if (MyGetTickCount64() - start < DURATION) {
is_sandbox = TRUE;
}
这个思路源自于MessageBox的源码,逆向MessageBox会发现其底层调用的其实是一个叫 MessageBoxTimeout 函数,此函数可以实现一个超时自动关闭窗口的弹框,并且可以停止阻塞当前进程,之前也分析过怎么用它实现延时效果以阻塞沙箱,细节内容见星球。
深层调试分析后发现此函数底层会调用一个定时器,来实现超时消息的处理,这里也模拟这个思路。
我们在主线程中设置一个超时器作为基准时间,然后在线程中Sleep,对比两个的返回时间是否一致,进而判断时间流速。伪代码如下:
void threadFunction(UINT_PTR *iTimerID) {
Sleep(10000);
}int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG Msg;
UINT_PTR iTimerID;
// Set our timer without window handle
iTimerID = SetTimer(NULL, 0x1, 9000, NULL);
HANDLE hThread = CreateThread(
NULL,
0,
(LPTHREAD_START_ROUTINE)threadFunction,
&iTimerID,
0,
NULL
);
BOOL is_sandbox = FALSE;
// Because we are running in a console app, we should get the messages from
// the queue and check if msg is WM_TIMER
while (GetMessage(&Msg, NULL, 0, 0))
{
if (Msg.message == WM_TIMER && Msg.wParam == iTimerID) {
// 看线程是否结束
//收到超时消息
DWORD exitCode = 0;
if (GetExitCodeThread(hThread, &exitCode)) {
if (exitCode == STILL_ACTIVE) {
is_sandbox = FALSE;
}
else {
is_sandbox = TRUE;
}
}
KillTimer(NULL, iTimerID);
break;
}
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return 0;
}
代码和思路星球里都有,除此之外还有各种大佬贴身交流,让你的技术之路不再孤单,别再犹豫,现在就是启程的最佳时刻。
2024年,新的开始,新的征程,不变的是继续提升安全技术矛与盾的决心。新年之际小编为大家准备了优惠券福利,让你的学习之旅更加轻松愉快。抓紧时间,名额有限,快来领取,开启你的新技能探索之旅吧!