информационная безопасность
без паники и всерьез
 подробно о проекте
Rambler's Top100За кого нас держат?Где водятся OGRыСтрашный баг в Windows
BugTraq.Ru
Русский BugTraq
 Анализ криптографических сетевых... 
 Модель надежности двухузлового... 
 Специальные марковские модели надежности... 
 Microsoft обещает радикально усилить... 
 Ядро Linux избавляется от российских... 
 20 лет Ubuntu 
главная обзор RSN блог библиотека закон бред форум dnet о проекте
bugtraq.ru / форум / FAQ / программирование
FAQ
главная
операционные системы
sysadmin
программирование
теория
общие вопросы





Программирование, установка и конфигурирование сервисов Windows NT
HandleX
Опубликовано: dl, 24.05.04 18:51

Часто у программистов и системных администраторов появляются вопросы на тему служб. Для того, чтобы не искать ответы на разных форумах, появился этот документ. Давайте рассмотрим эти типичные вопросы подробнее.

Чем отличаются службы от обычных приложений? Что такое Service Control Manager?

Исполняемый файл службы использует во время работы специальный протокол общения с особой подсистемой Windows NT — Service Control Manager (в дальнейшем SCM). API для «общения» с SCM экспортирует стандартная библиотека Windows advapi32.dll. Подсистема служб загружается в самом начале старта OS из исполняемого файла services.exe, одноимённый процесс вы можете наблюдать в списке активных процессов во всё время работы компьютера. Библиотека advapi32.dll общается с services.exe посредством RPC (Remote Procedure Call), поэтому всё, что вы можете делать со службами на локальной машине, вы можете делать и на удалённой машине по сети, что очень удобно.

SCM достаточно старая подсистема (она появилась ещё в NT3), и интерфейс взаимодействия с ней не менялся до сих пор, исключая мелкие косметические примочки вроде описаний служб, он кажется немножко сыроватым и запутанным. Однако работает надёжно.

Ещё одно отличие службы от «обычной программы» в том, что служба, будучи запущенной, работает и после выхода пользователя из системы — да-да, даже тогда, когда вы видите приглашение «Для входа в систему нажмите Ctrl+Alt+Delete». Поэтому в службы чаще всего оформляют такие программы, которые должны предоставлять некие услуги даже тогда, когда нет вошедших в систему пользователей. Из встроенных служб NT это, к примеру, Spooler (служба печати), Alerter (служба оповещения, часто используемая системными администраторами), Server (позволяет открывать файлы, named pipes и проч. на вашем компьютере другими машинами в сети) и т.п.

Кстати, SCM запускает также и драйверы, поэтому захват власти над ним является лакомой добычей хакеров и вирусов, поскольку именно оттуда они могут запустить драйвер ядра. А драйвер ядра может взять полный контроль над системой.

Cool! А как мне написать драйвер ядра?

Написание драйверов ядра занятие не для слабонервных. Чем-то похоже на сборку кораблика в бутылке с узким горлышком... в тёмной комнате :) И не всякий компилятор сможет это сделать. К примеру, Delphi, даже при создании простейшего исполняемого файла .exe, который запускается и тут же выходит, вставляет в него функции из стандартной дельфийской run-time library «System», в которой есть вызовы Windows user-mode API, и запуск такого «драйвера» вызовет ошибку. На момент написания этой статьи драйверы могут компилировать M$ VC, gcc+MinGW. Итак, для написания драйвера необходимо наличие «правильного» компилятора, DDK и MSDN от M$, в которой эта тема достаточно подробно разъясняется на нескольких сотнях страниц, плюс изрядный запас здорового оптимизма и терпения. Я видел людей, которые начинали изучать программирование в Windows именно с написания драйверов под неё! Вот это молодцы, уважаю! Это не мышью компоненты по форме раскладывать :)

Что будет, если «грохнуть» процесс Services.exe?

Это жизненно важный процесс, его принудительное завершение (отладочные привилегии + TerminateProcess) вызывает разное поведение OS в зависимости от её версии. К примеру, под Win2K и выше появится сообщение о том, что система будет перезагружена в течении 1 минуты. А под NT4 завершение services.exe вызывает труднопонимаемые подвисания пользовательских процессов, сам services.exe остаётся в списке активных процессов (хотя фунция TerminateProcess отрабатывает без ошибки), что говорит о глюках в ядре, а при попытке перезагрузки системы, она перезагружается минут эдак 15... В любом случае работа системы будет парализована до перезагрузки. Скажите, зачем вам это надо? :)

Насколько трудно переделать обычную программу в сервис — вероятно, нужно поддерживать некий специфический интерфейс?

Да, менять прийдётся немало. Поскольку вся работа службы происходит через вызовы SCM API, нужно использовать его, и по возможности, только его. У обычных программ имеется пользовательский интерфейс — оконный или консольный... Если сконфигурировать службу как Interactive Service (есть такой флаг), то можно показать пользовательский интерфейс прямо из самой службы, поскольку при запуске процесса службы он будет подключен к оконной станции вошедшего в систему пользователя... Однако это «плохой тон», поскольку при таком подходе взаимодействие пользователя со службой будет невозможно с другого компьютера. Для сложного взаимодействия (к примеру, для изменения параметров службы «на лету»), лучше вынести пользовательский интерфейс в отдельную программу и использовать какой-нибудь механизм IPC (InterProcess Communications) — будь то Named Pipes, Mailslots, Sockets и т.п., но очень желательно уместиться в рамки тех скудных возможностей, что предоставляет SCM — Start\Pause\Continue\Stop, а также User-Defined коды управления (о них ниже).

Краткий обзор некоторых важных функций API для работы со службами

OpenSCManager() Если вы хотите устанавливать, удалять или контролировать службы в системе (локальной или удалённой), то всё начинается с этой функции, которая возвращает вам дескриптор на SCМ. Самые важные параметры — имя хоста (позволяет подключаться к удалённым серверам) и уровень доступа. Если вы не уверены, что у вас есть административные привилегии, то не следует заказывать доступ SC_MANAGER_ALL_ACCESS, поскольку такой доступ вам система не даст, для экспериментов с правами есть чудесный флаг MAXIMUM_ALLOWED.

