长亭百川云 - 文章详情

反沙箱与反调试总结

T00ls安全

85

2024-07-14

反沙箱与反调试

我们要反沙箱,就要思考沙箱和真实物理机的区别,比如说内存大小用户名cpu核心数等等,下面会逐个进行介绍。

sleep

沙箱在执行样本的时候肯定是有时间限制的,所以我们可以先让我们的程序睡眠一段时间再执行,这样在沙箱的环境下,我们的程序还在sleep呢,沙箱就检测完了,肯定不会检测到任何异常

但是当我们简单的只使用sleep函数时,沙箱可能会对我们的sleep函数进行一个hook,因此我们需要替代类似的api来实现我们的sleep功能,下面列举了一些常见的api

Functions used

1. Sleep, SleepEx, NtDelayExecution  
2. WaitForSingleObject, WaitForSingleObjectEx, NtWaitForSingleObject  
3. WaitForMultipleObjects, WaitForMultipleObjectsEx, NtWaitForMultipleObjects  
4. SetTimer, SetWaitableTimer, CreateTimerQueueTimer  
5. timeSetEvent (multimedia timers)  
6. IcmpSendEcho  
7. select (Windows sockets)

下面是一些简单的demo(注意,有的demo并不能直接跑起来,需要自己再进行修改):

WaitForSingleObject

首先,CreateEvent 函数用于创建一个事件对象,第三个参数为初始状态,TRUE 表示初始为信号状态,FALSE 表示初始为非信号状态。

接着,WaitForSingleObject 函数被调用来等待事件对象,第二个参数为等待时间,以毫秒为单位。

在这里,传入 10000 表示等待 10 秒钟。如果事件对象在等待时间内被设置为信号状态,函数会立即返回,如果等待时间到期时事件对象仍为非信号状态,函数会超时返回。

HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);  
 WaitForSingleObject(hEvent, 10000);  
 CloseHandle(hEvent);

select

select的作用就是确定套接口的状态,对于每一个socket,调用者可以查询它的可读性、可写性及错误状态信息。

我们只需要建立一个socket连接,然后调用这个api去查询socket状态就可以了,这个调用消耗的时间就是select第五个参数timeval里设置的时间。

我们让我们的程序检测连接到指定 IP 地址和端口的网络可达性,并且设置了超时时间,当然他们是不可达的,所以当我们运行程序时他会select我们的socket一直等待直到超时时间,从而达到sleep的功能。

int iResult;  
 DWORD timeout = delay; // delay in milliseconds  
 bool OK = true;  
   
 SOCKADDR_IN sa = { 0 };  
 SOCKET sock = INVALID_SOCKET;  
   
 // this code snippet should take around Timeout milliseconds  
 do {  
     memset(&sa, 0, sizeof(sa));  
     sa.sin_family = AF_INET;  
     inet_pton(AF_INET, "8.8.8.8", &(sa.sin_addr));    // we should have a route to this IP address  
     sa.sin_port = htons(80); // we should not be able to connect to this port  
   
     sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
     if (sock == INVALID_SOCKET) {  
         OK = false;  
         break;  
     }  
   
     // setting socket timeout  
     unsigned long iMode = 1;  
     iResult = ioctlsocket(sock, FIONBIO, &iMode);  
   
     iResult = connect(sock, (SOCKADDR*)&sa, sizeof(sa));  
     if (iResult == SOCKET_ERROR) {  
         int error = WSAGetLastError();  
         if (error != WSAEWOULDBLOCK && error != WSAEINPROGRESS) {  
             OK = false;  
             break;  
         }  
     }  
   
     iMode = 0;  
     iResult = ioctlsocket(sock, FIONBIO, &iMode);  
     if (iResult != NO_ERROR) {  
         OK = false;  
         break;  
     }  
   
     // fd set data  
     fd_set Write, Err;  
     FD_ZERO(&Write);  
     FD_ZERO(&Err);  
     FD_SET(sock, &Write);  
     FD_SET(sock, &Err);  
     timeval tv = { 0 };  
     tv.tv_usec = timeout * 1000;  
   
     // check if the socket is ready, this call should take Timeout milliseconds  
     iResult = select(0, NULL, &Write, &Err, &tv);  
     if (iResult == SOCKET_ERROR) {  
         OK = false;  
         break;  
     }  
   
     if (FD_ISSET(sock, &Err)) {  
         OK = false;  
         break;  
     }  
   
 } while (false);  
   
 if (sock != INVALID_SOCKET)  
     closesocket(sock);

