Информационные технологииStfw.Ru 🔍

Запуск процесса из режима ядра

Секреты Win32 Запуск процесса из режима ядра [C] Cardinal
🕛 15.09.2005, 00:17
Скачать исходники _http://wasm.ru/pub/21/files/CreateProcessKrnlDrv.rar

Думаю, многие из Вас задавались подобным вопросом. Возможен ли запуск пользовательского приложения из драйвера режима ядра? В системах 9х (win95, 98, ME) для этого священнодействия существует специальный экспорт ShellVxd_ShellExecute, позволяющий при передаче соответствующего тега функции запустить приложение. В NT-системах специализированного для этого дела экспорта не существует.

Но, если его не существует, то это отнюдь не значит, что совершить данное действие не представляется возможным. Как раз таки наоборот - очень даже возможно. Конечно, Вы могли подумать, это ж какой кошмар, разобрать кучу системных объектов, внимательно изучить их свойства и методы и т.д. Успокойтесь, все намного проще. Давайте немного расслабимся и посмотрим на проблему с несколько нестандартной точки зрения.

Для начала оттолкнемся от действительности. Внимательно просмотрев экспорт модуля ntoskrnl.exe мы не находим готовой “прямой” функции, которая могла бы дать жизнь новому процессу. DDK об этом тоже умалчивает. Однако мы знаем, процесс порождается экспортами пользовательского модуля kernel32.dll:CreateProcessA, CreateProcessW, CreateProcessInternalA, CreateProcessInternalW, описание которых и наборы параметров легко найти в документации PSDK. Конечно, предпринимались попытки эмуляции данной функции в обход модуля kernel32.dll, и довольно успешные. Описание метода и соответствующий код можно найти в справочнике по Native Api WinNT Гарри Неббета. Собственно тут тоже нет ничего невероятного, процесс, тем не менее, всё равно запускался из пользовательского режима путём прямого обращения к экспорту вышестоящего модуля ntdll.dll. Но это все равно не давало ответа на вопрос, каким образом, находясь в режиме ядра выполнить то же самое?

Собственно, метод, предлагаемый мною, ничем экстравагантным не блещет в том плане, что мы не будем самостоятельно конструировать объекты ядра и приводить в движение соответствующие ядерные механизмы своими силами и разумом. Ведь мы знаем, что никто лучше самой операционной системы этого не сделает, и вряд ли ядро сможет полностью одобрить нашу самодеятельность. Но, мы всё-таки запустим приложение из драйвера режима ядра, но…в пользовательском режиме. Ну что ж, начнем наше повествование.

Метод предельно прост. Сейчас я немного объясню механизм последнего, и далее, статью продолжим уже разбором драйверного кода. Живая идея заключена в том, чтобы “поймать” пользовательский поток какого-либо процесса и “навязать” ему свои условия, т. е., изменить ход выполнения последнего и перенаправить его на заранее заготовленный в пользовательском адресном пространстве код, который в свою очередь вызовет функцию kernel32.dll:CreateProcessA.

Каким образом это можно сделать? Первым делом нужно найти то место, где поток из режима ядра возвращается в пользовательский. Как происходит переключение режимов процессора из пользовательского в ядерный и обратно? При помощи шлюза прерывания/быстрого системного вызова. В данном случае, хорошо подумав, мы не находим ничего лучшего как системный сервис INТ 0x2E/SYSENTER, именно в этом месте поток меняет свой уровень привилегий.

Конечно, есть и другие подобные сервисы, например отладчик int 0x2d, или таймер int 0x2a. Но, нам нужно нечто стабильное, используемое большинством процессов и как можно чаще. Так что, после всех поисков, лучшим претендентом на главную роль будет являться интерфейс шлюза системного сервиса INТ 0x2E/SYSENTER. Так каким же образом мы можем использовать эту лазейку? Само собой нужно знать, что при вызове прерывания происходит перезагрузка селекторов в сегментные регистры сохраненными ранее в TSS значениями, главным образом регистров CS и SS. После прохождения шлюза в сторону ядра в стеке сохраняется адрес возврата на следующую инструкцию после “шлюзовой”. В случае инструкции sysenter(winXP+) ситуация аналогична. Вот этот адрес нам и нужен, и как видите дотянуться до него совсем не проблема. Далее, следуя логике, нам необходимо перехватить функцию _KiSystemService методом врезки в нее кода нашего обработчика. И тут возникает вопрос, а как нам вообще найти функцию _KiSystemService?