CreateService() Позволяет установить новую службу в систему. Вы получите дескриптор на созданную службу, что позволит её запустить (функция StartService) или ещё поконфигурировать (функция ChangeServiceConfig), вообще большинство функций для работы со службами требуют открытый дескриптор на неё. Важные параметры практически все, поскольку они определяют:

  • hSCManager указывает на SCM системы, в которую будет установлена новая служба;
  • lpServiceName определяет имя службы;
  • lpDisplayName определяет имя, которое будет выводится в оснастке управления службы, и применяемое во встроенной команде «net start». Можете иначе называть это как User-Friendly Name;
  • dwDesiredAccess определяет уровень доступа, который будет иметь полученный после установки службы дескриптор на неё;
  • dwServiceType оперделяет тип службы. Указывает, что за службу вы ставите — драйвер ядра или файловой системы, просто службу, службу, разделяющую процесс с другой службой, интерактивна ли она.
  • dwStartType определяет тип запуска службы: SERVICE_BOOT_START, SERVICE_SYSTEM_START для драйверов ядра, SERVICE_AUTO_START — автоматический старт user-mode службы после загрузки OS, SERVICE_DEMAND_START для ручного запуска, и SERVICE_DISABLED для отключенных служб;

  • dwErrorControl определяет поведение OS при появлении ошибок запуска службы, вплоть до отката базы данных служб в состояние последнего благополучного запуска системы... Для user-mode служб используется флаг SERVICE_ERROR_NORMAL или SERVICE_ERROR_IGNORE;
  • lpBinaryPathName определяет полный путь к файлу образа службы (*.exe);
  • lpLoadOrderGroup — если служба отнесена к определённой группе, то пока не будут запущены все службы в группе, другие службы, зависящие от этой группы, стартовать в это время не будут... Понятно обяснил? :-)
  • lpdwTagId определяет порядок запуска служб в группе, имеет смысл только для драйверов ядра;
  • lpDependencies определяет список групп или служб, от которых зависит устанавливаемая служба. Иными словами, пока эти службы не будут запущены, устанавливаемую службу невозможно будет запустить;
  • lpServiceStartName определяет пользователя, от имени которого будет служба запущена. Если указать NULL, то служба будет запускаться под аккаунтом LocalSystem;
  • Пароль для аккаунта. Для LocalSystem, LocalService и NetworkService должен быть NULL.

OpenService() позволяет получить дескриптор для уже установленной службы. Три параметра — дескриптор на SCM, имя службы, желаемый уровень доступа. С этим всё понятно, но давайте разберём то, каким может быть этот загадочный уровень доступа к службам. Итак, это может быть сумма (логическая операция OR) вот таких констант:

  • SERVICE_CHANGE_CONFIG позволяет менять параметры установленной службы с помощью ChangeServiceConfig();
  • SERVICE_ENUMERATE_DEPENDENTS позволяет узнать зависимости данной службы от других служб;
  • SERVICE_INTERROGATE позволяет немедленно узнать состояние службы с помощью функции ControlService();
  • SERVICE_PAUSE_CONTINUE позволяет загнать службу в паузу и возвратить её из этого состояния функцией ControlService();
  • SERVICE_QUERY_CONFIG позволяет посмотреть параметры службы с помощью функции QueryServiceConfig();
  • SERVICE_START позволяет запустить службу с использованием функции StartService();
  • SERVICE_STOP позволяет остановить службу с помощи функции ControlService();
  • SERVICE_USER_DEFINED_CONTROL позволяет отправить службе т.н. User Defined Service Control Code. Может быть полезной штукой для управления своими службами. Отправление такого кода производится при помощи функции ControlService();
  • SERVICE_ALL_ACCESS — сумма всего вышеперичисленного. Если у вас маловато прав, то система откажет вам в доступе. Поэтому уровень доступа следует запрашивать только тот, который нужен, множество неприятностей в плане отказа работы некоторых программ из-под обычного пользователя часто случались только из-за лени программиста подумать и выполнить операцию OR над несколькими константами. Ещё можно использовать флаг MAXIMUM_ALLOWED, система отдаст вам дескриптор с теми правами, которые вам доступны... или не отдаст вообще.

StartService() позволяет запускать только что установленную службу, или службу, у которой тип запуска SERVICE_DEMAND_START, по открытому дескриптору, при этом передаёт ей указанное количество строковых параметров (если нужно). Здесь всё просто.

QueryServiceConfig() заполняет структуру QUERY_SERVICE_CONFIG параметрами интересующей вас службы. Практически все поля этой структуры идентичны параметрам, указываемым при вызове CreateService().

ChangeServiceConfig() изменяет параметры службы. Параметры функции практически идентичны тем, что имеются в функции CreateService();

EnumServicesStatus() позволяет перечислить службы в системе, узнать их имена и текущее состояние. Полезна для всяких утилит, показывающих список служб пользователю. Рассмотрим параметры этой функции:

  • hSCManager — дескриптор на SCM;
  • dwServiceType позволяет указать, что за службы мы хотим перечислить — драйверы ядра или user-mode, можно и те, и другие (операция OR);
  • dwServiceState указывает, что нам нужны активные (запущенные) или неактивные (остановленные) службы, можно и те, и другие (операция OR);
  • lpServices — указатель на буфер памяти (массив), который будет заполнен информацией о запрошенных службах... Массив структур типа ENUM_SERVICE_STATUS, в каждой структуре указатель на ServiceName, DisplayName, ServiceStatus;
  • cbBufSize  — размер в байтах того прокрустова ложа, что вы выделили под массив :)
  • pcbBytesNeeded — указатель на переменную, в которую будет возвращено реальное кол-во байт, необходимое для перечисления всех служб;
  • lpServicesReturned — по этому указателю функция возвратит кол-во служб, которое она вам выдала;
  • lpResumeHandle позволит вам перечислить все службы, не выделяя большое кол-во памяти. В MSDN делают так: выделяют 128 байт памяти (шутка). При первом вызове устанавливают значение переменной по указателю lpResumeHandle в 0. Крутят цикл и перечисляют все службы, доколе она возвращает ошибку вида ERROR_MORE_DATA. Можно сделать всё за две итерации, я обычно так и делаю. Выставляю lpServices = NULL, cbBufSize = 0. Вызываю функцию и получаю нужное кол-во байт в cbBytesNeeded... Ну а дальше дело техники — выделяю память и вызываю функцию повторно с «правильными» параметрами. Всё, нет никакого цикла.

