|
| ||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||
|
2006 г.
Смена пароля локального администратораЕвгений Бабенко, Королевство Delphi 1. Постановка задачиНа компьютерах с операционными системами Windows NT x.x при установке создается учетная запись локального администратора, которая имеет неограниченные права на данном компьютере. Если компьютер предполагается использовать в домене, то, как правило, технический персонал устанавливает один и тот же пароль для данной учетной записи. И как правило он не очень сложный. При наличии физического доступа к рабочей станции пароль администратора может быть легко подобран со всеми вытекающими отсюда последствиями. Задача администратора сети - установить достаточно сложный пароль для данной учетной записи и периодически его менять. Если в домене несколько десятков компьютеров, это может занять много времени. Если же в домене несколько сот компьютеров, а часто они еще и географически разнесены, то без автоматизации данного процесса не обойтись. Определимся, что должна делать программа - утилита. Т.е. составим простой алгоритм работы:
Для более комфортной работы утилиты необходимо обеспечить должную обработку ошибок, а результат работы записать в базу. 2. ADSIПосле того как определились, что надо сделать (в данном случае это не составляет труда), встает вопрос о реализации. Первой мыслью было использовать технологию WMI, но после краткого исследования проблемы решено было остановиться на ADSI. Далее вольный перевод нескольких предложений из MSDN: ADSI - Active Directory Service Interfaces. Микрософт создала набор COM-интерфейсов, предназначенных для доступа к различным службам каталогов. Служба каталогов - это распределенная система, которая предоставляет средства для поиска и использования сетевых ресурсов различных типов. Объектная модель ADSI базируется на COM - объектах. Программа клиент управляет объектами через интерфейсы. Следующая таблица перечисляет фундаментальные элементы ADSI.
Сложные 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
Для получения доступа к пространству имен каталога необходимо связаться с нужным объектом ADSI. Set objDomain = GetObject(strDomain) strDomain - строка связывания. Первая часть строки связывания определяет, к какой именно службе каталогов мы обращаемся. Примеры обращения к различным службам
Вторая часть строки связывания определяет положение объекта в каталоге. В следующих таблицах приводятся примеры строк связывания: LDAP
WinNT
Устанавливаем фильтр для выделения объектов - компьютеров. 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:
Замечание: При работе с 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 объектов способом. Коды ошибок делятся не четыре группы:
Кроме того, некоторые интерфейсы предоставляют дополнительные сведения об ошибке, которые могут быть получены при помощи функции ADsGetLastError. function ADsGetLastError(lpError: LPDWORD; lpErrorBuf: LPWSTR; dwErrorBufLen: DWORD; lpNameBuf: LPWSTR; dwNameBufLen: DWORD): HRESULT; stdcall; lpError - указатель на код ошибки; lpErrorBuf - указатель на буфер, куда будет передано описание ошибки; dwErrorBufLen - размер буфера; lpNameBuf - указатель на буфер, куда будет передано имя провайдера, который возбудил эту ошибку; dwNameBufLen - размер буфера; Простой пример использования этой функции можно будет посмотреть в исходных кодах, прилагаемых к статье. Для получения наиболее полной информации о произошедших ошибках обратитесь к MSDN. В частности, по ссылке приведен пример функций (на Си), которые в качестве аргумента принимают код ошибки и возвращают ее описание. Осталось их (эти функции) перевести на Pascal и использовать. Список литературы
|
|
CITForum © 1997–2025