✊Rot Front✊
-
Автор темы
- #1
Мне стало интересно, какие anti-debug трюки часто используют.
Здесь будет рассмотрены популярные anti-debug трюки,как они работают в ядре и в добавок посмотрим фишки протекторов(VMP,Themida/WinLicense ).
Для наглядности здесь будут сами примеры anti-отладки.
Детекты через FindWindows/поиск процесса - в данной статье не будут разбираться.
Так же в конце будут файлы, чтобы вы могли поиграться с ними, если захотите.
Отказ от ответственности.
Данная статья публикуется только в образовательных целях.
Автор не пытается никого оскорбить, а только лишь хочет поднять некоторые моменты, чтобы новичкам их было легче понять.
Заранее извиняюсь, что беру псевдокод из иды, но постараюсь объяснить некоторые моменты, чтобы вам было более понятно.
Оглавление:
-UM
-kernel32/kernelbase + примеры
-ntdll + примеры
-KM
-как начинается отладка
-как происходи отсоединение дебаггера
-анализ как работают трюки в KM
-Маленький анализ протекторов(VMP/Themida)
-Печальная темида
-VMP
-Идея автора VMP и небольшой пример обхода anti-debug VMP(x64).
- Начало анализа
Мы просто стучим в PEB и проверяем BeingDebugged(смещение - 0x2).
Здесь ничего интересного, поскольку можно просто перезаписать значение на 0.
Дальше будет объяснено, откуда появляется запись в BeingDebugged при начале отладки.
P.S для новичков.
Для получения адреса к PEB вы можете вызвать:
__readgsqword(0x60)(x64)/__readfsdword(0x30)(x32)
Здесь идёт вызов NtQueryInformationProcess с ProcessDebugPort.
Отладчик просто так не может работать DebugPort'а(далее будет объяснено почему).
Этот WinApi используют часто,поскольку он задокументирован(хотя лучше вызывать NTAPI напрямую).
CloseHandle вызывает NtClose и если закрыть невалидный HANDLE,то ничего не произойдёт, но вот в случае дебагинга процесса будет вызвано исключение!
(ну или если применить один трюк, но об этом позже :D ).
Маленький пример обнаружения дебаггера:
Однако можно сделать дескриптор защищённым и если попытаться закрыть HANDLE под дебаггером,то будет вызвано исключение.
Давайте посмотрим, код происходит вызов SetHandleInformation.
Можно установить у дескриптора HANDLE_FLAG_PROTECT_FROM_CLOSE и при закрытие HANDLE'а система выбросит исключение из-за присутствия отладчика или трюка.
Потом мы посмотрим, как это работает в ядре.
Пример детекта:
Второй вариант - DuplicateHandle т.к внутри себя он вызывает NtClose,если передать аргумент - DUPLICATE_CLOSE_SOURCE.
Об этом мы поговорим позже.
По факту - это попытка обойти просто хук NtClose.
Сам пример:
Сейчас пойдут мемы связанные с OutputDebugString:
В Windows 7~ всё так:
и это вызывает ->
Как по мне это самый худший anti-debug трюк, который существует на сегодняшний день.
Причина в том, что этот трюк работает ниже windows vista и попросту бесполезен на сегодняшний день.
Забавно, но с помощью него можно попытаться крашнуть OllyDbg ( версию 1.1 и ниже).
Сюрприз-сюрприз!
В Windows 10 реализовали вызов исключения с LPCWSTR!(Unicode)
Цепочка:RtlRaiseException -> ZwRaiseException & RtlpCaptureContext2(&ContextRecord);
Можно вызывать исключение и проверять какое исключение было вызвано, но как по мне этот трюк бесполезен.
Наверное, вам уже надоело читать код, поэтому ещё пару примеров и перейдём к ядру.
SetThreadInformation вызывает NtSetInformationThread и я не вижу смысла показывать дизассемблированный код,но вот пример anti-debug трюка:
Сам поток будет спрятан от дебаггера и у вас не получится дебажить программу.
Дальше будет объяснено в чём смысл данного трюка и принцип его работы в ядре.
Это выглядит ~ так:
Сейчас будет один мем, связанный с присоединением дебаггера.
При присоединении к процессу вызывается DbgUiDebugActiveProcess(если использовать DebugApi) :
и это вызывает
И наконец:
Но как добиться anti-debug эффекта?
Идея состоит в том, чтобы пропатчить DbgUiRemoteBreakin или DbgBreakPoint.
Например, это делает Themida/VMP.
Однако, разработчики x64dbg просто
Гениально и просто :D
Так же можно пройтись по процессорам и проверить записано ли в NtCurrentTeb()->DbgSsReserved[1] процессе объект отладки(частично забавный аналог ObjectTypesInformation).
Так же можно пройтись по всех HANDLE'ам и если процесс указывает имеет хэндл с правами R/W и присутствует DbgSsReserved[1] ,то,скорее всего,вас дебажут.
Теперь мы вернёмся к NtQueryInformationProcess:
Про ProcessDebugPort было уже сказано и мы вернёмся к нему скоро.
ProcessDebugObjectHandle на самом деле является доступ к DebugPort и мы дальше разберём что представляет из себя DebugPort на самом деле.
В случае отсутствие DebugPort'а функция вернёт STATUS_PORT_NOT_SET(если аргументы переданы правильно).
ProcessDebugFlag: - это на самом деле это небольшой забавный флаг, который вы можете установить/снять с помощью функции NtSetInformationProcess.
Сам пример обнаружения дебаггера:
Про NtQueryObject -
Эту функцию можно использовать для проверки присутствия DebugObject(ObjectTypesInformation) или существует ли у процессора DebugObject(ObjectTypeInformation) + можно проверить флаги у HANDLE'а(ObjectHandleFlagInformation).
На мой взгляд не имеет смысл проверять DebugObject т.к просто искать в системе отладчик - глупо.
Забавно, но все anti-anti-debug tool's до написания статьи
просто устанавливают значение на 0,если вызывается с ObjectTypesInformation, благодаря чему они могут быть легко пойманы.
Hello,ring0 via heroin!
Первоначально мы разберём, как работает NtCreateDebugObject и NtDebugActiveProcess т.к эти функции отвечают за создание объекта отладки и прикрепление объекта отладки к процессу.
Здесь идёт инициализация объекта отладки и после этого выделяется память для объекта.
Этот объекта будет использован в NtDebugActiveProcess(2 аргумент) для присоединения DebugPort'а к процессу.
Во-первых,вы не можете дебажить свой процесс или системный процесс,поэтому функция в данном случае вернёт STATUS_ACCESS_DENIED.
Во-вторых, вы не можете подключиться к защищённому процессу(proc->Protection) и у вас не получится открыть HANDLE к процессу(если вы пытаетесь открыть HANDLE,
да можно изменить права у дескриптора(например с PROCESS_QUERY_LIMITED_INFORMATION до PROCESS_ALL_ACCESS ),
но вот для подключения всё-равно нужно патчить снимать временно защиту).
В-третьих,у вас не получится дебажить процесс,если он работает под WoW64 и архитектура неожиданно станет неизвестной (можно подделать для x64 установив в proc->WoW64Process правильные значение)
и значение Machine не равно 452(IMAGE_FILE_MACHINE_ARMNT) или 332(IMAGE_FILE_MACHINE_I386).
Однако, если вы захотите попробовать данных трюк,чтобы добиться anti-debug эффекта,то есть побочный эффект - некоторые функции будут возвращать не тот результат.
В-четвёртых,если функция ObReferenceObjectByHandle закончится неудачно,то у вас так же не получится дебажить процесс(мы поговорим позже как можно добиться этого эффекта без хуков и триггера PG).
Вот мы и добрались до главной функции,которая устанавливает DebugPort.
Сюрприз-сюрприз!
DebugPort в ялре - это на самом деле PDEBUG_OBJECT.
Во-вторых, вы не можете подключить дебаггер к процессу, который уже дебажут/есть DebugPort.
Можно создать 2 процесс, который будет дебажить основной(вы можете это наблюдать данную технику в данном crackme )
На мой взгляд это самый мощный anti-debug из юзермода, поскольку можно сделать свою логику для дебаггера и для отладки такого приложения потребуется больше времени/усилий!
В-третьих, тут идёт установка в Flags флага NoDebugInherit(DebugFlag). Этот флаг можно перезаписать из UM(NtSetInformationProcess)
и некоторые anti-anti-debug plugin до сих пор не могут справиться с этим(печальный TitanHide и SharpOD)
Ага! Тут и идёт установка BeingDebugged в PEB.
Мы изучили 3 важных ntapi,поэтому вернёмся к анализу некоторых функций.
Погнали изучать другие моменты дальше!
Но как происходи отсоединение дебаггера?
Ну, привет NtRemoveProcessDebug.
и это вызывает
Данная функция просто убирает DebugPort с процесса и идёт работа с ивентами.
Забавно,но DebugFlag не изменяется в случае отсоединения.
Так же,если ProcessDebugObjectHandle вернул HANDLE,то можно просто убрать DebugPort с нашего процесса(с помощью NtRemoveProcessDebug),поэтому отладка просто будет невозможной.
Самые важные моменты изучили, поэтому идём изучать сами anti-debug трюки в ядре!
Ядро первоначально проверяет на запись буффер, перед тем как что-то начать с ним делать(вот пример с NtQueryInformationProcess):
Если передать неправильный адрес/не UM адрес , то функции вернёт STATUS_ACCESS_VIOLATION или STATUS_DATATYPE_MISALIGNMENT,если не выровнен адрес.
Во-первых,идёт проверка правильно ли передана длинна входного буфера.
Во-вторых, проверяется есть ли у HANDLE'а нужные права.
В-третьих, если существует DebugPort в PEPROCESS,то записывает в буфер -1.
Аналогично то же самое,что и с ProcessDebugPort почти,но идёт проверка флага в eprocess.
Если существует ProcessDebugFlags,то записывает в буфер 0,а в противном случае 1.
Теперь фаворит VMP:
Во-первых,она связанная с одним anti-debug трюком из VMP 3.1.x и выше.
Да, атака на ReturnLength,но как понимаю эта любимая функция автора VMP :D.
Во-вторых, вызывется функция DbgkOpenProcessDebugPort,которая записывает в буфер хэндл для работы с DebugPort в случае успеха.
При написании anti-debug плагина ни в коем случае нельзя вызывать оригинальную функцию с DbgkOpenProcessDebugPort т.к можно перезаписать ReturnLength,благодаря чему произойдёт утечка HANDLE'a
P.S почему же это фаворит VMP?
Ну,вот ответы:
1)
2)
3)и 3 детект будет в статье
Функция должна вернуть STATUS_PORT_NOT_SET,если отсутствует DebugPort + идёт проверка после этого на защищённый процесс.
В конце открывается доступ к DebugPort'у с правами MAXIMUM_ALLOWED и функция вернёт STATUS_SUCCESS в случае успеха ObOpenObjectByPointer.
Мы затронули ThreadHideFromDebugger,но как он работает на самом деле? Наш дебаггер взрывается или что с ним вообще происходит?
Так же не забудем о NtCreateThreadEx(с THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER и с THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE(начиная с windows 10 19H1) ):
Цепочка:
NtCreateThreadEx -> PspCreateThread -> PspMapThreadCreationFlags.
Именно в PspMapThreadCreationFlags идёт установка флагов и при создании можно сделать поток скрытым:
и в PspAllocateThread идёт установка в сам поток флагов.
THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE работает следующим образом:
Поток просто не будет приостановлен и это просто забавный трюк.
А теперь вернёмся к HideFromDebugger.
Установился флаг и установился! А дальше что?
Ну,тут ответ:DbgkForwardException
На самом деле проверяется бит ещё в следующих функциях:
Не будет доставлено уведомление через DbgkpSendApiMessage о создании/завершении потока или завершении процесса:DbgkExitThread,DbgkExitProcess,DbgkCreateThread.
Не будет доставлено уведомление через DbgkpSendApiMessage о мапе/анмапе секций:DbgkMapViewOfSection,DbgkUnMapViewOfSection.
Про это уже было сказано здесь.
Сама функция выглядит так~:
По названию понятно,что она связанна с исключениями.
Если поток скрыт и процесс дебажут,то исключение не будет передано на DebugPort при скрытом потоке,поэтому процесс рухнет.
На мой взгляд это мощный anti-debug трюк т.к сами Microsoft добавили данный трюк.
Правда,данный трюк легко обойти(особенно в ядре) т.к можно хукнуть саму функцию DbgkForwardException или просто очистить PS_CROSS_THREAD_FLAGS_HIDEFROMDBG у потока.
Теперь посмотрим NtClose:
На мой взгляд это альтернативный вариант проверки DebugPort т.к если закрыть invalid handle и
при этом вы дебажите процесс,то исключение для приложения будет выброшено и
в случае с защищённым handle'ом так же или если установлен системой FLG_ENABLE_CLOSE_EXCEPTIONS(NtGlobalFlag).
При закрытии handle'а (ObCloseHandleTableEntry) идёт проверка защищён ли он от закрытия:
Тут происходит вызов исключения,если процесс дебажут и HANDLE при этом защищён от закрытия/включена трассировка или установлен FLG_ENABLE_CLOSE_EXCEPTIONS в NtGlobalFlag .
Опять же, альтернатива проверки DebugPort,если не делать лишних действия.
Предлагаю вернуться к NtDuplicateObject.
в ObDuplicateObject вызывается NtClose,если передан флаг DUPLICATE_CLOSE_SOURCE:
Эту функцию можно использовать для замены NClose в качестве anti-debug трюка т.к с параметром DUPLICATE_CLOSE_SOURCE функция вызывает NtClose!
Благодаря этому можно достичь anti-debug эффекта!
Вернёмся к NtQueryObject.
Поскольку код не слишком изменился с Windows XP,то я возьму функцию из WRK и уберу некоторые моменты:
ObQueryTypeInfo просто переносит аргументы из ObjectType(POBJECT_TYPE) в ObjectTypeInfo(POBJECT_TYPE_INFORMATION):
Не вижу смысла описывать полностью т.к это исходный код windows и тут есть комментарии, поэтому перейдём к тому,что нас интересует!
Во-первых,для получения ObjectTypeInformation(количество объектов только для нашего процесса!) вызывается ObReferenceObjectByHandle и после этого конвертируется в POBJECT_TYPE.
Во-вторых,для получения ObjectTypesInformation мы получаем все объекты из глобальной переменной ObpObjectTypes,которая хранит информацию об объектах(POBJECT_TYPE )
и после вызывается ObQueryTypeInfo для передачи параметров в входной буфер.
В-третьих,можно проверить действительно ли установлен флаг защиты от закрытия HANDLE'а.
Теперь вернёмся к ObReferenceObjectByHandle via лечим инфекцию пилой.
Нет возможности дебажить - нет проблем для нас.
Если вы использовали SharpOD,то,возможно,видели возможность исправить какой-то ValidAccessMask.
Но что из себя представляет ValidAccessMask?
Ну,вот ответ:
На самом деле это просто переменная в глобальной структуре DbgkDebugObjectType с правами DEBUG_ALL_ACCESS.
Сам anti-debug трюк заключается в перезаписи ValidAccessMask на NULL т.к после этого некоторые функции связанные с отладчиком будут возвращать STATUS_ACCESS_DENIED/STATUS_NOT_SUPPORTED
На мой взгляд это сильный трюк,поскольку если вы не хотите,чтобы присутствовал отладчик в системе,то вы попросто можете сломать попытку присоединения/создания процесса с DebugPort.
Однако,если дебаггер уже подключился к процессу,то данный трюк бесполезен т.к объект отладчи будет создан и дебаггер будет уже подключен к процессу.
Вот небольшая цепочка, обозначающая неудачную попытку создать процесс/присеодиниться к процессу,если ValidAccessMask = 0.
(Create process)NtCreateProcess with DebugPort -> PspCreateProcess -> ObReferenceObjectByHandle with DEBUG_PROCESS_ASSIGN ->SeComputeDeniedAccesses -> STATUS_ACCESS_DENIED
(Attach to process)NtDebugActiveProcess ->( NT_SUCCESS return FALSE)ObReferenceObjectByHandle with DEBUG_OBJECT_ADD_REMOVE_PROCESS ->SeComputeDeniedAccesses -> STATUS_ACCESS_DENIED -> NT_SUCCESS(ObReferenceObjectByHandle) -> STATUS_NOT_SUPPORTED
Баста! Но я хочу ломать вообще все дебаггеры!(включая дебаггеры,которые уже работают)
Я босс системы и никто не имеет права ограничивать меня!
Ну, тогда скажите привет DbgkpProcessDebugPortMutex.
Вы уже видели это:
DbgkpProcessDebugPortMutex.Count = 1; (in DbgkpInitializePhase0)
DbgkpProcessDebugPortMutex используется в этих функциях:
DbgkpSetProcessDebugObject/DbgkCopyProcessDebugPort/DbgkpQueueMessage/DbgkpCloseObject/DbgkpMarkProcessPeb -> KeReleaseGuardedMutex/ExAcquireFastMutex -> ExpReleaseFastMutexContended
В ExAcquireFastMutex вызывается ExpAcquireFastMutexContended.
По идее,именно там и происходит бесконечный цикл,если переписать DbgkpProcessDebugPortMutex->Count на 0.
Вы не сможете дебажить после присоединения/начала дебага процесса,поскольку ваш отладчик тупо зависнет!
На мой взгляд это самый эффективный anti-debug трюк на данный момнт,который не тригерет при этом PG и позволяет затроллить реверсера
(представьте,что вы присоединились к лоадеру и ваш дебаггер просто ничего не может сделать,вы его даже не можете убить...).
Да, можно переписать данное значение на первоначальное, но если его будут лениво проверять через какое-то время - вы проиграли т.к DbgkpProcessDebugPortMutex используется во многих местах во время дебаггинга.
-Протекторы
Теперь перейдём к протекторам.
Первым будет Themida.
Themida/WinLicense:
У меня нет лицензии, поэтому я воспользуюсь взломанной версией(3.0.4.0)
x64
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtGetContextThread
NtSetInformationThread(ThreadHideFromDebugger)
x32
NtSetContextThread/NtGetContextThread
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetInformationThread(ThreadHideFromDebugger)
Так же они хукают DbgUiRemoteBreakin & DbgBreakPoint:
hook DbgUiRemoteBreakin -> LdrShutdownProcess
hook DbgBreakPoint -> ret
Так же поиск окон,но это мем и они даже не пытаются обойти в x32 банальный хук Wow64Transition.
Короче, Темида разочаровала сильно автора.
Теперь перейдём к моему фавориту т.е VMP:
На данный момент VMP( версия 3.6. build 1406) использует следующие трюки:
x32:
Manual read BeingDebugged in PEB
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetContextThread
NtSetInformationThread(ThreadHideFromDebugger)
CloseHandle(0xDEADC0DE) (not manual syscall)
NtSetInformationProcess with NULL (ProcessInstrumentationCallback) (not manual syscall)
ZwQuerySystemInformation with SystemKernelDebuggerInformation (Detect KM Debugger and not manual syscall)
ZwQuerySystemInformation with SystemModuleInformation (Detect KM Debugger and not manual syscall)
x64
Manual read BeingDebugged in PEB
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetInformationThread:ThreadHideFromDebugger
CloseHandle(0xDEADC0DE) (not manual syscall)
NtSetInformationProcess with NULL (ProcessInstrumentationCallback) (not manual syscall)
ZwQuerySystemInformation with SystemKernelDebuggerInformation (Detect KM Debugger and not manual syscall)
ZwQuerySystemInformation with SystemModuleInformation (Detect KM Debugger and not manual syscall)
Так же VMP уходит в manual syscall/sysentry(x32 system),чтобы обойти банальные хуки в ntdll начиная с версии 3.1.x.(если идёт поддерживаемая сборка т.к номера syscall'ов могут поменяться, иначе идёт вызов NTAPI)
Забавно,но для проверки CRC файла используется manual syscall NtClose,в то время для обнаружения отладчика просто используется CloseHandle.
Так же,если использовать anti-debug в GUI версии т.е встроенный в лоадер кода,то там будут идти проверки на CRC syscall(даже,если отключена опция защита памяти),
в то время как при использовании SDK функций такого нет.
Перед тем как скажу про heaven's gate,уточню один момент для новичков.
Для выполнения x32 кода в x64 системе был предуман WoW64.
Одна из многих целей - нормально выполнять syscall'ы и другие моменты в x32 коде.
В x64 системе у вас нет инструкции sysentry для выполнения+
у вас отсутствуют прямой доступ к регистрам r8/r9/r10 в x32 коде и другие моменты,хотя у вас есть syscall для которого эти параметры необходимы.
VMP в x32 code работающий под Wow64 смешивается с x64 кодом(heaven's gate):
jmp far 33:address т.е прыжок в x64 code.
Это делает забавный трюк,поскольку вы не можете нормально дебажить x64 code под WoW64 в x32 дебаггере.
Вот минимальный пример c выполнением x64 code под WoW64 с прямым syscall NtSetInformationThread(HideFromDebugger):
Так же идёт вызов одношагового исключения, чтобы проверить присутствие отладчика + идёт проверка на наличие HWBP.
Так же они хукают DbgUiRemoteBreakin и делают ~ следующие:
Так же VMP пытается злоупотреблять логикой ядра,чтобы обнаружить неправильную логику хуков у anti-anti-debug tool.
На данный момент это :
1)перезапись ProcessInformation с помощью ReturnLength(адрес у ProcessInformation и ReturnLength одинаковый).
Пример детекта:
2)Неправильный указатель.
Начиная с версии 3.5.1.x автор писал,что он добавил
Они отправляют неправильный указатель в ReturnLength(1),но в TitanHide нет проверки на входный буффер с помощью ProbeForRead /ProbeForWrite перед записью буффера.
~детект выглядит как-то так
Небольшой бонус для новичков ;) :
Dark_Bull писал 2 года назад обход anti-debug'a в VMP 3.5.1213.
Разработчики VMP спустя столько времени не исправили главную ошибку - они не шифруют syscall.
Да, они изменили вход в VM,но никто не запрещает искать по паттерну syscall: 0F 05( в x64 системе)
Если вы используете anti-debug от лоадера, то syscall будет располагаться в самой последней секции VMP,а syscall для SDK функции в самой первой секции VMP.
Да,если включена опция упаковки,то первая секция будет зашифрована,но посмотрите просто как работает распаковка.
Вы можете легко найти syscall и подделать результат(HWBP + немного ловкости рук и у вас всё легко получится).
Вот информация для новичков и RVA/паттерн anti-debug'a + файл(в конце статьи):
1)Для NtQueryInformationProcess вы можете подделать NTSTATUS на отрицательный(например на -1),после выполнения syscall'a(тесты показали,что VMP проверяет NTSTATUS так:NT_SUCCESS(nt_status),а не например nt_status != STATUS_PORT_NOT_SET)
2)Для NtSetInformationThread вы должны подделать класс/номер syscall'a до его выполнения,иначе поток будет спрятан.
3)Для CloseHandle вы должны вернуть именно STATUS_INVALID_HANDLE(если измените класс/номер syscall'a вручную),иначе будет обнаружения дебаггера.
Просто попробуйте и вы поймёте,что это не так сложно + автор добавил RVA/паттерн для файла, чтобы новичкам было легче:
- Идея VMP
Общаясь с Пермяковым, мне в одно время пришла идея:"А в чём проблема просто обнаружить все anti-anti-debug-tool?".
Ну, я написал
Я не буду разбирать все баги, поскольку нет смысла в этом.
Приведу 2 примера с описанием,а дальше все зависит от вас :D.
Начнём с SharpOD/SchyllaHide и хука NtSetInformationThread:
Если вы подумаете слегка, то увидите минимум 2 ошибки:
Во-первых, идёт проверка является ли HANDLE нашим процессом,
но никто не запрещает временно перезаписать UniqueProcess в TEB,чтобы скрыть поток или можно заставить вернуть STATUS_SUCCESS,
передав в качестве HANDL'а NULL и временно перезаписать UniqueProcess в TEB на NULL
Во-вторых, можно открыть потом(OpenThread) без прав THREAD_QUERY_INFORMATION ,
благодаря чему проверка с помощью GetProcessIdByThreadHandle(NtQueryInformationThread с ThreadBasicInformation) благополучно обосрётся.
Пример:
Дальше мем с которым не могли долгое время справиться почти все anti-anti-debug tool's и речь про NtClose.
Обычно функция перехватывается и просто возвращается ожидаемый NTSTATUS(STATUS_INVALID_HANDLE/STATUS_HANDLE_NOT_CLOSABLE) и не вызывали ошибку.
Однако, ядро вызовет KeRaiseUserException(STATUS_INVALID_HANDLE)/KeRaiseUserException(STATUS_HANDLE_NOT_CLOSABLE) не только при присутствии DebugPort у EPROCESS,но и если
в NtGlobalFlag установлен флаг FLG_ENABLE_CLOSE_EXCEPTIONS или включена трассировка HANDLE'ов.
Вы не можете установить FLG_ENABLE_CLOSE_EXCEPTIONS используя NtSetSystemInformation,поскольку система очищает некоторые флаги,
но можно включить трассировку с помощью ProcessHandleTracing(NtSetInformationProcess ) и
благодаря этому будет вызываться исключение, если закрыть невалидный/защищенный HANDLE!
Сам пример:
Дальше я скажу, про что мне лень было говорить:
1)На самом деле можно дебажить процесс без создания DebugPort'а,но вам придётся похукать много функций, но Китайцы сделали мемный
2)Для людей,которые не знают как работают Heaven’s Gate,рекомендую прочитать:
3)Я щё не изучил как работает anti-debug VMP в x32 системе полностью, но если окажется что-то неправильно в этой статье, то исправлю это в ближайшее время ;) .
Ну,вот и всё.
Данная забавная статья подошла к концу.
Надеюсь, вам было интересно или хотя бы весело :)
Огромное спасибо colby57 за опыт.
Здесь будет рассмотрены популярные anti-debug трюки,как они работают в ядре и в добавок посмотрим фишки протекторов(VMP,Themida/WinLicense ).
Для наглядности здесь будут сами примеры anti-отладки.
Детекты через FindWindows/поиск процесса - в данной статье не будут разбираться.
Так же в конце будут файлы, чтобы вы могли поиграться с ними, если захотите.
Отказ от ответственности.
Данная статья публикуется только в образовательных целях.
Автор не пытается никого оскорбить, а только лишь хочет поднять некоторые моменты, чтобы новичкам их было легче понять.
Заранее извиняюсь, что беру псевдокод из иды, но постараюсь объяснить некоторые моменты, чтобы вам было более понятно.
Оглавление:
-UM
-kernel32/kernelbase + примеры
-ntdll + примеры
-KM
-как начинается отладка
-как происходи отсоединение дебаггера
-анализ как работают трюки в KM
-Маленький анализ протекторов(VMP/Themida)
-Печальная темида
-VMP
-Идея автора VMP и небольшой пример обхода anti-debug VMP(x64).
- Начало анализа
C++:
BOOL __stdcall IsDebuggerPresent()
{
return NtCurrentPeb()->BeingDebugged;
}
Мы просто стучим в PEB и проверяем BeingDebugged(смещение - 0x2).
Здесь ничего интересного, поскольку можно просто перезаписать значение на 0.
Дальше будет объяснено, откуда появляется запись в BeingDebugged при начале отладки.
P.S для новичков.
Для получения адреса к PEB вы можете вызвать:
__readgsqword(0x60)(x64)/__readfsdword(0x30)(x32)
C++:
BOOL __stdcall CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent)
{
WINBOOL IsDebugPortExist; // ebx
NTSTATUS nt_status; // eax
BOOL result; // eax
__int64 IsDebugPort; // [rsp+40h] [rbp+8h] BYREF ( __int32 if x32 and work under wow64)
IsDebugPortExist = FALSE;
if ( hProcess && pbDebuggerPresent )
{
nt_status = NtQueryInformationProcess(hProcess, ProcessDebugPort, &IsDebugPort, sizeof(IsDebugPort), (PULONG)FALSE);
if (NT_SUCCESS(nt_status))
{
result = TRUE;
LOBYTE(IsDebugPortExist) = IsDebugPort != FALSE;
*pbDebuggerPresent = IsDebugPortExist;
return result;
}
BaseSetLastNTError(nt_status);
}
else
{
RtlSetLastWin32Error(ERROR_INVALID_PARAMETER);
}
return FALSE;
}
Здесь идёт вызов NtQueryInformationProcess с ProcessDebugPort.
Отладчик просто так не может работать DebugPort'а(далее будет объяснено почему).
Этот WinApi используют часто,поскольку он задокументирован(хотя лучше вызывать NTAPI напрямую).
C++:
BOOL __stdcall CloseHandle(HANDLE hObject)
{
HANDLE hObject_1; // rbx
void (*best_function)(void); // rax
NTSTATUS nt_status; // eax
void *phPrevValue; // [rsp+30h] [rbp+8h] BYREF
hObject_1 = hObject;
if ( (unsigned int)hObject >= 0xFFFFFFF4
&& (unsigned int)hObject <= 0xFFFFFFF6
&& SetStdHandleEx((DWORD)hObject, NULL, &phPrevValue) )
{
hObject_1 = phPrevValue;
}
best_function = (void (*)(void))SbSelectProcedure(0xABABABABi64, TRUE, "kLsE");
if ( best_function )
best_function();
nt_status = NtClose(hObject_1);
if (NT_SUCCESS(nt_status))
return TRUE;
BaseSetLastNTError((unsigned int)nt_status);
return FALSE;
}
CloseHandle вызывает NtClose и если закрыть невалидный HANDLE,то ничего не произойдёт, но вот в случае дебагинга процесса будет вызвано исключение!
(ну или если применить один трюк, но об этом позже :D ).
Маленький пример обнаружения дебаггера:
C++:
auto is_close_invalid_handle() -> bool
{
__try
{
CloseHandle((HANDLE)0xDEADBEEF);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return TRUE;
}
return FALSE;
}
Однако можно сделать дескриптор защищённым и если попытаться закрыть HANDLE под дебаггером,то будет вызвано исключение.
Давайте посмотрим, код происходит вызов SetHandleInformation.
C++:
BOOL __stdcall SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags)
{
char dwFlags_2; // si
char dwMask_2; // bp
HANDLE hObject_2; // rbx
NTSTATUS nt_status; // eax
BOOL result; // edi
OBJECT_HANDLE_FLAG_INFORMATION ObjectInformation; // [rsp+40h] [rbp+8h] BYREF
dwFlags_2 = dwFlags;
dwMask_2 = dwMask;
hObject_2 = hObject;
switch ( (_DWORD)hObject )
{
case 0xFFFFFFF4:
hObject_2 = NtCurrentPeb()->ProcessParameters->StandardError;
break;
case 0xFFFFFFF5:
hObject_2 = NtCurrentPeb()->ProcessParameters->StandardOutput;
break;
case 0xFFFFFFF6:
hObject_2 = NtCurrentPeb()->ProcessParameters->StandardInput;
break;
}
nt_status = NtQueryObject(hObject_2, ObjectHandleFlagInformation, &ObjectInformation, sizeof(OBJECT_HANDLE_FLAG_INFORMATION ), NULL);
if (!NT_SUCCESS(nt_status))
goto BAD_STATUS;
result = TRUE;
if ( (dwMask_2 & HANDLE_FLAG_INHERIT) != NULL )
ObjectInformation.Inherit = dwFlags_2 & HANDLE_FLAG_INHERIT;
if ( (dwMask_2 & HANDLE_FLAG_PROTECT_FROM_CLOSE) != NULL )
ObjectInformation.ProtectFromClose = (dwFlags_2 & HANDLE_FLAG_PROTECT_FROM_CLOSE) != NULL;
nt_status = NtSetInformationObject(hObject_2, ObjectHandleFlagInformation, &ObjectInformation, sizeof(OBJECT_HANDLE_FLAG_INFORMATION ));
if ( !NT_SUCCESS(nt_status))
{
BAD_STATUS:
BaseSetLastNTError((unsigned int)nt_status);
result = FALSE;
}
return result;
}
Можно установить у дескриптора HANDLE_FLAG_PROTECT_FROM_CLOSE и при закрытие HANDLE'а система выбросит исключение из-за присутствия отладчика или трюка.
Потом мы посмотрим, как это работает в ядре.
Пример детекта:
C++:
auto is_close_protected_handle() -> bool
{
auto thread_handle = CreateThread(NULL, NULL, NULL, NULL, CREATE_SUSPENDED, NULL);
if (!SetHandleInformation(thread_handle, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE) || !thread_handle)
return FALSE;
__try
{
CloseHandle(thread_handle);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return TRUE;
}
return FALSE;
}
Второй вариант - DuplicateHandle т.к внутри себя он вызывает NtClose,если передать аргумент - DUPLICATE_CLOSE_SOURCE.
Об этом мы поговорим позже.
По факту - это попытка обойти просто хук NtClose.
C++:
BOOL __stdcall DuplicateHandle(HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions)
{
NTSTATUS nt_status; // eax
switch ( (_DWORD)hSourceHandle )
{
case 0xFFFFFFF4:
hSourceHandle = NtCurrentPeb()->ProcessParameters->StandardError;
break;
case 0xFFFFFFF5:
hSourceHandle = NtCurrentPeb()->ProcessParameters->StandardOutput;
break;
case 0xFFFFFFF6:
hSourceHandle = NtCurrentPeb()->ProcessParameters->StandardInput;
break;
}
nt_status = NtDuplicateObject(
hSourceProcessHandle,
hSourceHandle,
hTargetProcessHandle,
lpTargetHandle,
dwDesiredAccess,
bInheritHandle ? 2 : NULL,
dwOptions);
if (NT_SUCCESS(nt_status))
return TRUE;
BaseSetLastNTError(nt_status);
return FALSE;
}
Сам пример:
C++:
auto is_close_protect_handle_ex() -> bool
{
HANDLE dublicate_handle = NULL;
__try
{
DuplicateHandle(NtCurrentProcess, NtCurrentProcess, NtCurrentProcess, &dublicate_handle, NULL, FALSE, NULL);
SetHandleInformation(dublicate_handle, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
DuplicateHandle(NtCurrentProcess, dublicate_handle, NtCurrentProcess, &dublicate_handle, NULL, FALSE, DUPLICATE_CLOSE_SOURCE);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return TRUE;
}
return FALSE;
}
Сейчас пойдут мемы связанные с OutputDebugString:
В Windows 7~ всё так:
C++:
VOID __stdcall OutputDebugStringW(LPCWSTR lpOutputString)
{
NTSTATUS nt_status; // ebx
const CHAR *output_string; // rcx
struct STRING AnsiString; // [rsp+20h] [rbp-28h] BYREF
_UNICODE_STRING DestinationString; // [rsp+30h] [rbp-18h] BYREF
AnsiString.MaximumLength = NULL;
AnsiString.Buffer = 0;
AnsiString.Length = 0;
nt_status = RtlInitUnicodeStringEx(&DestinationString, lpOutputString);
if ( !NT_SUCCESS(nt_status)|| (nt_status = RtlUnicodeStringToAnsiString(&AnsiString, &DestinationString, TRUE), !NT_SUCCESS(nt_status)) )
{
output_string = OutputString;//Global variable
AnsiString.Buffer = (PCHAR)OutputString;
}
else
{
output_string = AnsiString.Buffer;
}
OutputDebugStringA(output_string);
if (NT_SUCCESS(nt_status))
RtlFreeAnsiString(&AnsiString);
}
и это вызывает ->
C++:
/*
typedef struct _DBG_PRINTEXCEPTION_C_INFO
{
ULONG max_lenght_str;
WCHAR* buffer;
} DBG_PRINTEXCEPTION_C_INFO, * PDBG_PRINTEXCEPTION_C_INFO;
*/
void __stdcall OutputDebugStringA(LPCSTR lpOutputString)
{
const char *lpOutputString_1; // rdx
DBG_PRINTEXCEPTION_C_INFO dbg_print_excepthion_info;
lpOutputString_1 = lpOutputString;
if ( !lpOutputString )
lpOutputString_1 = OutputString;
dbg_print_excepthion_info.max_lenght_str = strlen(lpOutputString_1) + 1;
dbg_print_excepthion_info.buffer = lpOutputString_1;
RaiseException(DBG_PRINTEXCEPTION_C, 0, 2u, (ULONG_PTR)dbg_print_excepthion_info);
}
Как по мне это самый худший anti-debug трюк, который существует на сегодняшний день.
Причина в том, что этот трюк работает ниже windows vista и попросту бесполезен на сегодняшний день.
Забавно, но с помощью него можно попытаться крашнуть OllyDbg ( версию 1.1 и ниже).
C++:
OutputDebugStringW(L"%s")
Сюрприз-сюрприз!
В Windows 10 реализовали вызов исключения с LPCWSTR!(Unicode)
C++:
VOID __stdcall OutputDebugStringW(LPCWSTR lpOutputString)
{
/*
typedef struct _DBG_PRINTEXCEPTION_WIDE_INFO_WIN_10
{
ULONG max_lenght_str;
WCHAR* buffer_1;
ULONG max_lenght_str_2;
WCHAR* buffer_2;
} DBG_PRINTEXCEPTION_WIDE_INFO_WIN_10, * PDBG_PRINTEXCEPTION_WIDE_INFO_WIN_10;
*/
const WCHAR *buffer; // rsi
NTSTATUS nt_status; // edi
__int64 lenght_str_2; // rax
__int64 lenght_str; // rcx
_STRING DestinationString; // [rsp+20h] [rbp-48h] BYREF
UNICODE_STRING SourceString; // [rsp+30h] [rbp-38h] BYREF
DBG_PRINTEXCEPTION_WIDE_INFO_WIN_10 print_exception_info; // [rsp+40h] [rbp-28h] BYREF
DestinationString = NULL;
buffer = &word_1801C45C0;
if ( lpOutputString )
buffer = lpOutputString;
nt_status = RtlInitUnicodeStringEx(&SourceString, buffer);
if ( NT_SUCCESS(nt_status) )
nt_status = RtlUnicodeStringToAnsiString(&DestinationString, &SourceString, 1u);
if ( !NT_SUCCESS(nt_status) )
RtlInitAnsiString(&DestinationString, pszBase);
lenght_str_2 = -1i64;
lenght_str = -1i64;
do
++lenght_str;
while ( buffer[lenght_str] );
print_exception_info.max_lenght_str = lenght_str + 1;
print_exception_info.buffer = buffer;
do
++lenght_str_2;
while ( DestinationString.Buffer[lenght_str_2] );
print_exception_info.max_lenght_str_2 = lenght_str_2 + 1;
print_exception_info.buffer_2 = DestinationString.Buffer;
RaiseException(DBG_PRINTEXCEPTION_WIDE_C, 0, 4u, (ULONG_PTR)print_exception_info);
if (NT_SUCCESS(nt_status) )
RtlFreeAnsiString(&DestinationString);
}
Цепочка:RtlRaiseException -> ZwRaiseException & RtlpCaptureContext2(&ContextRecord);
Можно вызывать исключение и проверять какое исключение было вызвано, но как по мне этот трюк бесполезен.
Наверное, вам уже надоело читать код, поэтому ещё пару примеров и перейдём к ядру.
SetThreadInformation вызывает NtSetInformationThread и я не вижу смысла показывать дизассемблированный код,но вот пример anti-debug трюка:
C++:
auto is_hide_thread() -> bool
{
NT_SUCCESS(NtSetInformationThread(NtCurrentThread, ThreadHideFromDebugger, NULL, NULL));
}
Сам поток будет спрятан от дебаггера и у вас не получится дебажить программу.
Дальше будет объяснено в чём смысл данного трюка и принцип его работы в ядре.
Это выглядит ~ так:
Сейчас будет один мем, связанный с присоединением дебаггера.
При присоединении к процессу вызывается DbgUiDebugActiveProcess(если использовать DebugApi) :
C++:
NTSTATUS __fastcall DbgUiDebugActiveProcess(HANDLE proc)
{
NTSTATUS nt_status; // ebx
nt_status = NtDebugActiveProcess(proc, NtCurrentTeb()->DbgSsReserved[1]);
if (NT_SUCCESS(nt_status))
{
nt_status = DbgUiIssueRemoteBreakin(proc);
if (!NT_SUCCESS(nt_status) )
ZwRemoveProcessDebug(proc, NtCurrentTeb()->DbgSsReserved[1]);
}
return nt_status;
}
и это вызывает
C++:
NTSTATUS __fastcall DbgUiIssueRemoteBreakin(HANDLE proc)
{
int nt_status; // ebx
void *v3; // [rsp+30h] [rbp-48h]
_CLIENT_ID Handle; // [rsp+88h] [rbp+10h] BYREF
nt_status = RtlpCreateUserThreadEx(proc, NULL, 2u, NULL, NULL, 0x4000ui64, v3, DbgUiRemoteBreakin, NULL, &Handle);
if (NT_SUCCESS(nt_status))
NtClose(Handle.UniqueProcess);
return nt_status;
}
И наконец:
C++:
VOID __noreturn DbgUiRemoteBreakin()
{
if ( (NtCurrentPeb()->BeingDebugged || (KUSER_SHARED_DATA->KdDebuggerEnabled & KdDebuggerNotPresent) != NULL) && (NtCurrentTeb()->SomeTebFlags & DbgRanProcessInit) == NULL )//0x2 - KdDebuggerNotPresent ,0x20 - DbgRanProcessInit
{
if ( UseWOW64 )
{
if ( g_LdrpWow64PrepareForDebuggerAttach )
g_LdrpWow64PrepareForDebuggerAttach();
}
DbgBreakPoint();
}
RtlExitUserThread(NULL);
}
Но как добиться anti-debug эффекта?
Идея состоит в том, чтобы пропатчить DbgUiRemoteBreakin или DbgBreakPoint.
Например, это делает Themida/VMP.
Однако, разработчики x64dbg просто
Пожалуйста, авторизуйтесь для просмотра ссылки.
,чтобы обойти данный трюк.Гениально и просто :D
Так же можно пройтись по процессорам и проверить записано ли в NtCurrentTeb()->DbgSsReserved[1] процессе объект отладки(частично забавный аналог ObjectTypesInformation).
Так же можно пройтись по всех HANDLE'ам и если процесс указывает имеет хэндл с правами R/W и присутствует DbgSsReserved[1] ,то,скорее всего,вас дебажут.
Теперь мы вернёмся к NtQueryInformationProcess:
Про ProcessDebugPort было уже сказано и мы вернёмся к нему скоро.
ProcessDebugObjectHandle на самом деле является доступ к DebugPort и мы дальше разберём что представляет из себя DebugPort на самом деле.
В случае отсутствие DebugPort'а функция вернёт STATUS_PORT_NOT_SET(если аргументы переданы правильно).
C++:
auto is_debug_object_present() -> bool
{
HANDLE debug_object = NULL;
auto nt_status = NtQueryInformationProcess(NtCurrentProcess, ProcessDebugObjectHandle, &debug_object, sizeof(debug_object), NULL);
return debug_object != NULL || nt_status != STATUS_PORT_NOT_SET;
}
ProcessDebugFlag: - это на самом деле это небольшой забавный флаг, который вы можете установить/снять с помощью функции NtSetInformationProcess.
Сам пример обнаружения дебаггера:
C++:
auto is_debug_flag_present() -> bool
{
ULONG debug_flag = NULL;
if(NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess, ProcessDebugFlags, &debug_flag, sizeof (debug_flag), NULL )))
return debug_flag == NULL;
return FALSE;
}
Про NtQueryObject -
Эту функцию можно использовать для проверки присутствия DebugObject(ObjectTypesInformation) или существует ли у процессора DebugObject(ObjectTypeInformation) + можно проверить флаги у HANDLE'а(ObjectHandleFlagInformation).
На мой взгляд не имеет смысл проверять DebugObject т.к просто искать в системе отладчик - глупо.
Забавно, но все anti-anti-debug tool's до написания статьи
просто устанавливают значение на 0,если вызывается с ObjectTypesInformation, благодаря чему они могут быть легко пойманы.
Hello,ring0 via heroin!
Первоначально мы разберём, как работает NtCreateDebugObject и NtDebugActiveProcess т.к эти функции отвечают за создание объекта отладки и прикрепление объекта отладки к процессу.
C++:
NTSTATUS __fastcall NtCreateDebugObject(PHANDLE DebugObjectHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, ULONG Flags)
{
char Flags_1; // si
unsigned __int8 PreviousMode; // r10
__int64 v8; // rcx
NTSTATUS nt_status; // rax
PDEBUG_OBJECT DebugObject_1; // rbx
_EWOW64PROCESS *WoW64Process; // rax
unsigned __int16 Machine; // ax
__int64 unk; // [rsp+20h] [rbp-68h]
PDEBUG_OBJECT DebugObject; // [rsp+58h] [rbp-30h] BYREF
HANDLE Handle; // [rsp+60h] [rbp-28h] OVERLAPPED BYREF
Flags_1 = Flags;
Handle = NULL;
DebugObject = NULL;
PreviousMode = KeGetCurrentThread()->PreviousMode;
if ( PreviousMode != KernelMode )
{
__try
{
ProbeForWrite(DebugObjectHandle, 4, 1);
}
__except (ExSystemExceptionFilter ())
{
return GetExceptionCode ();
}
}
*DebugObjectHandle = NULL;
if ( (Flags & ~DEBUG_KILL_ON_CLOSE) != 0 ) // & ~DEBUG_KILL_ON_CLOSE
return STATUS_INVALID_PARAMETER;
nt_status = ObCreateObjectEx(
PreviousMode,
DbgkDebugObjectType,
ObjectAttributes,
PreviousMode,
unk,
sizeof(unk),
NULL,
NULL,
&DebugObject,
NULL);
if (NT_SUCCESS(nt_status) )
{
DebugObject_1 = DebugObject;
DebugObject->Mutex.Count = 1;
DebugObject_1->Mutex.Owner = NULL;
DebugObject_1->Mutex.Contention = NULL;
KeInitializeEvent(&DebugObject_1->Mutex.Event, SynchronizationEvent, 0);
DebugObject_1->EventList.Blink = &DebugObject_1->EventList;
DebugObject_1->EventList.Flink =1 &DebugObject_1->EventList;
KeInitializeEvent(&DebugObject_1->EventsPresent, NotificationEvent, 0);
if ( (Flags_1 & DEBUG_OBJECT_KILL_ON_CLOSE ) == NULL ) // DEBUG_OBJECT_KILL_ON_CLOSE
DebugObject_1->Flags = NULL;
else
DebugObject_1->Flags = DEBUG_OBJECT_KILL_ON_CLOSE; // DEBUG_OBJECT_KILL_ON_CLOSE
WoW64Process = PsGetCurrentProcess()->WoW64Process
if ( WoW64Process )
{
Machine = WoW64Process->Machine;
if ( Machine == IMAGE_FILE_MACHINE_I386 || Machine == IMAGE_FILE_MACHINE_ARMNT )
DebugObject_1->Flags |= 4u;
}
nt_status = ObInsertObjectEx(DebugObject, NULL, DesiredAccess, NULL, NULL, NULL, &Handle);
if (NT_SUCCESS(nt_status) )
{
*DebugObjectHandle = Handle;
}
}
return nt_status;
}
Здесь идёт инициализация объекта отладки и после этого выделяется память для объекта.
Этот объекта будет использован в NtDebugActiveProcess(2 аргумент) для присоединения DebugPort'а к процессу.
C++:
NTSTATUS __fastcall NtDebugActiveProcess(HANDLE ProcessHandle, HANDLE DebugObjectHandle)
{
char AccessMode; // bp
NTSTATUS nt_status_1; // eax
__int64 AccessMode_2; // rcx
struct _KTHREAD *v6; // rax
struct _EX_RUNDOWN_REF *proc_1; // rdi
_KPROCESS *current_thread; // rsi
int nt_status; // ebx
_EWOW64PROCESS IsWoW64; // rax
__int16 v11; // cx
unsigned __int64 v12; // rax
__int16 v13; // cx
BOOLEAN IsPortectOk; // al
struct _KEVENT *DebugObject; // rsi
int nt_status_2; // eax
int LastThread; // [rsp+40h] [rbp-28h] OVERLAPPED BYREF
PVOID proc; // [rsp+80h] [rbp+18h] BYREF
PVOID proc_2; // [rsp+88h] [rbp+20h] BYREF
proc = NULL;
AccessMode = KeGetCurrentThread()->PreviousMode;
nt_status_1 = ObReferenceObjectByHandleWithTag(
ProcessHandle,
PROCESS_SET_PORT,
(POBJECT_TYPE)PsProcessType,
AccessMode,
'OgbD',
&proc,
NULL);
if ( NT_SUCCESS(nt_status )
{
current_thread = KeGetCurrentThread();
proc_1 = proc;
current_thread = current_thread->ApcState.Process; // PsGetCurrentProcess()
if ( proc == current_thread || proc == PsInitialSystemProcess ) //Try self-debugging or debug system process
{
nt_status = STATUS_ACCESS_DENIED;
}
else
{
LOBYTE(AccessMode_2) = AccessMode;
if ( PsTestProtectedProcessIncompatibility(AccessMode_2, PsGetCurrentProcess()->ApcState.Process, proc) )
{
nt_status = STATUS_PROCESS_IS_PROTECTED;
}
else if ( (proc_1.SecureHandle & 1) == 0 || (nt_status = PsRequestDebugSecureProcess(proc_1, 1u), NT_SUCCESS(nt_status)) )
{
IsWoW64 = current_thread.WoW64Process;
if ( !IsWoW64
|| (Machine = IsWoW64.Machine, Machine != IMAGE_FILE_MACHINE_I386 ) && Machine != IMAGE_FILE_MACHINE_ARMNT
|| (IsWoW64 != 0 ) && ((Machine_2 = IsWoW64.Machine, Machine_2 == IMAGE_FILE_MACHINE_I386 ) || Machine_2 == IMAGE_FILE_MACHINE_ARMNT) )
{
proc_2 = NULL;
nt_status = ObReferenceObjectByHandle(
DebugObjectHandle,
DEBUG_OBJECT_ADD_REMOVE_PROCESS ,
DbgkDebugObjectType,
AccessMode,
&proc_2,
NULL);
if (NT_SUCCESS(nt_status))
{
IsPortectOk = ExAcquireRundownProtection_0(proc_1->RundownProtect);
DebugObject = proc_2;
if ( IsPortectOk )
{
nt_status_2 = DbgkpPostFakeProcessCreateMessages(proc_1,proc_2,&LastThread);
nt_status = DbgkpSetProcessDebugObject(proc_1, DebugObject, nt_status_2, LastThread);
ExReleaseRundownProtection_0(proc_1 + 0x8B);
}
else
{
nt_status = STATUS_PROCESS_IS_TERMINATING;
}
ObfDereferenceObjectWithTag(DebugObject, 'tlfD');
}
}
else
{
nt_status = STATUS_NOT_SUPPORTED;
}
}
}
ObfDereferenceObjectWithTag(proc_1, 'OgbD');
nt_status_1 = nt_status;
}
return nt_status_1;
}
Во-первых,вы не можете дебажить свой процесс или системный процесс,поэтому функция в данном случае вернёт STATUS_ACCESS_DENIED.
Во-вторых, вы не можете подключиться к защищённому процессу(proc->Protection) и у вас не получится открыть HANDLE к процессу(если вы пытаетесь открыть HANDLE,
да можно изменить права у дескриптора(например с PROCESS_QUERY_LIMITED_INFORMATION до PROCESS_ALL_ACCESS ),
но вот для подключения всё-равно нужно патчить снимать временно защиту).
В-третьих,у вас не получится дебажить процесс,если он работает под WoW64 и архитектура неожиданно станет неизвестной (можно подделать для x64 установив в proc->WoW64Process правильные значение)
и значение Machine не равно 452(IMAGE_FILE_MACHINE_ARMNT) или 332(IMAGE_FILE_MACHINE_I386).
Однако, если вы захотите попробовать данных трюк,чтобы добиться anti-debug эффекта,то есть побочный эффект - некоторые функции будут возвращать не тот результат.
В-четвёртых,если функция ObReferenceObjectByHandle закончится неудачно,то у вас так же не получится дебажить процесс(мы поговорим позже как можно добиться этого эффекта без хуков и триггера PG).
C++:
NTSTATUS __stdcall DbgkpSetProcessDebugObject(PEPROCESS Process, DEBUG_OBJECT *DebugObject, NTSTATUS MsgStatus, PETHREAD LastThread)
{
_LIST_ENTRY *CurrentThread; // r13
NTSTATUS nt_status; // edi
PETHREAD LastThread_2; // rbx
__int64 ethread_1; // r14
LIST_ENTRY *list_entry; // r14
DEBUG_EVENT *debug_event; // rbx
ULONG flag_debug_event; // eax
__int64 ethread; // r13
_LIST_ENTRY *v14; // rcx
_LIST_ENTRY *v15; // rax
__int64 v16; // rax
_LIST_ENTRY *v17; // rax
ULONG Flag; // eax
struct _DEBUG_EVENT *DebugEvent; // rcx
_LIST_ENTRY *v20; // rax
PVOID Object; // [rsp+30h] [rbp-30h] BYREF
_LIST_ENTRY *CurrentThread_1; // [rsp+38h] [rbp-28h]
PKGUARDED_MUTEX Mutex; // [rsp+40h] [rbp-20h]
PDEBUG_EVENT DebugEvent_1; // [rsp+48h] [rbp-18h] BYREF
DEBUG_EVENT *v26; // [rsp+50h] [rbp-10h]
char First; // [rsp+A8h] [rbp+48h]
char GlobalHeld; // [rsp+B0h] [rbp+50h]
PETHREAD LastThread_1; // [rsp+B8h] [rbp+58h] BYREF
LastThread_1 = LastThread;
CurrentThread = KeGetCurrentThread();
Object = NULL;
v26 = (DEBUG_EVENT *)&P;
P = &P;
nt_status = MsgStatus;
CurrentThread_1 = CurrentThread;
First = TRUE;
GlobalHeld = 0;
if ( MsgStatus >= 0 )
{
LastThread_2 = LastThread_1;
nt_status = STATUS_SUCCESS;
}
else
{
LastThread_2 = NULL;
LastThread_1 = NULL;
}
if (NT_SUCCESS(nt_status) )
{
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
while ( 1 )
{
if (Process-> DebugPort )
{
nt_status = STATUS_PORT_ALREADY_SET; //Мы не можем подключить отладчик к процессу,который дебажут/есть DebugPort
GlobalHeld = TRUE;
goto LABEL_11;
}
Process-> DebugPort = DebugObject;// EPROCESS-> DebugPort
ObfReferenceObjectWithTag(LastThread_2, 'OgbD');
GlobalHeld = TRUE;
ethread_1 = PsGetNextProcessThread(Process, LastThread_2);
if ( !ethread_1 )
goto LABEL_11;
Process-> DebugPort = NULL; // EPROCESS-> DebugPort
KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
GlobalHeld = 0;
ObfDereferenceObjectWithTag(LastThread_2, 'OgbD');
nt_status = DbgkpPostFakeThreadMessages(Process, DebugObject, ethread_1, &Object, &LastThread_1);
if (!NT_SUCCESS(nt_status))
break;
ObfDereferenceObjectWithTag(Object, 'OgbD');
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
LastThread_2 = LastThread_1;
}
LastThread_2 = NULL;
LastThread_1 = NULL;
}
LABEL_11:
Mutex = &DebugObject->Mutex;
ExAcquireFastMutex(&DebugObject->Mutex);
if (NT_SUCCESS(nt_status))
{
if ( (DebugObject->Flags & DebuggerInactive) != 0 ) // DebugObject -> DebuggerInactive
{
Process-> DebugPort = NULL; // EPROCESS-> DebugPort
nt_status = STATUS_DEBUGGER_INACTIVE;
}
else
{
_InterlockedOr(Process->Flags, PS_PROCESS_FLAGS_NO_DEBUG_INHERIT|PS_PROCESS_FLAGS_CREATE_REPORTED);// Set DebugFlag
ObfReferenceObject(DebugObject);
LastThread_2 = LastThread_1;
}
}
list_entry = DebugObject->EventList.Flink;
if ( list_entry == &DebugObject->EventList )
goto LABEL_37;
do
{
debug_event = (DEBUG_EVENT *)list_entry;
list_entry = list_entry->Flink;
flag_debug_event = debug_event->Flags;
if ( (flag_debug_event & DEBUG_EVENT_INACTIVE) == 0 || debug_event->BackoutThread != CurrentThread )// DEBUG_EVENT_INACTIVE
continue;
ethread = debug_event->Thread;
if (!NT_SUCCESS(nt_status) )
{
if ( (DEBUG_EVENT *)list_entry->Blink != debug_event
|| (v16 = (__int64)debug_event->EventList.Blink, *(DEBUG_EVENT **)v16 != debug_event) )
{
LABEL_45:
__fastfail(ERROR_PATH_NOT_FOUND); // ???
}
*(_QWORD *)v16 = list_entry;
list_entry->Blink = (_LIST_ENTRY *)v16;
goto LABEL_30;
}
if ( (flag_debug_event & DEBUG_EVENT_PROTECT_FAILED) != 0 ) // DEBUG_EVENT_PROTECT_FAILED
{
_InterlockedOr((volatile signed __int32 *)(ethread->CrossThreadFlags ), SkipCreationMsg);// Thread->CrossThreadFlags ->SkipCreationMsg
v14 = debug_event->EventList.Flink;
if ( (DEBUG_EVENT *)debug_event->EventList.Flink->Blink != debug_event )
goto LABEL_45;
v15 = debug_event->EventList.Blink;
if ( (DEBUG_EVENT *)v15->Flink != debug_event )
goto LABEL_45;
v15->Flink = v14;
v14->Blink = v15;
LABEL_30:
v17 = &v26->EventList;
if ( (PVOID *)v26->EventList.Flink != &P )
goto LABEL_45;
debug_event->EventList.Flink = (_LIST_ENTRY *)&P;
debug_event->EventList.Blink = v17;
v17->Flink = &debug_event->EventList;
v26 = debug_event;
goto LABEL_32;
}
if ( First )
{
debug_event->Flags &= ~DEBUG_EVENT_INACTIVE;// &= ~DEBUG_EVENT_INACTIVE
KeSetEvent(&DebugObject->EventsPresent, NULL, NULL);
First = FALSE;
}
debug_event->BackoutThread = NULL;
_InterlockedOr((volatile signed __int32 *)(ethread->CrossThreadFlags), 0x40);// ? Thread->CrossThreadFlags->BreakOnTermination
LABEL_32:
Flag = debug_event->Flags;
if ( (Flag & DEBUG_EVENT_RELEASE) != NULL ) // DEBUG_EVENT_RELEASE
{
debug_event->Flags &= ~DEBUG_EVENT_RELEASE; // &= ~DEBUG_EVENT_RELEASE
ExReleaseRundownProtection_0((PEX_RUNDOWN_REF)(ethread->RundownProtect));// ETHREAD -> RundownProtect
}
CurrentThread = CurrentThread_1;
}
while ( list_entry != &DebugObject->EventList );
LastThread_2 = LastThread_1;
LABEL_37:
KeReleaseGuardedMutex(Mutex);
if ( GlobalHeld )
KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
if ( LastThread_2 )
ObfDereferenceObjectWithTag(LastThread_2, 'OgbD');
while ( 1 )
{
DebugEvent = DebugEvent_1;
if ( DebugEvent_1 == (PDEBUG_EVENT)&DebugEvent_1 )
break;
if ( (PDEBUG_EVENT *)DebugEvent_1->EventList.Blink != &DebugEvent_1 )
goto LABEL_45;
v20 = DebugEvent_1->EventList.Flink;
if ( (PDEBUG_EVENT)DebugEvent_1->EventList.Flink->Blink != DebugEvent_1 )
goto LABEL_45;
DebugEvent_1 = (PDEBUG_EVENT)DebugEvent_1->EventList.Flink;
v20->Blink = (_LIST_ENTRY *)&DebugEvent_1;
DbgkpWakeTarget(DebugEvent);
}
if (NT_SUCCESS(nt_status))
DbgkpMarkProcessPeb((PEPROCESS)Process);
return nt_status;
}
Вот мы и добрались до главной функции,которая устанавливает DebugPort.
Сюрприз-сюрприз!
DebugPort в ялре - это на самом деле PDEBUG_OBJECT.
Во-вторых, вы не можете подключить дебаггер к процессу, который уже дебажут/есть DebugPort.
Можно создать 2 процесс, который будет дебажить основной(вы можете это наблюдать данную технику в данном crackme )
На мой взгляд это самый мощный anti-debug из юзермода, поскольку можно сделать свою логику для дебаггера и для отладки такого приложения потребуется больше времени/усилий!
В-третьих, тут идёт установка в Flags флага NoDebugInherit(DebugFlag). Этот флаг можно перезаписать из UM(NtSetInformationProcess)
и некоторые anti-anti-debug plugin до сих пор не могут справиться с этим(печальный TitanHide и SharpOD)
C++:
void __fastcall DbgkpMarkProcessPeb(PEPROCESS Process)
{
struct _EX_RUNDOWN_REF *RundownProtect; // rdi
_EWOW64PROCESS *WoW64Process; // rax
unsigned __int16 Machine; // cx
PRKAPC_STATE ApcState; // [rsp+28h] [rbp-40h] OVERLAPPED BYREF
ApcState = NULL;
RundownProtect = Process -> RundownProtect;
if ( ExAcquireRundownProtection_0(RundownProtect )
{
if (Process ->PEB )
{
KiStackAttachProcess(Process, FLAG_CHANGE_STACK_COUNT, &ApcState);
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
Process ->PEB ->BeingDebugged = Process -> DebugPort != 0
WoW64Process = Process -> WoW64Process;
if ( WoW64Process )
{
Machine = WoW64Process->Machine;
if ( Machine == IMAGE_FILE_MACHINE_I386 || Machine == IMAGE_FILE_MACHINE_ARMNT )
{
if ( WoW64Process->Peb )
Process ->PEB ->BeingDebugged = Process -> DebugPort
}
}
KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
KiUnstackDetachProcess(&ApcState, NULL);
}
ExReleaseRundownProtection_0(RundownProtect);
}
}
Ага! Тут и идёт установка BeingDebugged в PEB.
Мы изучили 3 важных ntapi,поэтому вернёмся к анализу некоторых функций.
Погнали изучать другие моменты дальше!
Но как происходи отсоединение дебаггера?
Ну, привет NtRemoveProcessDebug.
C++:
NTSTATUS __fastcall NtRemoveProcessDebug(HANDLE ProcessHandle, HANDLE DebugObjectHandle)
{
char PreviousMode; // si
__int64 PreviousMode_1; // rcx
PEPROCESS Process_1; // rdi
NTSTATUS nt_status; // ebx
__int64 SecureHandle; // rbx
PVOID DebugObject; // [rsp+40h] [rbp-A8h] BYREF
PEPROCESS Process; // [rsp+48h] [rbp-A0h] BYREF
__int64 buffer[14]; // [rsp+50h] [rbp-98h] BYREF
PreviousMode = KeGetCurrentThrad()->PreviousMode;
Process = NULL;
nt_status = ObpReferenceObjectByHandleWithTag(
ProcessHandle,
PROCESS_SET_PORT, // PROCESS_SET_PORT
PsProcessType,
PreviousMode,
'OgbD',
&Process,
NULL,
NULL);
if ( NT_SUCCESS(nt_status))
{
LOBYTE(PreviousMode_1) = PreviousMode;
Process_1 = Process;
if ( PsTestProtectedProcessIncompatibility(
PreviousMode_1,
KeGetCurrentThread()->ApcState.Process,
(Process) )
{
nt_status = STATUS_PROCESS_IS_PROTECTED;
}
else
{
SecureHandle = Process_1->SecureState.SecureHandle;
if ( (SecureHandle & 1) == NULL // Unused ??
|| (memset(buffer, NULL, 104ui64),
buffer[2] = NULL,
buffer[1] = SecureHandle,
nt_status = VslpEnterIumSecureMode(2u, 12, 0, (__int64)buffer),
nt_status >= 0) )
{
DebugObject = NULL;
nt_status = ObReferenceObjectByHandle(
DebugObjectHandle,
DEBUG_PROCESS_ASSIGN, // DEBUG_PROCESS_ASSIGN
DbgkDebugObjectType,
PreviousMode,
&DebugObject,
NULL);
if ( NT_SUCCESS(nt_status) )
{
nt_status = DbgkClearProcessDebugObject(Process_1, DebugObject);
ObfDereferenceObjectWithTag(DebugObject, 'tlfD');
}
}
}
ObfDereferenceObjectWithTag(Process_1, 'OgbD');
}
return nt_status;
}
и это вызывает
C++:
NTSTATUS __fastcall DbgkClearProcessDebugObject(PEPROCESS eprocess, PDEBUG_OBJECT SourceDebugObject)
{
PDEBUG_OBJECT DebugObject; // rbx
NTSTATUS nt_status; // edi
_LIST_ENTRY *NextEntry; // rdx
_LIST_ENTRY *curr; // rax
_LIST_ENTRY *v9; // rcx
_LIST_ENTRY *v10; // r9
PDEBUG_EVENT DebugEvent; // rcx
_LIST_ENTRY *Link; // rax
PDEBUG_EVENT DebugEvent_1; // [rsp+20h] [rbp-10h] BYREF
DEBUG_EVENT var8; // [rsp+28h] [rbp-8h] OVERLAPPED
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
DebugObject = eprocess->DebugPort ;// DebugPort
if ( DebugObject && (DebugObject == SourceDebugObject || !SourceDebugObject) )
{
eprocess-> DebugPort = NULL; // eprocess -> DebugPort = NULL
nt_status = STATUS_SUCCESS;
}
else
{
DebugObject = NULL;
nt_status = STATUS_PORT_NOT_SET;
}
KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
if ( nt_status >= 0 )
DbgkpMarkProcessPeb(eprocess);
if ( DebugObject )
{
var8.EventList.Flink = (_LIST_ENTRY *)&DebugEvent_1;
DebugEvent_1 = (PDEBUG_EVENT)&DebugEvent_1;
ExAcquireFastMutex(&DebugObject->Mutex);
NextEntry = DebugObject->EventList.Flink;
while ( NextEntry != &DebugObject->EventList )
{
curr = NextEntry;
NextEntry = NextEntry->Flink;
if ( (PEPROCESS)curr[3].Blink == eprocess )
{
if ( NextEntry->Blink != curr
|| (v9 = curr->Blink, v9->Flink != curr)
|| (v9->Flink = NextEntry,
NextEntry->Blink = v9,
v10 = var8.EventList.Flink,
var8.EventList.Flink->Flink != (_LIST_ENTRY *)&DebugEvent_1) )
{
fail:
__fastfail(ERROR_PATH_NOT_FOUND);
}
curr->Blink = var8.EventList.Flink;
curr->Flink = (_LIST_ENTRY *)&DebugEvent_1;
v10->Flink = curr;
var8.EventList.Flink = curr;
}
}
KeReleaseGuardedMutex(&DebugObject->Mutex);
HalPutDmaAdapter((PADAPTER_OBJECT)DebugObject);
while ( 1 )
{
DebugEvent = DebugEvent_1;
if ( DebugEvent_1 == (PDEBUG_EVENT)&DebugEvent_1 )
break;
if ( (PDEBUG_EVENT *)DebugEvent_1->EventList.Blink != &DebugEvent_1 )
goto fail;
Link = DebugEvent_1->EventList.Flink;
if ( (PDEBUG_EVENT)DebugEvent_1->EventList.Flink->Blink != DebugEvent_1 )
goto fail;
DebugEvent_1 = (PDEBUG_EVENT)DebugEvent_1->EventList.Flink;
Link->Blink = (_LIST_ENTRY *)&DebugEvent_1;
DebugEvent->Status = STATUS_DEBUGGER_INACTIVE;
DbgkpWakeTarget(DebugEvent);
}
}
return nt_status;
}
Данная функция просто убирает DebugPort с процесса и идёт работа с ивентами.
Забавно,но DebugFlag не изменяется в случае отсоединения.
Так же,если ProcessDebugObjectHandle вернул HANDLE,то можно просто убрать DebugPort с нашего процесса(с помощью NtRemoveProcessDebug),поэтому отладка просто будет невозможной.
Самые важные моменты изучили, поэтому идём изучать сами anti-debug трюки в ядре!
Ядро первоначально проверяет на запись буффер, перед тем как что-то начать с ним делать(вот пример с NtQueryInformationProcess):
C++:
auto PreviousMode = KeGetPreviousMode();
if (PreviousMode != KernelMode)
{
try
{
ProbeForRead (ProcessInformation,
ProcessInformationLength,
sizeof(ULONG));
if (ARGUMENT_PRESENT (ReturnLength))
{
ProbeForWrite(ReturnLength, 4, 1);
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
}
C++:
case ProcessDebugPort:
if (ProcessInformationLength_1 != sizeof (HANDLE) )
goto status_lenght_mismatch;
nt_status = ObReferenceObjectByHandleWithTag(
ProcessHandle,
PROCESS_QUERY_INFORMATION,
(POBJECT_TYPE)PsProcessType,
PreviousMode,
'yQsP',
&proc,
NULL);
if (!NT_SUCCESS(nt_status))
return nt_status;
Handle = proc->DebugPort;
ObfDereferenceObjectWithTag(proc, 'yQsP');
__try
{
*(_QWORD *)ProcessInformation_1 = Handle != 0 ? -1:0;
if ( ReturnLength )
*ReturnLength = sizeof (HANDLE);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
return STATUS_SUCCESS;
Во-первых,идёт проверка правильно ли передана длинна входного буфера.
Во-вторых, проверяется есть ли у HANDLE'а нужные права.
В-третьих, если существует DebugPort в PEPROCESS,то записывает в буфер -1.
C++:
case ProcessDebugFlags:
if ( (_DWORD)ProcessInformationLength_1 != sizeof (ULONG) )
goto status_lenght_mismatch;
nt_status = ObReferenceObjectByHandleWithTag(
ProcessHandle,
PROCESS_QUERY_INFORMATION,
PsProcessType,
PreviousMode,
'yQsP',
&proc,
NULL);
if (!NT_SUCCESS(nt_status))
return nt_status;
__try
{
*(ULONG *)ProcessInformation_1 = proc->Flags&PS_PROCESS_FLAGS_NO_DEBUG_INHERIT != NULL;
if(ReturnLength)
*ReturnLength = sizeof(ULONG);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
ObfDereferenceObjectWithTag(proc, 'yQsP');
return nt_status;
Аналогично то же самое,что и с ProcessDebugPort почти,но идёт проверка флага в eprocess.
Если существует ProcessDebugFlags,то записывает в буфер 0,а в противном случае 1.
Теперь фаворит VMP:
C++:
case ProcessDebugObjectHandle:
if ( (_DWORD)ProcessInformationLength_1 != sizeof (HANDLE) )
goto status_lenght_mismatch;
nt_status = ObReferenceObjectByHandleWithTag(
ProcessHandle,
PROCESS_QUERY_INFORMATION,
(POBJECT_TYPE)PsProcessType,
PreviousMode,
'yQsP',
&proc,
NULL);
if (!NT_SUCCESS(nt_status))
return nt_status;
nt_status = DbgkOpenProcessDebugPort(proc, PreviousMode, &DebugPort);
if (!NT_SUCCESS(nt_status))
DebugPort = NULL;
ObfDereferenceObjectWithTag(proc, 'yQsP');
__try
{
*(_QWORD *)ProcessInformation_1 = DebugPort;
if ( ReturnLength )
*ReturnLength = sizeof (HANDLE);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
return nt_status;
Во-первых,она связанная с одним anti-debug трюком из VMP 3.1.x и выше.
Да, атака на ReturnLength,но как понимаю эта любимая функция автора VMP :D.
Во-вторых, вызывется функция DbgkOpenProcessDebugPort,которая записывает в буфер хэндл для работы с DebugPort в случае успеха.
При написании anti-debug плагина ни в коем случае нельзя вызывать оригинальную функцию с DbgkOpenProcessDebugPort т.к можно перезаписать ReturnLength,благодаря чему произойдёт утечка HANDLE'a
P.S почему же это фаворит VMP?
Ну,вот ответы:
1)
Пожалуйста, авторизуйтесь для просмотра ссылки.
2)
Пожалуйста, авторизуйтесь для просмотра ссылки.
3)и 3 детект будет в статье
C++:
NTSTATUS __fastcall DbgkOpenProcessDebugPort(PEPROCESS proc, KPROCESSOR_MODE PreviousMode, PHANDLE pHandle)
{
NTSTATUS nt_status; // edi
struct _DMA_ADAPTER *debug_port; // rbx
__int64 PreviousMode_1; // rcx
nt_status = STATUS_PORT_NOT_SET;
if ( proc->DebugPort) // proc->DebugPort
{
ExAcquireFastMutex(&DbgkpProcessDebugPortMutex);
debug_port = proc->DebugPort;// proc->DebugPort
if ( debug_port )
ObfReferenceObject(proc->DebugPort]);// proc->DebugPort
KeReleaseGuardedMutex(&DbgkpProcessDebugPortMutex);
if ( debug_port )
{
LOBYTE(PreviousMode_1) = PreviousMode;
if ( PsTestProtectedProcessIncompatibility(
PreviousMode_1,
KeGetCurrentThread()->ApcState.Process,
proc) )
{
nt_status = STATUS_PROCESS_IS_PROTECTED;
HalPutDmaAdapter(debug_port);
return nt_status;
}
nt_status = ObOpenObjectByPointer(
debug_port,
PreviousMode == KernelMode ? OBJ_KERNEL_HANDLE : NULL,
NULL,
MAXIMUM_ALLOWED,
DbgkDebugObjectType,
PreviousMode,
pHandle);
if (!NT_SUCCESS(nt_status))
{
HalPutDmaAdapter(debug_port);
return nt_status;
}
}
}
return nt_status;
}
Функция должна вернуть STATUS_PORT_NOT_SET,если отсутствует DebugPort + идёт проверка после этого на защищённый процесс.
В конце открывается доступ к DebugPort'у с правами MAXIMUM_ALLOWED и функция вернёт STATUS_SUCCESS в случае успеха ObOpenObjectByPointer.
Мы затронули ThreadHideFromDebugger,но как он работает на самом деле? Наш дебаггер взрывается или что с ним вообще происходит?
C++:
case ThreadHideFromDebugger:
if ( ThreadInformationLength )
return STATUS_INFO_LENGTH_MISMATCH;
nt_status = ObReferenceObjectByHandleWithTag(
ThreadHandle_1,
THREAD_SET_INFORMATION,
PsThreadType,
PreviousMode,
'yQsP',
&thread,
NULL);
if (!NT_SUCCESS(nt_status) )
return nt_status;
_InterlockedOr(thread->CrossThreadFlags, PS_CROSS_THREAD_FLAGS_HIDEFROMDBG);// lock or dword ptr [rax+510h], 4
goto derederence_object;
Так же не забудем о NtCreateThreadEx(с THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER и с THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE(начиная с windows 10 19H1) ):
Цепочка:
NtCreateThreadEx -> PspCreateThread -> PspMapThreadCreationFlags.
Именно в PspMapThreadCreationFlags идёт установка флагов и при создании можно сделать поток скрытым:
C++:
void __fastcall PspMapThreadCreationFlags(char ThreadFlags, int *flag_result)
{
int res; // eax
res = NULL;
*flag_result = NULL;
if ( (ThreadFlags & THREAD_CREATE_FLAGS_CREATE_SUSPENDED ) != NULL )
{
*flag_result = THREAD_CREATE_FLAGS_CREATE_SUSPENDED ;
res = THREAD_CREATE_FLAGS_CREATE_SUSPENDED;
}
if ( (ThreadFlags & THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH ) != NULL )
{
res |= THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH ;
*flag_result = res;
}
if ( (ThreadFlags & THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER ) != NULL )
{
res |= THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER ;
*flag_result = res;
}
if ( (ThreadFlags & THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR ) != NULL )
{
res |= 0x80u;
*flag_result = res;
}
if ( (ThreadFlags & THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET ) != NULL )
{
res |= 0x100u;
*flag_result = res;
}
if ( (ThreadFlags & THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE) != NULL )
*flag_result = res | 0x200;
}
THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE работает следующим образом:
C++:
NTSTATUS __fastcall PsSuspendProcess(PEPROCESS proc)
{
struct _KTHREAD *current_thread; // rbp
struct _EX_RUNDOWN_REF *RundownProtect; // r14
__int64 next_thread; // rax
NTSTATUS nt_status; // ebx
__int64 current_thread_1; // rdi
current_thread = KeGetCurrentThread();
--current_thread->KernelApcDisable;
RundownProtect = proc->RundownProtect;
if ( ExAcquireRundownProtection_0(proc->RundownProtect) == 1 )
{
next_thread = PsGetNextProcessThread(proc, NULL);
nt_status = STATUS_SUCCESS;
while ( 1 )
{
current_thread_1 = next_thread;
if ( !next_thread )
break;
if ( (next_thread->MiscFlags & THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE) == NULL )
PsSuspendThread(next_thread, NULL);
next_thread = PsGetNextProcessThread(proc, current_thread_1);
}
ExReleaseRundownProtection_0(RundownProtect);
}
else
{
nt_status = STATUS_PROCESS_IS_TERMINATING;
}
if ( proc-> Flags3 & EnableThreadSuspendResumeLogging) != 0 )// EnableThreadSuspendResumeLogging
EtwTiLogSuspendResumeProcess(nt_status, current_thread, proc, NULL);
KeLeaveCriticalRegionThread(current_thread);
return nt_status;
}
А теперь вернёмся к HideFromDebugger.
Установился флаг и установился! А дальше что?
Ну,тут ответ:DbgkForwardException
На самом деле проверяется бит ещё в следующих функциях:
Не будет доставлено уведомление через DbgkpSendApiMessage о создании/завершении потока или завершении процесса:DbgkExitThread,DbgkExitProcess,DbgkCreateThread.
Не будет доставлено уведомление через DbgkpSendApiMessage о мапе/анмапе секций:DbgkMapViewOfSection,DbgkUnMapViewOfSection.
Про это уже было сказано здесь.
Сама функция выглядит так~:
C++:
bool __fastcall DbgkForwardException(PEXCEPTION_RECORD ExceptionRecord, BOOLEAN DebugException, BOOLEAN SecondChance)
{
struct _KTHREAD *current_thread; // rax
struct _KPROCESS *current_process; // rsi
struct _DMA_ADAPTER *port; // rbx
char LpcPort; // r14
int nt_status; // eax
__int64 v14; // [rsp+20h] [rbp-E0h] BYREF
DBGKM_APIMSG m; // [rsp+30h] [rbp-D0h] BYREF
v14 = NULL;
memset(m, 0, sizeof(m));
if ( SecondChance )
{
LODWORD(v14) = 1;
PsSetProcessFaultInformation(KeGetCurrentThread()->ApcState.Process, &v14);
}
m[10] = 0;
m.u1.Length = 0xD000A8;
m.u2.ZeroInit = 8;
current_thread = KeGetCurrentThread();
current_process = current_thread->ApcState.Process;
if ( DebugException )
{
if ( (KeGetCurrentThread()->CrossThreadFlags) & PS_CROSS_THREAD_FLAGS_HIDEFROMDBG) != NULL ) //Thread is hide
port = NULL;
else
port = current_process->DebugPort;
LpcPort = FALSE;
}
else
{
port = PsCaptureExceptionPort(current_thread->ApcState.Process);
m.h.u2.ZeroInit = LPC_EXCEPTION;
LpcPort = TRUE;
}
if ( !port && DebugException )
return FALSE;
KeCopyExceptionRecord(&m[12], ExceptionRecord);
m[50] = SecondChance == NULL;
if ( LpcPort )
{
if ( port )
{
nt_status = DbgkpSendApiMessageLpc(m, port, DebugException);
HalPutDmaAdapter(port);
}
else
{
nt_status = STATUS_SUCCESS;
m.ReturnedStatus = DBG_EXCEPTION_NOT_HANDLED;
}
}
else
{
nt_status = DbgkpSendApiMessage(current_process, DebugException != NULL, m);
}
if (!NT_SUCCESS(nt_status))
return FALSE;
nt_status = m.ReturnedStatus ;
if ( m.ReturnedStatus == DBG_EXCEPTION_NOT_HANDLED )
{
if ( DebugException )
return 0;
nt_status = DbgkpSendErrorMessage(ExceptionRecord, 2, m);
}
return NT_SUCCESS(nt_status);
}
По названию понятно,что она связанна с исключениями.
Если поток скрыт и процесс дебажут,то исключение не будет передано на DebugPort при скрытом потоке,поэтому процесс рухнет.
На мой взгляд это мощный anti-debug трюк т.к сами Microsoft добавили данный трюк.
Правда,данный трюк легко обойти(особенно в ядре) т.к можно хукнуть саму функцию DbgkForwardException или просто очистить PS_CROSS_THREAD_FLAGS_HIDEFROMDBG у потока.
Теперь посмотрим NtClose:
C++:
NTSTATUS __stdcall NtClose(HANDLE Handle)
{
EXHANDLE Handle_1; // rbx
char PreviousMode; // r14
struct _KTHREAD *current_thread; // rdi
bool is_safe; // r15
_KPROCESS *current_process; // r13
PEPROCESS current_process_1; // r12
_HANDLE_TABLE *kernel_handle_table; // rbp
_HANDLE_TABLE *handle_table_entry; // rax
_HANDLE_TABLE *handle_table_entry_1; // rsi
NTSTATUS nt_status; // edi
ULONG_PTR v12; // rcx
struct _HANDLE_TRACE_DEBUG_INFO *debug_trace; // [rsp+70h] [rbp+8h] BYREF
__int64 is_debug_info_or_flag_exception; // [rsp+78h] [rbp+10h] BYREF
Handle_1.Value = (ULONG_PTR)Handle;
PreviousMode = KeGetCurrentThread()->PreviousMode;
if ( (MmVerifierData & 0x100) != NULL && !PreviousMode && !ObpIsKernelHandle(Handle, NULL) )
VfCheckUserHandle(v12);
current_thread = KeGetCurrentThread();
is_safe = 0;
current_process = current_thread->ApcState.Process;
LOBYTE(debug_trace) = NULL;
if ( ObpIsKernelHandle(Handle_1.GenericHandleOverlay, PreviousMode) )
{
kernel_handle_table = ObpKernelHandleTable;
Handle_1.Value ^= 0xFFFFFFFF80000000ui64;
current_process_1 = PsInitialSystemProcess;
}
else
{
current_process_1 = current_process;
if ( KeGetCurrentThread()->ApcStateIndex != 1 )
{
kernel_handle_table = current_process ->ObjectTable;// current_process -> ObjectTable
if ( kernel_handle_table != ObpKernelHandleTable )
goto check_handle;
return STATUS_INVALID_HANDLE;
}
kernel_handle_table = ObReferenceProcessHandleTable(current_process);
if ( !kernel_handle_table )
return STATUS_INVALID_HANDLE;
is_safe = TRUE;
}
check_handle:
--current_thread->KernelApcDisable;
if ( (*(_WORD *)&Handle_1.u & 0x3FC) != NULL )
{
handle_table_entry = ExpLookupHandleTableEntry(kernel_handle_table, Handle_1.GenericHandleOverlay);
handle_table_entry_1 = handle_table_entry;
if ( handle_table_entry )
{
if ( ExLockHandleTableEntry(kernel_handle_table, handle_table_entry) )
{
//Тут идёт проверка установлена ли защита от закрытия защищённого хэндла и просиходи само закрытие
nt_status = ObCloseHandleTableEntry(
kernel_handle_table,
handle_table_entry_1,
current_process_1,
Handle_1.GenericHandleOverlay,
PreviousMode,
NULL);
goto exit;
}
}
}
KeLeaveCriticalRegionThread(current_thread);
if ( Handle_1.Value >= 0xFFFFFFFFFFFFFFFAui64 || Handle_1.Value == NULL )
goto LABEL_14;
ExQueryHandleExceptionsPermanency(kernel_handle_table, &is_debug_info_or_flag_exception, &debug_trace);
if (kernel_handle_table->Flags & RaiseUMExceptionOnInvalidHandleClose) != NULL && (_BYTE)debug_trace )// Duplicated or RaiseUMExceptionOnInvalidHandleClose
ExHandleLogBadReference(kernel_handle_table, Handle_1.GenericHandleOverlay);
if ( !PreviousMode )
{
if ( current_thread->Terminated == NULL// current_thread->Terminated != NULL
&& current_process->Peb // current_process->Peb
&& (_BYTE)KdDebuggerEnabled )
{
KeBugCheckEx(INVALID_KERNEL_HANDLE, Handle_1.Value, TRUE, NULL, NULL);
}
goto LABEL_14;
}
if ( (NtGlobalFlag & FLG_ENABLE_CLOSE_EXCEPTIONS) == NULL && !current_process ->DebugPort && !kernel_handle_table->DebugInfo )// current_process ->DebugPort
{
goto LABEL_14;
}
if ( KeGetCurrentThread()->ApcStateIndex == 1 )
nt_status = STATUS_INVALID_HANDLE;
else
nt_status = KeRaiseUserException(STATUS_INVALID_HANDLE); //Доставляем исключение в UM процесс
goto exit;
LABEL_14:
nt_status = STATUS_INVALID_HANDLE;
if ( Handle_1.Value + 6 <= 5 )
nt_status = STATUS_SUCCESS;
goto exit;
exit:
if ( is_safe )
ExReleaseRundownProtection_0(current_process_1->RundownProtect);// current_process_1 ->RundownProtect
return nt_status;
}
На мой взгляд это альтернативный вариант проверки DebugPort т.к если закрыть invalid handle и
при этом вы дебажите процесс,то исключение для приложения будет выброшено и
в случае с защищённым handle'ом так же или если установлен системой FLG_ENABLE_CLOSE_EXCEPTIONS(NtGlobalFlag).
При закрытии handle'а (ObCloseHandleTableEntry) идёт проверка защищён ли он от закрытия:
C++:
//boring code
if ( (GrantedAccess & OBJ_PROTECT_CLOSE) == 0 || IgnoreHandleProtection )
{
//boring code
}
if ( !PreviousMode )
KeBugCheckEx(INVALID_KERNEL_HANDLE, (ULONG_PTR)Handle_1, NULL, NULL, NULL);
_InterlockedExchangeAdd64(handle_table_entry_1, 1ui64);
_InterlockedOr(v50, 0);
if ( kernel_handle_table->HandleContentionEvent.Value )
ExfUnblockPushLock(&kernel_handle_table->HandleContentionEvent, NULL, v11);
KeLeaveCriticalRegion();
if ( is_attached )
KiUnstackDetachProcess(&ApcState, NULL);
if ( KeGetCurrentThread()->ApcStateIndex == 1
|| (NtGlobalFlag & FLG_ENABLE_CLOSE_EXCEPTIONS) == 0
&& !KeGetCurrentThread()->ApcState.Process->DebugPort
&& !kernel_handle_table->DebugInfo )
{
return STATUS_HANDLE_NOT_CLOSABLE; //Я ядреный, как кабан, я имею свой баян 🤘
}
return KeRaiseUserException(STATUS_HANDLE_NOT_CLOSABLE);
Тут происходит вызов исключения,если процесс дебажут и HANDLE при этом защищён от закрытия/включена трассировка или установлен FLG_ENABLE_CLOSE_EXCEPTIONS в NtGlobalFlag .
Опять же, альтернатива проверки DebugPort,если не делать лишних действия.
Предлагаю вернуться к NtDuplicateObject.
C++:
NTSTATUS __fastcall NtDuplicateObject(HANDLE SourceProcessHandle, HANDLE SourceHandle, HANDLE TargetProcessHandle, PHANDLE TargetHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Options)
{
PHANDLE TargetHandle_1; // rbx
struct _KPROCESS *TargetProcess_1; // rdi
char PreviousMode; // si
NTSTATUS nt_status; // eax
NTSTATUS nt_status_1; // er14
HANDLE SourceHandle_1; // rdx
PVOID SourceProcess_1; // r15
NTSTATUS nt_status_2; // esi
__int64 v17; // rdx
PVOID TargetProcess; // [rsp+48h] [rbp-40h] BYREF
PVOID SourceProcess; // [rsp+50h] [rbp-38h] BYREF
HANDLE NewHandle; // [rsp+58h] [rbp-30h] OVERLAPPED BYREF
TargetHandle_1 = TargetHandle;
TargetProcess_1 = NULL;
NewHandle = NULL;
SourceProcess = NULL;
TargetProcess = NULL;
PreviousMode = KeGetCurrentThread()->PreviousMode;
if ( TargetHandle && PreviousMode )
{
__try
{
ProbeForWrite(TargetHandle, 4, 1);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
}
nt_status = ObReferenceObjectByHandleWithTag(
SourceProcessHandle,
PROCESS_DUP_HANDLE, // PROCESS_DUP_HANDLE
(POBJECT_TYPE)PsProcessType,
PreviousMode,
'uDbO',
&SourceProcess,
NULL);
if ( NT_SUCCESS(nt_status))
{
if ( TargetProcessHandle )
{
nt_status_1 = ObReferenceObjectByHandleWithTag(
TargetProcessHandle,
PROCESS_DUP_HANDLE, // PROCESS_DUP_HANDLE
(POBJECT_TYPE)PsProcessType,
PreviousMode,
'uDbO',
&TargetProcess,
NULL);
if (!NT_SUCCESS(nt_status_1))
{
TargetProcess = NULL;
goto LABEL_7;
}
}
else
{
nt_status_1 = STATUS_SUCCESS;
}
TargetProcess_1 = TargetProcess;
LABEL_7:
SourceHandle_1 = SourceHandle;
SourceProcess_1 = SourceProcess;
nt_status_2 = ObDuplicateObject(
SourceProcess,
SourceHandle_1,
TargetProcess_1,
&NewHandle,
DesiredAccess,
HandleAttributes,
Options,
PreviousMode);
if ( TargetHandle_1 )
*TargetHandle_1 = NewHandle;
ObfDereferenceObjectWithTag(SourceProcess_1, 'uDbO');
if ( TargetProcess_1 )
ObfDereferenceObjectWithTag(TargetProcess_1, 'uDbO');
if ( !NT_SUCCESS(nt_status_1) )
nt_status_2 = nt_status_1;
nt_status = nt_status_2;
}
return nt_status;
}
в ObDuplicateObject вызывается NtClose,если передан флаг DUPLICATE_CLOSE_SOURCE:
C++:
//boring code
if ((Options & DUPLICATE_CLOSE_SOURCE) != 0 )
{
KeStackAttachProcess(SourceProcess_1, &ApcState);
NtClose(SourceHandle_1);
KeUnstackDetachProcess(&ApcState);
}
Эту функцию можно использовать для замены NClose в качестве anti-debug трюка т.к с параметром DUPLICATE_CLOSE_SOURCE функция вызывает NtClose!
Благодаря этому можно достичь anti-debug эффекта!
Вернёмся к NtQueryObject.
Поскольку код не слишком изменился с Windows XP,то я возьму функцию из WRK и уберу некоторые моменты:
C++:
NTSTATUS
NtQueryObject (
__in HANDLE Handle,
__in OBJECT_INFORMATION_CLASS ObjectInformationClass,
__out_bcount_opt(ObjectInformationLength) PVOID ObjectInformation,
__in ULONG ObjectInformationLength,
__out_opt PULONG ReturnLength
)
/*++
Routine description:
This routine is used to query information about a given object
Arguments:
Handle - Supplies a handle to the object being queried. This value
is ignored if the requested information class is for type
information.
ObjectInformationClass - Specifies the type of information to return
ObjectInformation - Supplies an output buffer for the information being
returned
ObjectInformationLength - Specifies, in bytes, the length of the
preceding object information buffer
ReturnLength - Optionally receives the length, in bytes, used to store
the object information
Return Value:
An appropriate status value
--*/
{
KPROCESSOR_MODE PreviousMode;
NTSTATUS Status;
PVOID Object;
POBJECT_HEADER ObjectHeader;
POBJECT_HEADER_QUOTA_INFO QuotaInfo;
POBJECT_HEADER_NAME_INFO NameInfo;
POBJECT_TYPE ObjectType;
POBJECT_HEADER ObjectDirectoryHeader;
POBJECT_DIRECTORY ObjectDirectory;
ACCESS_MASK GrantedAccess;
POBJECT_HANDLE_FLAG_INFORMATION HandleFlags;
OBJECT_HANDLE_INFORMATION HandleInformation = {0};
ULONG NameInfoSize;
ULONG SecurityDescriptorSize;
ULONG TempReturnLength;
OBJECT_BASIC_INFORMATION ObjectBasicInfo;
POBJECT_TYPES_INFORMATION TypesInformation;
POBJECT_TYPE_INFORMATION TypeInfo;
ULONG i;
PAGED_CODE();
//
// Initialize our local variables
//
TempReturnLength = 0;
//
// Get previous processor mode and probe output argument if necessary.
//
PreviousMode = KeGetPreviousMode();
if (PreviousMode != KernelMode) {
try {
if (ObjectInformationClass != ObjectHandleFlagInformation) {
ProbeForWrite( ObjectInformation,
ObjectInformationLength,
sizeof( ULONG ));
} else {
ProbeForWrite( ObjectInformation,
ObjectInformationLength,
1 );
}
//
// We'll use a local temp return length variable to pass
// through to the later ob query calls which will increment
// its value. We can't pass the users return length directly
// because the user might also be altering its value behind
// our back.
//
if (ARGUMENT_PRESENT( ReturnLength )) {
ProbeForWriteUlong( ReturnLength );
}
} except( EXCEPTION_EXECUTE_HANDLER ) {
return( GetExceptionCode() );
}
}
//
// If the query is not for types information then we
// will have to get the object in question. Otherwise
// for types information there really isn't an object
// to grab.
//
if (ObjectInformationClass != ObjectTypesInformation)
{
Status = ObReferenceObjectByHandle( Handle,
0,
NULL,
PreviousMode,
&Object,
&HandleInformation );
if (!NT_SUCCESS( Status ))
{
return( Status );
}
GrantedAccess = HandleInformation.GrantedAccess;
ObjectHeader = OBJECT_TO_OBJECT_HEADER( Object );
ObjectType = ObjectHeader->Type;
}
else
{
GrantedAccess = 0;
Object = NULL;
ObjectHeader = NULL;
ObjectType = NULL;
Status = STATUS_SUCCESS;
}
// Now process the particular information class being
// requested
switch( ObjectInformationClass )
{
case ObjectTypeInformation:
//
// Call a local worker routine
//
Status = ObQueryTypeInfo( ObjectType,
(POBJECT_TYPE_INFORMATION)ObjectInformation,
ObjectInformationLength,
&TempReturnLength );
break;
case ObjectTypesInformation:
try {
//
// The first thing we do is set the return length to cover the
// types info record. Later in each call to query type info
// this value will be updated as necessary
//
TempReturnLength = sizeof( OBJECT_TYPES_INFORMATION );
//
// Make sure there is enough room to hold the types info record
// and if so then compute the number of defined types there are
//
TypesInformation = (POBJECT_TYPES_INFORMATION)ObjectInformation;
if (ObjectInformationLength < sizeof( OBJECT_TYPES_INFORMATION ) ) {
Status = STATUS_INFO_LENGTH_MISMATCH;
} else {
TypesInformation->NumberOfTypes = 0;
for (i=0; i<OBP_MAX_DEFINED_OBJECT_TYPES; i++) {
ObjectType = ObpObjectTypes[ i ];
if (ObjectType == NULL) {
break;
}
TypesInformation->NumberOfTypes += 1;
}
}
//
// For each defined type we will query the type info for the
// object type and adjust the TypeInfo pointer to the next
// free spot
//
TypeInfo = (POBJECT_TYPE_INFORMATION)(((PUCHAR)TypesInformation) + ALIGN_UP( sizeof(*TypesInformation), ULONG_PTR ));
for (i=0; i<OBP_MAX_DEFINED_OBJECT_TYPES; i++) {
ObjectType = ObpObjectTypes[ i ];
if (ObjectType == NULL) {
break;
}
Status = ObQueryTypeInfo( ObjectType,
TypeInfo,
ObjectInformationLength,
&TempReturnLength );
if (NT_SUCCESS( Status )) {
TypeInfo = (POBJECT_TYPE_INFORMATION)
((PCHAR)(TypeInfo+1) + ALIGN_UP( TypeInfo->TypeName.MaximumLength, ULONG_PTR ));
}
}
} except( EXCEPTION_EXECUTE_HANDLER ) {
Status = GetExceptionCode();
}
break;
case ObjectHandleFlagInformation:
try {
//
// Set the amount of data we are going to return
//
TempReturnLength = sizeof(OBJECT_HANDLE_FLAG_INFORMATION);
HandleFlags = (POBJECT_HANDLE_FLAG_INFORMATION)ObjectInformation;
// Make sure we have enough room for the query, and if so we'll
// set the output based on the flags stored in the handle
if (ObjectInformationLength < sizeof( OBJECT_HANDLE_FLAG_INFORMATION))
{
Status = STATUS_INFO_LENGTH_MISMATCH;
}
else
{
HandleFlags->Inherit = FALSE;
if (HandleInformation.HandleAttributes & OBJ_INHERIT)
{
HandleFlags->Inherit = TRUE;
}
HandleFlags->ProtectFromClose = FALSE;
if (HandleInformation.HandleAttributes & OBJ_PROTECT_CLOSE)
{
HandleFlags->ProtectFromClose = TRUE;
}
}
} except( EXCEPTION_EXECUTE_HANDLER )
{
Status = GetExceptionCode();
}
break;
default:
// To get to this point we must have had an object and the
// information class is not defined, so we should dereference the
// object and return to our user the bad status
ObDereferenceObject( Object );
return( STATUS_INVALID_INFO_CLASS );
}
// Now if the caller asked for a return length we'll set it from
// our local copy
try
{
if (ARGUMENT_PRESENT( ReturnLength ) )
{
ReturnLength = TempReturnLength;
}
} except( EXCEPTION_EXECUTE_HANDLER )
{
// Fall through, since we cannot undo what we have done.
}
//
// In the end we can free the object if there was one and return
// to our caller
//
if (Object != NULL)
{
ObDereferenceObject( Object );
}
return( Status );
}
ObQueryTypeInfo просто переносит аргументы из ObjectType(POBJECT_TYPE) в ObjectTypeInfo(POBJECT_TYPE_INFORMATION):
C++:
NTSTATUS __fastcall ObQueryTypeInfo(POBJECT_TYPE ObjectType, POBJECT_TYPE_INFORMATION ObjectTypeInfo, ULONG Length, PULONG ReturnLength)
{
NTSTATUS nt_status; // ebx
nt_status = STATUS_INFO_LENGTH_MISMATCH;
*ReturnLength += sizeof( *ObjectTypeInfo ) + ALIGN_UP( ObjectType->Name.MaximumLength, ULONG );
if ( Length >= *ReturnLength )
{
ObjectTypeInfo->TotalNumberOfObjects = ObjectType->TotalNumberOfObjects;
ObjectTypeInfo->TotalNumberOfHandles = ObjectType->TotalNumberOfHandles;
ObjectTypeInfo->HighWaterNumberOfObjects = ObjectType->HighWaterNumberOfObjects;
ObjectTypeInfo->HighWaterNumberOfHandles = ObjectType->HighWaterNumberOfHandles;
ObjectTypeInfo->InvalidAttributes = ObjectType->TypeInfo.InvalidAttributes;
ObjectTypeInfo->GenericMapping = ObjectType->TypeInfo.GenericMapping;
ObjectTypeInfo->ValidAccessMask = ObjectType->TypeInfo.ValidAccessMask;
ObjectTypeInfo->SecurityRequired = (ObjectType->TypeInfo.ObjectTypeFlags & OB_FLAG_EXCLUSIVE_OBJECT ) != 0;
ObjectTypeInfo->MaintainHandleCount = (ObjectType->TypeInfo.ObjectTypeFlags & OB_FLAG_PERMANENT_OBJECT) != 0;
ObjectTypeInfo->PoolType = ObjectType->TypeInfo.PoolType;
ObjectTypeInfo->DefaultPagedPoolCharge = ObjectType->TypeInfo.DefaultPagedPoolCharge;
ObjectTypeInfo->DefaultNonPagedPoolCharge = ObjectType->TypeInfo.DefaultNonPagedPoolCharge;
ObjectTypeInfo->TypeIndex = ObjectType->Index;
nt_status = STATUS_SUCCESS;
ObjectTypeInfo->ReservedByte = STATUS_SUCCESS;
ObjectTypeInfo->TypeName.Buffer = &ObjectTypeInfo[1].TypeName.Length;
ObjectTypeInfo->TypeName.Length = ObjectType->Name.Length;
ObjectTypeInfo->TypeName.MaximumLength = ObjectType->Name.Length + 2;
memmove(&ObjectTypeInfo[1], ObjectType->Name.Buffer, ObjectType->Name.Length);
((PWSTR)(ObjectTypeInfo+1))[ ObjectType->Name.Length/sizeof(WCHAR) ] = UNICODE_NULL;
}
return nt_status;
}
Не вижу смысла описывать полностью т.к это исходный код windows и тут есть комментарии, поэтому перейдём к тому,что нас интересует!
Во-первых,для получения ObjectTypeInformation(количество объектов только для нашего процесса!) вызывается ObReferenceObjectByHandle и после этого конвертируется в POBJECT_TYPE.
Во-вторых,для получения ObjectTypesInformation мы получаем все объекты из глобальной переменной ObpObjectTypes,которая хранит информацию об объектах(POBJECT_TYPE )
и после вызывается ObQueryTypeInfo для передачи параметров в входной буфер.
В-третьих,можно проверить действительно ли установлен флаг защиты от закрытия HANDLE'а.
Теперь вернёмся к ObReferenceObjectByHandle via лечим инфекцию пилой.
Нет возможности дебажить - нет проблем для нас.
Если вы использовали SharpOD,то,возможно,видели возможность исправить какой-то ValidAccessMask.
Но что из себя представляет ValidAccessMask?
Ну,вот ответ:
C++:
void DbgkpInitializePhase0()
{
__int64 nt_status; // rax
__int64 some_lenght; // rbx
union _RTL_RUN_ONCE *call_back; // rcx
union _RTL_RUN_ONCE *v3; // rcx
UNICODE_STRING object_name; // [rsp+20h] [rbp-39h] BYREF
_OBJECT_TYPE_INITIALIZER object_type; // [rsp+30h] [rbp-29h] BYREF
object_name.Length = 0x180016i64;
object_name.Buffer = L"DebugObject";
memset(&object_type, NULL, sizeof(object_type));
DbgkpProcessDebugPortMutex.Owner = NULL;
DbgkpProcessDebugPortMutex.Contention = NULL;
DbgkpProcessDebugPortMutex.Event.Header.SignalState = NULL;
DbgkpProcessDebugPortMutex.Event.Header.WaitListHead.Blink = &DbgkpProcessDebugPortMutex.Event.Header.WaitListHead;
DbgkpProcessDebugPortMutex.Event.Header.WaitListHead.Flink = &DbgkpProcessDebugPortMutex.Event.Header.WaitListHead;
DbgkpProcessDebugPortMutex.Count = 1; //Я на ем панк-рок пистоню, не найти во мне изъян
LOWORD(DbgkpProcessDebugPortMutex.Event.Header.Lock) = 1;
DbgkpProcessDebugPortMutex.Event.Header.Size = 6;
nt_status = DbgkpGetServerSiloState();
if (NT_SUCCESS(DbgkpInitializePhase0SiloState(nt_status)))
{
object_type.InvalidAttributes = 0;
object_type.DefaultPagedPoolCharge = 0;
object_type.DefaultNonPagedPoolCharge = 0;
object_type.DeleteProcedure = (void (__fastcall *)(void *))AlpcMessageDeleteProcedure;
object_type.Length = sizeof(object_type);
object_type.CloseProcedure = DbgkpCloseObject;
some_lenght = sizeof(HANDLE);
LOBYTE(object_type.ObjectTypeFlags) |= 8u;
object_type.ValidAccessMask = DEBUG_ALL_ACCESS; //Первый парень на весь край, на меня все бабки в лай
object_type.GenericMapping.GenericAll = DEBUG_ALL_ACCESS;
object_type.PoolType = NonPagedPoolNx;
object_type.GenericMapping.GenericRead = 0x20001;
object_type.GenericMapping.GenericWrite = 0x20002;
object_type.GenericMapping.GenericExecute = 0x120000;
if ( NT_SUCCESS(ObCreateObjectType(&object_name, &object_type, NULL, &DbgkDebugObjectType)))
{
if ( !DbgkpMaxModuleMsgs )
DbgkpMaxModuleMsgs = 500;
call_back = &call_back_dbg;
do
{
CmSiRWLockInitialize(call_back);
call_back = v3 + 2;
--some_lenght;
}
while ( some_lenght );
}
}
}
На самом деле это просто переменная в глобальной структуре DbgkDebugObjectType с правами DEBUG_ALL_ACCESS.
Сам anti-debug трюк заключается в перезаписи ValidAccessMask на NULL т.к после этого некоторые функции связанные с отладчиком будут возвращать STATUS_ACCESS_DENIED/STATUS_NOT_SUPPORTED
На мой взгляд это сильный трюк,поскольку если вы не хотите,чтобы присутствовал отладчик в системе,то вы попросто можете сломать попытку присоединения/создания процесса с DebugPort.
Однако,если дебаггер уже подключился к процессу,то данный трюк бесполезен т.к объект отладчи будет создан и дебаггер будет уже подключен к процессу.
Вот небольшая цепочка, обозначающая неудачную попытку создать процесс/присеодиниться к процессу,если ValidAccessMask = 0.
(Create process)NtCreateProcess with DebugPort -> PspCreateProcess -> ObReferenceObjectByHandle with DEBUG_PROCESS_ASSIGN ->SeComputeDeniedAccesses -> STATUS_ACCESS_DENIED
(Attach to process)NtDebugActiveProcess ->( NT_SUCCESS return FALSE)ObReferenceObjectByHandle with DEBUG_OBJECT_ADD_REMOVE_PROCESS ->SeComputeDeniedAccesses -> STATUS_ACCESS_DENIED -> NT_SUCCESS(ObReferenceObjectByHandle) -> STATUS_NOT_SUPPORTED
Баста! Но я хочу ломать вообще все дебаггеры!(включая дебаггеры,которые уже работают)
Я босс системы и никто не имеет права ограничивать меня!
Ну, тогда скажите привет DbgkpProcessDebugPortMutex.
Вы уже видели это:
DbgkpProcessDebugPortMutex.Count = 1; (in DbgkpInitializePhase0)
DbgkpProcessDebugPortMutex используется в этих функциях:
DbgkpSetProcessDebugObject/DbgkCopyProcessDebugPort/DbgkpQueueMessage/DbgkpCloseObject/DbgkpMarkProcessPeb -> KeReleaseGuardedMutex/ExAcquireFastMutex -> ExpReleaseFastMutexContended
В ExAcquireFastMutex вызывается ExpAcquireFastMutexContended.
По идее,именно там и происходит бесконечный цикл,если переписать DbgkpProcessDebugPortMutex->Count на 0.
Вы не сможете дебажить после присоединения/начала дебага процесса,поскольку ваш отладчик тупо зависнет!
На мой взгляд это самый эффективный anti-debug трюк на данный момнт,который не тригерет при этом PG и позволяет затроллить реверсера
(представьте,что вы присоединились к лоадеру и ваш дебаггер просто ничего не может сделать,вы его даже не можете убить...).
Да, можно переписать данное значение на первоначальное, но если его будут лениво проверять через какое-то время - вы проиграли т.к DbgkpProcessDebugPortMutex используется во многих местах во время дебаггинга.
-Протекторы
Теперь перейдём к протекторам.
Первым будет Themida.
Themida/WinLicense:
У меня нет лицензии, поэтому я воспользуюсь взломанной версией(3.0.4.0)
x64
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtGetContextThread
NtSetInformationThread(ThreadHideFromDebugger)
x32
NtSetContextThread/NtGetContextThread
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetInformationThread(ThreadHideFromDebugger)
Так же они хукают DbgUiRemoteBreakin & DbgBreakPoint:
hook DbgUiRemoteBreakin -> LdrShutdownProcess
hook DbgBreakPoint -> ret
Так же поиск окон,но это мем и они даже не пытаются обойти в x32 банальный хук Wow64Transition.
Короче, Темида разочаровала сильно автора.
Теперь перейдём к моему фавориту т.е VMP:
На данный момент VMP( версия 3.6. build 1406) использует следующие трюки:
x32:
Manual read BeingDebugged in PEB
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetContextThread
NtSetInformationThread(ThreadHideFromDebugger)
CloseHandle(0xDEADC0DE) (not manual syscall)
NtSetInformationProcess with NULL (ProcessInstrumentationCallback) (not manual syscall)
ZwQuerySystemInformation with SystemKernelDebuggerInformation (Detect KM Debugger and not manual syscall)
ZwQuerySystemInformation with SystemModuleInformation (Detect KM Debugger and not manual syscall)
x64
Manual read BeingDebugged in PEB
NtQueryInformationProcess:ProcessDebugPort,ProcessDebugObjectHandle
NtSetInformationThread:ThreadHideFromDebugger
CloseHandle(0xDEADC0DE) (not manual syscall)
NtSetInformationProcess with NULL (ProcessInstrumentationCallback) (not manual syscall)
ZwQuerySystemInformation with SystemKernelDebuggerInformation (Detect KM Debugger and not manual syscall)
ZwQuerySystemInformation with SystemModuleInformation (Detect KM Debugger and not manual syscall)
Так же VMP уходит в manual syscall/sysentry(x32 system),чтобы обойти банальные хуки в ntdll начиная с версии 3.1.x.(если идёт поддерживаемая сборка т.к номера syscall'ов могут поменяться, иначе идёт вызов NTAPI)
Забавно,но для проверки CRC файла используется manual syscall NtClose,в то время для обнаружения отладчика просто используется CloseHandle.
Так же,если использовать anti-debug в GUI версии т.е встроенный в лоадер кода,то там будут идти проверки на CRC syscall(даже,если отключена опция защита памяти),
в то время как при использовании SDK функций такого нет.
Перед тем как скажу про heaven's gate,уточню один момент для новичков.
Для выполнения x32 кода в x64 системе был предуман WoW64.
Одна из многих целей - нормально выполнять syscall'ы и другие моменты в x32 коде.
В x64 системе у вас нет инструкции sysentry для выполнения+
у вас отсутствуют прямой доступ к регистрам r8/r9/r10 в x32 коде и другие моменты,хотя у вас есть syscall для которого эти параметры необходимы.
VMP в x32 code работающий под Wow64 смешивается с x64 кодом(heaven's gate):
jmp far 33:address т.е прыжок в x64 code.
Это делает забавный трюк,поскольку вы не можете нормально дебажить x64 code под WoW64 в x32 дебаггере.
Вот минимальный пример c выполнением x64 code под WoW64 с прямым syscall NtSetInformationThread(HideFromDebugger):
C-like:
jmp far 0x33:$+7 ;hello x64 code(cs under: x64 code - 0x33,x32 code & wow64 - 0x22,just windows x32 - 0x1B
/*
or
push 0x33
call $+5
add dwowd ptr ss:[esp],5
ret far
*/
mov rcx,-2 ;NtCurrentThread via psevdo-handle thread
mov edx,0x11 ;HideFromDebugger
xor r8d,r8d ;ThreadInformation
xor r9d,r9d ;ThreadInformationLength
mov r10,rcx
mov eax,0xD ;syscall number in Windows 10-11
syscall
call $+5 ;in x64 code we can't use jmp far 0x23:$+7
mov dword ptr ss:[esp+4],0x23
add dword ptr ss:[esp],5
ret far
//P.S 100% не работает в arm/arm64 т.к системные вызовы устроены по-другому
Так же они хукают DbgUiRemoteBreakin и делают ~ следующие:
C++:
//hook DbgUiRemoteBreakin -> push DEADC0DE -> VM Entry or
TerminateProcess(GetCurrentProcess,0xDEADC0DE);
Так же VMP пытается злоупотреблять логикой ядра,чтобы обнаружить неправильную логику хуков у anti-anti-debug tool.
На данный момент это :
1)перезапись ProcessInformation с помощью ReturnLength(адрес у ProcessInformation и ReturnLength одинаковый).
Пример детекта:
C++:
auto is_bad_overwrite_lenght() -> bool
{
HANDLE debug_object = NULL;
auto nt_status NtQueryInformationProcess(NtCurrentProcess, ProcessDebugObjectHandle, &debug_object, sizeof(debug_object), reinterpret_cast<PULONG>(&debug_object));
return debug_object != sizeof(debug_object) || NT_SUCCESS(nt_status);
}
2)Неправильный указатель.
Начиная с версии 3.5.1.x автор писал,что он добавил
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Они отправляют неправильный указатель в ReturnLength(1),но в TitanHide нет проверки на входный буффер с помощью ProbeForRead /ProbeForWrite перед записью буффера.
~детект выглядит как-то так
C++:
auto is_titan_hide() -> bool
{
HANDLE debug_object = reinterpret_cast<HANDLE>(1);
auto nt_status = NtQueryInformationProcess(NtCurrentProcess, ProcessDebugObjectHandle, &debug_object, lenght, reinterpret_cast<PULONG>(0x1));
return debug_object != reinterpret_cast<HANDLE>(1) && STATUS_ACCESS_VIOLATION ==nt_status;
}
Небольшой бонус для новичков ;) :
Dark_Bull писал 2 года назад обход anti-debug'a в VMP 3.5.1213.
Разработчики VMP спустя столько времени не исправили главную ошибку - они не шифруют syscall.
Да, они изменили вход в VM,но никто не запрещает искать по паттерну syscall: 0F 05( в x64 системе)
Если вы используете anti-debug от лоадера, то syscall будет располагаться в самой последней секции VMP,а syscall для SDK функции в самой первой секции VMP.
Да,если включена опция упаковки,то первая секция будет зашифрована,но посмотрите просто как работает распаковка.
Вы можете легко найти syscall и подделать результат(HWBP + немного ловкости рук и у вас всё легко получится).
Вот информация для новичков и RVA/паттерн anti-debug'a + файл(в конце статьи):
1)Для NtQueryInformationProcess вы можете подделать NTSTATUS на отрицательный(например на -1),после выполнения syscall'a(тесты показали,что VMP проверяет NTSTATUS так:NT_SUCCESS(nt_status),а не например nt_status != STATUS_PORT_NOT_SET)
2)Для NtSetInformationThread вы должны подделать класс/номер syscall'a до его выполнения,иначе поток будет спрятан.
3)Для CloseHandle вы должны вернуть именно STATUS_INVALID_HANDLE(если измените класс/номер syscall'a вручную),иначе будет обнаружения дебаггера.
Просто попробуйте и вы поймёте,что это не так сложно + автор добавил RVA/паттерн для файла, чтобы новичкам было легче:
- Идея VMP
Общаясь с Пермяковым, мне в одно время пришла идея:"А в чём проблема просто обнаружить все anti-anti-debug-tool?".
Ну, я написал
Пожалуйста, авторизуйтесь для просмотра ссылки.
и задену эту тему, поскольку это попросту интересно ^^.Я не буду разбирать все баги, поскольку нет смысла в этом.
Приведу 2 примера с описанием,а дальше все зависит от вас :D.
Начнём с SharpOD/SchyllaHide и хука NtSetInformationThread:
C++:
/*
https://github.com/x64dbg/ScyllaHide/blob/a0e5b8f2b1d90be65022545d25288f389368a94d/HookLibrary/HookedFunctions.cpp#L34
*/
NTSTATUS NTAPI HookedNtSetInformationThread(HANDLE ThreadHandle, THREADINFOCLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength)
{
if (ThreadInformationClass == ThreadHideFromDebugger && ThreadInformationLength == 0) // NB: ThreadInformation is not checked, this is deliberate
{
if (ThreadHandle == NtCurrentThread ||
HandleToULong(NtCurrentTeb()->ClientId.UniqueProcess) == GetProcessIdByThreadHandle(ThreadHandle)) //thread inside this process?
{
return STATUS_SUCCESS;
}
}
return HookDllData.dNtSetInformationThread(ThreadHandle, ThreadInformationClass, ThreadInformation, ThreadInformationLength);
}
Если вы подумаете слегка, то увидите минимум 2 ошибки:
Во-первых, идёт проверка является ли HANDLE нашим процессом,
но никто не запрещает временно перезаписать UniqueProcess в TEB,чтобы скрыть поток или можно заставить вернуть STATUS_SUCCESS,
передав в качестве HANDL'а NULL и временно перезаписать UniqueProcess в TEB на NULL
Во-вторых, можно открыть потом(OpenThread) без прав THREAD_QUERY_INFORMATION ,
благодаря чему проверка с помощью GetProcessIdByThreadHandle(NtQueryInformationThread с ThreadBasicInformation) благополучно обосрётся.
Пример:
C++:
auto is_bad_hide_hook() -> bool
{
HANDLE uniq_process_id = NtCurrentTeb()->ClientId.UniqueProcess;
NtCurrentTeb()->ClientId.UniqueProcess = reinterpret_cast<HANDLE>(NULL);
auto nt_status = NtSetInformationThread(NULL, ThreadHideFromDebugger, NULL, NULL);
NtCurrentTeb()->ClientId.UniqueProcess = reinterpret_cast<HANDLE>(uniq_process_id)
return nt_status == STATUS_SUCCESS;
}
Дальше мем с которым не могли долгое время справиться почти все anti-anti-debug tool's и речь про NtClose.
Обычно функция перехватывается и просто возвращается ожидаемый NTSTATUS(STATUS_INVALID_HANDLE/STATUS_HANDLE_NOT_CLOSABLE) и не вызывали ошибку.
Однако, ядро вызовет KeRaiseUserException(STATUS_INVALID_HANDLE)/KeRaiseUserException(STATUS_HANDLE_NOT_CLOSABLE) не только при присутствии DebugPort у EPROCESS,но и если
в NtGlobalFlag установлен флаг FLG_ENABLE_CLOSE_EXCEPTIONS или включена трассировка HANDLE'ов.
Вы не можете установить FLG_ENABLE_CLOSE_EXCEPTIONS используя NtSetSystemInformation,поскольку система очищает некоторые флаги,
но можно включить трассировку с помощью ProcessHandleTracing(NtSetInformationProcess ) и
благодаря этому будет вызываться исключение, если закрыть невалидный/защищенный HANDLE!
Сам пример:
C++:
auto is_very_lazy_hook() -> bool
{
bool is_lul_hook = FALSE;
PROCESS_HANDLE_TRACING_ENABLE tracing_handle = { 0 };
auto nt_status = NtSetInformationProcess(NtCurrentProcess, ProcessHandleTracing, &tracing_handle, sizeof(tracing_handle));
if (!NT_SUCCESS(nt_status))
return FALSE;
__try
{
NtClose((HANDLE)(0x13333337));
is_lul_hook = TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
is_lul_hook = FALSE;
}
NtSetInformationProcess(NtCurrentProcess, ProcessHandleTracing, &tracing_handle, NULL);
return is_lul_hook;
}
Дальше я скажу, про что мне лень было говорить:
1)На самом деле можно дебажить процесс без создания DebugPort'а,но вам придётся похукать много функций, но Китайцы сделали мемный
Пожалуйста, авторизуйтесь для просмотра ссылки.
:2)Для людей,которые не знают как работают Heaven’s Gate,рекомендую прочитать:
Пожалуйста, авторизуйтесь для просмотра ссылки.
Пожалуйста, авторизуйтесь для просмотра ссылки.
3)Я щё не изучил как работает anti-debug VMP в x32 системе полностью, но если окажется что-то неправильно в этой статье, то исправлю это в ближайшее время ;) .
Ну,вот и всё.
Данная забавная статья подошла к концу.
Надеюсь, вам было интересно или хотя бы весело :)
Пожалуйста, авторизуйтесь для просмотра ссылки.
Огромное спасибо colby57 за опыт.
Пожалуйста, авторизуйтесь для просмотра ссылки.
Последнее редактирование: