Врата рая: 64-битный код в 32-битном файле. Перевод из Valhalla zine
Это перевод статьи из Valhalla zine от 5 августа 2011.
Я обнаружил эту технику в 2009, затем дополнил в 2011.
Что это?
На 64-битной платформе есть только один ntoskrnl.exe и он 64-битный. Используется другое соглашение вызовов («fastcall», регистры) по сравнению с 32-битным кодом («stdcall», стек, старое название «pascal»). Как же 32-битный код выполняется на 64-битной платформе? В wow64cpu.dll есть преобразующий слой, который сохраняет 32-битное состояние, переводит параметры в 64-битную форму, затем запускает «Wow64SystemServiceEx» в wow64.dll. Но 64-битные регистры видны только в 64-битном режиме, как же тогда работает wow64cpu.dll? Есть способ, который называется «врата рая», но сначала нужно вернуться к ntdll.dll.
Преобразующий слой
Когда из DLL, например из kernel32.dll, вызывается функция, в ней происходят вызовы интерфейса ntdll.dll. Функции оттуда называются Native API, это богатая возможностями, но во многом недокументированная прослойка между пользовательским режимом и режимом ядра. Кое-что можете посмотреть в коде Chthon в 29A#6. Чтобы обратиться к режиму ядра нужно сделать вот что:
mov eax, service lea edx, dword ptr [esp + 4] int 2eh
В Windows XP для увеличения производительности возможно использовать команду sysenter вместо int 2eh. В 64-битной Windows добавляется «xor ecx, ecx», из-за 64-битного размера указателя, а int 2eh заменяется на:
call dword ptr fs:[0c0h]
и после этого остаётся всего один шаг до «врат рая». Поле в fs:[0c0h] называется WOW32Reserved и хранит в себе адрес в wow64cpu.dll. Если мы проследим, что находится после вызова, мы увидим переход. Дальний переход. Специальный дальний переход — «врата рая».
Врата рая
Переход в wow64cpu.dll это ворота в 64-битный режим. Мы можем прыгнуть через них в мир 64-битного кода: 64-битное адресное пространство, 64-битные регистры, 64-битные вызовы. Можно подумать, что бесполезно переходить внутрь wow64cpu.dll, ведь мы не можем контролировать, куда оно пойдёт после этого, но мы, конечно же, можем сами изменить адрес так, чтобы он указывал в любое нужное нам место. Мы можем изменить адрес внутри wow64cpu.dll, мы можем изменить адрес по fs:[0c0h], или мы можем просто сделать вызов через гейт самого себя. Гейт размечивает все 4 ГБ памяти, значение селектора всегда 33h. Мы также можем легко переключаться между режимами. Всё, что нам нужно это адрес возврата в стеке. Мы можем переключать режимы таким вот длинным способом:
call to64 ;дальше идёт 32-битный код to64: db 0eah ;jmp 33:in64 dd offset in64 dw 33h in64: ;дальше идёт 64-битный код
Переключиться обратно на 32-битный код можно так:
jmp fword ptr [offset to32 - offset fr64] fr64: to32: dd offset in32 dw 23h in32: ret
Пока мы находимся в 64-битном режиме, мы можем использовать только Native API из ntdll.dll. Переход jmp в стиле 0eah не поддерживается в 64-битном режиме. Там также нет абсолютной адресации памяти. Все адреса относительны по сравнению с rip, поэтому jmp является относительным по сравнению с меткой fr64.
Конечно, есть способ проще, он выглядит так:
db 9ah ;call 33:in64 dd offset in64 dw 33h ;дальше идёт 32-битный код in64: ;дальше идёт 64-битный код
Чтобы переключиться обратно в 32-битный код, просто используйте 32-битный retf. Это намного легче.
Поиск ntdll.dll
Пока мы в 64-битном режиме, мы можем использовать только интерфейс ntdll.dll, потому что kernel32.dll в памяти нашего процесса 32-битный, и не будет работать в 64-битном режиме. Мы можем получить базовый адрес ntdll.dll таким способом:
push 60h pop rsi gs:lodsq ;gs, а не fs mov rax, qword ptr [rax+18h] mov rax, qword ptr [rax+30h] mov rax, qword ptr [rax+10h]
Смешивание 32-битного и 64-битного кода
Yasm теперь позволяет смешивать 32-битный и 64-битный код в одном и том же файле. Когда я писал Shrug48 (на полпути между 32-битным и 64-битным кодом) это было невозможно, поэтому у меня было два файла исходного кода, которые надо было собирать раздельно и потом соединять. Теперь, с Yasm мы можем использовать «bits 32» перед 32-битным кодом и «bits 64» перед 64-битным кодом в любом месте файла, и это позволяет переключаться между режимами, например так:
bits 32 db 9ah ;call 33:in64 dd offset in64 dw 33h ;дальше идёт 32-битный код bits 64 in64: push 60h pop rsi gs:lodsq ;gs, а не fs mov rax, qword [rax+18h] mov rax, qword [rax+30h] mov rax, qword [rax+10h] retf
Другой способ — это прыжок способом, не зависящим от позиции:
push cs call to64 ;дальше идёт 32-битный код to64: push 0cb0033h ;объединённый селектор 33h и retf call to64 + 3 bits64 ;теперь в 64-битном режиме ;дальше идёт 64-битный код retf ;возврат в 32-битный режиме
Текущая директория
В 32-битном и 64-битном режиме раздельные текущие директории. Обычно 64-битный текущий каталог не используется, потому что все 32-битные API, работающие с текущей директорией не переключаются для этого в 64-битный режим. Мы можем делать текущие каталоги одинаковыми путём переписывания 64-битных указателей 32-битными. Конечно, сначала нужно попытаться найти расположение 64-битных указателей. ;)
Даже в 32-битном режиме есть 64-битный блок информации о потоке (Thread Information Block). Он находится по смещению 0x1000 после 32-битного блока информации о потоке. В 64-битном TIB находится указатель на 64-битные RTL_USER_PROCESS_PARAMETERS. За 0x28 байт перед структурой находится указатель на текущий каталог, используемый ntdll-функцией RtlDosPathNameToRelativeNtPathName_U. Есть и другие указатели на текущий каталог, но нам нужен именно этот.
Исключения
Мы можем, как обычно, использовать исключения в 64-битном режиме, но там нет SEH. Вместо этого там есть VEH (Vectored Exception Handlers). Есть одна вещь, которая меня удивила. 64-битный TIB имеет контекстную структуру для сохранения 32-битного состояния между переключениями режимов. Во время перехода esp обнуляется, а после перехода восстанавливается. Это предотвращает рекурсивное переключение от переписывания контекста. Сюда включается и тот случай, когда возникает исключение. Когда оно возникает, неважно в каком режиме, контекст сохраняется и esp обнуляется. Проблема в том, что после того, как исключение возвращается, esp не восстанавливается. Если после этого исключение возникло в 32-битном режиме, приложение упадёт. Поэтому, если вы хотите использовать исключения в 64-битном режиме, нужно сохранять состояние esp из TIB (оно там находится по gs:0x1480).
Заключение
Использование гейта это альтернативный способ проверки на предмет поддержки 64-битного режима без использования очевидного вызова API IsWow64Process. Просто поместите SEH около вызова, и, если возникнет исключение, это означает, что вы на 32-битной платформе. Также можно проверить ненулевое значение gs-селектора. Он не равен нулю только на 64-битной платформе.
64-битный код в 32-битных файлах. Убийца эмуляторов. ;)
Приветы:
Active - Benny - herm1t - hh86 - izee - jqwerty - Malum - Obleak - Prototype - Ratter - Ronin - RT Fishel - sars - SPTH - The Gingerbread Man - Ultras - uNdErX - Vallez - Vecna - Whitehead
Автор: roy g biv / defjam
Дата: июнь 2009/апрель 2011
Избранное
Остальное
По вопросам сотрудничества и другим вопросам по работе сайта пишите на cleogroup[собака]yandex.ru