Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
2006 г.

Смена пароля локального администратора

Евгений Бабенко, Королевство Delphi

  1. Постановка задачи
  2. ADSI
  3. VBS
  4. Реализация на Delphi

1. Постановка задачи

На компьютерах с операционными системами Windows NT x.x при установке создается учетная запись локального администратора, которая имеет неограниченные права на данном компьютере. Если компьютер предполагается использовать в домене, то, как правило, технический персонал устанавливает один и тот же пароль для данной учетной записи. И как правило он не очень сложный. При наличии физического доступа к рабочей станции пароль администратора может быть легко подобран со всеми вытекающими отсюда последствиями. Задача администратора сети - установить достаточно сложный пароль для данной учетной записи и периодически его менять. Если в домене несколько десятков компьютеров, это может занять много времени. Если же в домене несколько сот компьютеров, а часто они еще и географически разнесены, то без автоматизации данного процесса не обойтись.

Определимся, что должна делать программа - утилита. Т.е. составим простой алгоритм работы:

  1. получить список имен компьютеров в домене (возможно отфильтрованный по заданному критерию);
  2. подключиться к каждому компьютеру из списка и сменить пароль.

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

2. ADSI

После того как определились, что надо сделать (в данном случае это не составляет труда), встает вопрос о реализации. Первой мыслью было использовать технологию WMI, но после краткого исследования проблемы решено было остановиться на ADSI. Далее вольный перевод нескольких предложений из MSDN:

ADSI - Active Directory Service Interfaces. Микрософт создала набор COM-интерфейсов, предназначенных для доступа к различным службам каталогов.

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

Объектная модель ADSI базируется на COM - объектах. Программа клиент управляет объектами через интерфейсы. Следующая таблица перечисляет фундаментальные элементы ADSI.

ИнтерфейсыОписание
IADsИспользуется для идентификации объекта. Как фундаментальный интерфейс, поддерживаемый всеми ADSI объектами, позволяет получить доступ к метаданным объекта, включая описание объекта в схеме Active Directory .
IADsContainerИспользуется для извлечения и управления объектом. Все ADSI объекта - контейнеры требуют использование этого интерфейса для доступа к объектам в контейнере и манипулирования ими.
IADsPropertyListИспользуется для работы со свойствами объекта.

Сложные ADSI объекты могут поддерживать дополнительные интерфейсы.

3. VBS

Первая реализация задачи была сделана на VBS. И это понятно. Достаточно зайти на сайт Микрософт и скачать готовые скрипты. И немного их подправить под свои нужды. Кроме того, на VB код получается очень короткий и легкий для восприятия. Вот пример создания списка компьютеров из домена, расположенных в определенном organization unit в Active Directory (AD):

  Set objDictionary = CreateObject("Scripting.Dictionary")
  strDomain = "LDAP://ou=Test, ou=Mine, dc=mydomain, dc=com"
  Set objDomain = GetObject(strDomain)
  objDomain.Filter = Array("computer")
  i = 0
  For Each objComputer In objDomain
    objDictionary.Add i, Mid(objComputer.Name,4)
    i = i + 1
  Next


Рисунок 1

Для получения доступа к пространству имен каталога необходимо связаться с нужным объектом ADSI.

Set objDomain = GetObject(strDomain)

strDomain - строка связывания.

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

Примеры обращения к различным службам

"LDAP://"Служба каталогов, созданная на основе протокола LDAP (Active Directory в том числе)
"WinNt://"Служба каталогов в сети Windows NT 4.0 или на рабочей станции Windows XP/2000

Вторая часть строки связывания определяет положение объекта в каталоге.

В следующих таблицах приводятся примеры строк связывания:

LDAP

LDAP:Связь с корнем пространства имен LDAP
LDAP://server01Связь с конкретным сервером
LDAP://server01:390Связь с конкретным сервером через указанный порт
LDAP://CN=Jeff Smith,CN=users,DC=fabrikam,DC=comСвязь с конкретным объектом
LDAP://server01/CN=Jeff Smith,CN=users,DC=fabrikam,DC=comСвязь с конкретным объектом через указанный сервер

WinNT

WinNT://<domain name>
WinNT://<domain name>/<server>
WinNT://<domain name>/<path>
WinNT://<domain name>/<object name>
WinNT://<domain name>/<object name>,<object class>
WinNT://<server>
WinNT://<server>/<object name>
WinNT://<server>/<object name>,<object class>

Устанавливаем фильтр для выделения объектов - компьютеров.

objDomain.Filter = Array("computer")

И затем перебираем элементы коллекции.

