Это не новость, это не новость и определенно не мое исследование, но я использовал его недавно, так что вот моя попытка объяснить какую-то интересную концепцию WOW64. Я также хочу отдохнуть от чтения руководства AMD / Intel, чтобы написать этот гипервизор. Я также считаю, что термин «Небесные врата» вполне уместен и является самой крутой вещью на свете, так что вот он.

Введение

Я обычно добавляю сюда несколько фотографий, чтобы показать, как я начал свой путь, но поскольку это было 2 месяца назад на бесплатном резерве (привет GuidedHacking), у меня больше нет журнала. В любом случае, это было примерно так…

Me: Yoooooooo any good technique to catch a manual syscall?!?!?
GH: That is going to be tough.
GH: Wait, is it Wow64?
Me: Yes
GH: You can’t manual syscall on Wow64, you coconut.
Me: ????

Итак, у вас есть такая вещь, как ручной системный вызов на WOW64. Что ж, есть один способ, но я открою эту тему позже. (Подсказка: Небесные врата)

Во-первых, нам нужно немного разобраться в WOW64.

WoW64 (W, 32-битный o n W, 64-битный)

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

WOW64 применяется к 32-битным приложениям, работающим на 64-битной машине. Это означает, что, хотя между 32-битным и 64-битным ядром очень мало различий, нет никаких сомнений в несовместимости. Эта подсистема пытается уменьшить эти несовместимости с помощью различных интерфейсов, таких как wow64.dll, wow64win.dll и wow64cpu.dll. Существует также другая среда реестра для приложений wow64 и нативных 64-битных приложений, но давайте не будем вдаваться в этот беспорядок.

Интересное поведение, которое следует заметить при выполнении приложения WOW64, заключается в том, что все компоненты режима ядра на 64-битной машине всегда будут выполняться в 64-битном режиме, независимо от того, являются ли инструкции приложения 64-битными или нет.

В заключение это означает, что приложения WOW64 работают немного иначе, чем собственные 64-битные приложения. Мы собираемся этим воспользоваться. Давайте посмотрим на разницу, когда дело доходит до вызова WINAPI.

NTDLL.dll против NTDLL.dll

Ntdll.dll на компьютере с Windows широко распространен, и я не буду вдаваться в подробности. Нас интересует только функция ntdll.dll при выполнении вызова WINAPI, который требует системного вызова. Давайте выберем Cheat Engine в качестве отладчика (потому что он может видеть обе библиотеки DLL) и Teamviewer в качестве нашего приложения WOW64.

Если бы это был живой разговор, я бы замучил вас этим вопросом, но это не живая сессия. Замечено, что есть те 3 библиотеки DLL интерфейса wow64, о которых я упоминал ранее, но особенно вы хотите обратить внимание на two ntdll.dll. Что еще более странно, так это то, что один из ntdll.dll в настоящее время находится в 64-битном адресном пространстве. Wtf? Как? Это 32-битное приложение!

Ответ: WOW64.

Различия

Я уверен, что между двумя DLL существует еще масса различий, но давайте рассмотрим самое первое очевидное различие - системные вызовы.

Мы все знаем (если нет, то теперь знаете), что ntdll.dll в обычном собственном приложении отвечает за выполнение syscall / sysenter, передавая выполнение ядру. Но я также упоминал ранее, что вы не можете выполнить системный вызов в приложении WOW64. Так как же приложение WOW64… что-нибудь делает?

Переходя к примеру функции, такой как NtReadVirtualMemory, мы должны ожидать, что service id будет помещен в регистр eax и за ним последует инструкция syscall/sysenter.

Ладно, это странно. Нет syscall. Вместо этого есть call, и я точно знаю, что вы не можете просто войти в область ядра с call.. Давайте следовать call!

Теперь мы находимся в каком-то месте внутри wow64cpu.dll под названием Wow64Transition, которое теперь выполняется с 64-битным набором инструкций. Мы также видим, что он ссылается на сегмент CS:0x33. Что здесь происходит?

В блоге Алекса Лонеску он сказал:

Фактически, в 64-битной Windows первым фрагментом кода, выполняемым в * любом * процессе, всегда является 64-битная NTDLL, которая обеспечивает инициализацию процесса в пользовательском режиме (как 64-битный процесс!) . Лишь позже интерфейс Windows-on-Windows (WoW64) вступает во владение, загружает 32-битную NTDLL, и выполнение начинается в 32-битном режиме через большой переход к сегменту кода совместимости. В 64-битный мир больше никогда не попадают, кроме случаев, когда 32-битный код пытается выполнить системный вызов. Загруженная 32-битная NTDLL вместо ожидаемой инструкции SYSENTER на самом деле содержит серию инструкций для возврата в 64-битный режим, чтобы системный вызов мог быть выполнен с помощью инструкции SYSCALL, и чтобы параметры могли отправляться с использованием x64 ABI с расширением знаков при необходимости.

