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

Дата публикации 14 сен 2005

Запуск процесса из режима ядра — Архив 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 является переходником к функциям ядра, следовательно, внутри функции присутствует код прямого вызова ядерного сервиса. Посмотрим на код ниже.

Код (Text):
  1.  
  2.     0008:804DA113    5A        POP    EDX
  3.     0008:804DA114    FF0538F6DFFF    INC    DWORD PTR [FFDFF638]
  4.     0008:804DA11A    8BF2        MOV    ESI,EDX
  5.     0008:804DA11C    8B5F0C        MOV    EBX,[EDI+0C]
  6.     0008:804DA11F    33C9        XOR    ECX,ECX
  7.     0008:804DA121    8A0C18        MOV    CL,[EBX+EAX]
  8.     0008:804DA124    8B3F        MOV    EDI,[EDI]
  9.     0008:804DA126    8B1C87        MOV    EBX,[EAX*4+EDI]
  10.     0008:804DA129    2BE1        SUB    ESP,ECX
  11.     0008:804DA12B    C1E902        SHR    ECX,02
  12.     0008:804DA12E    8BFC        MOV    EDI,ESP
  13.     0008:804DA130    3B35D4C75480    CMP    ESI,[ntoskrnl!MmUserProbeAddress]
  14.     0008:804DA136    0F83A1010000    JAE    804DA2DD
  15.     0008:804DA13C    F3A5        REPZ    MOVSD
  16. <font color="blue">    0008:804DA13E    FFD3        CALL    EBX</font>
  17.     0008:804DA140    8BE5        MOV    ESP,EBP
  18.  

Мы видим фрагмент _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, надеюсь не вечно :smile3: // похоже, адрес найден, снимаем обработчик 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:

Код (Text):
  1.  
  2.     __declspec(naked) ArtificialNtReadFile()
  3.     {  __asm
  4.       {
  5.         push dword ptr [esp]             // !!!
  6.         pop NtReadFileReturnAddress
  7.         inc dword ptr FindingForWhile    // нашли адрес требуемой функции
  8.         jmp dword ptr OriginalNtReadFile
  9.       }
  10.     }
  11.  

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

Код (Text):
  1.  
  2. // такова разница м\д точкой входа и точкой возврата внутри KiSystemService
  3. return NtReadFileReturnAddress - 0xC4;
  4.  

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

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

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

Код (Text):
  1.  
  2.     typedef struct  _KMODULEINFOLISTENTRY {
  3.       DWORD  Flink;
  4.       DWORD  Blink;
  5.       DWORD  ModuleIBase;
  6.       DWORD  DllEntryPoint;
  7.       DWORD  Unknown2;
  8.       DWORD  Unknown3;
  9.       PWCHAR ModuleName;
  10.     } KMODULEINFOLISTENTRY, *PKMODULEINFOLISTENTRY, **PPKMODULEINFOLISTENTRY;
  11.  

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

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

Код (Text):
  1.  
  2.     BOOL CreateProcess(
  3.       LPCTSTR lpApplicationName,                 // name of executable module
  4.       LPTSTR lpCommandLine,                      // command line string
  5.       LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
  6.       LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
  7.       BOOL bInheritHandles,                      // handle inheritance option
  8.       DWORD dwCreationFlags,                     // creation flags
  9.       LPVOID lpEnvironment,                      // new environment block
  10.       LPCTSTR lpCurrentDirectory,                // current directory name
  11.       LPSTARTUPINFO lpStartupInfo,               // startup information
  12.       LPPROCESS_INFORMATION lpProcessInformation // process information
  13.     );
  14.  
  15.     typedef struct _STARTUPINFO {
  16.       DWORD   cb;
  17.       LPTSTR  lpReserved;
  18.       LPTSTR  lpDesktop;
  19.       LPTSTR  lpTitle;
  20.       DWORD   dwX;
  21.       DWORD   dwY;
  22.       DWORD   dwXSize;
  23.       DWORD   dwYSize;
  24.       DWORD   dwXCountChars;
  25.       DWORD   dwYCountChars;
  26.       DWORD   dwFillAttribute;
  27.       DWORD   dwFlags;
  28.       WORD    wShowWindow;
  29.       WORD    cbReserved2;
  30.       LPBYTE  lpReserved2;
  31.       HANDLE  hStdInput;
  32.       HANDLE  hStdOutput;
  33.       HANDLE  hStdError;
  34.     } STARTUPINFO, *LPSTARTUPINFO;
  35.  
  36.     typedef struct _PROCESS_INFORMATION {
  37.       HANDLE hProcess;
  38.       HANDLE hThread;
  39.       DWORD  dwProcessId;
  40.       DWORD  dwThreadId;
  41.     } PROCESS_INFORMATION;
  42.  

