反沙箱(sanbox)多种技术-下篇

一、延迟执行介绍


延迟执行是一种常见的技术,用于绕过沙盒环境。沙盒通常具有时间限制,会阻止它们长时间分析二进制文件。因此,恶意软件可以在代码执行中引入长时间的暂停,迫使沙盒在能够分析二进制文件之前终止,如果恶意软件样本在解密和执行有效负载之前执行一个等待函数三分钟,那么分析时间限制为两分钟的沙盒将无法分析有效负载。

二、实现方法

检测时间快进

一些恶意软件利用执行延迟,因此大多数沙箱都实行了缓解措施来解决执行延迟问题。这些缓解措施可能会通过以下方式快进延迟持续时间:通过 API hook 更改传递的参数或其他方法。验证延迟是否发生至关重要,可以利用 WinAPI GetTickCount64 来实现。

延迟函数应如下所示:

BOOL DelayFunction(DWORD dwMilliSeconds){

  DWORD T0 = GetTickCount64();
  
  // 需要延迟执行 'dwMilliSeconds' ms 的代码
  
  DWORD T1 = GetTickCount64();
  
  // 睡眠时间至少达到 'dwMilliSeconds' ms,则“DelayFunction”成功
  if ((DWORD)(T1 - T0) < dwMilliSeconds)
    return FALSE;
  else
    return TRUE;
}

通过 WaitForSingleObject 延迟执行

WinAPI 函数 WaitForSingleObject 用于等待某个对象处于信号态或等待超时。WaitForSingleObject 还可以用于等待使用 CreateEvent 创建的一个空事件,这意味着它将等待超时。
DelayExecutionVia_WFSO 函数有一个参数 ftMinutes,它表示以分钟为单位延迟执行的时间。如果 WaitForSingleObject 成功地延迟了执行指定的时间,则该函数返回 TRUE。

BOOL DelayExecutionVia_WFSO(FLOAT ftMinutes) {

 // 将分钟转换为毫秒
 DWORD dwMilliSeconds = ftMinutes * 60000;
 HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL); // HANDLE:句柄
 DWORD _T0 = NULL, _T1 = NULL;

 _T0 = GetTickCount64();

 // 休眠 'dwMilliSeconds' 毫秒
 if (WaitForSingleObject(hEvent, dwMilliSeconds) == WAIT_FAILED) {
  printf("[!] WaitForSingleObject 操作失败,错误为:%d \n", GetLastError());
  return FALSE;
 }

 _T1 = GetTickCount64();

 // 至少休眠了 'dwMilliSeconds' 毫秒,则 'DelayExecutionVia_WFSO' 成功,否则失败
 if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
  return FALSE;

 CloseHandle(hEvent);

 return TRUE;
}

通过 MsgWaitForMultipleObjectsEx 延迟执行

MsgWaitForMultipleObjectsEx WinAPI 是一个可以用于执行延迟的 WinAPI。它基本上实现了与 WaitForSingleObject 相同的任务
DelayExecutionVia_MWFMOEx 函数使用上节中显示的相同逻辑,但这里它使用 MsgWaitForMultipleObjectsEx WinAPI。该函数有一个参数 ftMinutes,表示以分钟为单位延迟执行的时间。如果 MsgWaitForMultipleObjectsEx 成功地将执行延迟了指定的时间,则该函数返回 TRUE

BOOL DelayExecutionVia_MWFMOEx(FLOAT ftMinutes) {

  // 将分钟转换为毫秒
  DWORD   dwMilliSeconds    = ftMinutes * 60000;
  HANDLE  hEvent            = CreateEvent(NULL, NULL, NULL, NULL);
  DWORD   _T0               = NULL,
          _T1               = NULL;

  _T0 = GetTickCount64();
  
  // 休眠 'dwMilliSeconds' 毫秒 
  if (MsgWaitForMultipleObjectsEx(1, &hEvent, dwMilliSeconds, QS_HOTKEY, NULL) == WAIT_FAILED) {
    printf("[!] MsgWaitForMultipleObjectsEx 失败,错误代码为:%d\n", GetLastError());
    return FALSE;
  }

  _T1 = GetTickCount64();

  // 至少休眠了 'dwMilliSeconds' 毫秒,则 'DelayExecutionVia_MWFMOEx' 成功,否则失败
  if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
    return FALSE;

  CloseHandle(hEvent);

  return TRUE;
}

通过 NtWaitForSingleObject 延迟执行

代码执行延迟也可以通过 NtWaitForSingleObject 系统调用完成。NtWaitForSingleObject 是 WaitForSingleObject 的本机 API 版本,执行相同的功能。NtWaitForSingleObject 如下所示:

NTSTATUS NtWaitForSingleObject(
  [in] HANDLE         Handle,       // 等待对象的句柄
  [in] BOOLEAN        Alertable,    // 当对象处于等待状态时,是否可以发出警报
  [in] PLARGE_INTEGER Timeout       // 指向指定等待时间的 LARGE_INTEGER 结构
);

NtWaitForSingleObject 的等待时间以 100 纳秒的负间隔指定,通常称为“刻度”。一个刻度相当于 0.0001 毫秒。通过 Timeout 参数传递给系统调用的值应为 dwMilliSeconds x 10000 的负值,其中 dwMilliSeconds 是以毫秒为单位的等待时间
下面的 DelayExecutionVia_NtWFSO 函数使用 NtWaitForSingleObject 系统调用来延迟执行,时间由 ftMinutes 参数指定。ftMinutes 表示延迟执行的时间(以分钟为单位)。如果 NtWaitForSingleObject 成功延迟执行指定的时间,则返回 TRUE。

typedef NTSTATUS (NTAPI* fnNtWaitForSingleObject)(
	HANDLE         Handle,
	BOOLEAN        Alertable,
	PLARGE_INTEGER Timeout
);

BOOL DelayExecutionVia_NtWFSO(FLOAT ftMinutes) {

 	// 将分钟转换为毫秒
	DWORD                   dwMilliSeconds          = ftMinutes * 60000;
	HANDLE                  hEvent                  = CreateEvent(NULL, NULL, NULL, NULL);
	LONGLONG                Delay                   = NULL;
	NTSTATUS                STATUS                  = NULL;
	LARGE_INTEGER           DelayInterval           = { 0 };
	fnNtWaitForSingleObject pNtWaitForSingleObject  = (fnNtWaitForSingleObject)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtWaitForSingleObject");
	DWORD                   _T0                     = NULL,
	                        _T1                     = NULL;

  	// 将毫秒转换为 100 纳秒的负时间间隔
	Delay = dwMilliSeconds * 10000;
	DelayInterval.QuadPart = - Delay;

	_T0 = GetTickCount64();

  	// 休眠 'dwMilliSeconds' 毫秒
	if ((STATUS = pNtWaitForSingleObject(hEvent, FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
		printf("[!] NtWaitForSingleObject Failed With Error : 0x%0.8X \n", STATUS);
		return FALSE;
	}

	_T1 = GetTickCount64();

  	// 已休眠至少 'dwMilliSeconds' 毫秒,则 'DelayExecutionVia_NtWFSO' 成功
	if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
		return FALSE;

	CloseHandle(hEvent);
	
	return TRUE;
}

通过 NtDelayExecution 延迟执行

使用 NtDelayExecution 系统调用。该名称显而易见,指出该系统调用是为了延迟执行代码以进行同步。NtDelayExecution 与 NtWaitForSingleObject 类似,不同之处在于不需要对象句柄来等待;它的功能类似于 Sleep,挂起当前代码的执行周期。NtDelayExecution 如下所示:

NTSTATUS NtDelayExecution(
	IN BOOLEAN              Alertable,      // 是否可以在对象处于等待状态时交付警报
	IN PLARGE_INTEGER       DelayInterval   // 指向指定等待时间的 LARGE_INTEGER 结构
);

下面的 DelayExecutionVia_NtDE 函数使用 NtDelayExecution 系统调用延迟执行,给定的时间 ftMinutes 表示等待的时间(分钟)。如果 NtDelayExecution 成功延迟执行指定的持续时间,则返回 TRUE

typedef NTSTATUS (NTAPI *fnNtDelayExecution)(
	BOOLEAN              Alertable,
	PLARGE_INTEGER       DelayInterval
);

BOOL DelayExecutionVia_NtDE(FLOAT ftMinutes) {

	// 将分钟转换为毫秒
	DWORD               dwMilliSeconds        = ftMinutes * 60000;
	LARGE_INTEGER       DelayInterval         = { 0 };
	LONGLONG            Delay                 = NULL;
	NTSTATUS            STATUS                = NULL;
	fnNtDelayExecution  pNtDelayExecution     = (fnNtDelayExecution)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtDelayExecution");
	DWORD               _T0                   = NULL,
                        _T1                   = NULL;

	// 将毫秒转换为 100 纳秒的负时间间隔
	Delay = dwMilliSeconds * 10000;
	DelayInterval.QuadPart = - Delay;

	_T0 = GetTickCount64();

	// 休眠 'dwMilliSeconds' 毫秒
	if ((STATUS = pNtDelayExecution(FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
		printf("[!] NtDelayExecution 出错:0x%0.8X \n", STATUS);
		return FALSE;
	}

	_T1 = GetTickCount64();

	// 休眠至少 'dwMilliSeconds' 毫秒,则 'DelayExecutionVia_NtDE' 成功,否则它将失败
	if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
		return FALSE;

	return TRUE;
}

三、API hammering 介绍

API hammering(API 遍历)是一种沙盒绕过技术,其中随机调用 WinAPI 来延迟程序执行。它还可以用于混淆实现中正在运行的线程的调用堆栈。这意味着实现逻辑中的恶意函数调用将被随机的良性 WinAPI 调用隐藏。
本文将通过两种方式演示 API hammering。第一种方法在一个后台线程中执行 API hammering,该线程从执行恶意代码的主线程调用不同的 WinAPI。第二种方法使用 API hammering 通过耗时的操作来延迟执行。
I/O 函数
API hammering可以使用任何 WinAPI,不过,本模块将使用以下三个 WinAPI。

CreateFileW - 用于创建并打开一个文件。

WriteFile - 用于将数据写入文件。

ReadFile - 用于从文件读取数据。

之所以选择这些 WinAPI,是因为它们在处理大量数据时能够消耗大量处理时间,因此适合用于 API hammering。

API Hammering Process

CreateFileW 将用于在 Windows 临时文件夹中创建一个临时文件。此文件夹通常存储由 Windows 操作系统或第三方应用程序创建的 .tmp 文件。这些临时文件通常用于在安装应用程序或从互联网下载文件等计算过程中存储临时数据。任务完成后,这些文件通常会被删除。
创建 .tmp 文件后,将使用 WriteFile WinAPI 调用向其中写入一个随机生成的、固定大小的缓冲区。完成后,使用 CreateFileW 关闭文件句柄,然后重新打开文件句柄。不过,这一次会使用一个特殊标记来标记文件,以便在文件句柄关闭后将其删除
在再次关闭句柄之前,将使用 ReadFile 将之前写入的数据读入本地缓冲区。然后,清理并释放该缓冲区。最后,关闭文件句柄,删除文件
可以清楚地看出,上述任务还没有意义,但很耗时。此外,为了增加时间浪费,所有这些都将在循环内进行。
下面的 ApiHammering 函数执行上面概述的步骤。函数需要的唯一参数是 dwStress,表示重复整个过程的次数。

除 GetTempPathW WinAPI 函数外,其余的代码看起来应该很熟悉,该函数用于检索临时目录的路径,C:\Users<username>\AppData\Local\Temp。之后,文件名 TMPFILE 会附加到路径中,并传递给 CreateFileW 函数

#include <iostream>
#include <windows.h>
// Monitor mouse clicks for 20 seconds
// 要创建的文件名
#define TMPFILE	L"MaldevAcad.tmp"

BOOL ApiHammering(DWORD dwStress) {

	WCHAR     szPath[MAX_PATH * 2],
		szTmpPath[MAX_PATH];
	HANDLE    hRFile = INVALID_HANDLE_VALUE,
		hWFile = INVALID_HANDLE_VALUE;

	DWORD   dwNumberOfBytesRead = NULL,
		dwNumberOfBytesWritten = NULL;

	PBYTE   pRandBuffer = NULL;
	SIZE_T  sBufferSize = 0xFFFFF;	// 1048575 字节

	INT     Random = 0;

	// 获取临时文件夹路径
	if (!GetTempPathW(MAX_PATH, szTmpPath)) {
		printf("[!] GetTempPathW 失败,错误:%d \n", GetLastError());
		return FALSE;
	}

	// 构建文件路径
	wsprintfW(szPath, L"%s%s", szTmpPath, TMPFILE);

	for (SIZE_T i = 0; i < dwStress; i++) {

		// 以写入模式创建文件
		if ((hWFile = CreateFileW(szPath, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL)) == INVALID_HANDLE_VALUE) {
			printf("[!] CreateFileW 失败,错误:%d \n", GetLastError());
			return FALSE;
		}

		// 分配缓冲区并用随机值填充它
		pRandBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBufferSize);
		srand(time(NULL));
		Random = rand() % 0xFF;
		memset(pRandBuffer, Random, sBufferSize);

		// 将随机数据写入文件
		if (!WriteFile(hWFile, pRandBuffer, sBufferSize, &dwNumberOfBytesWritten, NULL) || dwNumberOfBytesWritten != sBufferSize) {
			printf("[!] WriteFile 失败,错误:%d \n", GetLastError());
			printf("[i] 已写入 %d 字节,总计 %d \n", dwNumberOfBytesWritten, sBufferSize);
			return FALSE;
		}

		// 清除缓冲区并关闭文件句柄
		RtlZeroMemory(pRandBuffer, sBufferSize);
		CloseHandle(hWFile);

		// 以读取模式打开文件,并标记为关闭时删除
		if ((hRFile = CreateFileW(szPath, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL)) == INVALID_HANDLE_VALUE) {
			printf("[!] CreateFileW 失败,错误:%d \n", GetLastError());
			return FALSE;
		}

		// 读取之前写入的随机数据
		if (!ReadFile(hRFile, pRandBuffer, sBufferSize, &dwNumberOfBytesRead, NULL) || dwNumberOfBytesRead != sBufferSize) {
			printf("[!] ReadFile 失败,错误: %d \n", GetLastError());
			printf("[i] 已读入 %d 字节,总计 %d \n", dwNumberOfBytesRead, sBufferSize);
			return FALSE;
		}

		// 清除缓冲区并释放它
		RtlZeroMemory(pRandBuffer, sBufferSize);
		HeapFree(GetProcessHeap(), NULL, pRandBuffer);

		// 关闭文件句柄 - 删除文件
		CloseHandle(hRFile);
	}


	return TRUE;
}


通过 API Hammering 延迟执行

要使用 API Hammering 延迟执行,计算 ApiHammering 函数执行一定数量循环所需的时间。为此,使用 GetTickCount64 WinAPI 来测量 ApiHammering 调用前后的时间。在此示例中,循环次数为 1000。

int main() {


  DWORD T0  = NULL,
        T1  = NULL;

  T0 = GetTickCount64();

  // 延迟执行 5 秒钟的循环数
  if (!ApiHammering(SECTOSTRESS(5))) {
    return -1;
  }

  T1 = GetTickCount64();

  printf(">>> ApiHammering 延迟执行:%d \n", (DWORD)(T1 - T0));

  printf("[#] 按 <Enter> 退出 ... ");
  getchar();

  return 0;
}

image
输出显示,在当前机器上执行 1000 个循环大约需要 2.2 秒。该数字将根据目标系统的硬件规格而略有不同。


线程中的 API 攻击

iHammering 函数可在后台线程中执行,直至主线程执行完毕。这可通过使用 CreateThread WinAPI 完成。应将 -1 值传递给 ApiHammering 函数,这将使其无限次循环处理 process。

以下所示主函数创建一个新线程,并使用 -1 值调用 ApiHammering 函数。

int main() {

	DWORD dwThreadId = NULL;  // 线程 ID

	if (!CreateThread(NULL, NULL, ApiHammering, -1, NULL, &dwThreadId)) {
		printf("[!] CreateThread With Error : %d \n", GetLastError());
		return -1;
	}

	printf("[+] 线程 %d 已创建,将在后台运行 ApiHammering\n", dwThreadId);


	/*
	
		注入代码可置于此处

	*/


	printf("[#] 按 <Enter> 键退出 ... ");
	getchar();

	return 0;
}
posted @   aoaoaoao  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示