• XSS.stack #1 – первый литературный журнал от юзеров форума

Проверка на хуки против песочниц и эмуляторов

cppjunior

ripper
КИДАЛА
Регистрация
25.05.2022
Сообщения
57
Реакции
3
Гарант сделки
2
Пожалуйста, обратите внимание, что пользователь заблокирован
На сколько целесообразно проверять определенные апи на хуки и детектить песочницу или эмулятор ав?
Как вообще можно задетектить ав песочницу?
Код как пример пример.
C++:
FARPROC Address = GetProcAddressBy(GetModuleHandle("kernel32.dll"),"Sleep");
if (*(BYTE*)Address == 0xE9 || *(BYTE*)Address == 0x90 || *(BYTE*)Address == 0xC3)
{
 printf("Sleep hooked\n");
}

P.S Возможно есть другой какой то метод детектить хуки, буду благодарен если подскажите как лучше.
 
Чаще всего вы увидите хуки, размещенные в ntdll.dll, а не в kernel32.dll. Также это зависит от запущенного AV/EDR, поскольку каждое решение имеет разные подходы.

Одним из распространенных способов является загрузка ntdll.dll и проверка соответствия экспортируемой функции заглушке системного вызова, которая всегда имеет значение 4C8BD1B8 на x64 или B8 на x86.
 
Чаще всего вы увидите хуки, размещенные в ntdll.dll, а не в kernel32.dll. Также это зависит от запущенного AV/EDR, поскольку каждое решение имеет разные подходы.
Одним из распространенных способов является загрузка ntdll.dll и проверка соответствия экспортируемой функции заглушке системного вызова, которая всегда имеет значение 4C8BD1B8 на x64 или B8 на x86.
как это можно сделать на c# ? по идее ntdll всегда в процессе подгружена, что за функция заглушка ?
 
Последнее редактирование:
как это можно сделать на c# ? по идее ntdll всегда в процессе подгружена, что за функция заглушка ?
Вы можете создать дочерний процесс в приостановленном состоянии, в котором будет загружена только ntdll, которая еще не будет перехвачена, поэтому вы можете использовать ее вместо перехваченной ntdll в своем родительском процессе. В Windows каждый вызов в какой-то момент будет проходить через ntdll, чтобы перейти из пользовательского режима в режим ядра, система должна использовать системные вызовы, которые очень хорошо документированы в отношении структуры как для x64, так и для x86.
 
Most of AV/EDR as stated above uses inline hooking on ntdll, checking the first byte thunk might be enough, it depends entirely, most of the EDR solutions just hook the thunk, others follow up the function thunk to the actual system call procedure, an alternative could be comparing original ntdll with the loaded ntdll module.
 
Самое верное решение - сравнение стабов функций на диске и в памяти, на диске хуков никогда не будет, и если в памяти и на диске есть несоответствия - значит имеются хуки
 
могу предложить несколько другой вариант, можно проверять .text секцию ntdll или же пейдж в котором находится конкретная функция на приватную память (перезаписанную). Открыв в Process Hacker вкладку Memory можно увидеть следующее:
1677382260716.png

если запатчить какую-то функцию в ntdll, то мы увидим следующее:
1677382107679.png
Один пейдж из .text секции ntdll.dll стал приватным, соответственно таким образом мы можем проверить, изменен ли ntdll без хендла и чтения файла.
Пример кода:
C++:
#include <iostream>
#include <Windows.h>
#include <ntstatus.h>

#pragma comment(lib, "ntdll.lib")

#define CurrentProcess() (HANDLE)-1
#define PAGE_SIZE 0x1000
#define PAGE_ALIGN(Va) ((PVOID)((ULONG_PTR)(Va) & ~(PAGE_SIZE - 1)))

typedef enum _MEMORY_INFORMATION_CLASS
{
    MemoryBasicInformation,
    MemoryWorkingSetInformation,
} MEMORY_INFORMATION_CLASS;

EXTERN_C NTSYSCALLAPI NTSTATUS NTAPI ZwQueryVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _In_opt_ PVOID BaseAddress,
    _In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
    _Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
    _In_ SIZE_T MemoryInformationLength,
    _Out_opt_ PSIZE_T ReturnLength
);

