Статья Как избавиться от регистра FS при установке SEH

Windows позволяет нам устанавливать свои юзер-обработчики исключений, которые реализованы чз механизм SEH на системах х32 (Structured Exception Handler, структурный), и VEH на х64 (векторный). Тема давно заезжена вдоль и поперёк, а потому не будем в очередной раз мусолить её от и до - здесь хотелось-бы обсудить другой вопрос. Как в дизасм-листинге спрятать обращение к сегментному регистру FS, ведь инструкция mov eax,[fs:0] сдаёт наш план с потрохами, что не есть хорошо. Обнаружив её взломщик сразу поймёт, что это попытка установить SEH, а значит софт будет стрелять исключениями, и сам-же перехватывать их. В общем как ни крути, регистр FS в данной ситуации нужно срочно отправлять в топку, тем более, что у нас имеется неплохой выбор альтернативных вариантов, а который из них использовать - решать уже вам.

1. Вводная часть
2. Чтение FS функцией GetThreadContext()
3. Модифицировать указатель в системном SEH
4. Перехватить системный обработчик исключений
5. Заключение



1. Общие сведения

По умолчанию система предлагает нам свой обработчик исключений, который описывает фрейм в стеке из двух двордов - первый дворд это линк на сл.обработчик, а второй хранит адрес процедуры текущего обработчика. Таким образом мы можем связывать несколько обработчиков в длинную цепочку "Chain". Указатель на голову этой цепи прописывается в первом-же поле "ExceptionList" структуры TEB потока, а маркером конца является значение 0xFFFFFFFF. При этом на саму ТЕВ указывает как-раз регистр FS на системах х32, и GS на х64. При наличии файла скриптов, заполненную ТЕВ (да и любую структуру) можно посмотреть в отладчике x64Dbg:

SehFrame.webp

Здесь видно, что на данный момент в стеке имеем всего один SEH-фрейм, который предоставила нам сама система. Его обработчик исключений зарыт где-то в нёдрах Ntdll.dll по адресу 0x77694DCD. Всё, на что он способен - это вывод окна с характером критической ошибки, да кнопкой "Закрыть", т.е. бесполезен от слова вообще. Поэтому ОС даёт нам возможность самим заниматься обработкой своих исключений, для чего всего-то нужно вписать ещё один фрейм, до системного. Программно это реализуется всего тремя строчками кода так:

C-подобный:
.code
start:  push  mySeh        ;// указатель на наш обработчик
        push  [fs:0]       ;// указатель на предыдущий фрейм
        mov   [fs:0],esp   ;// регистрируем на SEH-фрейм,
                           ;// ..в поле ТЕВ.ExceptionList

Конструкция подобного рода знакома всем, кто хоть краем уха слышал про реверс, а виной тому манипуляции с регистром FS. Посмотрим, как и на что его можно заменить, чтобы не мозолил глаза в дизассемблерах и отладчиках пользовательского режима.


2. Чтение FS функцией GetThreadContext()

Первый вариант заключается в вызове функции GetThreadContext(), которая дампит состояние всех регистров текущего потока, в одноимённую структуру CONTEXT. Среди прочего, по смещению 0х90 в этой структуре будет лежать и FS, а значит мы можем взять его в любой другой сегментный регистр, например безобидный ES или DS, что замаскирует факт установки собственного фрейма исключений.

C-подобный:
;//...
       mov     [context],CONTEXT_SEGMENTS   ;// флаг = 0x10004,
       invoke  GetThreadContext,-2,context  ;// ..только сегм.регистры
;//...
       push    es                    ;//
       mov     es,word[context+90h]  ;// читаем FS в ES
       mov     eax,[es:0]            ;// EAX = TEB.ExceptionList
       pop     es                    ;//
;//...
       push    mySeh     ;// регистрируем свой SEH
       push    eax
       mov    [eax],esp
;//...

Чтобы окончательно запутать начинающего хацкера, желательно вызывать GetThreadContext() где-нибудь в начале, а обращаться к структуре CONTEXT намного позже, предварив непосредственную регистрацию SEH-фрейма обфускацией кода. Вариант конечно не фонтан, однако имеет право на жизнь.


3. Мод указателя в системном SEH

Выше упоминалось, что предлагаемый системой обработчик исключений отнюдь не наделён интеллектом, а потому можно отправить его на скамейку запасных, а самим встать на его место просто перезаписав указатель. В этом случае регистр FS вообще не будет фигурировать нигде. Единственная проблема - это найти системный SEH-фрейм в стеке текущего потока.

В качестве сигнатуры можно использовать маркер окончания цепочки SEH со-значением 0xFFFFFFFF, только искать её придётся в девственном стеке, пока ещё никто не исказил стек левыми push -1. Как только найдём, то перезаписываем следующий после маркера дворд, чтобы он указывал на нашу пользовательскую процедуру обработки эксепшенов. Так это можно реализовать на практике:

C-подобный:
      mov     esi,esp        ;// ESI = стек
@@:   cmp     dword[esi],-1  ;// это 0xFFFFFFFF ?
      je      @ok            ;// да - на выход!
      add     esi,4          ;// нет - сл.дворд в стеке
      jmp     @b             ;// на повтор..

@ok:  add     esi,4          ;// okey - прыгаем к указателю
      mov    [esi],mySeh     ;// подменить его на свой обработчик!

Если не знать всей предыстории, то теперь сложно будет догадаться, что именно делает данный участок кода, ведь манипуляции с регистром FS отсутствуют как таковые. В общем способ не плохой, но можно по аналогичной схеме копнуть ещё глубже.


4. Перехват системного обработчика исключений.