NtDelayExecution

NtDelayExecution 是Windows系统内部的一个函数,它用于在当前线程上引入一个指定的延迟时间。这个函数是在Windows NT内核中实现的,而不是用户空间的API。它被用于内核级别的编程,通常在设备驱动程序或其他需要精确控制时间的内核模块中使用。

注意我们设置的时间是负数,这是因为在Windows内部,正数表示绝对时间(从1970年1月1日开始的100纳秒间隔),而负数表示相对时间(从现在开始的100纳秒间隔)

#include <iostream>  
 #include <windows.h>  
   
 typedef NTSTATUS(NTAPI* pfnNtDelayExecution)(BOOL Alertable, PLARGE_INTEGER DelayInterval);  
   
 int main() {  
     // 加载 ntdll.dll  
     HMODULE hModule = LoadLibrary(L"ntdll.dll");  
     if (hModule == NULL) {  
         std::cout << "Failed to load ntdll.dll" << std::endl;  
         return 1;  
     }  
     // 获取 NtDelayExecution 函数地址  
     pfnNtDelayExecution fnNtDelayExecution = (pfnNtDelayExecution)GetProcAddress(hModule, "NtDelayExecution");  
     if (fnNtDelayExecution == NULL) {  
         std::cout << "Failed to get address of NtDelayExecution" << std::endl;  
         FreeLibrary(hModule);  
         return 1;  
     }  
   
     // 构造延迟时间  
     LARGE_INTEGER delayTime;  
     delayTime.QuadPart = -5000000;  // 单位为 100纳秒  
   
     // 调用 NtDelayExecution 函数  
     NTSTATUS status = fnNtDelayExecution(FALSE, &delayTime);  
     if (status != 0) {  
         std::cout << "NtDelayExecution failed with status: " << status << std::endl;  
         FreeLibrary(hModule);  
         return 1;  
     }  
   
     std::cout << "Delay completed." << std::endl;  
   
     // 释放 ntdll.dll  
     FreeLibrary(hModule);  
   
     return 0;  
 }

对抗沙箱加速

沙箱为了防止恶意代码长时间sleep而不进行恶意行为,大部分沙箱都会选择进行时间加速

但是问题就出现在这里,如果进行了时间加速,那Sleep函数中的时间流速是必然不同于正常值的,如果我们可以选择一个不会被修改的时间作为基准,就很容易识别出其中的差异。

ntp时间

NTP (Network Time Protocol,网络时间协议) 是一种用于同步计算机系统时钟的协议,它可以提供高精度的时间同步服务。NTP 时间是指从 NTP 服务器获取的网络时间,它可以通过互联网进行同步

NTP 时间是一个以秒为单位的双精度浮点数,表示自从 1900 年 1 月 1 日 0 时 0 分 0 秒起至当前时刻所经过的秒数,其中整数部分表示经过的天数,小数部分表示当前天内已经过去的秒数。NTP 时间的精度可以达到纳秒级别,可以满足各种应用的时间同步需求。

下面是一个getNTPTime函数的demo,我们可以在sleep功能前执行一下获取当前时间,sleep后再执行一下获取时间,然后比较两次时间差进行判断我们的sleep是否被沙箱加速了。

