PDA

Просмотр полной версии : Как избавиться от регистра FS при установке SEH


Marylin
08.02.2026, 19:09
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:

https://forum.antichat.xyz/attachments/4950814/img_8f4e5b3fd6.png
Здесь видно, что на данный момент в стеке имеем всего один 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
+
90
h
]
;
// читаем 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-фреймах одинаковые значения, а имена функций разные:

https://forum.antichat.xyz/attachments/4950814/img_1e865c81a7.png
Ладно, оставим этот нюанс за бортом, а сами посмотрим на содержимое системного обработчика. Как видим это типичная 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 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 – фильтр необработанных исключений - Форум информационной безопасности - античат (https://forum.antichat.xyz/threads/569317/)
Самотрассировка, или марш-бросок по периметру отладчика (https://forum.antichat.xyz/threads/570133/)
ASM – Динамическое шифрование кода - Форум информационной безопасности - античат (https://forum.antichat.xyz/threads/578745/)

Ахимов
08.02.2026, 23:30
push KGDT_R3_(CM)TEB/pop ds/fetch ds:[TEB.disp] -> fs to ds ?

Marylin
09.02.2026, 18:42
Ахимов сказал(а):

push KGDT_R3_(CM)TEB


что-то я плохо понял эту схему - какая связь между KGDT и ТЕВ?
кстати для чтения LDT есть

GetThreadSelectorEntry()

, и если передать ей

FS=53h

, можно получить весь дескриптор. Это так.. к слову, поскольку записать этот дескриптор в кэш сегментных всё-равно не получится.

Ахимов
09.02.2026, 19:42
Marylin

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

kitrap08.blogspot.com/2010/12/blog-post.html?m=1