typedef struct _MEMORY_WORKING_SET_BLOCK
{
    ULONG_PTR Protection : 5;
    ULONG_PTR ShareCount : 3;
    ULONG_PTR Shared : 1;
    ULONG_PTR Node : 3;
#ifdef _WIN64
    ULONG_PTR VirtualPage : 52;
#else
    ULONG VirtualPage : 20;
#endif
} MEMORY_WORKING_SET_BLOCK, * PMEMORY_WORKING_SET_BLOCK;

typedef struct _MEMORY_WORKING_SET_INFORMATION
{
    ULONG_PTR NumberOfEntries;
    _Field_size_(NumberOfEntries) MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1];
} MEMORY_WORKING_SET_INFORMATION, * PMEMORY_WORKING_SET_INFORMATION;

NTSTATUS query_working_set_info(PMEMORY_WORKING_SET_INFORMATION* info)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;

    auto size = 0x10000;
    auto buf = malloc(size);

    if (!buf)
        return STATUS_MEMORY_NOT_ALLOCATED;

    status = ZwQueryVirtualMemory(CurrentProcess(), 0, MemoryWorkingSetInformation, buf, size, 0);

    if (status == STATUS_SUCCESS) {
        *info = reinterpret_cast<PMEMORY_WORKING_SET_INFORMATION>(buf);
    }
    else {
        free(buf);
    }

    return status;
}

struct section_data_t {
    uintptr_t base_address;
    size_t size;
};

bool verify_ntdll()
{
    PMEMORY_WORKING_SET_INFORMATION info = nullptr;
    const auto status = query_working_set_info(&info);

    if (!info)
        return false;

    const auto ntdll = GetModuleHandleA("ntdll.dll");

    if (!ntdll)
        return false;

    const auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(ntdll);
    const auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>((char*)ntdll + dos->e_lfanew);

    auto section = reinterpret_cast<PIMAGE_SECTION_HEADER>(nt + 1);

    section_data_t text_section = {};

    for (auto i = 0; i < nt->FileHeader.NumberOfSections; i++, section++) {
        if (!strcmp((char*)section->Name, ".text")) {
            text_section.base_address = reinterpret_cast<uintptr_t>(ntdll) + section->VirtualAddress;
            text_section.size = section->Misc.VirtualSize;
        }
    }

    if (!text_section.base_address)
        return false;

    for (auto i = 0; i < info->NumberOfEntries; i++) {
        const auto& data = info->WorkingSetInfo[i];
        const auto page = data.VirtualPage * PAGE_SIZE;

        if (page >= text_section.base_address && page < text_section.base_address + text_section.size)
            if (!data.Shared)
                return false;
    }

    free(info);

    return true;
}

int main()
{
    printf("ntdll '.text' verify: %s\n", verify_ntdll() ? "true" : "false");
    return 0;
}
 
могу предложить несколько другой вариант, можно проверять .text секцию ntdll или же пейдж в котором находится конкретная функция на приватную память (перезаписанную). Открыв в Process Hacker вкладку Memory можно увидеть следующее:
Посмотреть вложение 52232
если запатчить какую-то функцию в ntdll, то мы увидим следующее:Посмотреть вложение 52231Один пейдж из .text секции ntdll.dll стал приватным, соответственно таким образом мы можем проверить, изменен ли ntdll без хендла и чтения файла.
Пример кода:
C++:
#include <iostream>
#include <Windows.h>
#include <ntstatus.h>

#pragma comment(lib, "ntdll.lib")

#define CurrentProcess() (HANDLE)-1
#define PAGE_SIZE 0x1000
#define PAGE_ALIGN(Va) ((PVOID)((ULONG_PTR)(Va) & ~(PAGE_SIZE - 1)))

typedef enum _MEMORY_INFORMATION_CLASS
{
    MemoryBasicInformation,
    MemoryWorkingSetInformation,
} MEMORY_INFORMATION_CLASS;

EXTERN_C NTSYSCALLAPI NTSTATUS NTAPI ZwQueryVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _In_opt_ PVOID BaseAddress,
    _In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
    _Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
    _In_ SIZE_T MemoryInformationLength,
    _Out_opt_ PSIZE_T ReturnLength
);