#define NTP_TIMESTAMP_DELTA 2208988800ull  
   
 struct NTPPacket  
 {  
     union  
     {  
         struct _ControlWord  
         {  
             unsigned int uLI : 2;       // 00 = no leap, clock ok     
             unsigned int uVersion : 3;  // version 3 or version 4  
             unsigned int uMode : 3;     // 3 for client, 4 for server, etc.  
             unsigned int uStratum : 8;  // 0 is unspecified, 1 for primary reference system,  
             // 2 for next level, etc.  
             int nPoll : 8;              // seconds as the nearest power of 2  
             int nPrecision : 8;         // seconds to the nearest power of 2  
         };  
   
         int nControlWord;             // 4  
     };  
   
     int nRootDelay;                   // 4  
     int nRootDispersion;              // 4  
     int nReferenceIdentifier;         // 4  
   
     __int64 n64ReferenceTimestamp;    // 8  
     __int64 n64OriginateTimestamp;    // 8  
     __int64 n64ReceiveTimestamp;      // 8  
   
     int nTransmitTimestampSeconds;    // 4  
     int nTransmitTimestampFractions;  // 4  
 };  
   
 int getNTPTime(time_t& ttime)  
 {  
     ttime = 0;  
     WSADATA wsaData;  
     // Initialize Winsock  
     int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);  
     if (iResult != 0) return 0;  
     int result, count;  
     int sockfd = 0, rc;  
     sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  
     if (sockfd < 0) return 0;  
     fd_set pending_data;  
     timeval block_time;  
     NTPPacket ntpSend = { 0 };  
     ntpSend.nControlWord = 0x1B;  
     NTPPacket ntpRecv;  
     SOCKADDR_IN addr_server;  
     addr_server.sin_family = AF_INET;  
     addr_server.sin_port = htons(123);//NTP服务默认为123端口号  
     addr_server.sin_addr.S_un.S_addr = inet_addr("120.25.115.20"); //该地址为阿里云NTP服务器的公网地址,其他NTP服务器地址可自行百度搜索。  
     SOCKADDR_IN sock;  
     int len = sizeof(sock);  
   
     if ((result = sendto(sockfd, (const char*)&ntpSend, sizeof(NTPPacket), 0, (SOCKADDR*)&addr_server, sizeof(SOCKADDR))) < 0)  
     {  
         int err = WSAGetLastError();  
         return 0;  
     }  
     FD_ZERO(&pending_data);  
     FD_SET(sockfd, &pending_data);  
     //timeout 10 sec  
     block_time.tv_sec = 10;  
     block_time.tv_usec = 0;  
     if (select(sockfd + 1, &pending_data, NULL, NULL, &block_time) > 0)  
     {  
         //获取的时间为1900年1月1日到现在的秒数  
         if ((count = recvfrom(sockfd, (char*)&ntpRecv, sizeof(NTPPacket), 0, (SOCKADDR*)&sock, &len)) > 0)  
             ttime = ntohl(ntpRecv.nTransmitTimestampSeconds - NTP_TIMESTAMP_DELTA);  
     }  
     closesocket(sockfd);  
     WSACleanup();  
     return 1;  
 }

GetTickCount64

GetTickCount64 函数获取系统自启动以来处于工作状态的时间,我们可以通过sleep前后的分别GetTickCount64(),然后看时间差是否符合我们的预期如果符合的话就不是沙箱,不符合的话就可能被沙箱加速了。(代码这里就不展示了,下面会有一个自实现的GetTickCount64)

线程同步事件

这里也用到了上面的WaitForSingleObject,这里的思路是我们先初始化一个时间,初始为非信号状态,然后创建一个线程,线程里面干两件事:先sleep10秒,然后再将事件置为有信号状态。主线程使用 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;  
   
 }

使用计时器

我们这里的思路和线程同步事件差不多,先创建一个定时器,其超时时间为9000毫秒。

然后创建一个线程,线程函数为threadFunction,并将定时器ID的指针作为参数传递给线程函数。

然后循环获取消息队列中的消息,当收到定时器消息时,通过GetExitCodeThread函数判断线程是否结束,若线程结束则将is_sandbox设置为TRUE,即可以判断是沙箱加速了,否则设置为FALSE,然后终止定时器并跳出循环。

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;  
 }

自实现sleep

上面的思路归根结底还都是用了系统,如果沙箱hook的api够多的话,还是很难办的,所以我们可以尝试自实现一些可以sleep的函数来防止被hook。

MyGetTickCount64

GetTickCount64这个函数大概率已经被沙箱hook,那我们要怎么获取到相同的效果呢?

我们这里可以先逆向分析一下 GetTickCount64 的函数实现:

然后我们根据逆向的结果进行自实现。

