Правда о NtLdtSetEntries — Архив WASM.RU

Все статьи

Правда о NtLdtSetEntries — Архив WASM.RU

Введение

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

2002-2013 (c) wasm.ru