Правда о NtLdtSetEntries
🕛 25.03.2008, 13:01
ВведениеРечь пойдёт об обломе эмуляторов и отладчиков, которые не учитывают то, что в защищенном режиме есть сегментные регистры, и они, к тому же, играют большую роль. Связанно это, в частности, с тем, что сегменты кода и данных в 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