ControlService() позволяет посылать коды управления запущенной службе. Параметры:

  • hService ;— ранее открытый дескриптор на службу;
  • dwControl определяет код управления, который будет послан службе. Это может быть SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE (понятно из названий), а также SERVICE_CONTROL_INTERROGATE (позволяет узнать о состоянии службы в данный момент) и, так называемый USER DEFINED CONTROL CODE, т.е. любое число от 128 до 255 включительно. Служба, получив эти управляющие коды, должна вести себя в соответствии со здравым смыслом программиста :)
  • pServiceStatus — указатель на структуру SERVICE_STATUS, в которую отобразится состояние службы после полученного ею кода управления.

DeleteService() помечает службу на удаление. Будет удалена из базы SCM после того, как будут закрыты все дескрипторы на неё. Иными словами, служба может быть запущенной и работать, но после этой функции она уже не жилец :)

CloseServiceHandle() освобождает открытый дескриптор на объект службы или на SCM. Желательно не забывать вызывать эту функцию, когда дескриптор становится не нужным.

В Win2K и выше появилась функция ChangeServiceConfig2(), позволяющая установить параметр ServiceDescription. Это такой дополнительный строковый параметр, который выводится в оснастке управления службами, как разъяснение пользователю о том, зачем эта служба нужна и чем она занимается. Также, с помощью этой функции, можно выставить параметры FailureActions, которые указывают системе, что ей делать, если служба сбойнула (неожиданно завершился её процесс без оповещения SCM о завершении работы) — к примеру, система может попробовать перезапустить службу, перезагрузиться, и даже выполнить внешнюю команду, например, отправить SMS администратору по e-mail :)

Итак, мы рассмотрели функции, нужные для работы со службами и их конфигурирования. Напомню, что SCM запускает службы, исходя из того, какие параметры имеются в базе данных служб. Эту базу данных можно найти в реестре по адресу HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. Там каждый раздел — имя установленной службы. Некоторые могут спросить: «А не проще ли добавлять службы через реестр?» Отвечу вам: нет, не проще. Во-первых, вам прийдётся перезагружать машину после такой «установки» службы, поскольку база грузится в SCM единожды при старте системы. А во-вторых, M$ не гарантирует, что ключи реестра в следующих версиях Windows будут обозначать то же самое, что они обозначают сейчас. Более того, никто не гарантирует, что в следующих версиях эта база вообще будет находиться в реестре. У вирмейкеров и троянщиков особое мнение на этот счёт — анализ подозрительной программы, в которой импортируется вызов CreateService() только усилит подозрения, а так там будет «безобидная» работа с реестром... Но мы знаем, что у этих «товарищей» вообще на многие вещи особое мнение ;-)

Однако вернёмся к user-mode службам, а именно к той части API, которую они используют для работы с SCM. Сперва службу нужно написать... Там всё просто, в MSDN есть примеры :) Поскольку службу запускает SCM, он некоторое время ждёт, когда служба вызовет StartServiceCtrlDispatcher(), параметром этой функции является массив из записей вида «Имя службы — адрес входа в процедуру MainServiceProc()», из чего делаем вывод, что в одном исполняемом файле службы может содержаться несколько служб с разными именами. Возврат из StartServiceCtrlDispatcher() происходит только после остановки всех прописанных в этом вызове служб. Если StartServiceCtrlDispatcher() возвращает false, значит что-то не так, и нам поможет GetLastError() :)

В конце концов, если всё в порядке, SCM запускает новый поток и передаёт управление на ServiceMain(). В этой функции программист должен первым делом зарегистрировать свою функцию Handler() через функцию API RegisterServiceCtrlHandler(), она возвратит дескриптор, используя который, служба будет «отчитываться» SCM о своём состоянии и о своих ошибках, а в Handler() SCM будет бросать сообщения о том, что нужно делать службе — перейти в паузу, выйти из паузы, остановиться и прочее — т.е. те команды, которые указывают при вызове ControlService() «снаружи».

Необходимо сказать о многопоточности. Handler() и ServiceMain() исполняются в отдельных потоках, это нужно учитывать. Но это и кое-что облегчает, часто программисты, для перевода службы в паузу, просто исполняют SuspendThread() для потока ServiceMain(), а с помощью ResumeThread() восстанавливают работу службы. «Монстроидальные» службы, от которых многое зависит, могут по-разному имплементировать своё состояние паузы. К примеру, служба lanmanserver (да-да, та самая, которая «Supports file, print, and named-pipe sharing over the network»), в паузе перестаёт открывать новые файлы, а в остальном продолжает работать как обычно.

Как уже говорилось, служба должна отчитываться SCM о своём состоянии, для этого используется функция API SetServiceStatus(), где в списке параметров есть указатель на структуру SERVICE_STATUS. В этой структуре служба указывает, какие команды она может воспринимать, в каком она состоянии в данный момент находится, если была ошибка, то заполнит поле для кода ошибки. В частности, службу можно запрограммировать так, что остановить её после запуска, или загнать в паузу, будет невозможно, эдакий «неубиваемый» сервис... Единственное, его можно грохнуть через TerminateProcess(), но это отдельная история...