vs2022 x64 C/C++和汇编混编_vs2022 64位 inlineasm-CSDN博客

可以看到自实现的效果和GetTickCount64的效果一样,因此可以使用自实现的GetTickCount64来进行反沙箱。

质数运算

我们可以在代码中实现一个质数运算的功能,让程序来计算从而达到延时效果。

bool isPrime(int number) {  
     if (number <= 1)  
         return false;  
   
     for (int i = 2; i * i <= number; ++i) {  
         if (number % i == 0)  
             return false;  
     }  
   
     return true;  
 }

检测环境

下面列举的都是检测环境的东西,比较简单,重点是思路。

检测用户名

因为沙箱都有固定的用户名,我们可以将沙箱的用户名都收集起来,然后进行匹配判断,代码如下:

int gensandbox_username() {  
   
char username[200];  
   
size_t i;  
   
DWORD usersize = sizeof(username);  
   
GetUserNameA(username, &usersize);  
   
   
for (i = 0; i < strlen(username); i++) {   
   
  
username = toupper(username);//注意使用toupper来进行大写匹配  
   
}  
   
if (strstr(username, "JOHN-PC") != NULL) {  
   
  
return TRUE;  
   
}  
   
return FALSE;  
 }

检测内存

使用GlobalMemoryStatusEx来获取内存大小,从而进行判断。

bool checkMemory() {     
     MEMORYSTATUSEX memoryStatus;  
     memoryStatus.dwLength = sizeof(memoryStatus);  
     GlobalMemoryStatusEx(&memoryStatus);  
     DWORD RAMMB = memoryStatus.ullTotalPhys / 1024 / 1024;  
     if (RAMMB < 4096)   
         return false;  
 }

检测cpu核心数

一般来说,沙箱的核心数肯定会被限制的,许多在线检测的虚拟机沙盘是2核心,我们可以通过核心数来判断是否为真实机器或检测用的虚拟沙箱。GetSystemInfo()将系统信息写入类型为SYSTEM_INFO的结构体,其中成员dwNumberOfProcessors就是CPU核心数。

bool checkCPU() {  
     SYSTEM_INFO systemInfo;  
     GetSystemInfo(&systemInfo);  
     DWORD numberOfProcessors = systemInfo.dwNumberOfProcessors;  
     if (numberOfProcessors < 4) return false;  
 }

检测开机时间

许多沙箱检测完毕后会重置系统,我们可以检测开机时间来判断是否为真实的运行状况。GetTickCount这个api用于获取自系统启动以来经过的毫秒数。

bool checkuptime() {  
     DWORD uptime = GetTickCount();  
     printf("uptime:%u\n", uptime);  
     if (uptime < 3600000)  
         return false;  
     else  
         return true;  
 }

检测文件名

在上传文件后有些沙箱会重命名我们的文件,我们就可以以此来检测是否是沙箱环境。

if (strstr(argv[0], "aaa.exe") > 0)  
   
{  
   
  
printf("111");//做一些无害的操作即可  
   
}
if (IsDebuggerPresent())  
     ExitProcess(-1);

检测语言

正常情况下,我们接触到的都是国内项目,系统都是中文的,但是许多沙箱都是默认配置搭建起来的,所以使用英文系统。获取当前系统首选语言也是一种有效的检测方法。

LANGID langId = GetUserDefaultUILanguage();  
std::cout << "操作系统语言: " << PRIMARYLANGID(langId) << "-" << SUBLANGID(langId) << std::endl;检测虚拟机

关于检测环境我没有说反虚拟机相关的东西,是因为有些单位就是跑在超融合虚拟化下的,本身就是虚拟机,这种情况下反虚拟机毫无意义。

一些其他的骚操作

  • • 在吐司看到的大佬评论,检测电脑中后缀为.docx文件的数量

  • • 忘了在哪看的文章,禁止非微软签名访问进程(用到的结构体PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY )

  • • 自己搞一个反连平台

  • • 定义一个域名(真实环境的),如果在目标域环境中,自然就匹配通过。

反调试

反调试的话说实话很难做到完全让分析人员分析不了,所以我在这里只是列一些常见的操作。

IsDebuggerPresent