Как видите, функция принимает на стек 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. Во вторых, обработчик будет внедрять имплантант в память пользовательского режима. Всего перехватчиков будет два.

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

Код (Text):
  1.  
  2.     0008:804DA07C   6A00            PUSH    00
  3.     0008:804DA07E   55          PUSH    EBP
  4.     0008:804DA07F   53          PUSH    EBX
  5.     0008:804DA080   56          PUSH    ESI
  6.     0008:804DA081   57          PUSH    EDI
  7.     0008:804DA082   0FA0            PUSH    FS
  8.     0008:804DA084   BB30000000      MOV EBX,00000030
  9.     0008:804DA089   668EE3          MOV FS,BX
  10.     0008:804DA08C   FF3500F0DFFF        PUSH    DWORD PTR [FFDFF000]
  11.     0008:804DA092   C70500F0DFFFFFFFFFFF    MOV DWORD PTR [FFDFF000],FFFFFFFF
  12.     0008:804DA09C   8B3524F1DFFF        MOV ESI,[FFDFF124]
  13.     0008:804DA0A2   FFB640010000        PUSH    DWORD PTR [ESI+00000140]
  14.     0008:804DA0A8   83EC48          SUB ESP,48
  15.  

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

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

Код (Text):
  1.  
  2. <font color="blue"> 0008:804DA07C   FF25008578FC        JMP [ArtificialKiSystemService]</font>
  3.     0008:804DA082   0FA0            PUSH    FS
  4.     0008:804DA084   BB30000000      MOV EBX,00000030
  5.     0008:804DA089   668EE3          MOV FS,BX
  6.     0008:804DA08C   FF3500F0DFFF        PUSH    DWORD PTR [FFDFF000]
  7.     0008:804DA092   C70500F0DFFFFFFFFFFF    MOV DWORD PTR [FFDFF000],FFFFFFFF
  8.     0008:804DA09C   8B3524F1DFFF        MOV ESI,[FFDFF124]
  9.     0008:804DA0A2   FFB640010000        PUSH    DWORD PTR [ESI+00000140]
  10.     0008:804DA0A8   83EC48          SUB ESP,48
  11.  

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

Код (Text):
  1.  
  2.     __declspec(naked) KiSystemServiceHandler()
  3.     {
  4.       SaveKISSRetAddr // сохраним точку возврата из сервиса в пользовательский режим
  5.       _asm{
  6.         OriginalKiSystemServiceInlineStartUpCode  // восстановим оригинальный код  
  7.         push dword ptr [OriginalKiSystemService]
  8.         add dword ptr [esp],OriginalKiSystemServiceStartUpCodeSize
  9.         ret
  10.       }
  11.     }
  12.  

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

Код (Text):
  1.  
  2.     0008:804DA07C   FF25008578FC        JMP [ArtificialKiSystemService]
  3.     0008:804DA082   0FA0            PUSHFS
  4.     0008:804DA084   BB30000000      MOV EBX,00000030
  5.     0008:804DA089   668EE3          MOV FS,BX
  6.     0008:804DA08C   FF3500F0DFFF        PUSH    DWORD PTR [FFDFF000]
  7.     0008:804DA092   C70500F0DFFFFFFFFFFF    MOV DWORD PTR [FFDFF000],FFFFFFFF
  8.     0008:804DA09C   8B3524F1DFFF        MOV ESI,[FFDFF124]
  9.     0008:804DA0A2   FFB640010000        PUSH    DWORD PTR [ESI+00000140]
  10.     0008:804DA0A8   83EC48          SUB ESP,48
  11.     0008:804DA0AB   8B5C246C        MOV EBX,[ESP+6C]
  12.     0008:804DA0AF   83E301          AND EBX,01
  13.     0008:804DA0B2   889E40010000        MOV [ESI+00000140],BL
  14.     0008:804DA0B8   8BEC            MOV EBP,ESP
  15.     0008:804DA0BA   8B9E34010000        MOV EBX,[ESI+00000134]
  16.     0008:804DA0C0   895D3C          MOV [EBP+3C],EBX
  17.     0008:804DA0C3   89AE34010000        MOV [ESI+00000134],EBP
  18.     0008:804DA0C9   FC          CLD
  19.     0008:804DA0CA   F6462CFF        TEST    BYTE PTR [ESI+2C],FF
  20.     0008:804DA0CE   0F85D6FEFFFF        JNZ 804D9FAA
  21. <font color="red">  0008:804DA0D4   FB          STI // - понижается уровень IRQL</font>
  22. <font color="blue"> 0008:804DA0D5   FF25F08478FC        JMP [ArtificialKiSystemServiceSafedCode]</font>
  23.     0008:804DA0DB   CC          INT 3
  24.     0008:804DA0DC   CC          INT 3
  25.     0008:804DA0DD   8BCF            MOV ECX,EDI
  26.  

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