И вот, скелет службы создан, и руки чешутся нажать на кнопку «Run» любимой IDE... Однако спешка нужна... сами знаете где... Теперь службу нужно «прописать» в систему, т.е. или в самой программе службы встроить реакцию на, скажем, параметр командной строки типа -install, либо написать программу-установщик, которая зарегистрирует службу в системе. Делается это, как вы помните, при помощи вызова функций OpenSCManager() и CreateService(), параметры которых мы достаточно подробно рассмотрели.

Ну и наконец, после того, как служба инсталлирована в систему, можно попробовать её запустить... Делается это встроенным в OS «запускателем» командной строки «net start ИмяСлужбы» или через оснастку управления службами.

Отладка служб тоже занятие не для слабонервных :) Однако, есть подсказки от M$ о том, как это делается. Суть метода такова: если служба прописана в систему, но запускается не с помощью SCM, а другой программой (к примеру, отладчиком), то OS терпеливо выжидает (порядка 10 секунд), пока вы судорожно в оснастке управления служб запускаете свою многострадальную... Время ожидания, кстати, часто зависит от расположения планет :) Если же у вас так не получается (у меня кое-когда не получалось), то вы можете сделать отладочную версию службы, которая запустившись, сразу уходит в паузу. Вы подключаетесь к процессу службы отладчиком, и делаете ей Continue через оснастку. Я надеюсь, что к этому времени вы расставите точки останова где следует :) Если вы отлаживаете Handler(), и вывалились в точке останова в отладчик, не следует долго держать Handler() в остановленном состоянии, поскольку SCM может «обидеться» и запишет в логи системы сообщение о том, что «служба не ответила за отведённое ей время», и дальнейшее поведение SCM по отношению к вашей службе будет достаточно непредсказуемым... В тяжёлых, критических по времени случаях, лучше не использовать отладчик, а писать нужную информацию (флаги, переменные) в текстовые логи для дальнейшего анализа.

Бывают сложные ситуации... Есть профессиональные отладчики уровня ядра, к примеру Compuware SoftICE и M$ kernel debugger, которые полностью останавливают систему в точках останова, и получасовое медитирование на переменными (да-да, современные отладчики прекрасно работают с символами и позволяют отлаживать не хуже, чем в том же VC (отладка в исходниках, просмотр локальных переменных, добавление watch-ей и проч.) поможет понять проблему и не вызовет побочных эффектов со стороны SCM. Надеюсь, фортуна вам улыбнётся.

Насколько я понимаю, устанавливать и запускать службы может только администратор?

При вызове функции OpenSCManager() одним из параметров является уровень доступа к службе. По умолчанию, обычный пользователь не может установить службу. А вот дальше доступ, т.е. сможет ли пользователь запустить, остановить или даже удалить службу, задаётся после создания службы при помощи функции SetServiceObjectSecurity(), иныим словами группа администраторов для каждой службы может сконфигурировать любой Список Контроля Доступа (DACL) для любых пользователей и групп в системе. По умолчанию, после установки службы в систему, Windows определяет для неё такой DACL:

  • SYSTEM: QueryServiceConfig, QueryServiceStatus, EnumerateDependentServices, StartService, StopService, PauseContinueService, InterrogateService, UserDefinedControl, Read_DACL. Интересно, правда? Процесс под LocalSystem не может удалять и конфигурировать службы :)
  • Администраторы: могут всё;
  • Прошедшие проверку: QueryServiceConfig, QueryServiceStatus, EnumerateDependentServices, InterrogateService, UserDefinedControl, Read_DACL. Объясняю по русски: обычный пользователь может перечислить службы (зависимые), прочитать ACL для неё, посмотреть её конфигурацию и в каком она состоянии, послать UserDefinedControl code. Но запустить или остановить её он не может, как и удалить;
  • Опытные пользователи: то же самое, что и «Прошедшие проверку», но могут запустить и остановить службу, загнать её в паузу.

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

program ShowServiceDacl;
{$APPTYPE CONSOLE}
uses
  SysUtils, Windows, WinSVC;

Var scHndl, sHndl: THandle;

  Function toOEM(aStr: String): String;
  Begin
    Result := aStr;
    UniqueString(Result);
    ANSItoOEM(PChar(Result), PChar(Result));
  End;

  Function GetServiceNameByDisplayName(aDispName: String): String;
  Var bufSize: DWORD;
  Begin
    Result  := '';
    bufSize := 0;
    GetServiceKeyName(scHndl, PChar(ParamStr(1)), Nil, bufSize);
    If GetLastError <> ERROR_INSUFFICIENT_BUFFER Then RaiseLastWin32Error;
    SetLength(Result, bufSize Div 2);
    If Not GetServiceKeyName(scHndl, PChar(ParamStr(1)), PChar(Result), bufSize) Then
      RaiseLastWin32Error;
  End;

//Эти объявления взяты из winnt.h, в станд. библиотеках Delphi этого нет :(
Const
  ACCESS_ALLOWED_ACE_TYPE = 0;
  ACCESS_DENIED_ACE_TYPE  = 1;