typedef struct _MEMORY_WORKING_SET_BLOCK
{
    ULONG_PTR Protection : 5;
    ULONG_PTR ShareCount : 3;
    ULONG_PTR Shared : 1;
    ULONG_PTR Node : 3;
#ifdef _WIN64
    ULONG_PTR VirtualPage : 52;
#else
    ULONG VirtualPage : 20;
#endif
} MEMORY_WORKING_SET_BLOCK, * PMEMORY_WORKING_SET_BLOCK;

typedef struct _MEMORY_WORKING_SET_INFORMATION
{
    ULONG_PTR NumberOfEntries;
    _Field_size_(NumberOfEntries) MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1];
} MEMORY_WORKING_SET_INFORMATION, * PMEMORY_WORKING_SET_INFORMATION;

NTSTATUS query_working_set_info(PMEMORY_WORKING_SET_INFORMATION* info)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;

    auto size = 0x10000;
    auto buf = malloc(size);

    if (!buf)
        return STATUS_MEMORY_NOT_ALLOCATED;

    status = ZwQueryVirtualMemory(CurrentProcess(), 0, MemoryWorkingSetInformation, buf, size, 0);

    if (status == STATUS_SUCCESS) {
        *info = reinterpret_cast<PMEMORY_WORKING_SET_INFORMATION>(buf);
    }
    else {
        free(buf);
    }

    return status;
}

struct section_data_t {
    uintptr_t base_address;
    size_t size;
};

bool verify_ntdll()
{
    PMEMORY_WORKING_SET_INFORMATION info = nullptr;
    const auto status = query_working_set_info(&info);

    if (!info)
        return false;

    const auto ntdll = GetModuleHandleA("ntdll.dll");

    if (!ntdll)
        return false;

    const auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(ntdll);
    const auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>((char*)ntdll + dos->e_lfanew);

    auto section = reinterpret_cast<PIMAGE_SECTION_HEADER>(nt + 1);

    section_data_t text_section = {};

    for (auto i = 0; i < nt->FileHeader.NumberOfSections; i++, section++) {
        if (!strcmp((char*)section->Name, ".text")) {
            text_section.base_address = reinterpret_cast<uintptr_t>(ntdll) + section->VirtualAddress;
            text_section.size = section->Misc.VirtualSize;
        }
    }

    if (!text_section.base_address)
        return false;

    for (auto i = 0; i < info->NumberOfEntries; i++) {
        const auto& data = info->WorkingSetInfo[i];
        const auto page = data.VirtualPage * PAGE_SIZE;

        if (page >= text_section.base_address && page < text_section.base_address + text_section.size)
            if (!data.Shared)
                return false;
    }

    free(info);

    return true;
}

int main()
{
    printf("ntdll '.text' verify: %s\n", verify_ntdll() ? "true" : "false");
    return 0;
}
Драйвера ав могут хучить вывод ZwQueryVirtualMemory в ядре и тогда из юзермода ты не узнаешь модифицированы аттрибуты памяти или нет
Так же ZwQueryVirtualMemory может быть похукана в юзермоде, тем же перехватчиком, который перехватил остальные функции\сисколы, для этого даже драйвер не нужен
Но так скорее будет скрываться малварь, руткиты, ав обычно такое не делают, я не проверял массово, но скорее всего твой детект опознает таким способом большинство хуков ав, которые явно не скрывают свои хуки
Так же абсолютно все сисколы имеют несколько стандартных прологов на всей линейке Windows, поэтому детект можно производить по нарушению шаблона тела сискола, с обычными функциями будет сложнее, там гораздо больше вариантов
Код:
x86
Windows XP

B8 ?? ?? ?? ??                mov     eax, ??
BA 00 03 FE 7F                mov     edx, 7FFE0300h
FF D2                         call    edx
[C2 ?? ?? | C3]               retn    [??]

Windows XP (SP3), Windows 7, Windows 7 (SP1)

B8 ?? ?? ?? ??                mov     eax, ??
BA 00 03 FE 7F                mov     edx, 7FFE0300h
FF 12                         call    dword ptr [edx]
[C2 ?? ?? | C3]               retn    [??]

Windows 8, Windows 8.1, Windows 10

B8 ?? ?? ?? ??                mov     eax, ??
E8 ?? 00 00 00                call    $+??
[C2 ?? ?? | C3]               retn    [??]
8B D4                         mov     edx, esp
0F 34                         sysenter
C3                            retn

WoW64
Windows XP