Код (Text):
  1.  
  2.     __declspec(naked)KiSystemServiceHandler2()
  3.     {
  4.       __asm mov CanUnload,FALSE   // выставим флаг невозможности выгрузки драйвера
  5.       saveregisters      // предохраняемся
  6.       if (!isP)
  7.       {
  8.         isP++;              // устанавливаем флаг повторной невходимости
  9.         CreateImplant();    // внедряем имплантант
  10.       }
  11.       restoreregisters  // возврат регистров
  12.       __asm
  13.       {
  14.         OriginalKiSystemServiceInLineSafedCode
  15.         push dword ptr [KiSystemServiceSafedCode]
  16.         add dword ptr [esp],OriginalAfterStiCodeSize
  17.         mov CanUnload,TRUE       // теперь драйвер можно безболезненно выгрузить <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  18.         ret
  19.       }
  20.     }
  21.  

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

Код (Text):
  1.  
  2. // заменяем точку возврата из KiSystemService
  3. // в пользовательском режиме на адрес кода имплантанта
  4.     mov eax,ImpStartAddr  
  5.     mov ebx,[KISS_SP]
  6.     // запихиваем параметры задом наперед в стек
  7.     mov [ebx],eax
  8.  
  9. // pushad
  10.     mov bl,0x60  
  11.     mov [eax],bl
  12.     inc eax
  13.  
  14.     mov bl, 0xb8
  15.     mov [eax], bl
  16.     inc eax
  17.     mov ebx,pUprocessInformation  // PI
  18.     mov [eax],ebx                 // mov eax,PI
  19.     add eax,4
  20.     mov [eax],0x50                // push eax
  21.     inc eax
  22.  
  23.     mov bl,0xb8
  24.     mov [eax],bl
  25.     inc eax
  26.     mov ebx,pUstartUpInfo
  27.     mov [eax],ebx                 // mov eax,SI
  28.     add eax,4
  29.     mov [eax],0x50                // push eax
  30.     inc eax
  31.  
  32.     mov bl,0xb8
  33.     mov [eax],bl
  34.     inc eax
  35.     mov ebx,0
  36.     mov [eax],ebx                 // mov eax,0
  37.     add eax,4
  38.     mov [eax],0x50                // push eax
  39.     inc eax
  40.  
  41.     mov bl,0xb8
  42.     mov [eax],bl
  43.     inc eax
  44.     mov ebx,0
  45.     mov [eax],ebx                 // mov eax,0
  46.     add eax,4
  47.     mov [eax],0x50                // push eax
  48.     inc eax  
  49.  
  50.     mov bl,0xb8
  51.     mov [eax],bl
  52.     inc eax
  53.     mov ebx,0x04000000
  54.     // mov eax,0x04000000 = Create_default_error_mode
  55.     mov [eax],ebx
  56.     add eax,4
  57.     // push eax
  58.     mov [eax],0x50
  59.     inc eax  
  60.  
  61.     mov bl,0xb8
  62.     mov [eax],bl
  63.     inc eax
  64.     mov ebx,0
  65.     // mov eax,0
  66.     mov [eax],ebx
  67.     add eax,4
  68.     // push eax
  69.     mov [eax],0x50
  70.     inc eax  
  71.  
  72.     mov bl,0xb8
  73.     mov [eax],bl
  74.     inc eax
  75.     mov ebx,0
  76.     // mov eax,0
  77.     mov [eax],ebx
  78.     add eax,4
  79.     // push eax
  80.     mov [eax],0x50
  81.     inc eax  
  82.  
  83.     mov bl,0xb8
  84.     mov [eax],bl
  85.     inc eax
  86.     mov ebx,0
  87.     // mov eax,0
  88.     mov [eax],ebx
  89.     add eax,4
  90.     // push eax
  91.     mov [eax],0x50
  92.     inc eax  
  93.  
  94.     mov bl,0xb8
  95.     mov [eax],bl
  96.     inc eax
  97.     mov ebx,ProgExeNameAddrinUser
  98.     // mov eax,ProgExeNameAddrinUser
  99.     mov [eax],ebx
  100.     add eax,4
  101.     // push eax
  102.     mov [eax],0x50
  103.     inc eax  
  104.  
  105.     mov bl,0xb8
  106.     mov [eax],bl
  107.     inc eax
  108.     mov ebx,0
  109.     // mov eax,0
  110.     mov [eax],ebx
  111.     add eax,4
  112.     // push eax
  113.     mov [eax],0x50
  114.     inc eax
  115.  
  116.     // теперь сам вызов процедуры
  117.     mov bl,0xb8
  118.     mov [eax],bl
  119.     inc eax
  120.     mov ebx,CreateProcessA_OEP
  121.     // mov eax,CreateProcessA_OEP
  122.     mov [eax],ebx
  123.     add eax,4
  124.     mov bx,0xD0FF
  125.     mov [eax], bx
  126.     // call eax
  127.     add eax,2
  128.  
  129.     // popad
  130.     mov bl,0x61
  131.     mov [eax],bl
  132.     inc eax
  133.  
  134.     mov bl,0xbb
  135.     mov [eax],bl
  136.     inc eax
  137.     mov ebx,KiSystemServiceReturnAddress
  138.     // mov ebx,KiSystemServiceReturnAddress
  139.     mov [eax],ebx
  140.     add eax,4
  141.     mov bx,0xE3FF
  142.     // jmp ebx ... а теперь снова прописываем
  143.     // оригинальную точку возврата из KiSystemService
  144.     mov [eax], bx
  145.  

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

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

