反沙箱与反调试
反沙箱
我们要反沙箱,就要思考沙箱和真实物理机的区别,比如说内存大小、用户名、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博客)
可以看到自实现的效果和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;
参考:
- 反调试:调试标志 --- Anti-Debug: Debug Flags (checkpoint.com)
- Evasions: Timing (checkpoint.com)
- mp.weixin.qq.com/s/hATwflcVvA4JeIljP1nE1Q
- mp.weixin.qq.com/s/sbu0AyV1HPiA-X6T6gR49g
- 反调试技术-上 (qq.com)