B8 ?? ?? ?? ??                mov     eax, ??
[33 C9 | B9 ?? ?? ?? ??]      [xor     ecx, ecx | mov     ecx, ??]
8D 54 24 04                   lea     edx, [esp+4]
64 FF 15 C0 00 00 00          call    large dword ptr fs:0C0h
[C2 ?? ?? | C3]               retn    [??]

Windows 7, Windows 7 (SP1)

B8 ?? ?? ?? ??                mov     eax, ??
[33 C9 | B9 ?? ?? ?? ??]      [xor     ecx, ecx | mov     ecx, ??]
8D 54 24 04                   lea     edx, [esp+4]
64 FF 15 C0 00 00 00          call    large dword ptr fs:0C0h
83 C4 04                      add     esp, 4
[C2 ?? ?? | C3]               retn    [??]

Windows 8, Windows 8.1

B8 ?? ?? ?? ??                mov     eax, ??
64 FF 15 C0 00 00 00          call    large dword ptr fs:0C0h
[C2 ?? ?? | C3]               retn    [??]

Windows 10

B8 ?? ?? ?? ??                mov     eax, ??
BA ?? ?? ?? ??                mov     edx, ??
FF D2                         call    edx
[C2 ?? ?? | C3]               retn    [??]

x64
All (Windows XP, Windows 7, Windows 7 (SP1), Windows 8, Windows 8.1, Windows 10)

4C 8B D1                      mov     r10, rcx
B8 ?? ?? ?? ??                mov     eax, ??
0F 05                         syscall
C3                            retn
 
Драйвера ав могут хучить вывод ZwQueryVirtualMemory в ядре и тогда из юзермода ты не узнаешь модифицированы аттрибуты памяти или нет
в теории могут, тот же аваст хукает сисколы обходя PatchGuard без гипервизора, но на практике это используется редко, и не для этих целей
Так же ZwQueryVirtualMemory может быть похукана в юзермоде, тем же перехватчиком, который перехватил остальные функции\сисколы, для этого даже драйвер не нужен
ничего не мешает вызывать сискол самому
 
Обнаружение AV-песочниц может осуществляться путем поиска определенных характеристик среды, которые указывают на то, что это песочница или виртуализированная среда. Например, некоторые "песочницы" эмулируют определенные функции, имеют ограниченный набор доступных API или используют виртуализированное оборудование. Вредоносная программа может проверить эти характеристики и соответствующим образом изменить свое поведение, чтобы избежать обнаружения.
Вот пример фрагмента кода, который проверяет, была ли подключена функция Sleep в kernel32.dll:

SCSS:
FARPROC Address = GetProcAddressBy(GetModuleHandle("kernel32.dll"),"Sleep");
if (*(BYTE*)Address == 0xE9 || *(BYTE*)Address == 0x90 || *(BYTE*)Address == 0xC3)
{
    printf("Sleep hooked\n");
}

Этот фрагмент кода использует функцию GetProcAddressBy для получения адреса функции Sleep в kernel32.dll, а затем проверяет первый байт функции, чтобы узнать, была ли она подключена. Значение байта 0xE9 указывает на инструкцию относительного перехода, 0x90 - на инструкцию NOP, а 0xC3 - на инструкцию RET. Если любое из этих значений байта встречается в начале функции Sleep, то, скорее всего, функция была перехвачена.

Это лишь один пример того, как можно обнаружить хуки API. Существует множество других методов и техник обнаружения хуков, и они зависят от конкретного API и метода, используемого для обнаружения. Аналогичным образом, обнаружение AV-песочниц предполагает поиск определенных характеристик среды, которые могут варьироваться в зависимости от используемой песочницы или технологии виртуализации.
 
ничего не мешает вызывать сискол самому
в таком случае мы снова возвращаемся к чтению ntdll с диска для выдергивания нужных номеров сисколов)))
на разных редакциях Windows номера для одних и тех же сисколов разные, захардкодить значения не получится
а распутать перехват, что бы найти там нужный номер сискола - задача не тривиальная, нужно составлять граф следования, учитывать все переходы, установленные перехватчиком
 