Type
  _ACE_HEADER = Packed Record
    AceType, AceFlags: BYTE;
    AceSize: WORD;
  End;
  ACE_HEADER = _ACE_HEADER;
  PACE_HEADER = ^ACE_HEADER;

  _ACCESS_ALLOWED_ACE = Record
    Header: ACE_HEADER;
    Mask: ACCESS_MASK;
    SidStart: DWORD;
  End;
  PACCESS_ALLOWED_ACE = ^_ACCESS_ALLOWED_ACE;

  _SID = Record
    Revision: BYTE;
    SubAuthorityCount: BYTE;
    IdentifierAuthority: SID_IDENTIFIER_AUTHORITY;
    SubAuthority: DWORD;
  End;
  TSID = _SID;
  PSID = ^TSID;

  Function SidToStr(aSID: PSID): String;
  Var
    AuthID: PSidIdentifierAuthority;
    SubAuthCount: PUCHAR;
    SubAuth: PDWORD;
    J: Integer;
  begin
    Win32Check(IsValidSID(aSID));

    Result := 'S-' + IntToStr(aSID.Revision) + '-';

    AuthID := GetSidIdentifierAuthority(aSID);
    Result := Result + IntToStr(AuthID.Value[High(AuthID.Value)]) + '-';
    For J := High(AuthID.Value) - 1 DownTo Low(AuthID.Value) Do
      If AuthID.Value[J] <> 0 Then
        Result := Result + IntToStr(AuthID.Value[J]) + '-';

    SubAuthCount := GetSIDSubAuthorityCount(aSID);
    For J := 0 To SubAuthCount^ - 1 Do
    Begin
      SubAuth := GetSIDSubAuthority(ASID, J);
      Result := Result + IntToStr(SubAuth^) + '-';
    End;

    J := Length(Result);
    If J <> 0 Then
      SetLength(Result, J - 1);
  End;

  Function GetAccountBySID(ServerName: String; aSID: PSID): String;
  Var
    nSize, dSize, pUse: DWORD;
    aName, dName: PChar;
  Begin
    If Not IsValidSID(aSID) Then
    Begin
      Result := 'Erroneus SID!';
      Exit;
    End;
    nSize := 0; dSize := 0;
    If Not LookupAccountSid(PChar(ServerName), aSID, Nil, nSize, Nil, dSize, pUse) Then
      If GetLastError <> ERROR_INSUFFICIENT_BUFFER Then
        If GetLastError <> ERROR_NONE_MAPPED Then RaiseLastWin32Error
        Else Begin
          Result := SIDToStr(aSID);
          Exit;
        End;
    aName := StrAlloc(nSize);
    dName := StrAlloc(dSize);
    Try
      Win32Check(LookupAccountSid(
        PChar(ServerName), aSID, aName, nSize, dName, dSize, pUSE));
      Result := aName + ' (' + dName + ')';
    Finally
      StrDispose(aName);
      StrDispose(dName);
    End;
  End;

Var
  TempStr: String;
  SD: Pointer; aDACL: PACL; anACE: PACCESS_ALLOWED_ACE;
  bytesNeeded, tempMask: DWORD;
  DummyFlag1, DummyFlag2: BOOL;
  J, I: Integer;

Const
  AccessMaskStringMap: Array[0..31] Of String = (
    'Query Service Config', 'Change Service Config', 'Query Service Status',
    'Enumerate Dependent Services', 'Start Service', 'Stop Service',
    'Pause\Continue Service', 'Interrogate Service', 'User Defined Service Control',
    '', '', '', '', '', '', '', 'Delete Service', 'Read DACL',
    'Write DACL', 'Write Owner', '', '', '', '', 'Access System ACL',
    '', '', '', '', '', '', ''
  );

begin
  Try
    If ParamCount < 1 Then
      Raise Exception.Create('Service DisplayName must be first input parameter!');

    scHndl := OpenSCManager(Nil, Nil, SC_MANAGER_CONNECT);
    Try
      TempStr := GetServiceNameByDisplayName(ParamStr(1));

      sHndl := OpenService(scHndl, PChar(TempStr), READ_CONTROL);
      If sHndl = 0 Then RaiseLastWin32Error;
      Try
        GetMem(SD, 1024);
        If Not QueryServiceObjectSecurity(
          sHndl, DACL_SECURITY_INFORMATION,
          SD, 1024, bytesNeeded)
        Then RaiseLastWin32Error;

        //На этом этапе имеем Security Descriptor (в SD). Начинаем его "ковырять"
        //Получаем указатель на DACL
        If Not GetSecurityDescriptorDACL(SD, DummyFlag1, aDACL, DummyFlag2) Then
          RaiseLastWin32Error;

        WriteLn('DACL information for Service "', ParamStr(1), '"');
        WriteLn('ACE''s count: ', aDACL.AceCount);

        If aDACL.AceCount = 0 Then
        Begin
          WriteLn('Nothing to show!');
          Exit;
        End;

        WriteLn('Start dumping...');
        WriteLn;

        //Вытаскиваем ACE's...
        For J := 0 To aDACL.AceCount - 1 Do
        Begin
          If Not GetACE(aDACL^, J, Pointer(anACE)) Then RaiseLastWin32Error;

          Case anACE.Header.AceType Of
            ACCESS_ALLOWED_ACE_TYPE: TempStr := 'Allowed';
            ACCESS_DENIED_ACE_TYPE: TempStr := 'Denied';
            else TempStr := 'Unknown';
          End;

          //Выводим пользователя в этом ACE...
          Writeln(
            'Access ', TempStr, ' Ace (at index ', J, ') for user "',
            toOEM(GetAccountBySID('', @anACE.SidStart)), '"'
          );

          WriteLn('Access mask:');
          //Выводим AccessRights для ACE:
          For I := 0 To 31 Do
          Begin //Проверяем каждый бит в 32-битной маске доступа...
            TempMask := 1 shl I;
            If (anACE.Mask And TempMask) = TempMask Then
              WriteLn('   ', AccessMaskStringMap[I]); //Выводим описание бита в маске
          End;
          WriteLn;
        End;

        Writeln('Information for ', aDACL.AceCount, ' ACE''s printed successfully!');
        Writeln(toOEM('Благодарности слать на alex_wh@mail.ru'));
        Write(toOEM('Можно пивом! ;-) Нажмите Enter для выхода...'));
        ReadLN;
      Finally
        CloseServiceHandle(sHndl);
      End;
    Finally
      CloseServiceHandle(scHndl);
    End;
  Except
    On E: Exception Do
    If Not (E is EAbort) Then
    Begin
      WriteLn('Exception Class ' + E.ClassName + ' occured.');
      WriteLn('Error message: "' + toOEM(E.Message) + '"');
      WriteLn('Program terminated.');
    End;
  End;
end.

