- Постановка задачи
- ADSI
- VBS
- Реализация на Delphi
1. Постановка задачи
На компьютерах с операционными системами Windows NT x.x при установке создается учетная запись локального администратора, которая имеет неограниченные права на данном компьютере. Если компьютер предполагается использовать в домене, то, как правило, технический персонал устанавливает один и тот же пароль для данной учетной записи. И как правило он не очень сложный. При наличии физического доступа к рабочей станции пароль администратора может быть легко подобран со всеми вытекающими отсюда последствиями. Задача администратора сети - установить достаточно сложный пароль для данной учетной записи и периодически его менять. Если в домене несколько десятков компьютеров, это может занять много времени. Если же в домене несколько сот компьютеров, а часто они еще и географически разнесены, то без автоматизации данного процесса не обойтись.
Определимся, что должна делать программа - утилита. Т.е. составим простой алгоритм работы:
- получить список имен компьютеров в домене (возможно отфильтрованный по заданному критерию);
- подключиться к каждому компьютеру из списка и сменить пароль.
Для более комфортной работы утилиты необходимо обеспечить должную обработку ошибок, а результат работы записать в базу.
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. В процессе работы оказалось, что необходимые функции не описаны в библиотеке. Далее в статье будут приведены описания всех необходимых функций.
Первым этапом попытаемся установить связь 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;
end;
if AnsiLowerCase(IAds(objChild).Class_)='container' then
begin
Continue;
end;
iArr:=null;
hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
end;
end;
Часть кода в примере закомментирована. Код взят из рабочей программы и слегка исправлен. В закомментированных частях видно, что подпрограмма вызывается рекурсивно. Это было сделано что бы просканировать всю указанную ветку из AD, включая содержащиеся внутри ветки.
Здесь все просто. Формируем строку связывание для доступа к объекту с именем "Администратор". Класс объекта - "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;
Если вызов 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 и использовать.
Список литературы
- http://security.software-testing.ru/wiki/Lokal'najaUgroza
- MSDN
- "Администрирование Windows с помощью WMI и WMIC" , Андрей Попов и Евгений Шикин.
Исходные коды проекта и пример базы данных (22 K)