反沙箱与反调试总结

反沙箱与反调试

我们要反沙箱,就要思考沙箱和真实物理机的区别,比如说内存大小用户名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, 0sizeof(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(0NULL, &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(22), &wsaData);
     if (iResult != 0return 0;
     int result, count;
     int sockfd = 0, rc;
     sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
     if (sockfd < 0return 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, NULLNULL, &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(NULL0x19000NULL);
 

     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, NULL00))
     {
         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 < 4return false;
 }

检测开机时间

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

bool checkuptime() {
     DWORD uptime = GetTickCount();
     printf("uptime:%un", 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为 %dn", 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


原文始发于微信公众号(T00ls安全):反沙箱与反调试总结

版权声明:admin 发表于 2024年2月4日 上午11:53。
转载请注明:反沙箱与反调试总结 | CTF导航

相关文章