Это означает, что когда 32-битный код пытается выполнить syscall, он проходит через 32-битный ntdll.dll, а затем в этот конкретный переходной элемент (Небесные врата) и выполняет инструкцию far jump, которая переключает в сегмент кода с включенным длинным режимом (64-бит). Это 0033:wow64cpu.dll+0x7009, который вы видите на последнем снимке экрана. Теперь, когда мы находимся в 64-битном контексте, мы, наконец, можем перейти к 64-битному ntdll.dll, где и выполняется настоящий системный вызов.

Вот и все, полная цепочка системных вызовов WOW64. Подведем итоги.

32-bit ntdll.dll -> wow64cpu.dll’s Heaven’s Gate -> 64-bit ntdll.dll -> syscall into the kernel

Теперь, когда мы понимаем всю цепочку выполнения, давайте приступим!

Зацепив небесные врата

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

План

Наш план довольно прост. Мы будем делать то, что обычно делаем с обычным объездным крюком.

  1. Мы поместим какой-нибудь jmp на переходные врата / Небесные врата, которые затем перейдут к нашему шеллкоду.
  2. Наш шелл-код выберет, какой идентификатор службы нужно перехватить, и перейдет к соответствующему перехвату.
  3. Как только наш хук закончится, он прыгнет к переходным воротам / Небесным воротам.
  4. Transition Gate / Heaven’s Gate продолжит переключение контекста на 64-битный и будет выполняться как обычно.

Но сначала, как приложение узнает, где находятся врата в рай?

Ответ: FS: 0xC0 или TIB + 0xC0

Итак, теоретически - мы могли бы определить, где находятся Небесные врата, используя этот фрагмент кода.

const DWORD_PTR __declspec(naked) GetGateAddress()
{
    __asm
    {
        mov eax, dword ptr fs : [0xC0]
        ret
    }
}

Теперь, когда мы знаем, где находятся текущие Небесные врата, и собираемся их подключить, давайте создадим «резервную копию» кода, который мы собираемся изменить.

const LPVOID CreateNewJump()
{
    lpJmpRealloc = VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT,
        PAGE_EXECUTE_READWRITE);
    memcpy(lpJmpRealloc, (void *)GetGateAddress(), 9);

    return lpJmpRealloc;
}

Это эффективно выделит новую страницу и скопирует 9 байт far jmp с небес. Почему мы это делаем, не будет раскрыто, но если вы хотите узнать конкретный термин, мы создаем батут для нашего объездного крюка. Это позволит нам сохранить инструкции far jmp, которые мы собираемся перезаписать на следующем шаге.

Затем мы собираемся заменить этот дальний jmp на PUSH Addr, RET, эффективно действующий как переход по абсолютному адресу. (Нажмите адрес, который вы хотите переместить в стек, Ret вытолкнет его из стека и jmp там)

void __declspec(naked) hk_Wow64Trampoline()
{
    __asm
    {
        cmp eax, 0x3f //64bit Syscall id of NtRVM
        je hk_NtReadVirtualMemory
        cmp eax, 0x50 //64bit Syscall id of NtPVM
        je hk_NtProtectVirtualMemory
        jmp lpJmpRealloc
    }
}

const LPVOID CreateNewJump()
{
    DWORD_PTR Gate = GetGateAddress();
    lpJmpRealloc = VirtualAlloc(nullptr, 0x1000, MEM_RESERVE | MEM_COMMIT,
        PAGE_EXECUTE_READWRITE);
    memcpy(lpJmpRealloc, (void *)Gate, 9);

    return lpJmpRealloc;
}

const void WriteJump(const DWORD_PTR dwWow64Address, const void *pBuffer, size_t ulSize)
{
    DWORD dwOldProtect = 0;
    VirtualProtect((LPVOID)dwWow64Address, 0x1000, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    (void)memcpy((void *)dwWow64Address, pBuffer, ulSize);
    VirtualProtect((LPVOID)dwWow64Address, 0x1000, dwOldProtect, &dwOldProtect);
}


const void EnableWow64Redirect()
{
    LPVOID Hook_Gate = &hk_Wow64Trampoline;

    char trampolineBytes[] =
    {
        0x68, 0xDD, 0xCC, 0xBB, 0xAA,       /*push 0xAABBCCDD*/
        0xC3,                               /*ret*/
        0xCC, 0xCC, 0xCC                    /*padding*/
    };
    memcpy(&trampolineBytes[1], &Hook_Gate, 4);
    WriteJump(GetGateAddress(), trampolineBytes, sizeof(trampolineBytes));
}

Этот код перезапишет 9-байтовый FAR JMP вместе со всем необходимым VirtualProtect.

Давайте рассмотрим hk_Wow64Trampoline.

Итак, мы знаем, что до того, как произойдет какой-либо системный вызов, идентификатор службы ВСЕГДА находится в регистре EAX. Следовательно, мы можем использовать инструкцию cmp, чтобы определить, что вызывается, и перейти к соответствующей функции перехвата. В нашем случае мы делаем 2 cmp (но вы можете делать сколько угодно), один с 0x3f и один с 0x50 - NtRVM и NtPVM. Если регистр EAX содержит правильный системный вызов, будет выполняться je или jump-equal, эффективно переходя к нашей функции перехвата. Если это не тот системный вызов, который нам нужен, он будет использовать jmp для lpJmpRealloc (который мы создали в нашей функции CreateNewJump. Это 9 исходных байтов, которые мы скопировали перед перезаписью).

void __declspec(naked) hk_NtProtectVirtualMemory()
{
    __asm {
        mov Backup_Eax, eax
        mov eax, [esp + 0x8]
        mov Handle, eax
        mov eax, [esp + 0xC]
        mov Address_1, eax
        mov eax, [esp + 0x10]
        mov DwSizee, eax
        mov eax, [esp + 0x14]
        mov New, eax
        mov eax, [esp + 0x18]
        mov Old, eax
        mov eax, Backup_Eax
        pushad
    }

    printf("NtPVM Handle: [%x] Address: [0x%x]  Size: [%d]  NewProtect: [0x%x]\n", Handle, Address_1, *DwSizee, New);

    __asm popad
    __asm jmp lpJmpRealloc
}


void __declspec(naked) hk_NtReadVirtualMemory()
{
    __asm pushad

    printf("Calling NtReadVirtualMemory.\n");

    __asm popad
    __asm jmp lpJmpRealloc
}

Обратите внимание, что перед тем, как выполнять какие-либо действия с функцией перехвата, вы должны нажать pushad / pushfd, а затем - popfd / popad, чтобы сохранить регистры и флаги. Если вы этого не сделаете, ожидайте, что программа выйдет из строя в кратчайшие сроки.

Точно так же я очень старался получить значения из функции declspec (naked) с помощью аргументов, но это просто невозможно, потому что вы в конечном итоге используете ECX как регистр, а ECX просто хранит 64-битное значение, по моему опыту. .

Пожалуйста, дайте мне знать, если вы знаете, как заставить что-то подобное работать.

DWORD __declspec(naked) hk_NtProtectVirtualMemory(
    IN HANDLE               ProcessHandle,
    IN OUT PVOID            *BaseAddress,
    IN OUT PULONG           NumberOfBytesToProtect,
    IN ULONG                NewAccessProtection,
    OUT PULONG              OldAccessProtection
)

Резюме

Таким образом, когда вы работаете как процесс Wow64, вы не можете напрямую получить доступ к ядру. Чтобы перейти в 64-битный режим, вам нужно пройти через переходные ворота, известные как Heaven’s Gate. Этот переход можно зацепить традиционным обходным путем, о котором идет речь в этом посте.

Этот метод позволяет превратить переходной элемент в фальшивый, который выполняет условный переход на основе служебного номера к правильной функции перехвата. Как только функция перехвата завершает выполнение, происходит переход к шлюзу перехода, резервную копию которого мы сделали. Это изменит наш 32-битный режим на 64-битный, в котором мы продолжим выполнение, перейдя в 64-битный Ntdll. 64-битный Ntdll затем выполнит системный вызов / sysenter и войдет в область ядра.

32bit Ntdll-> Heaven’s Gate (hooked) -> Fake Gate -> hook_function -> Heaven’s Gate Trampoline -> 64bit Ntdll -> Kernel land

Результат

Взгляните на пример кода здесь.

Еще одна вещь, на которую следует обратить внимание, это то, что вы не можете просто напечатать идентификатор системного вызова внутри ловушки Wow64, и это потому, что printf требует системного вызова (я так считаю), и если вы перехватите системный вызов printf при вызове printf внутри ловушки, у вас будет плохое время (бесконечный цикл).

Вывод

Прихватывание - это техника, состоящая из нескольких методов. То, как вы зацепитесь, зависит от вашей креативности и понимания системы. Пока что мы доказали, что можем перехватить любую функцию практически на всех этапах. Может быть, в следующий раз мы займемся SSDT-крючком или чем-то еще. Однако завтра у меня экзамен в ОБСЕ, так что желаю мне удачи. Мне потребовалось больше месяца, чтобы закончить это, потому что я так увлекся. Прошу простить меня, если ко 2-му тайму будет больше ошибок!

-Fs0x30