Главный минус данной реализации (на мой взгляд) - это низкая скорость работы. Для перебора ~150 рабочих станция и смены на них пароля понадобилось около часа времени.

Основные задержки приходятся на операцию связывания. Особенно большие таймауты при попытке связывания с выключенным или не существующим компьютером ( или если по какой-то причине отказано в доступе). Решением данной проблемы является организация многопоточности. Поэтому от VBS пришлось отказаться.

4. Реализация на Delphi.

Задача была реализована на Delphi6 sp2. В процессе работы оказалось, что необходимые функции не описаны в библиотеке. Далее в статье будут приведены описания всех необходимых функций.

4.1 Извлечение имен компьютеров домена из AD.

Первым этапом попытаемся установить связь AD. Для этого воспользуемся функцией ADsGetObject. Описание из MSDN:

HRESULT ADsGetObject(
  LPWSTR lpszPathName,
  REFIID riid,
  VOID** ppObject);

lpszPathName - строка связывания;

riid - идентификатор интерфейса;

ppObject - указатель на указатель интерфейса, возвращаемый функцией.

Эта функция эквивалентна функции GetObject из VB (в данном контексте).Она берет строку связывания и возвращает указатель на запрашиваемый интерфейс. Связывание производится в контексте защиты вызывающего потока, используя опции ADS_SECURE_AUTHENTICATION. Если требуется указать конкретного пользователя, необходимо использовать функцию ADsOpenObject (прошу прощения за корявый перевод).

Далее пример использования ADsGetObject для связывания с AD:

interface 
Uses :. , ActiveDs_TLB;
:
function ADsGetObject(lpszPathName: WideString; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall;


implementation

function ADsGetObject; external 'activeds.dll';

Procedure TForm1.Test
Var  hr: HResult;
        objDomain: Pointer;
begin

   hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', IID_IADsContainer,       
        objDomain);
   if Failed(hr) then Exit;
end;

Чтобы данный пример мог быть откомпилирован необходимо импортировать библиотеку типов Activeds.tlb, как показано на рисунке 2:


Рисунок 2

Замечание:

При работе с ADsGetObject бывали ситуации, когда при попытке прочитать какое-либо свойство полученного объекта выходила ошибка 'The directory property cannot be found in cache'. К сожалению, это было достаточно давно, и восстановить ситуацию не удалось. Тем не менее ошибка была. Обойти ее удалось при использовании функции ADsOpenObject . Вот пример использования данной функции:

interface 
Uses :. , ActiveDs_TLB;
:
function ADsOpenObject(lpszPathName: WideString; lpszUserName: WideString; lpszPassword: WideString;
  dwReserved: DWORD; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall;


implementation

function ADsOpenObject; external 'activeds.dll';

Procedure TForm1.Test
Var  hr: HResult;
        objDomain: Pointer;
begin

    hr:= ADsOpenObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', '', '',   
                             DS_SECURE_AUTHENTICATION,  IID_IADsContainer, objDomain);}
   if Failed(hr) then Exit;
end;

Далее в статье будет использоваться только ADsGetObject.

В данных примерах мы пытаемся получить ссылку на интерфейс IID_IADsContainer.

IID_IADsContainer используют для получения коллекции ADSI объектов. Полный список интерфейсов, с которыми можно работать при помощи ADsGetObject, и их описание можно найти в MSDN.

После того, как мы получили ссылку на контейнер, осталось перебрать его объекты и считать их имена. Для этого нам понадобятся еще две функции - AdsBuildEnumerator и ADsEnumerateNext.

AdsBuildEnumerator- создает объект Enumerator (перечеслитель) для конкретного объекта контейнера ADSI.

function ADsBuildEnumerator(pADsContainerL: IADsContainer; ppEnumVariant: PIEnumVARIANT): HRESULT; stdcall;
function ADsBuildEnumerator; external 'activeds.dll';

pADsContainerL - указатель на IADsContainer;

ppEnumVariant - указатель на указатель IEnumVariant интерфейс, который связывает создаваемый объект Enumerator с соответствующим объектом контейнером.

Интерфейс IEnumVARIANT описан в модуле ActiveX.

ADsEnumerateNext - позволяет перемещать указатель по элементам коллекции.

function ADsEnumerateNext(pEnumVariant: IEnumVARIANT; cElements: ULONG; pvar: POleVariant;
   pcElementsFetched: PULONG): HRESULT; stdcall;

function ADsEnumerateNext;  external 'activeds.dll';

pEnumVariant - получаем после вызова ADsBuildEnumerator;

cElements - количество элементов, которые мы хотим извлечь из коллекции за один раз;

pvar - указатель на массив, в который помещаются извлеченные из коллекции объекты;

