Весенний шалом. ПРЕДИСЛОВИЕ Думаю у меня будет ещё одна статья связанная с лоадером, и на этом мой реверс всяких других лоадеров прекратиться, потому что надоело постяонно видеть схожие трюки, которые я видел до этого. Поэтому буду думать, что реверсить дальше. Спасибо @Senior_Gamania за то, что откопал мне этот лоадер :) ЛОАДЕР Лоадер полностью на х64 и оснащен VMProtect, также помимо фулл-пресета ( виртуализации важных функции, антидебаг, крц и т.д. ) там решили абузить SDK VMP, это означает, что помимо самописных трюков я встречу и трюки VMP ( так и было ) 1. ИНИЦИАЛИЗАЦИЯ ЗАЩИТЫ Перед тем как рисовать меню лоадера вызывается инит защиты со всякими трюками, о которых я поведаю чуть позже Вся фишка в том, что пролог функции чист, но для выполнения основного кода лоадер прыгает в ВМ, чтобы выполнить всякие трюки, о которых я щас и поведаю На ВМ сейчас не будем тратить время, так как там до вызова первого трюка ничего такого не исполнялось, на что можно было бы обратить внимание, однако и там будут свои приколы :) 1.1. Шелл-код для попытки сломать аттач к процессу После выхода из ВМ мы попадаем на наш первый трюк, где в связке шеллкода и лейзиимпорта мы получаем две пропатченные ntdll.dll функции А именно - DbgUiRemoteBreakin && DbgBreakPoint. Они заменяются непосредственно на вызов FatalExit ( другое название ExitProcess, только для х64 ) Вначале лоадер берет адреса трёх функции через [Вызов осуществляется с помощью LazyImporter] GetProcAddress( GetModuleHandleA( "kernel32.dll" ); "ExitProcess" ); [Вызов осуществляется с помощью LazyImporter] GetProcAddress( GetModuleHandleA( "ntdll.dll" ); "DbgUiRemoteBreakin" ); [Вызов осуществляется с помощью LazyImporter] GetProcAddress( GetModuleHandleA( "ntdll.dll" ); "DbgBreakPoint" ); Как контрить LazyImport я рассказывал в своей предыдущей теме, прочитать можно тут - https://yougame.biz/threads/247347 Тут коротко говоря эти функции вызываются через два регистра: RBX && RDI, ставим на них бряки и выходим на эти WinAPI-функции Следующим этапом идет сам вызов функции с шеллкодом, который принимает два параметра: Адрес в который будут записывать и адрес, откуда будут брать байты Сурс-код функции DbgShellcodePatch BOOL __fastcall* DbgShellcodePatch( void* first_addr, void* second_addr ) { DWORD old_protect; std::uint8_t shellcode[ ] = { 0x50, 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x87, 0x04, 0x24, 0xC3 }; *reinterpret_cast< void** > ( shellcode + 0x4 ) = second_addr; VirtualProtect( first_addr, 15, PAGE_EXECUTE_READWRITE, &old_protect ); memcpy( first_addr, shellcode, 15 ); VirtualProtect( first_addr, 15, old_protect, &old_protect ); return TRUE; } Возможно я тут где-то намудрил, но не суть важно :) В итоге в лоадере получается такой шеллкод С помощью последней инструкции xchg qword ptr ss:[rsp], rax Подменяется адрес возврата на тот, что сидит в rax, то есть FatalExit, эта инструкция также используется для подмены адреса возврата, чтобы вызвать нужный импорт в VMP. Однако это уже совсем другая история :) Почему же патчат именно DbgUiRemoteBreakin? Давно натыкался на статью в Хабре, этому трюку аж 9 лет уже, и вот скрин с объяснением Данный трюк более не актуален для людей, которые часто обновляют x64dbg, потому что дебаггер не использует эту функцию во время отладки Если мне приходилось в прошлом году сидеть и ломать точно такой же трюк у лоадера интериума патчами, то тут уже сами разрабы дебаггера избавили меня от "неебического" анти-аттача :) Точно не могу сказать с какой версии дебаггера такой трюк зафиксили, поэтому +- на ком-то этот трюк может и сработать После этого трюка мы уходим снова в ВМ протектора и спустя некоторое время попадаем на новый трюк 1.2. Детект запускаемых процессов и драйверов Этот трюк не раз будет появляться, он появится во время попытки авторизации, но сейчас нас интересует текущий вызов. По всем канонам паблик методов антидебага нас встречает связка из нескольких вызовов: HANDLE hSnapshot = CreateToolhelp32Snapshot( 2, 0 ); Process32First( hSnapshot, &processEntry32Struct ); Process32Next( hSnapshot, &processEntry32Struct ); Увы, но детект происходит через функцию strstr в цикле, берется один условный системный процесс и сравнивается с 3 процессами: idaq, processhacker, cheatengine Вам ничего не мешает хукнуть strstr в vcruntime140 и возвращать 0 в некоторых случаях. Если при переборе обнаружили процесс, то в dil присваивается 1 и в последующем при проверке вызывается функция, открывающая хендл процесса и закрывает сам процесс Выглядит это всё дело так: HANDLE hProcess = OpenProcess( 0x1FFFFF, 0, ProcessId ); TerminateProcess( hProcess, 0 ); CloseHandle( hProcess ); Переходим к детекту драйверов, тут вызываются несколько функции ( в том числе и strstr ), а именно: K32EnumDeviceDrivers K32GetDeviceDriverBaseNameA Опять же при детекте задается 1 регистру bl и после сравнения вас кидает на закрытие сервиса найденного драйвера. К слову ищет он: kprocesshacker.sys, sbie (Sandboxie), gwdrv (GlassWire), npf.sys, uupacket.sys, kdhack64 и 360antih Вот сурс-код закрытия сервиса: BOOL __fastcall ShutdownService( const char* DriverName ) { SC_HANDLE hManager = OpenSCManagerA( 0, 0, 2, ); struct _SERVICE_STATUS ServiceStatus; if ( hManager ) { SC_HANDLE hDriverService = OpenServiceA( hManager, DriverName, 0xF01FF ); if ( hDriverService ) { ControlService( hDriverService, 1, &ServiceStatus ); } CloseServiceHandle( hManager ); CloseServiceHandle( hDriverService ); return TRUE; } return FALSE; } Закончили с детектами, переходим к следующему трюку 1.3. Защита системных вызовов На этом этапе защиты я столкнулся уже непосредственно с самими системными вызовами, из ВМ перебираются абсолютно все ntdll-функции, имеющие свой системный номер и вызывается функция, которая принимает два параметра: поинтер на строку нужной функции, и её системный номер Далее мы возвращаемся снова в ВМ, где очень часто будет читаться наш поинтер на строку, в общем, после выхода из ВМ протектора мы попадаем к инициализации самого шеллкода. Аллоцируется память через malloc, затем заполняется некий шеллкод, который ну уж очень сильно палился в открытом виде. Заготовка для шеллкода выглядит так: Если переводить с байт-кода на ассемблер, то мы получаем вот такие 4 инструкции mov r10, rcx mov eax, 0x0 syscall ret Почему 0х0? Я не зря назвал это заготовкой для системного номера, в дальнейшем когда выполнение программы вернётся в ВМ именно там начнется запись шеллкода, вместо 0х0 в ВМе будет записываться системный номер нужной нтдлл функции, который мы получили еще на первой функции Вот небольшой список тех функции, чей системный номер он брал, пока я мониторил поведение функции NtAcceptConnectPort NtAccessCheck NtAccessCheckAndAuditAlarm NtAccessCheckByType NtAccessCheckByTypeAndAuditAlarm NtAccessCheckByTypeResultList NtAccessCheckByTypeResultListAndAuditAlarmByHandl NtAcquireCrossVmMutant NtAcquireProcessActivityReference NtAddAtom NtAddAtomEx NtAddBootEntry NtAddDriverEntry NtAdjustGroupsToken NtAdjustPrivilegesToken NtAdjustTokenClaimsAndDeviceGroups NtAlertResumeThread NtAlertThread NtAlertThreadByThreadId NtAllocateLocallyUniqueId NtAllocateReserveObject NtAllocateUserPhysicalPages NtAllocateUserPhysicalPagesEx NtAllocateUuids NtAllocateVirtualMemory NtAllocateVirtualMemoryEx NtAlpcAcceptConnectPort NtAlpcCancelMessage NtAlpcConnectPort NtAlpcConnectPortEx NtAlpcCreatePort NtAlpcCreatePortSection NtAlpcCreateResourceReserve NtAlpcCreateSectionView NtAlpcCreateSecurityContext NtAlpcDeletePortSection NtAlpcDeleteResourceReserve NtAlpcDeleteSectionView.NtAlpcDeleteSecurityContext NtAlpcDisconnectPort NtAlpcImpersonateClientContainerOfPort NtAlpcImpersonateClientOfPort NtAlpcOpenSenderProcess NtAlpcOpenSenderThread NtAlpcQueryInformation NtAlpcQueryInformationMessage NtAlpcRevokeSecurityContext NtAlpcSendWaitReceivePort NtAlpcSetInformation NtApphelpCacheControl NtAreMappedFilesTheSame NtAssignProcessToJobObject NtAssociateWaitCompletionPacket NtCallEnclave NtCallbackReturn NtCancelIoFile NtCancelIoFileEx NtCancelSynchronousIoFile NtCancelTimer NtCancelTimer2 NtCancelWaitCompletionPacket NtClearEvent NtClose NtCloseObjectAuditAlarm NtCommitComplete NtCommitEnlistment NtCommitRegistryTransaction NtCommitTransaction NtCompactKeys NtCompareObjects NtCompareSigningLevels NtCompareTokens NtCompleteConnectPort NtCompressKey NtConnectPort NtContinue NtContinueEx NtConvertBetweenAuxiliaryCounterAndPerformanceCounter NtCreateCrossVmEvent NtCreateCrossVmMutant NtCreateDebugObject NtCreateDirectoryObject NtCreateDirectoryObjectEx NtCreateEnclave NtCreateEnlistment NtCreateEvent NtCreateEventPair NtCreateFile NtCreateIRTimer NtCreateIoCompletion NtCreateJobObject NtCreateJobSet NtCreateKey NtCreateKeyTransacted NtCreateKeyedEvent NtCreateLowBoxToken NtCreateMailslotFile NtCreateMutant NtCreateNamedPipeFile NtCreatePagingFile NtCreatePartition NtCreatePort NtCreatePrivateNamespace NtCreateProcess NtCreateProcessEx NtCreateProfile NtCreateProfileEx NtCreateRegistryTransaction NtCreateResourceManager NtCreateSection NtCreateSectionEx NtCreateSemaphore NtCreateSymbolicLinkObject NtCreateThread NtCreateThreadEx NtCreateTimer NtCreateTimer2 NtCreateToken NtCreateTokenEx NtCreateTransaction NtCreateTransactionManager NtCreateUserProcess NtCreateWaitCompletionPacket NtCreateWaitablePort NtCreateWnfStateName NtCreateWorkerFactory NtDebugActiveProcess NtDebugContinue NtDelayExecution NtDeleteAtom NtDeleteBootEntry NtDeleteDriverEntry NtDeleteFile NtDeleteKey NtAcceptConnectPort Конечно не все эти функции будут использоваться в дальнейшем, и это далеко не все функции, у которых брались системные номера. Была ещё одна проблема, эти сисколлы зачастую бывали разбросаны по разным регионам памяти ( возможно из-за malloc, я точно не знаю ), а вылавливать их бряками было бы полным самоубийством. Был вариант поставить поставить патч на инструкцию mov word ptr ds:[rax+0x30], 0x50F Вместо syscall мы бы записали ud2, т.е. 0F 0B и на выходе мы бы получили у всех сисколлов такой вид: mov r10, rcx mov eax, syscall_number ud2 ret Тем самым мы бы получили эксепшн и вышли бы моментально на тот системный вызов, который выполнялся, например при авторизации или инжекте Опять же проблема заключается в том, что тут сильно абузят SDK протектора, когда я анализировал чтение секции у протектора для создания хэша, то в вызове функции VMProtectIsValidImageCRC насчитал где-то 5 калькуляторов. Изначально я подумывал написать тамполайн хук, который бы сверял адрес, который берет для чтения, так к примеру при чтении адреса с "mov word ptr ds:[rax+0x30], 0x50F" вместо 0B ставили 05, как и должно быть по оригиналу. Но протектор мешается ещё тем, что в функции VM остальные 4 калькулятора тоже проходятся катком по друг другу, т.е. сверяя байты друг у друга. Т.е. мой тамполайн хук тут тяжело очень реализовать. Пришлось бы писать целую систему из проверок :roflanEbalo: И, кстати, вот какой бы получился сисколл при моем патче 1.4. Промапленный ntdll.dll и парсер системных номеров После того как я зафиксил полностью импорты в лоадере я обнаружил два интересных импорта, а именно fopen_s и fread. На первый взгляд кажется, шо они не так важны и можно забить на них. Только вот эти два импорта вызываются в виртуальной машине протектора, а как мы знаем в лоадере вантапа пытаются самое важное виртуализировать. Суммируем эти два фактора и получаем вполне логичное основание хукнуть fopen_s. Ставим хук и получаем два вызова, один читает у нас файл hosts, а другой читает уже сам ntdll.dll Пройдясь по регистру я нашел адрес, в котором сидел полностью промапленный ntdll.dll Дабы полностью удостовериться, что это именно он, я решил пройтись по файлу и найти таблицу системных номеров И к счастью я её нашёл :) Спустя ещё некоторое время я подумал, каков вообще смысл его было читать? Неужели сисколлы берутся именно отсюда? Именно так всё и было, но с одним НО, чтение сисколлов производится в виртуальной машине протектора. +- я уже понимал логику получения системного номера Как раз во время трассировки я обнаружил вместе с чтением в стеке и NtLoadDriver. С первым комментарием я чуть-чуть ошибся, но не важно :) Итоги На этом мой реверс лоадер Onetap полностью приостанавливается из-за нехватки времени на полный реверс, хотелось бы разобрать и рассказать про трюки во время авторизации, где опять же происходит злоупотребление сдк протектора, но сейчас на вантап у меня не осталось времени. Довольно интересный лоадер, жалко, что у меня не было подписки, потому что во время авторизации ни один сисколл в аллоцированной памяти не выполняется, возможно они бы выполнялись во время инжекта в игру. Спасибо за прочтение данной статьи :) Ещё конечно же хотелось бы поблагодарить мою команду, а именно: @Arting, @nelfo57, @Dark_Bull, @easton Хотим ещё раз напомнить, что у нас есть пока что маленькое коммьюнити реверсеров на нашем дискорд-сервере, поэтому залетайте туда :D Ссылка на Team Enterial | Community: https://discord.gg/dDpdBUf8jM