类人行为

描述

利用了用户和虚拟环境以不同方式执行某些操作的方式来判断是否处于虚拟环境中。

1.注册表检测方法

注册表是不同信息的存储。例如,最近打开的 URL 和文档、软件安装日志都存储在这里。所有这些都可以用来确定机器是否由人类用户操作并且不是沙箱。
以检查最近打开的文档数量为例:
Public Function DKTxHE() As Boolean
DKTxHE = RecentFiles.Count < 3
End Function
以检查浏览器历史记录是否至少包含 10 个 URL为例:
bool chrome_history_evasion(int min_websites_visited = 10)
{
  sqlite3 *db;
  int rc;
  bool vm_found = false;

  rc = sqlite3_open("C:\\Users\\<USER_NAME>\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\History", &db);
  if (!rc)
  {
    char **results = nullptr;
    char *error = nullptr;
    int rows, columns;

    rc = sqlite3_get_table(db, "SELECT DISTINCT title FROM urls;", &results, &rows, &columns, &error);
    if (!rc)
      vm_found = rows < min_websites_visited;
    sqlite3_free_table(results);
  }

  sqlite3_close(db);
  return vm_found;
}
也可以使用FindFirstUrlCacheEntryA 函数来枚举Internet缓存条目:
#include <windows.h>
#include <stdio.h>
#include <wininet.h>
#include <ctime>

#pragma comment (lib, "wininet")
#pragma warning(disable : 4996)

//定义函数指针,用于动态加载API函数
typedef HANDLE(WINAPI* PFN_FINDFIRSTURLCACHEENTRYA)(LPCSTR, LPINTERNET_CACHE_ENTRY_INFOA, LPDWORD);
typedef BOOL(WINAPI* PFN_FINDNEXTURLCACHEENTRYA)(HANDLE, LPINTERNET_CACHE_ENTRY_INFOA, LPDWORD);