С одной стороны ясно и понятно, что на нее указывает вектор INT 0x2E. Но, ситуация оказывается неоднозначной в случае ядер WinXP+. Вы знаете, что в зависимости от фичей процессоров последние, более новые версии где-то 97-года выпуска и выше содержат инструкцию быстрого системного вызова SYSENTER/SYSEXIT. Инструкция SYSENTER аналогична инструкции INT с той лишь разницей, что не хранит вектор прерывания в таблице IDT с последующий его выборкой и передачей управления, а вызывает код, адрес которого расположен в одном из регистров MSR. А зачем в таком случае нам лишние поиски? К тому же еще и известно, что MSR вещь далеко непостоянная. Но, несмотря на это вектор int 0x2e в таблице IDT всё равно будет указывать на начало кода _KiSystemService, хотя и не будет использоваться, то есть, найти её, видимо, не проблема.

Боюсь разочаровать - Вы ошибаетесь. А что если INT 0x2E будет перехвачена? Например, это любит делать ntice. Тогда во время отладки кода мы не будем иметь возможности “воочию лицезреть” данную функцию. Кроме того, в экспорте ntoskrnl.exe она не упоминается. Но, не все так плохо. Подумаем, как ее можно найти. Сканировать всю память на нахождение определенных уникальных функции _KiSystemService наборов инструкций не имеет смысла - не так уж и уникально ее содержимое. Решение здесь довольно простое. Как мы знаем, _KiSystemService является переходником к функциям ядра, следовательно, внутри функции присутствует код прямого вызова ядерного сервиса. Посмотрим на код ниже.

 0008:804DA113 5A POP EDX 0008:804DA114 FF0538F6DFFF INC DWORD PTR [FFDFF638] 0008:804DA11A 8BF2 MOV ESI,EDX 0008:804DA11C 8B5F0C MOV EBX,[EDI+0C] 0008:804DA11F 33C9 XOR ECX,ECX 0008:804DA121 8A0C18 MOV CL,[EBX+EAX] 0008:804DA124 8B3F MOV EDI,[EDI] 0008:804DA126 8B1C87 MOV EBX,[EAX*4+EDI] 0008:804DA129 2BE1 SUB ESP,ECX 0008:804DA12B C1E902 SHR ECX,02 0008:804DA12E 8BFC MOV EDI,ESP 0008:804DA130 3B35D4C75480 CMP ESI,[ntoskrnl!MmUserProbeAddress] 0008:804DA136 0F83A1010000 JAE 804DA2DD 0008:804DA13C F3A5 REPZ MOVSD 0008:804DA13E FFD3 CALL EBX 0008:804DA140 8BE5 MOV ESP,EBP

Мы видим фрагмент _KiSystemService и ту самую инструкцию CALL EBX которая вызывает определенную ядерную функцию, предварительно выбранную из таблицы сервисов, загружая адрес в регистр EBX(об этом подробно можно почитать в предыдущей моей статье “Слежение за вызовом функций Native Api”).

Функция _KiSystemService непосредственно недоступна из таблицы экспорта ntoskrnl.exe, но зато у нас имеется экспорт указателя на таблицу KeServiceDescriptorTable, из которой можем извлечь адрес требуемой функции Native Api, к примеру, NtReadFile. Далее, функция будет вызвана в контексте какого-либо процесса той самой инструкцией CALL EBX и возвратится в окрестность кода _KiSystemService на следующую инструкцию.

В предыдущей статье освещался вопрос, касавшийся перехвата NativeApi функций путем внедрения своих обработчиков в таблицу сервисов взамен оригинального, и затем последовательный вызов обоих. Этот же метод применен и здесь. К примеру, мы перехватили функцию NtReadFile как самую наиболее часто вызываемую, затем, внутри нашего обработчика отследили по стеку адрес возврата функции обратно в _KiSystemService.
Продемонстрируем это на коде:

 DWORD FindKiSystemServiceOriginalEntryPoint() { KPRIORITY CurrentThreadPriority; // установим перехватчик на NtReadFile, как наиболее часто вызываемая функция CurrentThreadPriority = KeQueryPriorityThread(KeGetCurrentThread()); // чтобы система не деградировала KeSetPriorityThread(KeGetCurrentThread(),1); 
 if( *NtBuildNumber == 2195 ) 

OriginalNtReadFile = 

*KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195]; if( *NtBuildNumber == 2600 ) 

OriginalNtReadFile = 

*KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600];
 disableinterruptions clearwp
 if( *NtBuildNumber == 2195 ) 

KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195] = 

ArtificialNtReadFile; if( *NtBuildNumber == 2600 ) 

KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600] = 

ArtificialNtReadFile;
 restorewp enableinterruptions
 while( !FindingForWhile ); 
// ждать, пока будет вызвана NtReadFile, надеюсь не вечно :)

// похоже, адрес найден, снимаем обработчик ArtificialNtReadFile() disableinterruptions clearwp
 if( *NtBuildNumber == 2195 ) 
KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195] = 
*OriginalNtReadFile; if( *NtBuildNumber == 2600 ) 
KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600] = 
*OriginalNtReadFile; 
 restorewp enableinterruptions
 KeSetPriorityThread(KeGetCurrentThread(),CurrentThreadPriority);
// восстанавливаем прежний
// теперь вычислим оригинальную точку входа в KiSystemService return NtReadFileReturnAddress - 0xC4;
// такова разница м\д точкой входа и точкой возврата внутри KiSystemService }

Обработчик NtReadFile: 

_declspec(naked) ArtificialNtReadFile()
{ _asm { push dword ptr [esp] // !!! pop NtReadFileReturnAddress inc dword ptr FindingForWhile // нашли адрес требуемой функции jmp dword ptr OriginalNtReadFile }
}

Что это дает Вы уже, надеюсь, догадались. Анализ кода функции _KiSystemService в различных версиях win nt показал идентичность последней почти во всех ядрах NT кроме WinXPSP2 и 2K3. (В данном случае в код драйвера были внесены некоторые незначительные изменения для обеспечения совместимости.) Этим можно воспользоваться. Теперь, я думаю, стало понятно, что, если мы нашли некоторый адрес внутри функции, а функция имеет определенный размер, то нетрудно найти ее startup. Так вот, основываясь на раскопках, расстояние от инструкции, следующей за call ebx, адрес которой мы нашли в стеке обработчика и до startup составляет 0xC4 байт (Win XP, 2K). Теперь нетрудно подсчитать адрес начала _KiSystemService.

// такова разница м\д точкой входа и точкой возврата внутри KiSystemService
return NtReadFileReturnAddress - 0xC4; 

Как следствие всего вышепроделанного мы заполучили оригинальную точку входа в _KiSystemService.

Итак, первый этап пройден, идем дальше. Следующим великим шагом к достижению цели должен стать поиск площадки для размещения кода, вызывающего kernel32:CreateProcessA. Разместить данный код мы, естественно, можем только в пользовательском адресном пространстве, причем, в области, доступной и обозреваемой большинством процессов. Ничего более подходящего на ум не приходит, кроме как использовать пустующее, после выравнивания место в первой странице модуля kernel32.dll, следующего сразу за заголовком. После загрузки в память и выравнивания из 0x1000 байт заголовок PE занимает где-то 0x400 байт, остальное место заполняется нулями и вполне может быть использовано нами. Но, это уже вопрос второстепенный, а первичный заключается в том, что для начала нужно отыскать базу kernel32.dll, при этом находясь в режиме ядра.

Впрочем, это тоже вопрос не столь сложный. Собственно, все, что нам надо мы можем найти в структуре переменных окружения процесса PEB. Некоторое описание и консистенцию данной структуры можно найти в книге Свена Шрайбера по недокументированной win nt. Но, я не нашел в книге описания конкретной структуры, которая могла бы нам указать на список загруженных в процесс пользовательских модулей. Поэтому пришлось вооружиться отладчиком и слегка покопать. Если взглянуть на PEB, то поле pPeb->ProcessModuleInfo->ModuleHeader.List3 вызывает неподдельный интерес. Именно так этот элемент (List3) был именован в книге. Поэкспериментировав с ним, я понял, что это структура типа ListEntry примерно следующего вида, как я ее описал(возможно она представлена здесь в далеком от совершенства виде, в отличие от того, как изначально была определена кодерами из Майкрософт):

typedef struct _KMODULEINFOLISTENTRY { DWORD Flink; DWORD Blink; DWORD ModuleIBase; DWORD DllEntryPoint; DWORD Unknown2; DWORD Unknown3; PWCHAR ModuleName;
} KMODULEINFOLISTENTRY, *PKMODULEINFOLISTENTRY, **PPKMODULEINFOLISTENTRY;

Некоторые поля для меня так и остались загадкой, да собственно только лишь потому, что не особо интересовали, остальные, я думаю, в пояснении не нуждаются. Таким образом, используя данную структуру, мы находим базовый адрес kernel32.dll при помощи функции DWORD kwsGetModule(PWCHAR ModuleNameW) которую можно посмотреть в исходных текстах драйвера. Собственно, данную функцию можно использовать для поиска различных модулей в окружении процесса.

Теперь вернемся к поиску наиболее пригодной среды для размещения кода, вызывающего функцию kernel32:CreateProcessA. Как мы ранее договорились, код будет размещен в первой странице модуля kernel32.dll непосредственно за заголовком в свободном пространстве. Таким образом, мы убиваем двух зайцев: код, выложенный в данном месте, будет виден всем процессам, и, кроме того, мы получаем достаточно места для размещения имплантата и необходимых функции CreateProcessA структур. Все бы хорошо, но и здесь есть один нюанс. Дело в том, что первая страница PE модуля доступна в пользовательском пространстве только для чтения, имеет атрибут readonly. Чтобы детально понять, в чем заключена проблема, обратимся к документации PSDK и рассмотрим функцию CreateProcess более подробно:

BOOL CreateProcess( LPCTSTR lpApplicationName, // name of executable module LPTSTR lpCommandLine, // command line string LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD BOOL bInheritHandles, // handle inheritance option DWORD dwCreationFlags, // creation flags LPVOID lpEnvironment, // new environment block LPCTSTR lpCurrentDirectory, // current directory name LPSTARTUPINFO lpStartupInfo, // startup information LPPROCESS_INFORMATION lpProcessInformation // process information
);

typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; 
} STARTUPINFO, *LPSTARTUPINFO;

typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; 
} PROCESS_INFORMATION; 

Как видите, функция принимает на стек 10 двойных слов, большая часть которых является указателями. Некоторые из них необязательны и могут быть опущены передачей NULL, а вот, к примеру, lpStartupInfo, lpProcessInformation и ProgExeNameAddrinUser должны быть обязательно определены. (более детальную информацию о правилах вызова функции и передаче ей параметров я приводить не буду, для этого использовать MSDN) Соответственно, для них нужно выделить место. Но это лишь часть проблемы.

Другая ее часть заключается в том, что кроме обеспечения реального существования самих структур, CreateProcess должна еще и писать в их поля. Такой возможности у нас нет, поскольку страница доступна только для чтения, и, любая попытка записи в нее приведет к исключению, которое может уронить текущий процесс - донор. Для того, что бы заиметь возможность писать в данную страницу в пользовательском режиме, мы должны изменить атрибуты в PTE дескрипторе данной страницы. Это можно сделать двумя способами: Вызвать NtProtectVirtualMemory в режиме ядра, при этом надо учесть, что открытого экспорта нет, и нам придется искать точку входа в таблице SST, либо непосредственно самостоятельно найти нужный страничный дескриптор и исправить эту досадную “ошибку”. Что мы собственно и сделали, вызвав специально написанную по этому случаю функцию PageAccessProp(…), принимающую на стек 4 параметра, которые в свою очередь я думаю, в объяснении не нуждаются.

Кроме того, дополню, что данную функцию можно использовать и в том случае, когда, к примеру, необходимо произвести манипуляции со страницей такого рода, что бы заполучить возможность записи в системную страницу памяти из UserMode. Для этого нужно соответствующим образом инвертировать бит U, но смотрите, если ядро использует 4х-килобайтные страницы, то дополнительно в дескрипторе может быть выставлен и бит G, говорящий о том, что “страница” глобальна и дескриптор её из TLB не выгружается и не обновляется при переключении задач и перегрузке CR3. То есть, если вы инвертировали бит U, сбросили флаг G и решили писать в старшие 2 Га из пользовательского режима, то получите исключение. Необходимо сбросить бит PGE в регистре CR4 и затем производить подобные манипуляции и писать в системную страницу. Кроме того, не забываем про бит WP в CR0. На этом в принципе проблемы с записью закончены.

Следуя за вышеперечисленным, ставится еще одна интересная задача. Необходимо найти сам экспорт kernel32:CreateProcessA. Этим вопросом в нашем драйвере ведает функция GetExportedFuncAddr(DWORD ModuleImageBase,PCHAR FuncName), принимающая на стек адрес модуля в памяти и имя искомой функции, и в случае успеха поиска последней в таблице экспорта возвращает её базу в памяти. Думаю, объяснять функциональность данного кода излишне, за более детальной информацией следует обратиться к документации по PE, а так же непосредственно к исходному коду GetExportedFuncAddr. Все используемые в драйвере структуры описаны в модуле struc.h. Вообще все вышеперечисленные фрагменты практически в таком же порядке можно проследить в функции CreateImplant().

К данной части статьи мы обсудили практически все вспомогательные моменты, касающиеся функций, ведущих нас к заветной цели. Теперь рассмотрим алгоритм драйвера в общем виде по порядку.

Основным эпизодом драйвера является функция ReplaceKiSystemServiceCode(), внутри которой первым делом находим startup _KiSystemService, методом, описанным выше, а затем врезаемся непосредственно в сердцевину кода последней, для того, чтобы получить над ней власть. Это нам необходимо, чтобы, во-первых, заполучить точку возврата из _KiSystemService, для того, что бы провести анализ адреса и в случае удобства для нас последнего, подменить этот адрес другим, указывающим на код внедренного нами имплантанта в первой странице kernel32.dll. Во вторых, обработчик будет внедрять имплантант в память пользовательского режима. Всего перехватчиков будет два.

Теперь немного подробнее о первом перехватчике. Посмотрим код:

0008:804DA07C6A00PUSH00
0008:804DA07E55PUSHEBP
0008:804DA07F53PUSHEBX
0008:804DA08056PUSHESI
0008:804DA08157PUSHEDI
0008:804DA0820FA0PUSHFS
0008:804DA084BB30000000MOVEBX,00000030
0008:804DA089668EE3MOVFS,BX
0008:804DA08CFF3500F0DFFFPUSHDWORD PTR [FFDFF000]
0008:804DA092C70500F0DFFFFFFFFFFFMOVDWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C8B3524F1DFFFMOVESI,[FFDFF124]
0008:804DA0A2FFB640010000PUSHDWORD PTR [ESI+00000140]
0008:804DA0A883EC48SUBESP,48

Оригинальный startup _KiSystemService.

Мы внедряем первый обработчик непосредственно в начало кода, методом сплайсинга, с сохранением оригинального участка и последующим его восстановлением. Получается нечто следующего вида.

0008:804DA07CFF25008578FCJMP[ArtificialKiSystemService]
0008:804DA0820FA0PUSHFS
0008:804DA084BB30000000MOVEBX,00000030
0008:804DA089668EE3MOVFS,BX
0008:804DA08CFF3500F0DFFFPUSHDWORD PTR [FFDFF000]
0008:804DA092C70500F0DFFFFFFFFFFFMOVDWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C8B3524F1DFFFMOVESI,[FFDFF124]
0008:804DA0A2FFB640010000PUSHDWORD PTR [ESI+00000140]
0008:804DA0A883EC48SUBESP,48

Затем сам обработчик KiSystemServiceHandler() адрес которого хранится в ArtificialKiSystemService.

_declspec(naked) KiSystemServiceHandler()
{ SaveKISSRetAddr // сохраним точку возврата из сервиса в пользовательский режим _asm{ OriginalKiSystemServiceInlineStartUpCode // восстановим оригинальный код push dword ptr [OriginalKiSystemService] add dword ptr [esp],OriginalKiSystemServiceStartUpCodeSize ret }
}

Надеюсь, понимаете, почему перехват был осуществлен на начало startup-кода. Именно в этом месте мы можем без лишних усилий извлечь из ядерного стека потока нужный адрес. Но более сложных манипуляций внутри обработчика KiSystemServiceHandler() я производить не советую, данный код выполняется при закрытых прерываниях, то есть уровень IRQL самый высокий, и, кроме того, не настроен должным образом регистр fs. Иные действия внутри обработчика неминуемо приведут к краху системы. Теперь следующий момент - второй перехватчик. Смотрим в код:

0008:804DA07CFF25008578FCJMP[ArtificialKiSystemService]
0008:804DA0820FA0PUSHFS
0008:804DA084BB30000000MOVEBX,00000030
0008:804DA089668EE3MOVFS,BX
0008:804DA08CFF3500F0DFFFPUSHDWORD PTR [FFDFF000]
0008:804DA092C70500F0DFFFFFFFFFFFMOVDWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C8B3524F1DFFFMOVESI,[FFDFF124]
0008:804DA0A2FFB640010000PUSHDWORD PTR [ESI+00000140]
0008:804DA0A883EC48SUBESP,48
0008:804DA0AB8B5C246CMOVEBX,[ESP+6C]
0008:804DA0AF83E301ANDEBX,01
0008:804DA0B2889E40010000MOV[ESI+00000140],BL
0008:804DA0B88BECMOVEBP,ESP
0008:804DA0BA8B9E34010000MOVEBX,[ESI+00000134]
0008:804DA0C0895D3CMOV[EBP+3C],EBX
0008:804DA0C389AE34010000MOV[ESI+00000134],EBP
0008:804DA0C9FCCLD
0008:804DA0CAF6462CFFTESTBYTE PTR [ESI+2C],FF
0008:804DA0CE0F85D6FEFFFFJNZ804D9FAA
0008:804DA0D4FBSTI// - понижается уровень IRQL
0008:804DA0D5FF25F08478FCJMP[ArtificialKiSystemServiceSafedCode]
0008:804DA0DBCCINT3
0008:804DA0DCCCINT3
0008:804DA0DD8BCFMOVECX,EDI

Теперь посмотрим на обработчик KiSystemServiceHandler2(), адрес которого в ArtificialKiSystemServiceSafedCode:

_declspec(naked)KiSystemServiceHandler2()
{ _asm mov CanUnload,FALSE // выставим флаг невозможности выгрузки драйвера saveregisters // предохраняемся if (!isP) { isP++; // устанавливаем флаг повторной невходимости CreateImplant();// внедряем имплантант } restoreregisters// возврат регистров _asm { OriginalKiSystemServiceInLineSafedCode push dword ptr [KiSystemServiceSafedCode] add dword ptr [esp],OriginalAfterStiCodeSize mov CanUnload,TRUE // теперь драйвер можно безболезненно выгрузить :) ret }
}

Внутри данного обработчика мы можем делать всё, что нам вздумается. Уровень IRQL здесь самый низкий, поэтому мы и вызываем функцию CreateImplant(), которая и выполняет ранее перечисленные действия, включая внедрение кода имплантанта. После её отработки, и возврата из _KiSystemService будет вызван имплантант, и после уже его отработки, поток снова вернется в то место, где и был до этого прерван, точнее - в предшлюзовую заглушку внутри ntdll.dll. Ниже приводится код, являющийся частью функции CreateImplant(), создающий имплантант.

// заменяем точку возврата из KiSystemService
// в пользовательском режиме на адрес кода имплантанта
mov eax,ImpStartAddr 
mov ebx,[KISS_SP]
// запихиваем параметры задом наперед в стек
mov [ebx],eax

// pushad
mov bl,0x60 
mov [eax],bl
inc eax

mov bl, 0xb8
mov [eax], bl
inc eax
mov ebx,pUprocessInformation // PI
mov [eax],ebx // mov eax,PI
add eax,4
mov [eax],0x50 // push eax
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,pUstartUpInfo
mov [eax],ebx // mov eax,SI
add eax,4
mov [eax],0x50 // push eax
inc eax

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
mov [eax],ebx // mov eax,0
add eax,4
mov [eax],0x50 // push eax
inc eax

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
mov [eax],ebx // mov eax,0
add eax,4
mov [eax],0x50 // push eax
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0x04000000
// mov eax,0x04000000 = Create_default_error_mode
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,ProgExeNameAddrinUser
// mov eax,ProgExeNameAddrinUser
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax 

mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
 // теперь сам вызов процедуры 
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,CreateProcessA_OEP
// mov eax,CreateProcessA_OEP
mov [eax],ebx
add eax,4
mov bx,0xD0FF
mov [eax], bx
// call eax
add eax,2
 // popad
mov bl,0x61
mov [eax],bl
inc eax