Если реверсер продвинутый, он сразу обнаружит невалидный указатель в системном SEH-фрейме, который должен смотреть в либу Ntdll.dll, в то время как у нас он сейчас нацелен на обработчик в области памяти нашего-же процесса. Кстати дефолтная функция системы в Ntdll.dll называется ExceptHandler(), и как видно по значению в стеке, она расположена по адресу 0x777c4dcd. Что примечательно, отладчик x64Dbg ничего не знает про неё (в окне маячит какое-то левое имя), а вот ядерный WinDbg уже в курсе всех событий. Обратите внимание, что в обоих SEH-фреймах одинаковые значения, а имена функций разные:

SehChain.webp

Ладно, оставим этот нюанс за бортом, а сами посмотрим на содержимое системного обработчика. Как видим это типичная API с прологом и эпилогом, а поскольку мы подрядись её перехватить, то указатель в SEH-фрейме останется уже прежний, что снимет с нас все подозрения. Правда для этого нужно будет сначала добавить атрибут записи в страницу Ntdll.dll (т.к. в дефолте стоит page_execute_read), прописать в пролог указатель на свой обработчик исключений, после чего восстановить прежние атрибуты на место.

Код:
0:000:x86> !address 777с4dcd

Usage:            Image
Allocation Base:  77720000
Base Address:     77730000
End Address:      77807000
Region Size:      000d7000
Type:             01000000   MEM_IMAGE
State:            00001000   MEM_COMMIT
Protect:          00000020   PAGE_EXECUTE_READ  <---------//

;//-----------------------------------

0:000:x86> uf 777c4dcd
ntdll32!_except_handler4:

777c4dcd  8bff            mov     edi,edi
777c4dcf  55              push    ebp
777c4dd0  8bec            mov     ebp,esp
777c4dd2  83ec14          sub     esp,14h
777c4dd5  53              push    ebx
777c4dd6  8b5d0c          mov     ebx,dword ptr [ebp+0Ch]
777c4dd9  56              push    esi
777c4dda  8b7308          mov     esi,dword ptr [ebx+8]
777c4ddd  333588207277    xor     esi,dword ptr [ntdll32!__security_cookie]
777c4de3  57              push    edi
777c4de4  8b06            mov     eax,dword ptr [esi]
777c4de6  c645ff00        mov     byte ptr [ebp-1],0
777c4dea  c745f801000000  mov     dword ptr [ebp-8],1
777c4df1  8d7b10          lea     edi,[ebx+10h]
777c4df4  83f8fe          cmp     eax,0FFFFFFFEh
777c4df7  0f854fe50100    jne     ntdll32!_except_handler4+0x2c
.....

Библиотека Ntdll.dll у каждого процесса своя, т.к. система проецирует/мапит её в каждый процесс отдельно. Поэтому фактически мы будем править код Ntdll только своей либы. Пока юзер не осуществляет запись в пространство системных dll, всё идёт в штатном режиме. Но при попытки записи тут-же включается механизм CoW (CopyOnWrite), и ядро оси создаёт копию своей библиотеки, чтобы эта запись не затронула оригинал. Вот пример реализации такого варианта установки пользовательских SEH:

C-подобный:
;//.....
      mov     esi,esp        ;// ESI = стек
@@:   cmp     dword[esi],-1  ;// это 0xFFFFFFFF ?
      je      @ok            ;// да - на выход!
      add     esi,4          ;// нет - сл.дворд в стеке
      jmp     @b             ;// на повтор..

@ok:  add     esi,4          ;// okey - прыгаем к указателю
      mov     eax,[esi]      ;// EAX = линк на системный обработчик
      push    eax eax

;// Добавить атрибут WRITE к странице Ntdll.dll
      invoke  VirtualProtect,eax,4096,\
                             PAGE_EXECUTE_READWRITE,\
                             oldFlags
      call    @f
      push    mySeh          ;// кодируем в опкодах переход
      ret                    ;// всего = 6 байт

@@:   pop     esi edi        ;// перехват функции!
      mov     ecx,6
      rep     movsb

;// Восстановить атрибут страницы Ntdll.dll
      pop     eax
      invoke  VirtualProtect,eax,4096,[oldFlags],oldFlags
;//.....

Теперь в момент любого эксепшена в нашем приложении, управление получит кастомный обработчик, внутри которого мы должны сохранить контекст всех регистров уже знакомой функцией GetThreadContext(), и обработав должным образом ошибку, прописать в структуре контекста новое значение регистра EIP, чтобы код продолжил исполнение в обычном режиме. Для восстановления контекста регистров предусмотрена функция SetThreadContext().


5. Заключение

Здесь мы рассмотрели несколько вариантов скрытия регистра FS с радаров дизассемблера и отладчика при установки пользовательских SEH. Надеюсь в природе существуют ещё аналогичные финты, и если вы можете предложить что-то своё, хотелось-бы взять их на вооружение. Удачи всем в исследованиях, пока!

Ссылки по теме:
SEH – фильтр необработанных исключений - Форум информационной безопасности - Codeby.net
Самотрассировка, или марш-бросок по периметру отладчика
ASM – Динамическое шифрование кода - Форум информационной безопасности - Codeby.net
 
что-то я плохо понял эту схему - какая связь между KGDT и ТЕВ?
кстати для чтения LDT есть GetThreadSelectorEntry(), и если передать ей FS=53h, можно получить весь дескриптор. Это так.. к слову, поскольку записать этот дескриптор в кэш сегментных всё-равно не получится.
 
Marylin

Схема такая - меняем fs на ds например, загружаем в него селектор teb(= kgdt_*, мы используем константы из сурков nt). Можно и так push fs/pop ds.

kitrap08.blogspot.com/2010/12/blog-post.html?m=1
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab