Запуск процесса из режима ядра — Архив WASM.RU

Все статьи

Запуск процесса из режима ядра — Архив WASM.RU

Скачать исходники

Думаю, многие из Вас задавались подобным вопросом. Возможен ли запуск пользовательского приложения из драйвера режима ядра? В системах 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:804DA07C	6A00			PUSH	00
	0008:804DA07E	55			PUSH	EBP
	0008:804DA07F	53			PUSH	EBX
	0008:804DA080	56			PUSH	ESI
	0008:804DA081	57			PUSH	EDI
	0008:804DA082	0FA0			PUSH	FS
	0008:804DA084	BB30000000		MOV	EBX,00000030
	0008:804DA089	668EE3			MOV	FS,BX
	0008:804DA08C	FF3500F0DFFF		PUSH	DWORD PTR [FFDFF000]
	0008:804DA092	C70500F0DFFFFFFFFFFF	MOV	DWORD PTR [FFDFF000],FFFFFFFF
	0008:804DA09C	8B3524F1DFFF		MOV	ESI,[FFDFF124]
	0008:804DA0A2	FFB640010000		PUSH	DWORD PTR [ESI+00000140]
	0008:804DA0A8	83EC48			SUB	ESP,48

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

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

	0008:804DA07C	FF25008578FC		JMP	[ArtificialKiSystemService]
	0008:804DA082	0FA0			PUSH	FS
	0008:804DA084	BB30000000		MOV	EBX,00000030
	0008:804DA089	668EE3			MOV	FS,BX
	0008:804DA08C	FF3500F0DFFF		PUSH	DWORD PTR [FFDFF000]
	0008:804DA092	C70500F0DFFFFFFFFFFF	MOV	DWORD PTR [FFDFF000],FFFFFFFF
	0008:804DA09C	8B3524F1DFFF		MOV	ESI,[FFDFF124]
	0008:804DA0A2	FFB640010000		PUSH	DWORD PTR [ESI+00000140]
	0008:804DA0A8	83EC48			SUB	ESP,48

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

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

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

	0008:804DA07C	FF25008578FC		JMP	[ArtificialKiSystemService]
	0008:804DA082	0FA0			PUSHFS
	0008:804DA084	BB30000000		MOV	EBX,00000030
	0008:804DA089	668EE3			MOV	FS,BX
	0008:804DA08C	FF3500F0DFFF		PUSH	DWORD PTR [FFDFF000]
	0008:804DA092	C70500F0DFFFFFFFFFFF	MOV	DWORD PTR [FFDFF000],FFFFFFFF
	0008:804DA09C	8B3524F1DFFF		MOV	ESI,[FFDFF124]
	0008:804DA0A2	FFB640010000		PUSH	DWORD PTR [ESI+00000140]
	0008:804DA0A8	83EC48			SUB	ESP,48
	0008:804DA0AB	8B5C246C		MOV	EBX,[ESP+6C]
	0008:804DA0AF	83E301			AND	EBX,01
	0008:804DA0B2	889E40010000		MOV	[ESI+00000140],BL
	0008:804DA0B8	8BEC			MOV	EBP,ESP
	0008:804DA0BA	8B9E34010000		MOV	EBX,[ESI+00000134]
	0008:804DA0C0	895D3C			MOV	[EBP+3C],EBX
	0008:804DA0C3	89AE34010000		MOV	[ESI+00000134],EBP
	0008:804DA0C9	FC			CLD
	0008:804DA0CA	F6462CFF		TEST	BYTE PTR [ESI+2C],FF
	0008:804DA0CE	0F85D6FEFFFF		JNZ	804D9FAA
	0008:804DA0D4	FB			STI	// - понижается уровень IRQL
	0008:804DA0D5	FF25F08478FC		JMP	[ArtificialKiSystemServiceSafedCode]
	0008:804DA0DB	CC			INT	3
	0008:804DA0DC	CC			INT	3
	0008:804DA0DD	8BCF			MOV	ECX,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:77E6047A	60		PUSHAD
	0010:77E6047B	B84C04E677	MOV	EAX,77E6044C - UPROCESS_INFORMATION
	0010:77E60480	50		PUSH	EAX
	0010:77E60481	B80804E677	MOV	EAX,77E60408 - USTARTUPINFO
	0010:77E60486	50		PUSH	EAX
	0010:77E60487	B800000000	MOV	EAX,00000000
	0010:77E6048C	50		PUSH	EAX
	0010:77E6048D	B800000000	MOV	EAX,00000000
	0010:77E60492	50		PUSH	EAX
	0010:77E60493	B800000004	MOV	EAX,04000000 - Create_default_error_mode
	0010:77E60498	50		PUSH	EAX
	0010:77E60499	B800000000	MOV	EAX,00000000
	0010:77E6049E	50		PUSH	EAX
	0010:77E6049F	B800000000	MOV	EAX,00000000
	0010:77E604A4	50		PUSH	EAX
	0010:77E604A5	B800000000	MOV	EAX,00000000
	0010:77E604AA	50		PUSH	EAX
	0010:77E604AB	B85C04E677	MOV	EAX,77E6045C - ProgExeNameAddrinUser
	0010:77E604B0	50		PUSH	EAX
	0010:77E604B1	B800000000	MOV	EAX,00000000
	0010:77E604B6	50		PUSH	EAX
	0010:77E604B7	B8BC1BE677	MOV	EAX,KERNEL32!CreateProcessA
	0010:77E604BC	FFD0		CALL	EAX
	0010:77E604BE	61		POPAD
	0010:77E604BF	BB0403FE7F	MOV	EBX,7FFE0304 - адрес возврата в ntdll.dll
	0010:77E604C4	FFE3		JMP	EBX

В общем плане все это выглядит довольно просто. После того, как поток покинет _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

2002-2013 (c) wasm.ru