int main()
{
    LPCSTR lpszUrlSearchPattern = NULL; //要搜索的URL模式,NULL表示获取所有缓存条目
    LPINTERNET_CACHE_ENTRY_INFOA lpCacheEntryInfo = NULL; //用于存储缓存条目信息的指针
    DWORD dwSize = 0; //缓冲区大小
    HANDLE hEnumHandle = NULL; //枚举句柄
    BOOL bResult = FALSE; //操作结果
    PFN_FINDFIRSTURLCACHEENTRYA pfnFindFirstUrlCacheEntryA; //FindFirstUrlCacheEntryA函数的地址
    PFN_FINDNEXTURLCACHEENTRYA pfnFindNextUrlCacheEntryA; //FindNextUrlCacheEntryA函数的地址
    HMODULE hModule; //wininet.dll模块的句柄

    //加载wininet.dll模块,并获取API函数的地址
    hModule = LoadLibraryA("wininet.dll");
    pfnFindFirstUrlCacheEntryA = (PFN_FINDFIRSTURLCACHEENTRYA)GetProcAddress(hModule, "FindFirstUrlCacheEntryA");
    pfnFindNextUrlCacheEntryA = (PFN_FINDNEXTURLCACHEENTRYA)GetProcAddress(hModule, "FindNextUrlCacheEntryA");

    //获取第一个缓存条目,并打印其信息
    hEnumHandle = pfnFindFirstUrlCacheEntryA(lpszUrlSearchPattern, NULL, &dwSize);
    if (hEnumHandle == NULL && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
    {
        lpCacheEntryInfo = (LPINTERNET_CACHE_ENTRY_INFOA)malloc(dwSize);
        if (lpCacheEntryInfo != NULL)
        {
            hEnumHandle = pfnFindFirstUrlCacheEntryA(lpszUrlSearchPattern, lpCacheEntryInfo, &dwSize);
            if (hEnumHandle != NULL)
            {
                printf("The cache entry URL is: %s\n", lpCacheEntryInfo->lpszSourceUrlName);
                printf("The cache entry file name is: %s\n", lpCacheEntryInfo->lpszLocalFileName);
                printf("The cache entry size is: %lu\n", lpCacheEntryInfo->dwSizeLow);
                printf("The cache entry last modified time is: %s\n", std::ctime((time_t*)&lpCacheEntryInfo->LastModifiedTime));
                free(lpCacheEntryInfo);
            }
        }
    }
    //获取下一个缓存条目,并打印其信息,直到没有更多条目
    do
    {
        bResult = pfnFindNextUrlCacheEntryA(hEnumHandle, NULL, &dwSize);
        if (bResult == FALSE && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            lpCacheEntryInfo = (LPINTERNET_CACHE_ENTRY_INFOA)malloc(dwSize);
            if (lpCacheEntryInfo != NULL)
            {
                bResult = pfnFindNextUrlCacheEntryA(hEnumHandle, lpCacheEntryInfo, &dwSize);
                if (bResult == TRUE)
                {
                    printf("The cache entry URL is: %s\n", lpCacheEntryInfo->lpszSourceUrlName);
                    printf("The cache entry file name is: %s\n", lpCacheEntryInfo->lpszLocalFileName);
                    printf("The cache entry size is: %lu\n", lpCacheEntryInfo->dwSizeLow);
                    printf("The cache entry last modified time is: %s\n", std::ctime((time_t*)&lpCacheEntryInfo->LastModifiedTime));
                    free(lpCacheEntryInfo);
                }
            }
        }
    } while (bResult == TRUE && GetLastError() != ERROR_NO_MORE_ITEMS);

    //关闭枚举句柄
    FindCloseUrlCache(hEnumHandle);

    return 0;
}
以通过注册表检查安装了哪些软件包来判断是否处于虚拟环境中:
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Format-Table -AutoSize | Measure-Object -Line

2.在执行进程时检查用户是否存在

利用了用户与计算机的交互和虚拟环境的操作之间的差异来判断当前环境是否为虚拟环境。
以检查鼠标移动情况为例:
#include <windows.h>
#include <stdio.h>
#include <math.h>

int main()
{
    // 定义一个变量存储初始光标位置
    POINT initialCursorPos;

    // 调用GetCursorPos函数获取初始光标位置
    BOOL result = GetCursorPos(&initialCursorPos);

    // 如果成功,继续执行
    if (result)
    {
        // 打印初始光标位置
        printf("The initial cursor position is (%d, %d)\n", initialCursorPos.x, initialCursorPos.y);

        // 定义一个变量存储当前光标位置
        POINT currentCursorPos;

        // 定义一个变量存储开始时间
        DWORD startTime = GetTickCount();
        // 定义一个变量存储结束时间
        DWORD endTime;

        // 定义一个变量存储是否移动的标志
        BOOL moved = FALSE;

        // 定义一个常量存储规定时间(毫秒)
        const DWORD TIME_LIMIT = 5000;

        // 定义一个常量存储特定距离(像素)
        const int DISTANCE_LIMIT = 100;

        // 循环检查光标位置和时间
        do
        {
            // 调用GetCursorPos函数获取当前光标位置
            result = GetCursorPos(&currentCursorPos);

            // 如果成功,继续执行
            if (result)
            {
                // 计算光标移动的距离
                int distance = sqrt(pow(currentCursorPos.x - initialCursorPos.x, 2) + pow(currentCursorPos.y - initialCursorPos.y, 2));

                // 判断是否超过特定距离
                if (distance > DISTANCE_LIMIT)
                {
                    // 如果超过特定距离,设置移动标志为真,并退出循环
                    moved = TRUE;
                    break;
                }
            }

            // 调用GetTickCount函数获取结束时间
            endTime = GetTickCount();
            // 判断是否超过规定时间
            if (endTime - startTime > TIME_LIMIT)
            {
                // 如果超过规定时间,退出循环
                break;
            }

            // 等待一段时间,避免占用过多CPU资源
            Sleep(100);

        } while (TRUE);

        // 判断是否移动了光标
        if (moved)
        {
            // 如果移动了光标,运行恶意代码
            printf("The cursor has moved enough, executing.\n");
            // 这里插入恶意代码,比如显示一个弹窗消息
            MessageBoxA(NULL, "You have been infected by a malware!", "Malware Alert", MB_OK | MB_ICONWARNING);
            return 1;
        }
        else
        {
            // 如果没有移动光标,不运行恶意代码
            printf("The cursor has not moved enough, aborting.\n");
            return 0;
        }
    }
    else
    {
        // 如果失败,打印错误信息
        printf("GetCursorPos failed with error %d\n", GetLastError());
        return -1;
    }
}
某些恶意软件样本包含需要用户交互的 GUI 安装程序。例如,用户必须单击“安装”或“下一步”按钮。因此,除非单击该按钮,否则恶意软件可能不会采取任何操作。像 Cuckoo 这样的标准沙箱有一个模拟用户活动的模块。它搜索并单击带有上述标题的按钮。为了防止自动单击,恶意软件可以创建类名不同于“Button”或具有不同标题(而不是“Install”或“Next”)的按钮。这样沙箱就无法检测到并点击该按钮。
   // we use extended style flags to make a static look like a button
   HWND hButton = CreateWindowExW(
       WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE,  // extended style flags
       TEXT("static"),         // class "static" instead of "button"
       TEXT("Real next"),      // caption different from “Install” or “Next”
       WS_VISIBLE | WS_CHILD | WS_GROUP | SS_CENTER,  // usual style flags
       10, 10, 80, 25,         // arbitrary position and size, may be any
       hWnd,                   // parent window
       NULL,                   // no menu
       NULL,                   // a handle to the instance of the module to be associated with the window
       NULL);                  // pointer to custom value is not required
驻留在 Office 文档(即 *.docm *.docx)中的恶意软件可以在文档滚动到特定页面(第二个、第三个等)之前不会执行任何操作。人类用户通常滚动浏览文档,而虚拟环境可能不会执行此步骤。
RTF 文档由普通文本、控制字和组组成。Microsoft 的 RTF 规范包括一个形状绘制函数,该函数又包括一系列使用以下语法的属性:
{\sp{\sn propertyName}{\sv propertyValueInformation}}
在此代码中,\sp是绘图属性的控制字,\sn是属性名称,\sv包含有关属性值的信息。
恶意软件可以重复的段落标记(./par)将利用代码推送到 RTF 文档的第二页。因此,除非文档向下滚动以将漏洞利用代码调入活动窗口,否则恶意代码不会执行。因为这更有可能是人类的行为,而不是虚拟机中的模拟移动。
只有当 RTF 向下滚动到第二页时,才会触发漏洞代码并下载有效负载。在沙箱中,任何鼠标活动都是随机的或预先编程的,所以RTF 文档的第二页永远不会出现。因此,恶意代码永远不会执行。
我们还可以通过GetLastInputInfo 检索最后一个输入事件的时间来判断主机之前否有用户操作过:
 bool sandbox_detected = false;
    
    Sleep(30000);

    DWORD ticks = GetTickCount();

    LASTINPUTINFO li;
    li.cbSize = sizeof(LASTINPUTINFO);
    BOOL res = GetLastInputInfo(&li);

    if (ticks - li.dwTime > 6000)
    {
        sandbox_detected = true;
    }