反沙箱和反调试总结

反沙箱与反调试

反沙箱

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

1.sleep

沙箱在执行样本的时候肯定是有时间限制的,所以我们可以先让我们的程序睡眠一段时间再执行,这样在沙箱的环境下,我们的程序还在sleep呢,沙箱就检测完了,肯定不会检测到任何异常。
但是当我们简单的只使用sleep函数时,沙箱可能会对我们的sleep函数进行一个hook,因此我们需要替代类似的api来实现我们的sleep功能,下面列举了一些常见的api。

Functions used::

  • Sleep, SleepEx, NtDelayExecution
  • WaitForSingleObject, WaitForSingleObjectEx, NtWaitForSingleObject
  • WaitForMultipleObjects, WaitForMultipleObjectsEx, NtWaitForMultipleObjects
  • SetTimer, SetWaitableTimer, CreateTimerQueueTimer
  • timeSetEvent (multimedia timers)
  • IcmpSendEcho
  • 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;
}

2.对抗沙箱加速

沙箱为了防止恶意代码长时间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;
}

3.自实现sleep

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

MyGetTickCount64

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

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

然后我们根据逆向的结果进行自实现。(在vs中配置汇编环境参考博客:vs2022 x64 C/C++和汇编混编_vs 嵌入汇编-CSDN博客)

image-20240114102655895

image-20240114102630537

可以看到自实现的效果和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;
}

4.检测环境

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

检测用户名

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

int gensandbox_username() {
	char username[200];
	size_t i;
	DWORD usersize = sizeof(username);
	GetUserNameA(username, &usersize);

	for (i = 0; i < strlen(username); i++) { 
		username[i] = toupper(username[i]);//注意使用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");//做一些无害的操作即可
	}

检测语言

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

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

检测虚拟机

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

5.一些其他的骚操作

  • 在吐司看到的大佬评论,检测电脑中后缀为.docx文件的数量
  • 忘了在哪看的文章,禁止非微软签名访问进程(用到的结构体PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY )
  • 自己搞一个反连平台
  • 定义一个域名(真实环境的),如果在目标域环境中,自然就匹配通过。

反调试

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

1.IsDebuggerPresent

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

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

2.CheckRemoteDebuggerPresent

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

c++Copy CodeBOOL 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;
}

3.检测进程

如果当前计算机进程存在ida.exe,x64dbg.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;
}

4.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;

参考:

posted @ 2024-01-14 21:19  fdx_xdf  阅读(702)  评论(0编辑  收藏  举报