Код (Text):
  1.  
  2.     0010:77E6047A   60      PUSHAD
  3.     0010:77E6047B   B84C04E677  MOV EAX,77E6044C - UPROCESS_INFORMATION
  4.     0010:77E60480   50      PUSH    EAX
  5.     0010:77E60481   B80804E677  MOV EAX,77E60408 - USTARTUPINFO
  6.     0010:77E60486   50      PUSH    EAX
  7.     0010:77E60487   B800000000  MOV EAX,00000000
  8.     0010:77E6048C   50      PUSH    EAX
  9.     0010:77E6048D   B800000000  MOV EAX,00000000
  10.     0010:77E60492   50      PUSH    EAX
  11.     0010:77E60493   B800000004  MOV EAX,04000000 - Create_default_error_mode
  12.     0010:77E60498   50      PUSH    EAX
  13.     0010:77E60499   B800000000  MOV EAX,00000000
  14.     0010:77E6049E   50      PUSH    EAX
  15.     0010:77E6049F   B800000000  MOV EAX,00000000
  16.     0010:77E604A4   50      PUSH    EAX
  17.     0010:77E604A5   B800000000  MOV EAX,00000000
  18.     0010:77E604AA   50      PUSH    EAX
  19.     0010:77E604AB   B85C04E677  MOV EAX,77E6045C - ProgExeNameAddrinUser
  20.     0010:77E604B0   50      PUSH    EAX
  21.     0010:77E604B1   B800000000  MOV EAX,00000000
  22.     0010:77E604B6   50      PUSH    EAX
  23.     0010:77E604B7   B8BC1BE677  MOV EAX,KERNEL32!CreateProcessA
  24.     0010:77E604BC   FFD0        CALL    EAX
  25.     0010:77E604BE   61      POPAD
  26.     0010:77E604BF   BB0403FE7F  MOV EBX,7FFE0304 - адрес возврата в ntdll.dll
  27.     0010:77E604C4   FFE3        JMP EBX
  28.  

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

© Cardinal

0 1.841
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532