飘云阁安全论坛

 找回密码
 快速注册

QQ登录

只需一步,快速开始

查看: 401|回复: 3

[病毒分析] [翻译]规避技术: 定时

  [复制链接]
  • TA的每日心情
    开心
    2019-3-15 11:00
  • 签到天数: 262 天

    [LV.8]以坛为家I

    发表于 2021-6-2 15:46:42 | 显示全部楼层 |阅读模式
    本帖最后由 梦幻的彼岸 于 2021-6-2 16:06 编辑

    备注
    原文地址:https://evasions.checkpoint.com/techniques/timing.html
    原文标题:Evasions: Timing
    更新日期:2021年6月2日
    此文后期:根据自身所学进行内容扩充
    因自身技术有限,只能尽自身所能翻译国外技术文章,供大家学习,若有不当或可完善的地方,希望可以指出,用于共同完善这篇文章。



    目录
    • 基于时间的沙箱规避技术
    • 1. 延迟执行
    • 1.1. 简单的延迟操作
    • 1.2. 使用任务计划程序延迟执行
    • 1.3. 在重新启动之前没有可疑的行动
    • 1.4. 只在特定日期运行
    • 2. 睡眠跳过检测
    • 2.1. 使用不同的方法进行并行延时
    • 2.2. 使用不同的方法测量时间间隔
    • 2.3. 使用不同的方法获得系统时间
    • 2.4. 检查调用延迟函数后延迟值是否发生变化
    • 2.5. 使用绝对超时
    • 2.6. 从另一个进程获取时间
    • 3. 从外部来源(NTP、HTTP)获取当前日期和时间
    • 4. 虚拟机和主机中的时间测量差异
    • 4.1. RDTSC(使用CPUID强制退出虚拟机)
    • 4.2. RDTSC(带有GetProcessHeap和CloseHandle的Locky版本)
    • 5. 使用不同的方法检查系统的最后启动时间
    • 反制措施
    • 归功于


    基于时间的沙箱规避技术
    沙盒伪装通常持续时间很短,因为沙盒装载了大量的样本。伪装时间很少超过3-5分钟。因此,恶意软件可以利用这一事实来避免被发现:它可能在开始任何恶意活动之前进行长时间的延迟。
    为了抵制这种情况,沙盒可能会实现操纵时间和执行延迟的功能。例如,Cuckoo沙箱有一个跳过睡眠的功能,用一个非常短的值取代延迟。这应迫使恶意软件在分析超时前开始其恶意活动。
    1.png
    然而,这也可以用来检测沙盒。
    在一些指令和API函数的执行时间上也有一些差异,可以用来检测虚拟环境。

    没有为这一类技术提供签名建议,因为执行本章中描述的函数并不意味着它们被用于规避目的。很难区分旨在执行规避代码的代码和以非规避目的使用相同函数的代码。
    1. 延迟执行
    执行延迟用于避免在伪装期间检测到恶意活动。
    1.1. 简单的延迟操作
    使用的函数:
    • Sleep, SleepEx, NtDelayExecution
    • WaitForSingleObject, WaitForSingleObjectEx, NtWaitForSingleObject
    • WaitForMultipleObjects, WaitForMultipleObjectsEx, NtWaitForMultipleObjects
    • SetTimer, SetWaitableTimer, CreateTimerQueueTimer
    • timeSetEvent (multimedia timers)
    • IcmpSendEcho
    • select (Windows sockets)

    虽然这些函数的大部分使用是显而易见的,但我们展示了使用多媒体API的timeSetEvent函数和Windows套接字API的select函数的例子。
    代码示例(使用 "select "函数进行延迟):
    [C++] 纯文本查看 复制代码
    int iResult;
    DWORD timeout = delay; // delay in milliseconds
    DWORD 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;
        sa.sin_addr.s_addr = inet_addr("8.8.8.8");    // 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 == false) {
            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
        select(0, NULL, &Write, &Err, &tv);
        
        if (FD_ISSET(sock, &Err)) {
            OK = FALSE;
            break;
        }
    
    } while (false);
    
    if (sock != INVALID_SOCKET)
        closesocket(sock);

    代码示例(使用 "timeSetEvent "函数进行延迟):
    [C++] 纯文本查看 复制代码
    VOID CALLBACK TimerFunction(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2)
    {
        bProcessed = TRUE;
    }
    
    VOID timing_timeSetEvent(UINT delayInSeconds)
    {
        // Some vars
        UINT uResolution;
        TIMECAPS tc;
        MMRESULT idEvent;
    
        // We can obtain this minimum value by calling
        timeGetDevCaps(&tc, sizeof(TIMECAPS));
        uResolution = min(max(tc.wPeriodMin, 0), tc.wPeriodMax);
    
        // Create the timer
        idEvent = timeSetEvent(
            delayInSeconds,
            uResolution,
            TimerFunction,
            0,
            TIME_ONESHOT);
    
        while (!bProcessed){
            // wait until our function finishes
            Sleep(0);
        }
    
        // destroy the timer
        timeKillEvent(idEvent);
    
        // reset the timer
        timeEndPeriod(uResolution);
    }

    该代码样本的作者:al-khaser项目
    1.2. 使用任务计划程序延迟执行
    这种方法既可用于延迟执行,也可用于躲避沙盒追踪。
    代码样本(PowerShell):
    [C++] 纯文本查看 复制代码
    $tm = (get-date).AddMinutes(10).ToString("HH:mm")
    $action = New-ScheduledTaskAction -Execute "some_malicious_app.exe"
    $trigger = New-ScheduledTaskTrigger -Once -At $tm
    Register-ScheduledTask TaskName -Action $action -Trigger $trigger


    1.3. 在重新启动之前没有可疑的行动
    这种技术背后的想法是,在模拟恶意样本的过程中,沙盒不会重启虚拟机。恶意软件可能只是使用任何可用的方法设置了持久性,并悄悄退出。只有在系统重新启动后,才会进行恶意操作。
    1.4. 只在特定日期运行
    恶意软件样本可能会检查当前日期,只在某些日期执行恶意行动。例如,在Sazoora恶意软件中使用了这种技术,它检查当前日期并验证这一天是否是某个月的第16、17或18号。

    示例:
    2.png
    反制措施:
    这类规避技术的对策应该是全面的,并且包括所有描述的攻击向量。实现不可能简单,对它的描述应该单独一条。因此,我们在此仅提供一般性建议:
    • 实现睡眠跳过。
    • 全系统动态时间流速度操纵。
    • 在不同日期多次运行仿真。

    虽然在Cuckoo沙盒中已经实现了睡眠跳过,但它很容易被欺骗。创建新线程或进程后,将禁用睡眠跳过,以避免检测到睡眠跳过。但是,它仍然可以很容易地检测到,如下所示。
    2. 睡眠跳过检测
    这种类型的技术一般是针对Cuckoo监控器的睡眠跳过功能和其他时间操纵技术,这些技术可以在沙盒中使用,以跳过恶意软件执行的长时间延迟。
    2.1. 使用不同的方法进行并行延时
    这些技术背后的想法是并行地执行不同类型的延迟,并测量运行时间。

    代码样本:
    [C++] 纯文本查看 复制代码
    DWORD StartingTick, TimeElapsedMs;
    LARGE_INTEGER DueTime;
    HANDLE hTimer = NULL;
    TIMER_BASIC_INFORMATION TimerInformation;
    ULONG ReturnLength;
    
    hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    DueTime.QuadPart = Timeout * (-10000LL);
    
    StartingTick = GetTickCount();
    SetWaitableTimer(hTimer, &DueTime, 0, NULL, NULL, 0);
    do
    {
        Sleep(Timeout/10);
        NtQueryTimer(hTimer, TimerBasicInformation, &TimerInformation, sizeof(TIMER_BASIC_INFORMATION), &ReturnLength);
    } while (!TimerInformation.TimerState);
    
    CloseHandle(hTimer);
    
    TimeElapsedMs = GetTickCount() - StartingTick;
    printf("Requested delay: %d, elapsed time: %d\n", Timeout, TimeElapsedMs);
    
    if (abs((LONG)(TimeElapsedMs - Timeout)) > Timeout / 2)
        printf("Sleep-skipping DETECTED!\n");

    在上面的代码示例中,延迟超时是用SetWaitableTimer()计时器函数设置的。Sleep()函数被循环调用,直到定时器超时。在Cuckoo沙盒中,由Sleep()函数执行的延迟会被跳过(用一个很短的超时来代替),实际运行时间会比要求的超时高很多:
    [C++] 纯文本查看 复制代码
    Requested delay: 60000, elapsed time: 1906975
    Sleep-skipping DETECTED!

    2.2. 使用不同的方法测量时间间隔
    我们需要执行一个将在沙盒中跳过的延迟,并使用不同的方法来测量运行时间。虽然Cuckoo监控器钩住了GetTickCount(), GetLocalTime(), GetSystemTime()并使它们返回跳过的时间,但我们仍然可以找到Cuckoo监控器没有处理的测量时间的方法。

    使用的函数:
    • GetTickCount64
    • QueryPerformanceFrequency, QueryPerformanceCounter
    • NtQuerySystemInformation

    代码样本(使用“QueryPerformanceCounter”测量运行时间):
    [C++] 纯文本查看 复制代码
    LARGE_INTEGER StartingTime, EndingTime;
    LARGE_INTEGER Frequency;
    DWORD TimeElapsedMs;
    
    QueryPerformanceFrequency(&Frequency);
    QueryPerformanceCounter(&StartingTime);
    
    Sleep(Timeout);
    
    QueryPerformanceCounter(&EndingTime);
    TimeElapsedMs = (DWORD)(1000ll * (EndingTime.QuadPart - StartingTime.QuadPart) / Frequency.QuadPart);
    
    printf("Requested delay: %d, elapsed time: %d\n", Timeout, TimeElapsedMs);
    
    if (abs((LONG)(TimeElapsedMs - Timeout)) > Timeout / 2)
        printf("Sleep-skipping DETECTED!\n");

    代码样本(使用 "GetTickCount64 "测量运行时间):
    [C++] 纯文本查看 复制代码
    ULONGLONG tick;
    DWORD TimeElapsedMs;
    
    tick = GetTickCount64();
    Sleep(Timeout);
    TimeElapsedMs = GetTickCount64() - tick;
    
    printf("Requested delay: %d, elapsed time: %d\n", Timeout, TimeElapsedMs);
    
    if (abs((LONG)(TimeElapsedMs - Timeout)) > Timeout / 2)
        printf("Sleep-skipping DETECTED!\n");

    我们也可以使用我们自己的GetTickCount实现来检测睡眠跳动。在接下来的代码示例中,我们直接从KUSER_SHARED_DATA结构中获取tick计数。这样,即使GetTickCount()函数被拦截了,我们也能得到原始的tick计数值。
    代码样本(从KUSER_SHARED_DATA结构中获取tick计数):
    [C++] 纯文本查看 复制代码
    #define KI_USER_SHARED_DATA         0x7FFE0000
    #define SharedUserData  ((KUSER_SHARED_DATA * const) KI_USER_SHARED_DATA)
    #define MyGetTickCount() ((DWORD)((SharedUserData->TickCountMultiplier * (ULONGLONG)SharedUserData->TickCount.LowPart) >> 24))
    
    // ...
    StartingTick = MyGetTickCount();
    Sleep(Timeout);
    TimeElapsedMs = MyGetTickCount() - StartingTick;
    
    printf("Requested delay: %d, elapsed time: %d\n", Timeout, TimeElapsedMs);
    
    if (abs((LONG)(TimeElapsedMs - Timeout)) > Timeout / 2)
        printf("Sleep-skipping DETECTED!\n");

    2.3. 使用不同的方法获得系统时间
    这种方法与前一种方法类似。我们尝试用不同的方法来获得当前的系统时间,而不是测量间隔时间。
    代码样本:
    [C++] 纯文本查看 复制代码
    SYSTEM_TIME_OF_DAY_INFORMATION  SysTimeInfo;
    ULONGLONG time;
    LONGLONG diff;
    
    Sleep(60000); // should trigger sleep skipping
    GetSystemTimeAsFileTime((LPFILETIME)&time);
    
    NtQuerySystemInformation(SystemTimeOfDayInformation, &SysTimeInfo, sizeof(SysTimeInfo), 0);
    diff = time - SysTimeInfo.CurrentTime.QuadPart;
    if (abs(diff) > 10000000) // differ in more than 1 second
        printf("Sleep-skipping DETECTED!\n);

    2.4. 检查调用延迟函数后延迟值是否发生变化
    睡眠跳过通常被实现为用一个较小的间隔来替换延迟值。让我们看一下NtDelayExecution函数。延迟值是用一个指针传递给这个函数的:
    [C++] 纯文本查看 复制代码
    NTSYSAPI NTSTATUS NTAPI
    NtDelayExecution(
        IN BOOLEAN              Alertable,
        IN PLARGE_INTEGER       DelayInterval );

    因此,我们可以检查DelayInterval的值在函数执行后是否改变。如果该值与初始值不同,则跳过了延迟。
    代码样本:
    [C++] 纯文本查看 复制代码
    LONGLONG SavedTimeout = Timeout * (-10000LL);
    DelayInterval->QuadPart = SavedTimeout;
    status = NtDelayExecution(TRUE, DelayInterval);
    if (DelayInterval->QuadPart != SavedTimeout)
        printf("Sleep-skipping DETECTED!\n");

    2.5. 使用绝对超时
    对于执行延迟的Nt-函数,我们可以使用相对延迟间隔或绝对超时时间。延迟间隔的负值意味着相对超时,而正值意味着绝对超时。高级别的API函数,如WaitForSingleObject()或Sleep(),都是用相对的时间间隔来操作。因此,沙盒开发人员可能不关心绝对超时,并错误地处理它们。在Cuckoo沙盒中,这种延迟会被跳过,但跳过的时间和刻度会被错误地计算。这可以用来检测睡眠跳过。
    代码样本:
    [C++] 纯文本查看 复制代码
    void SleepAbs(DWORD ms)
    {
        LARGE_INTEGER SleepUntil;
    
        GetSystemTimeAsFileTime((LPFILETIME)&SleepUntil);
        SleepTo.QuadPart += (ms * 10000);
        NtDelayExecution(TRUE, &SleepTo);
    }

    2.6. 从另一个进程获取时间
    Cuckoo沙盒中的睡眠跳过不是全系统的。因此,如果有执行延迟,时间在不同的进程中以不同的速度移动。在延迟之后,我们应该同步进程并比较两个进程的当前时间。如果测量到的时间值有很大的差异,说明进行了睡眠跳转。
    3.png
    当前版本的Cuckoo监控器在创建新线程或进程后禁用了睡眠跳转。因此,我们应该使用不被Cuckoo监控器跟踪的进程创建方法,例如,使用一个计划任务。
    3. 从外部来源(NTP、HTTP)获取当前日期和时间
    沙盒可以设置不同的日期,以检查分析样本的行为是如何根据日期而改变的。恶意软件可以使用外部日期和时间源来防止虚拟机内的时间操纵企图。这种方法也可用于测量时间间隔,执行延迟,并检测跳过睡眠的尝试。NTP服务器,以及HTTP头 "Date "可以作为日期和时间的外部来源。例如,恶意软件可以连接到google.com来检查当前日期,并将其作为DGA种子。

    反制措施:

    实施假的网络基础设施或欺骗NTP数据和由真正的服务器返回的HTTP头。返回/欺骗的日期和时间应与虚拟机中的日期和时间同步。
    4. 虚拟机和主机中的时间测量差异
    一些API函数和指令的执行在虚拟机和通常的主机系统中可能需要不同的时间。这些特殊性可以用来检测虚拟环境。
    4.1. RDTSC(使用CPUID强制退出虚拟机)
    代码样本:
    [C++] 纯文本查看 复制代码
    BOOL rdtsc_diff_vmexit()
    {
        ULONGLONG tsc1 = 0;
        ULONGLONG tsc2 = 0;
        ULONGLONG avg = 0;
        INT cpuInfo[4] = {};
    
        // Try this 10 times in case of small fluctuations
        for (INT i = 0; i < 10; i++)
        {
            tsc1 = __rdtsc();
            __cpuid(cpuInfo, 0);
            tsc2 = __rdtsc();
    
            // Get the delta of the two RDTSC
            avg += (tsc2 - tsc1);
        }
    
        // We repeated the process 10 times so we make sure our check is as much reliable as we can
        avg = avg / 10;
        return (avg < 1000 && avg > 0) ? FALSE : TRUE;
    }

    该代码样本的作者:al-khaser项目
    4.2. RDTSC(带有GetProcessHeap和CloseHandle的Locky版本
    代码样本:
    [C++] 纯文本查看 复制代码
    #define LODWORD(_qw)    ((DWORD)(_qw))
    BOOL rdtsc_diff_locky()
    {
        ULONGLONG tsc1;
        ULONGLONG tsc2;
        ULONGLONG tsc3;
        DWORD i = 0;
    
        // Try this 10 times in case of small fluctuations
        for (i = 0; i < 10; i++)
        {
            tsc1 = __rdtsc();
    
            // Waste some cycles - should be faster than CloseHandle on bare metal
            GetProcessHeap();
    
            tsc2 = __rdtsc();
    
            // Waste some cycles - slightly longer than GetProcessHeap() on bare metal
            CloseHandle(0);
    
            tsc3 = __rdtsc();
    
            // Did it take at least 10 times more CPU cycles to perform CloseHandle than it took to perform GetProcessHeap()?
            if ((LODWORD(tsc3) - LODWORD(tsc2)) / (LODWORD(tsc2) - LODWORD(tsc1)) >= 10)
                return FALSE;
        }
    
        // We consistently saw a small ratio of difference between GetProcessHeap and CloseHandle execution times
        // so we're probably in a VM!
        return TRUE;
    }

    该代码样本的作者:al-khaser项目
    反制措施
    实施RDTSC指令 "拦截"。有可能使RDTSC成为一条特权指令,只能在内核模式下调用。在用户模式下调用 "挂钩 "的RDTSC会导致我们的处理程序的执行,它可以返回任何想要的值。
    5. 使用不同的方法检查系统的最后启动时间
    这种技术是常规操作系统查询中描述的技术的组合:检查系统的正常运行时间是否很小,以及WMI:检查最后的启动时间部分。根据获取系统最后启动时间的方法,测得的沙盒操作系统运行时间可能太小(几分钟),或者相反,太大(几个月甚至几年),因为系统通常在分析开始后从快照中恢复。
    我们可以通过比较通过WMI和NtQuerySystemInformation(SystemTimeOfDayInformation)获得的最后一次启动时间的两个值来检测沙箱。
    代码样本:
    [C++] 纯文本查看 复制代码
    bool check_last_boot_time()
    {
        SYSTEM_TIME_OF_DAY_INFORMATION  SysTimeInfo;
        LARGE_INTEGER LastBootTime;
        
        NtQuerySystemInformation(SystemTimeOfDayInformation, &SysTimeInfo, sizeof(SysTimeInfo), 0);
        LastBootTime = wmi_Get_LastBootTime();
        return (wmi_LastBootTime.QuadPart - SysTimeInfo.BootTime.QuadPart) / 10000000 != 0; // 0 seconds
    }

    反制措施:
    • 调整KeBootTime值
    • 在调整KeBootTime后,重置WMI资源库或重新启动 "winmgmt "服务

    反制措施
    反制措施见于上述适当的分节。
    归功于
    归功于开放源码项目,代码样本取自这些项目:

    评分

    参与人数 1威望 +1 飘云币 +1 收起 理由
    zhczf + 1 + 1 PYG有你更精彩!

    查看全部评分

  • TA的每日心情
    奋斗
    前天 22:27
  • 签到天数: 202 天

    [LV.7]常住居民III

    发表于 2021-6-2 23:50:24 | 显示全部楼层
    感谢楼主分享
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    昨天 22:07
  • 签到天数: 51 天

    [LV.5]常住居民I

    发表于 2021-6-4 07:22:22 | 显示全部楼层

    感谢楼主分享
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    6 天前
  • 签到天数: 15 天

    [LV.4]偶尔看看III

    发表于 2021-6-4 16:46:35 | 显示全部楼层
    太难了,学习中。。。
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 快速注册

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    快速回复 返回顶部 返回列表