Пожалуйста, обратите внимание, что пользователь заблокирован
в таком случае мы снова возвращаемся к чтению ntdll с диска для выдергивания нужных номеров сисколов)))
Как то давно видел на тытрубе такую идею, что если взять Zw-функции из ntdll, отсортировать их имена лексико-графически, то номер сискола будет совпадать с позицией имени в списке. Или что-то такое. Но лучше меня по этому поводу не цитировать, тк сам не проверял, и что-то не смог найти, где это было. Может, мне это вообще приснилось)).
 
Как то давно видел на тытрубе такую идею, что если взять Zw-функции из ntdll, отсортировать их имена лексико-графически, то номер сискола будет совпадать с позицией имени в списке. Или что-то такое. Но лучше меня по этому поводу не цитировать, тк сам не проверял, и что-то не смог найти, где это было. Может, мне это вообще приснилось)).
как же быть с номерами сисколов на WOW64 типа 0xFF023 ?
такого количества функций в NTDLL нет, а эти 0xFF добавляются в некоторые номера сисколов, я предполагаю что это некоторые флаги, но без них не работает

1677425756902.png

1677425869664.png

Все же я склоняюсь к тому, что метод с сортировкой - либо правда тебе приснился, либо был выдуман любителем молочки и грибов и форсился где-то им же собственноручно))
 
в таком случае мы снова возвращаемся к чтению ntdll с диска для выдергивания нужных номеров сисколов)))
Ну не обязательно, когда-то игрался с чем-то похожим. Составлял базу сисколов для каждой версии винды на сервере, если версия клиента уже есть в базе - ему не нужно ничего читать, сервер все отдаст, если нет - тогда читалось.
Но это если имеется общение с сервером, и читать на редких версиях все равно придется.
Сейчас бы я взял базу например тут, вшить их в сразу в приложение или при необходимости на сервер, и спокойно реализовывать свои сисколы
 
Пожалуйста, обратите внимание, что пользователь заблокирован
как же быть с номерами сисколов на WOW64 типа 0xFF023 ?
Через хевенсгейт переходить в 64-битный режим и дергать сисколл. И да, бывает что хук стоит в 64-битной ntdll, а не в 32-битной, насколько я помню, семантек так делал раньше.

я предполагаю что это некоторые флаги
Это флаги о том, как аргументы передавать, в принципе они не должны меняться от системы к системе, то есть, ты можешь номер выводить, а известные заранее флаги через побитовое или добавлять.

Все же я склоняюсь к тому, что метод с сортировкой - либо правда тебе приснился, либо был выдуман любителем молочки и грибов и форсился где-то им же собственноручно))
Хз, надо будет проверить, когда руки дойдут.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Как то давно видел на тытрубе такую идею, что если взять Zw-функции из ntdll, отсортировать их имена лексико-графически, то номер сискола будет совпадать с позицией имени в списке
А, вроде нашел, немножко я попутал концепт (сортировать Zw-функции нужно по адресу), но (см пункт 8): https://www.mdsec.co.uk/2020/12/byp...ect-invocation-of-system-calls-for-red-teams/
 
Вы можете создать дочерний процесс в приостановленном состоянии, в котором будет загружена только ntdll, которая еще не будет перехвачена, поэтому вы можете использовать ее вместо перехваченной ntdll в своем родительском процессе. В Windows каждый вызов в какой-то момент будет проходить через ntdll, чтобы перейти из пользовательского режима в режим ядра, система должна использовать системные вызовы, которые очень хорошо документированы в отношении структуры как для x64, так и для x86.
Если АВ ставит хуки, то делает это, как правило, во всех процессах. Потому самое верное решение - это все-таки подгрузить ntdll с диска и сравнить
 
Если АВ ставит хуки, то делает это, как правило, во всех процессах. Потому самое верное решение - это все-таки подгрузить ntdll с диска и сравнить
Объясните мне, как можно поставить хуки в приостановленное состояние процесса? Я провел небольшое исследование и знаю, что AV не будет помещать перехватчики в процесс приостановленного состояния, потому что существует большая задержка, когда AV начинает перехват.
 
Объясните мне, как можно поставить хуки в приостановленное состояние процесса? Я провел небольшое исследование и знаю, что AV не будет помещать перехватчики в процесс приостановленного состояния, потому что существует большая задержка, когда AV начинает перехват.
Если в приостановленном - то да. Но тогда нужно лезть в память чужого процесса, а это более палевно, чем просто мапинг ntdll с диска
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх