Правда о NtLdtSetEntries
Введение
Речь пойдёт об обломе эмуляторов и отладчиков, которые не учитывают то, что в защищенном режиме есть сегментные регистры, и они, к тому же, играют большую роль. Связанно это, в частности, с тем, что сегменты кода и данных в OS Windows имеют базу 0 и при формировании линейного адреса он совпадает со смещением. Облом программ такого рода данным методом сводится к добавлению ещё одного дескриптора в LDT и работу через селектор, который содержит номер данного дескриптора.
NtLdtSetEntries
Эта замечательная функция из ntdll.dll дает возможность добавить элемент (даже 2 элемента) в локальную таблицу дескрипторов. Вызов данной функции приводит к вызову функции PsSetLdtEntries в ядре. В этой функции производится довольно тщательная проверка дескриптора и селектора. Возможно, добавить дескриптор только если: Его тип — ReadWrite, ReadOnly, ExecuteRead, ExecuteOnly, или Invalid. Это не системный дескриптор (что автоматом лишает нас прорулить в ядро с помощью Callgate).
DPL=3 Base<MM_HIGHEST_USER_ADDRESS (7FFEFFFF) Base+Limit<MM_HIGHEST_USER_ADDRESS
После этих проверок происходит вызов Ke386SetLdtProcess->Ki386LoadTargetLdtr->KiLoadLdtr-> asm lldt.
Антиэмуляция
Основана на том, что автоматические системы не учитывают, что при формировании линейного адреса к смещению прибавляется база дескриптора, номер которого содержится в одном из сегментных регистров. Рассмотрим небольшой пример. Допустим, есть такой код:
xor edi,edi stosd
Большинство считает, что, будучи выполненным в OS Windows, данный код сгенерирует исключение и произойдет вызов SEH. Но это так не во всех случаях. Ведь stosd эквивалентна паре инструкций:
mov dword[es:edi],eax add(sub) edi,4
В общем случае в пользовательской программе es=23h, указывает на 4ый дескриптор в LDT, который описывает сегмент с базой 0. Тогда обращение произойдет по нулевому линейному адресу и действительно возникнет исключение. Но если, например, добавить свой дескриптор с базой 401000h и в es поместить селектор, который содержит его номер, то в результате выполнения вышеприведенного кода произойдет обращение по адресу 401000h, где может находиться доступный для записи сегмент данных программы.
Приведу в качестве иллюстрации код, который вводит в заблуждение эмулятор NOD’a (на других АВ не проверял) и некоторых программистов.
format PE GUI 4.0 entry start include '%fasminc%\win32a.inc' ; ;Данная строка находится по адресу 401000h ; mess db '1234AGE',0 ; ;Дескриптор, который необходимо добавить в LDT ; LDT_Entry: ; ;Младшие 16бит лимита ; dw 0100h ; ;Младшие 24бита базы (401000h) ; db 0,10h,40h ; ;Тип: 1010 - так как S=1, 0-сегмент данных 010-для чтения/записи ;S(тип дескриптора): 1 (сегмент данных или кода) ;DPL: 11 (ring3) ;P: 1 (сегмент присутствует) ; db 11110010b ; ;16-19 биты лимита: 1111b ;AVL: 0 - чо угодно ;Reserved: 0 - надо чтоб был 0 иначе конец света ;G: 1 - лимит умножаем на 1000h ; db 11000000b ; ;Cтарший байт базы ; db 0 start: ; ;Добавим дескриптор в LDT ; invoke NtSetLdtEntries,1111111b,dword[LDT_Entry],dword[LDT_Entry+4],0,0,0 ; ;Селектор с номером данного дескриптора - в es ; push es push 1111111b pop es ;!!!!!! ;Обращение произойдет по адресу 401000h, а не 0 ;!!!!!! mov eax,'MESS' xor edi,edi stosd ; ;Восстановим es ; pop es invoke MessageBox,0,mess,mess,MB_OK invoke ExitProcess,0 data import library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL',\ comdlg32,'comdlg32.dll',\ ntdll,'ntdll.dll' include '%fasminc%\apia\comdlg32.inc' include '%fasminc%\apia\user32.inc' include '%fasminc%\apia\kernel32.inc' include '%fasminc%\ntdll.inc'; import ntdll, NtSetLdtEntries,'NtSetLdtEntries' end data
Следует обратить внимание также на то, что перед вызовом АПИ следует обязательно восстановить значения сегментных регистров, так как обращение к какому-либо адресу приведет к тому, что на самом деле произойдет обращение по адресу бОльшему на базу, указанному в дескрипторе. Другими словами, если где-то в коде MessageBoxA встретится команда типа mov eax,es:[77d91234h], то на самом деле будет попытка записи в еах значения ячейки по адресу 78192234h, что, скорее всего, вызовет исключение.
Антиотладка
Основана на том же принципе, только с учетом того, что формируется дескриптор для сегмента кода. Всем известный отладчик OllyDbg при изменении значения cs путем выполнения дальнего вызова, либо перехода в сегмент с другой базой тихонько выпадает в осадок. Приведу код, иллюстрирующий данный подход.
format PE GUI 4.0 entry start include '%fasminc%\win32a.inc' ; ;Опять же адрес данной строки 401000h ; mess db 'MESSAGE',0 data import library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL',\ comdlg32,'comdlg32.dll',\ ntdll,'ntdll.dll' include '%fasminc%\apia\comdlg32.inc' include '%fasminc%\apia\user32.inc' include '%fasminc%\apia\kernel32.inc' include '%fasminc%\ntdll.inc' end data ; ;Для удобства вызова функций, рассчитанных на ;работу в сегменте с base=0 ; macro invokes [arg] { common if ~ arg eq reverse pushd arg common end if call invoker } ; ;На этот раз дескриптор кода ; LDT_Entry: ; ;Младшие 16бит лимита ; dw 0ffffh ; ;Младшие 24бита базы 401000h ; db 0,10h,40h ; ;Тип: 1010 - так как S=1, 1-сегмент кода 010-для чтения/записи ;S(тип дескриптора): 1 (сегмент данных или кода) ;DPL: 11 (ring3) ;P: 1 (сегмент присутствует) ; db 11111010b ; ;16-19 биты лимита: 0000 ;AVL: 0 - чо угодно ;Reserved: 0 - надо чтоб был 0, иначе конец света ;G: 1 - лимит умножаем на 1000h ; db 11000000b ; ;Старший байт базы ; db 0 start: ; ;Добавляем дескриптор ; invoke NtSetLdtEntries,1111111b,dword[LDT_Entry],dword[LDT_Entry+4],0,0,0 mov ax,cs ; ;Мега фокус-покус ; jmp 1111111b:ёпт-401000h ёпт: ; ;База кода теперь 401000, а не 0 :) ;Сохраним старое значение cs для успешного вызова АПИ ; mov [cseg],ax ; ;Вызов АПИ следует осуществлять через "переходник" ; invokes MessageBox,0,mess,mess,MB_OK invokes ExitProcess,0 ; ;Переходник работает так: ; переход к базе кода 401000 ; вызов АПИ ; переход к базе 0 ; invoker: ; ;дальний переход, чтоб загрузить оригинальный cs ; db 0eah ;опкод jmp far dd inv ;смещение cseg dw ? ;сегмент ; ;Возврат из процедуры ; rets: jmp [reteng] reteng dd ? ; ;Тут база 0 ; inv: ; ;Запомним адрес вызываемой процы(относительно 0) ; mov eax,dword[esp+4] ; ;Запомним адрес возврата из процедуры(относительно 0) ; mov edx,dword[esp] mov [reteng],edx ; ;Вершина стека должна указывать на параметры, переданные процедуре ; add esp,8 ; ;Вызов процедуры ; call dword[eax] ; ;Прыжок, дабы вернуться к базе 0 ; db 0eah ; ;Смещение относительно 401000h ; dd rets-401000h dw 1111111b
Удачная комбинация
Также можно рассмотреть комбинацию двух данных подходов — добавить два дескриптора с одинаковой базой: один для кода, один для данных. После этого пройтись по всем модулям процесса, пофиксить релоки с учетом новой базы, да и сам PEB тоже, наткнутся на кучу «подводных камней», после чего спокойно, загрузив в сегментные регистры новые селекторы, вызывать АПИ, не восстанавливая оригинальные значения сегментных регистров.
На этом заканчиваю статью, wasm.ru forever 🙂
[C] FreeMan
Источник: wasm.ru от 11.11.2007