Я понимаю, что в низкоуровневое Security API достаточно тяжело залезать начинающемо программисту, но Security требует жертв... а также хороший повод всё-таки залезть туда :) После изучения этой программы может возникнуть другой вопрос: как же менять DACL для службы? Практически также, как и читать. Конструируете Security Descriptor, в котором делаете DACL, какой вам нужен, и вызываете SetServiceObjectSecurity() с этим Security Descriptor'ом. Будьте осторожны — неверно выставленные права могут лишить вас доступа к службе, прийдётся удалять её через реестр с последующей перезагрузкой системы.

Если же надо просто подправить AccessMask для существующего пользователя в DACL, можно поступить проще: вытаскиваем SD для службы при помощи QueryServiceObjectSecurity(). Находим DACL. Находим нужный ACE. Правим маску. Вызываем SetServiceObjectSecurity()... Вуаля!

Ну и на засыпку, для интересующихся и начинающих программировать, приведу ниже простой пример службы на Delphi, которая может себя устанавливать и удалять, при запуске «пикает» в системный спикер, может войти в паузу и быть остановлена.

После изучения примера у вас могут заскрести кошки: «Всё как-то сложно... Неудобно... :-( Прога, которая у меня и так работает, в трее сидит и никому не мешает, мне только надо, чтобы она не выгружалась при смене пользователя... Может есть другой вариант?» Да, вариант есть, написали его уважаемые программеры от M$ для того, чтобы... лентяи продолжали лентяйничать :-) Программа называется srvany.exe + маленький установщик служб. Суть идеи такая: устанавливаем в систему интерактивную службу srvany.exe. Идёте в реестр, находите ключ реестра для этой вновь созданной службы. Там добавляете подключ «Parameters», в котором указываете, что за программу (вашу) должна запустить эта служба + дополнительные параметры командой строки для вашей программы (если надо). Запускаете службу srvany, которая стартует вашу программу. Возможные подводные камни:

  • При остановке службы программа будет «убита» через TerminateProcess(). А службы останавливаются при остановке системы. Т.е. чаще всего ваша программа будет «завершаться» именно таким образом. Для некоторых программ это неприемлемо.
  • При завершении сеанса (когда пользователь выходит из системы), окно вашей программы получит WM_QUERYENDSESSION. Некоторые программы в ужасе завершаются от этого сообщения... :)
  • Если ваша программа должна сидеть значком в трее, и тип запуска службы srvany вы установили как автоматический, то ваша программа будет запущена ещё до входа пользователя в систему. Соответственно, Explorer в режиме рабочего стола ещё не запущен, и значок, который программа будет пытаться зарегистрировать в трей, после входа пользователя в систему в трее так и не появится.
  • То же самое с треем и при смене пользователя — значок, зарегистрированный в сеансе предыдущего пользователя, будет «забыт» системой.

Я это всё к тому, что скорее всего некоторые изменения в программе для запуска под srvany делать всё равно прийдётся (реагировать на смены пользователей и т.п.). Так что может оказаться, что проще «до основанья, а затем»... всё-таки переписать программу в службу :)

Успехов на ниве программирования служб, ваш HandleX.

program SimpleServ;
{$APPTYPE CONSOLE}
uses Windows, WinSVC; //Не используем SysUtils - размер *.exe будет ~25 Kb

Const
  ServicesCount = 1;
  ServiceName = 'SimpleServ';
  DisplayName = 'Simple Service';
  ServiceType = SERVICE_WIN32_OWN_PROCESS;
  ServiceDescription = 'SimpleServ by HandleX... This sample program will help ' +
                       'you to understand Windows NT Services. ' +
                       'Mail to alex_wh@mail.ru.';