IsDebuggerPresent 是一个 Windows API 函数,用于检测当前进程是否处于被调试的状态。如果当前进程正在被调试,该函数将返回非零值(TRUE),否则返回零值(FALSE)。

if (IsDebuggerPresent())    ExitProcess(-1);

CheckRemoteDebuggerPresent

CheckRemoteDebuggerPresent用于检测指定进程是否正在被远程调试器监视。其函数原型如下:

BOOL WINAPI CheckRemoteDebuggerPresent(  
   HANDLE hProcess,  
   PBOOL  pbDebuggerPresent  
 );
  • • hProcess:要检测的目标进程的句柄。。通常使用 GetCurrentProcess() 函数获取当前进程的句柄。

  • • pbDebuggerPresent:一个指向 BOOL 值的指针,用于接收检测结果。如果目标进程正在被远程调试器监视,则该值将被设置为非零值(TRUE),否则为零值(FALSE)。

HANDLE hProcess =  GetCurrentProcess() ;  
 BOOL debuggerPresent = FALSE;  
 if (hProcess != NULL) {  
     if (CheckRemoteDebuggerPresent(hProcess, &debuggerPresent) && debuggerPresent) {  
         std::cout << "指定进程正在被远程调试器监视" << std::endl;  
     } else {  
         std::cout << "指定进程未被远程调试器监视" << std::endl;  
     }  
     CloseHandle(hProcess);  
 } else {  
     std::cout << "无法打开指定进程" << std::endl;  
 }

检测进程

如果当前计算机进程存在ida.exex64dbg.exe等等,则直接退出或者执行一系列的无害操作。

#include <windows.h>  
 #include <tlhelp32.h>  
 #include <stdio.h>  
   
 int main() {  
     HANDLE hProcessSnap;  
     PROCESSENTRY32 pe32;  
   
     hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);  
     if (hProcessSnap == INVALID_HANDLE_VALUE) {  
         printf("错误:无法创建进程快照\n");  
         return 1;  
     }  
   
     pe32.dwSize = sizeof(PROCESSENTRY32);  
   
     if (!Process32First(hProcessSnap, &pe32)) {  
         printf("错误:无法获取第一个进程\n");  
         CloseHandle(hProcessSnap);  
         return 1;  
     }  
   
     do {  
         if (strcmp(pe32.szExeFile, "ida.exe") == 0) {  
             printf("ida.exe 运行中,进程ID为 %d\n", pe32.th32ProcessID);  
         }  
     } while (Process32Next(hProcessSnap, &pe32));  
   
     CloseHandle(hProcessSnap);  
     return 0;  
 }

PEB

PEB(Process Environment Block)是Windows操作系统中的一个数据结构,它存储了进程相关的信息。每个在Windows上运行的进程都有一个唯一的PEB。关于peb的东西这里就不多说了,现在知道他是重要的结构就可以了。

PEB!BeingDebugged Flag

此方法只是检查 PEB 的 BeingDebugged 标志而不调用 IsDebuggerPresent() 的另一种方法。

#ifndef _WIN64  
 PPEB pPeb = (PPEB)__readfsdword(0x30);  
 #else  
 PPEB pPeb = (PPEB)__readgsqword(0x60);  
 #endif // _WIN64  
   
  
 if (pPeb->BeingDebugged)  
     goto being_debugged;NtGlobalFlag

NtGlobalFlag 是PEB的一个字段,通常,当进程未被调试时,NtGlobalFlag字段包含值0x0。调试进程时,该字段通常包含值0x70。

#define FLG_HEAP_ENABLE_TAIL_CHECK   0x10  
 #define FLG_HEAP_ENABLE_FREE_CHECK   0x20  
 #define FLG_HEAP_VALIDATE_PARAMETERS 0x40  
 #define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS)  
   
 #ifndef _WIN64  
 PPEB pPeb = (PPEB)__readfsdword(0x30);  
 DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);  
 #else  
 PPEB pPeb = (PPEB)__readgsqword(0x60);  
 DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);  
 #endif // _WIN64  
  
  
 if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)  
     goto being_debugged;

参考

原文链接

https://www.t00ls.com/articles-71119.html

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2