mov bl,0xbb
mov [eax],bl
inc eax
mov ebx,KiSystemServiceReturnAddress
// mov ebx,KiSystemServiceReturnAddress
mov [eax],ebx
add eax,4
mov bx,0xE3FF
// jmp ebx ... а теперь снова прописываем
// оригинальную точку возврата из KiSystemService
mov [eax], bx

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



На рисунке показан участок дампа памяти модуля kernel32.dll. Красной рамочкой обведен непосредственно сам код, а в синей рамочке структуры USTARTUPINFO, UPROCESS_INFORMATION и ProgExeName, это аналоги соответствующих структур для CreateProcessA, как я обозвал их в драйвере. Что в действительности представляет собой код в красной рамочке, показано ниже:

0010:77E6047A60PUSHAD
0010:77E6047BB84C04E677MOVEAX,77E6044C - UPROCESS_INFORMATION
0010:77E6048050PUSHEAX
0010:77E60481B80804E677MOVEAX,77E60408 - USTARTUPINFO
0010:77E6048650PUSHEAX
0010:77E60487B800000000MOVEAX,00000000
0010:77E6048C50PUSHEAX
0010:77E6048DB800000000MOVEAX,00000000
0010:77E6049250PUSHEAX
0010:77E60493B800000004MOVEAX,04000000 - Create_default_error_mode
0010:77E6049850PUSHEAX
0010:77E60499B800000000MOVEAX,00000000
0010:77E6049E50PUSHEAX
0010:77E6049FB800000000MOVEAX,00000000
0010:77E604A450PUSHEAX
0010:77E604A5B800000000MOVEAX,00000000
0010:77E604AA50PUSHEAX
0010:77E604ABB85C04E677MOVEAX,77E6045C - ProgExeNameAddrinUser
0010:77E604B050PUSHEAX
0010:77E604B1B800000000MOVEAX,00000000
0010:77E604B650PUSHEAX
0010:77E604B7B8BC1BE677MOVEAX,KERNEL32!CreateProcessA
0010:77E604BCFFD0CALLEAX
0010:77E604BE61POPAD
0010:77E604BFBB0403FE7FMOVEBX,7FFE0304 - адрес возврата в ntdll.dll
0010:77E604C4FFE3JMPEBX

В общем плане все это выглядит довольно просто. После того, как поток покинет _KiSystemService инструкцией ret/sysexit, путём подмены адреса возврата в ядерном стеке потока, получает управление созданный раннее вышеприведенными ассемблерными инструкциями код имплантанта, который вызывает CreateProcessA и, инструкцией JMP EBX возвращается снова в ntdll.dll, куда он и должен был изначально попасть по закону. В результате чего, при удачном стечении обстоятельств, или, точнее, если мы правильно все сделали, последует вызов кода имплантанта, который в свою очередь создаст процесс.

Далее, запустив, к примеру, Process Explorer от Марка Руссиновича, можно будет его увидеть . Однако спешу предупредить вот еще о чем. Если, к примеру, Вы создаете GUI-процесс, то в некоторых случаях можете и не увидеть окна данного приложения, хотя Process Explorer исправно показывает его наличие. Здесь нет повода для беспокойства, дело в том, что при некоторых, точно неизвестных мне обстоятельствах, процесс не подключается к WindowStation, а значит, не получает Desktop, к примеру, это происходит в том случае, когда “родителем” процесса становится процесс Services.



К примеру, Вы видите процесс CMD.EXE, “порожденный” в недрах WinAmp, который по всем правилам получил Desktop и виден “на поверхности”.

А вот уже другой случай.



Думаю, комментарии в данном случае излишни.

После всех описательных процедур, имевших место в данной статье, думаю, стало понятно, каким достаточно нехитрым образом мы осуществили задуманное, и, все-таки дали жизнь пользовательскому процессу из режима ядра, так сказать, “не мудрствуя лукаво”. Теперь, за всеми разъяснениями и дополнениями к вышесказанному Вы можете обратиться к исходному коду драйвера. Для его загрузки используйте утилиту KmdManager из пакета KmdKit от Four-F. Собственно, ему же и благодарность за содействие в решении вопроса, определившегося в концовке статьи, а также благодарю господина lial’а за великодушное содействие в решении вопроса, связанного с версткой данной статьи.

Предложения и замечания так же жду по адресу troguar@yandex.ru
[C] Cardinal

Разное в ИТ   Теги:

Читать IT-новости в Telegram
Информационные технологии
Мы в соцсетях ✉