Var
  ServThrHndl: THandle = 0;
  StopEvent: THandle = 0;
  aServHndl: DWord = 0;
  aServStatus: SERVICE_STATUS;

  Function IntToStr(Value: Integer): String; //Included we don't use SysUtils
  Var aSign: Bool;
  Begin
    Result := '';

    aSign := Value >= 0;
    If Not aSign Then Value := -Value;

    Repeat
      Result := Char(Value - (Value Div 10) * 10 + Byte('0')) + Result;
      Value := Value Div 10;
    Until Value = 0;

    If Not aSign Then Result := '-' + Result;
  End;

  // Helper function for windows error strings
  //Included because we don't use SysUtils
  function SysErrorMessage(ErrorCode: Integer): string;
  var
    Len: Integer;
    Buffer: array[0..255] of Char;
  begin
    Len := FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM or
      FORMAT_MESSAGE_ARGUMENT_ARRAY, nil, ErrorCode, 0, Buffer,
      SizeOf(Buffer), nil);
    while (Len > 0) and (Buffer[Len - 1] in [#0..#32]) do Dec(Len);
    SetString(Result, Buffer, Len);
    UniqueString(Result);
    ANSItoOEM(PChar(Result), PChar(Result));
    If Result <> '' Then
      Result := IntToStr(ErrorCode) + ' ' + Result;
  end;

  Procedure ShowInfo;
  Begin
    WriteLn;
    WriteLn('                    -=* SIMPLE TRAINING SERVICE BY HandleX *=-');
  End;

  Procedure ProcessStartupParams; //Реакция на install, uninstall

    //Устанавливает "описание" для службы, под Win2k и выше
    Function SetServiceDescription(aSHndl: THandle; aDesc: String): Bool;
    Const SERVICE_CONFIG_DESCRIPTION: DWord = 1;
    Var
      DynChangeServiceConfig2: Function(
        hService: SC_HANDLE;                    // handle to service
        dwInfoLevel: DWORD;                     // information level
        lpInfo: Pointer): Bool; StdCall;        // new data
      aLibHndl: THandle;
      TempP: PChar;
    Begin
      aLibHndl := GetModuleHandle(advapi32);
      Result := aLibHndl <> 0; If Not Result Then Exit;
      DynChangeServiceConfig2 := GetProcAddress(aLibHndl, 'ChangeServiceConfig2A');
      Result := @DynChangeServiceConfig2 <> Nil; If Not Result Then Exit;
      TempP := PChar(aDesc); //ChangeServiceConfig2 хочет указатель на указатель строки
      Result := DynChangeServiceConfig2(aSHndl, SERVICE_CONFIG_DESCRIPTION, @TempP);
    End;

  Type
    TToDo = (tdError, tdInstall, tdUninstall);
    TToDo_s = Set of TToDo;
  Const
    ParamStrings: Array[tdInstall..tdUninstall] of String = ('install', 'uninstall');

  Function MapParam(aParam: String): TToDo; //Узнаёт из параметра о вашем желании ;-)
  Var
    J: TToDo;
    TempStr: String;
  Begin
    Result := tdError;
    TempStr := aParam;
    If TempStr[1] In ['/', '-'] Then
      TempStr := Copy(TempStr, 2, Length(TempStr) - 1);
    UniqueString(TempStr);
      CharLower(PChar(TempStr));
    For J := Low(ParamStrings) to High(ParamStrings) Do
      If ParamStrings[J] = TempStr Then
      Begin
        Result := J;
        Exit;
      End;
  End;

  Var
    J: Integer;
    scHndl, sHndl: THandle;
    aStatus: TServiceStatus;
    toDo: TTodo_s;
  Begin
    toDo := [];
    For J := 1 to ParamCount Do
    Begin
      Include(ToDo, MapParam(ParamStr(J)));
      If tdError in toDo Then
      Begin
        ExitCode := ERROR_INVALID_PARAMETER;
        WriteLn('Unknown parameter - ' + ParamStr(J) + '. RTFM, please...');
        Exit;
      End;
    End;

    If [tdInstall, tdUninstall] <= toDo Then
    Begin
      ExitCode := ERROR_INVALID_PARAMETER;
      WriteLn('Error: you can not install and uninstall service simultaniosly. Check params.');
      Exit;
    End;

    If tdInstall in toDo Then //Устанавливаем сервис
    Begin
      Write('Connecting Service Control Manager...');
      scHndl := OpenSCManager(Nil, Nil, SC_MANAGER_CREATE_SERVICE);
      If scHndl = 0 Then
      Begin
        ExitCode := GetLastError;
        WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
        Exit;
      End;
      Try
        WriteLn('Ok');
        Write('Creating service database record...');
        sHndl := CreateService(
          SCHndl, ServiceName, DisplayName,
          SERVICE_QUERY_CONFIG Or SERVICE_CHANGE_CONFIG, ServiceType, SERVICE_DEMAND_START,
          SERVICE_ERROR_NORMAL, PChar(ParamStr(0)), Nil, Nil, Nil, Nil, Nil);

        If sHndl = 0 Then
        Begin
          ExitCode := GetLastError;
          WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
          Exit;
        End;
        Try
          WriteLn('Ok');

          If ServiceDescription <> '' Then
          Begin
            Write('Setting service description...');
            If Not SetServiceDescription(sHndl, ServiceDescription) Then
            Begin
              WriteLn('Failed!');
              WriteLn('Warning: ', SysErrorMessage(GetLastError));
              WriteLn('Warning: SetServiceDesc() failed, but service is installed!');
            End Else WriteLn('Ok');
          End;
        Finally
          CloseServiceHandle(sHndl);
        End;
      Finally
        CloseServiceHandle(SCHndl);
      End;
      WriteLn('Service "', DisplayName, '" install success.');
    End;

    If tdUninstall in toDo Then //Удаляем сервис...
    Begin
      Write('Connecting Service Control Manager...');
      scHndl := OpenSCManager(Nil, Nil, GENERIC_EXECUTE);
      If scHndl = 0 Then
      Begin
        ExitCode := GetLastError;
        WriteLn('Failed!');
        WriteLn('Error: ', SysErrorMessage(ExitCode));
        Exit;
      End;
      Try
        WriteLn('Ok');

        Write('Opening and Quering Service...');
        sHndl := OpenService(SCHndl, ServiceName,
          STANDARD_RIGHTS_REQUIRED Or SERVICE_QUERY_STATUS Or SERVICE_STOP);
        If sHndl = 0 Then
        Begin
          ExitCode := GetLastError;
          WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
          Exit;
        End;
        Try
          If Not QueryServiceStatus(sHndl, aStatus) Then
          Begin
            ExitCode := GetLastError;
            WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
            Exit;
          End;
          WriteLn('Ok');

          If aStatus.dwCurrentState <> SERVICE_STOPPED Then
          Begin
            Write('Service is running, wait until stopped...');
            If Not ControlService(sHndl, SERVICE_CONTROL_STOP, aStatus) Then
            Begin
              ExitCode := GetLastError;
              WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
              Exit;
            End;
            While aStatus.dwCurrentState <> SERVICE_STOPPED Do
            Begin
              Sleep(250); Write('.');
              If Not QueryServiceStatus(sHndl, aStatus) Then
              Begin
                ExitCode := GetLastError;
                WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
                Exit;
              End;
            End;
            WriteLn('Stopped');
          End;

          Write('Deleting Service...');
          If Not DeleteService(sHndl) Then
          Begin
            ExitCode := GetLastError;
            WriteLn('Failed!'); WriteLn('Error: ', SysErrorMessage(ExitCode));
            Exit;
          End;
          WriteLn('Ok');
        Finally
          CloseServiceHandle(sHndl);
        End;
      Finally
        CloseServiceHandle(SCHndl);
      End;
      WriteLn('Service uninstall success.');
    End;

  End;

    Function SetState(aState: DWORD): DWORD;
    Begin
      aServStatus.dwCurrentState := aState;
      If aServHndl <> 0 Then
        SetServiceStatus(aServHndl, aServStatus);
      Result := aServStatus.dwCurrentState;
    End;

    Procedure ServiceHandler(fdwControl: DWORD); StdCall;
    Begin
      Case fdwControl Of
        //Requests the service to stop.
        SERVICE_CONTROL_STOP: Begin
          SetState(SERVICE_STOP_PENDING);
          SetEvent(StopEvent);
          //Если сервис был в паузе, то рабочий поток надо возобновить
          ResumeThread(ServThrHndl);
        End;
        //Requests the service to pause.
        SERVICE_CONTROL_PAUSE: Begin
          SetState(SERVICE_PAUSE_PENDING);
          SuspendThread(ServThrHndl); //Останавливаем рабочий поток сервиса
          SetState(SERVICE_PAUSED);
        End;
        //Requests the paused service to resume.
        SERVICE_CONTROL_CONTINUE: Begin
          SetState(SERVICE_CONTINUE_PENDING);
          ResumeThread(ServThrHndl); //Восстанавливаем рабочий поток сервиса
          SetState(SERVICE_RUNNING);
        End;
        //Requests the service to update immediately its current status
        //information to the service control manager.
        SERVICE_CONTROL_INTERROGATE: Begin
          //Говорим SCM о том, в каком состоянии находится наша служба
          SetState(aServStatus.dwCurrentState);
        End;
        //Протяжно пищим в спикер -- кто-то послал USER DEFINED CONTROL CODE.
        //В реальных применениях может использоваться для подачи различных сигналов
        //самописным службам. Удобны тем, что передаются даже удалённым хостам
        //при помощи ControlService() и соотв. дескриптора удалённого SCM.
        128..255: Begin
          SuspendThread(ServThrHndl);
          Windows.Beep(1000, 500);
          //Возвращать результаты можно вызовом SetServiceStatus().
          aServStatus.dwWin32ExitCode := ERROR_SUCCESS;
          SetState(aServStatus.dwCurrentState);
          ResumeThread(ServThrHndl);
        End;
      End;
    End;

  Procedure MainServiceProc(      // Каждая служба может иметь параметры запуска...
    dwArgc: DWORD;                // Кол-во параметров - у нас не используется
    lpszArgv: Pointer); StdCall   // Массив параметров - у нас не используется
  Begin
    aServHndl := RegisterServiceCtrlHandler(ServiceName, @ServiceHandler);
    If aServHndl = 0 Then
    Begin
      ExitCode := GetLastError;
      Exit; //Какая-то ошибка, выходим, SCM будет ругаться, но сообщить мы ему ничего не можем...
    End;
    ZeroMemory(@aServStatus, SizeOf(aServStatus));
    aServStatus.dwServiceType := ServiceType;
    aServStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP Or SERVICE_ACCEPT_PAUSE_CONTINUE;
    //Здесь может быть подсказка для небыстрых служб о том, как долго она реагирует на команды
    //aServStatus.dwWaitHint := 500;

    //Извещаем SCM, что начинается старт службы...
    SetState(SERVICE_START_PENDING);

    //Пошла процедура инициализации...

    //Получаем реальный дескриптор потока службы...
    If Not DuplicateHandle(
      GetCurrentProcess, GetCurrentThread, GetCurrentProcess,
      @ServThrHndl, 0, FALSE, DUPLICATE_SAME_ACCESS)
    Then Begin
      aServStatus.dwWin32ExitCode := GetLastError;
      SetState(SERVICE_STOPPED);
      Exit;
    End;

    //Создаём unnamed event для остановки службы по сигналу из Handler...
    StopEvent := CreateEvent(Nil, True, False, Nil);
    If StopEvent = 0 Then //Ошибка создания Event'а - выходим...
    Begin
      aServStatus.dwWin32ExitCode := GetLastError;
      SetState(SERVICE_STOPPED);
      Exit;
    End;

    SetState(SERVICE_RUNNING); //Инит прошёл, работаем!

    //Крутим цикл, бипаем по таймауту, если срабатывает event - выходим...
    //В реальной службе именно здесь может быть конкретная полезная работа
    While WaitForSingleObject(StopEvent, 500) = WAIT_TIMEOUT Do
      Windows.Beep(10000, 10); 

    //Выполняем остановку сервиса, вычищаемся...
    CloseHandle(ServThrHndl); ServThrHndl := 0;
    CloseHandle(StopEvent); StopEvent := 0;

    SetState(SERVICE_STOPPED); //Извещаем, SCM, что работа службы остановлена...
  End;                         //Поток ЭТОЙ службы завершил свою работу.

Var
  ServTableEntryArray: Array[0..ServicesCount] Of TServiceTableEntryA;

begin

  //Старт программы...

  If ParamCount > 0 Then //От нас что-то хотят...
  Begin
    ShowInfo;
    ProcessStartupParams; //Выясняем что и выходим...
    Exit;
  End;

  //Готовимся к вызову StartServiceCtrlDispatcher
  ZeroMemory(@ServTableEntryArray, SizeOf(ServTableEntryArray));
  ServTableEntryArray[0].lpServiceName := ServiceName;
  ServTableEntryArray[0].lpServiceProc := @MainServiceProc;

  If Not StartServiceCtrlDispatcher(ServTableEntryArray[0]) Then
  Begin
    ExitCode := GetLastError;
    ShowInfo; //Какая-то ошибка, выводим в консоль сообщение и выходим...
    WriteLn('Error: ', SysErrorMessage(ExitCode));
    WriteLn('This program is Windows NT Service, so it CAN NOT be run from command prompt.');
    WriteLn('You can install it with "/install" parameter.');
    WriteLn('If this service is already installed, you can run it with "net start" command.');
  End;
  //Процесс службы завершает работу, всем до свидания...
  //Если вы разместили в своём *.exe несколько служб, то здесь 
  //вы окажетесь только после остановки ВСЕХ служб процесса.
end.


Слова для поиска: обновлено 10.01.06

обсудить  |  все отзывы (0)

[64351]



Rambler's Top100
Рейтинг@Mail.ru



  Copyright © 2001-2024 Dmitry Leonov   Page build time: 0 s   Design: Vadim Derkach