pcElementsFetched - указатель на фактическое количество найденных элементов.

Далее, собственно, пример, демонстрирующий как получить список компьютеров домена из AD:

procedure TForm1.Button1Click(Sender: TObject);
var objDomain: Pointer;
    objChild:  Pointer;
    hr: HResult;
    s: String;
    i: Integer;
    iArr : OleVariant;
    iEnum: IEnumVARIANT;
    iFetch: ULONG;
    iAPath: String;
begin

  ListBox1.Clear;
  hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=bogatyr, dc=kz', IID_IADsContainer, objDomain);
  if Failed(hr) then Exit;
  hr:=ADsBuildEnumerator(IADsContainer(objDomain), @iEnum);
  if Failed(hr) then Exit;
  hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
  while (S_OK = hr) and (1 = iFetch) do

  begin
    hr:=IDispatch(iArr).QueryInterface(IADs,objChild);
    if Failed(hr) then Exit;
    if AnsiLowerCase(IAds(objChild).Class_)='computer' then

    begin
      s:=IAds(objChild).Name;
      System.Delete(s,1,3);
      ListBox1.Items.Add(s);
    end;
    if AnsiLowerCase(IAds(objChild).Class_)='organizationalunit' then

    begin
      Continue;
{      s:=IAds(objChild).Name;
      iAPath:=PAPAth;
      System.Delete(iAPath, 1, 7);
      iAPath:='LDAP:// '+s+','+iAPath;
      if not NextNode_Computer(iAPath) then exit;}
    end;
    if AnsiLowerCase(IAds(objChild).Class_)='container' then

    begin
      Continue;
{      s:=IAds(objChild).Name;
      iAPath:=PAPAth;
      System.Delete(iAPath, 1, 7);
      iAPath:='LDAP:// '+s+','+iAPath;
      if not NextNode_Computer(iAPath) then exit;}
    end;
    iArr:=null;
    hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
  end;
end;

Часть кода в примере закомментирована. Код взят из рабочей программы и слегка исправлен. В закомментированных частях видно, что подпрограмма вызывается рекурсивно. Это было сделано что бы просканировать всю указанную ветку из AD, включая содержащиеся внутри ветки.

4.2 Смена пароля локального администратора.

Здесь все просто. Формируем строку связывание для доступа к объекту с именем "Администратор". Класс объекта - "user". Объект расположен на рабочей станции "Computer01".

iPath:='WinNT://'+NameWs+'/Администратор,user';

И, собственно, реализация.

procedure ChangePassword;

var   objUser: Pointer;
         hr: HResult;
         iPath: String;
         i: Integer;
begin
   iPath:='WinNT://Computer01/Администратор,user';
   hr:= ADsGetObject(iPath, IID_IADsUser, objUser);
   if hr<>S_OK then Exit;
   IADsUser(objUser).SetPassword('anykey');

end;

4.3 Обработка ошибок.

Если вызов ADSI функции завершился неудачей, функция вернет код ошибки стандартным для COM объектов способом. Коды ошибок делятся не четыре группы:

  • Универсальные коды ошибок COM;
  • Универсальные коды ошибок ADSI;
  • Коды ошибок Win32 для ADSI;
  • Коды ошибок LDAP для ADSI;

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

function ADsGetLastError(lpError: LPDWORD; lpErrorBuf: LPWSTR; dwErrorBufLen: DWORD;
  lpNameBuf: LPWSTR;  dwNameBufLen: DWORD): HRESULT; stdcall;

lpError - указатель на код ошибки;

lpErrorBuf - указатель на буфер, куда будет передано описание ошибки;

dwErrorBufLen - размер буфера;

lpNameBuf - указатель на буфер, куда будет передано имя провайдера, который возбудил эту ошибку;

dwNameBufLen - размер буфера;

Простой пример использования этой функции можно будет посмотреть в исходных кодах, прилагаемых к статье. Для получения наиболее полной информации о произошедших ошибках обратитесь к MSDN. В частности, по ссылке приведен пример функций (на Си), которые в качестве аргумента принимают код ошибки и возвращают ее описание. Осталось их (эти функции) перевести на Pascal и использовать.

Список литературы

  1. http://security.software-testing.ru/wiki/Lokal'najaUgroza
  2. MSDN
  3. "Администрирование Windows с помощью WMI и WMIC" , Андрей Попов и Евгений Шикин.

Исходные коды проекта и пример базы данных (22 K)

Новости мира IT:

Архив новостей

Последние комментарии:

Loading

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 985 1945361
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList liveinternet.ru: показано число просмотров за 24 часа, посетителей за 24 часа и за сегодня This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2015 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...