/*! * @file adsi_interface.cpp * @brief Definitions for functions that directly interact with ADSI * through the (awful) COM interface. */ extern "C" { #include "extapi.h" #include <Iads.h> #include <Adshlp.h> #include <AdsErr.h> #include "adsi_interface.h" } #pragma comment(lib, "Activeds.lib") #define VALUE_SIZE 1024 #define PATH_SIZE 256 typedef BOOL (WINAPI *PCONVERTSIDTOSTRINGSID)(PSID pSid, LPSTR* pStr); /*! @brief The GUID of the Directory Search COM object. */ static const IID IID_IDirectorySearch = { 0x109BA8EC, 0x92F0, 0x11D0, { 0xA7, 0x90, 0x00, 0xC0, 0x4F, 0xD8, 0xD5, 0xA8 } }; static PCONVERTSIDTOSTRINGSID pConvertSidToStringSid = NULL; static HMODULE hAdvapi32 = NULL; /*! * @brief Render a SID to a string. * @param pSid Pointer to the SID to render. * @param pStr Pointer to the variable that will receive the string. Free with `LocalFree`. * @returns Indication of success or failure. */ BOOL ConvertSidToStringSid(PSID pSid, LPSTR* pStr) { if (pConvertSidToStringSid == NULL) { if (hAdvapi32 == NULL) { hAdvapi32 = LoadLibraryA("Advapi32.dll"); } pConvertSidToStringSid = hAdvapi32 == NULL ? NULL : (PCONVERTSIDTOSTRINGSID)GetProcAddress(hAdvapi32, "ConvertSidToStringSidA"); } return pConvertSidToStringSid == NULL ? FALSE : pConvertSidToStringSid(pSid, pStr); } /*! * @brief Render a byte array as a GUID string. * @param bytes Pointer to the GUID bytes. * @param buffer Pointer to the target buffer. * @param bufferSize size of the memory available in \c buffer. * @remark Assumes the caller knows what they're doing, and assumes that there are 16 bytes in the array. */ void guid_to_string(LPBYTE bytes, char* buffer, DWORD bufferSize) { sprintf_s(buffer, bufferSize, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]); } /*! * @brief Render a byte array as a string. * @param bytes Pointer to the bytes. * @param count Number of bytes to write. * @param buffer Pointer to the target buffer. * @param bufferSize size of the memory available in \c buffer. * @param byteFormat optional per-byte format (defaults to \c %02x). * @param byteFormatMaxLen optional per-byte format max length (to make sure we allocated enough memory). * @param delim optional delimiter to render between bytes (defaults to \c -). * @remark Assumes the caller knows what they're doing, and assumes that there are 16 bytes in the array. */ char* bytes_to_string(LPBYTE bytes, DWORD count, char* byteFormat = "%02x", DWORD byteFormatMaxLen = 2, char* delim = "-") { dprintf("[EXTAPI ADSI] Stringifying a binary of %u bytes", count); if (bytes == NULL || count == 0) { return NULL; } size_t delimLen = delim == NULL ? 0 : strlen(delim); size_t requiredSize = count * byteFormatMaxLen + (count - 1) * delimLen + 1; char* string = (char*)malloc(requiredSize); char* csr = string; if (string) { for (DWORD i = 0; i < count; ++i) { if (i != 0 && delimLen > 0) { csr += sprintf_s(csr, delimLen + 1, "%s", delim); } csr += sprintf_s(csr, byteFormatMaxLen + 1, byteFormat, bytes[i]); } } dprintf("[EXTAPI ADSI] Stringified a binary of %u bytes to: %s", count, string); return string; } /*! * @brief Perform a domain query via ADSI. * @param lpwDomain Name of the domain that is to be queried. * @param lpwFilter The filter to use when reading objects (LDAP style). * @param lpwQueryCols Array of column names representing fields to extract. * @param queryColCount Number of columns in \c lpwQueryCols. * @param maxResults The maximum number of results to return. * @param pageSize The size of the page of results to return. * @param response The response \c Packet to add the results to. */ DWORD domain_query(LPCWSTR lpwDomain, LPWSTR lpwFilter, LPWSTR* lpwQueryCols, UINT queryColCount, DWORD maxResults, DWORD pageSize, Packet* response) { HRESULT hResult; WCHAR cbPath[PATH_SIZE]; swprintf_s(cbPath, PATH_SIZE - 1, L"LDAP://%s", lpwDomain); if ((hResult = CoInitialize(NULL)) == S_OK) { IDirectorySearch* pDirSearch = NULL; ADS_SEARCH_HANDLE hSearch = NULL; do { // start by trying to create the search object which we can use to run searches hResult = ADsOpenObject(cbPath, NULL, NULL, ADS_SECURE_AUTHENTICATION | ADS_READONLY_SERVER, IID_IDirectorySearch, (void**)&pDirSearch); if (hResult != S_OK) { dprintf("[ADSI] Unable to open domain: %x", hResult); break; } // set the limit of results so that we don't take forever on large domains ADS_SEARCHPREF_INFO prefInfo[4]; prefInfo[0].dwSearchPref = ADS_SEARCHPREF_SIZE_LIMIT; prefInfo[0].vValue.dwType = ADSTYPE_INTEGER; prefInfo[0].vValue.Integer = (ADS_INTEGER)maxResults; prefInfo[1].dwSearchPref = ADS_SEARCHPREF_PAGESIZE; prefInfo[1].vValue.dwType = ADSTYPE_INTEGER; prefInfo[1].vValue.Integer = (ADS_INTEGER)pageSize; prefInfo[2].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE; prefInfo[2].vValue.dwType = ADSTYPE_INTEGER; prefInfo[2].vValue.Integer = ADS_SCOPE_SUBTREE; prefInfo[3].dwSearchPref = ADS_SEARCHPREF_CACHE_RESULTS; prefInfo[3].vValue.dwType = ADSTYPE_BOOLEAN; prefInfo[3].vValue.Boolean = false; dprintf("[ADSI] Setting Max results to %u", (ADS_INTEGER)maxResults); dprintf("[ADSI] Setting Page size to %u", (ADS_INTEGER)pageSize); if (FAILED(hResult = pDirSearch->SetSearchPreference(prefInfo, 4))) { dprintf("[ADSI] Failed to set search settings %u %x", pageSize, hResult); } dprintf("[ADSI] Search executing"); hResult = pDirSearch->ExecuteSearch(lpwFilter, lpwQueryCols, queryColCount, &hSearch); if (hResult != S_OK) { dprintf("[ADSI] Unable to execute the search"); break; } // These buffers are used to store the values that we're reading out of AD Tlv* entries = (Tlv*)malloc(queryColCount * sizeof(Tlv)); char* values = (char*)malloc(queryColCount * VALUE_SIZE); DWORD rowsProcessed = 0; // now we iterate through the search results while (SUCCEEDED((hResult = pDirSearch->GetNextRow(hSearch))) && (maxResults == 0 || rowsProcessed < maxResults)) { if (hResult == S_ADS_NOMORE_ROWS) { hResult = S_OK; // out of results, so bomb out of the loop break; } DWORD dwIndex = 0; size_t charsConverted; ADS_SEARCH_COLUMN col; // iterate through the columns, adding Tlv entries as we go, but only // if we can get the values out. for (DWORD colIndex = 0; colIndex < queryColCount; ++colIndex) { char* valueTarget = values + dwIndex * VALUE_SIZE; entries[dwIndex].buffer = (PUCHAR)valueTarget; entries[dwIndex].header.type = TLV_TYPE_EXT_ADSI_VALUE; // try to do something sane based on the type that's being used to store // the value. HRESULT hr = pDirSearch->GetColumn(hSearch, lpwQueryCols[dwIndex], &col); if (SUCCEEDED(hr)) { WCHAR* source = NULL; switch (col.dwADsType) { case ADSTYPE_LARGE_INTEGER: { _i64toa_s(col.pADsValues->LargeInteger.QuadPart, valueTarget, VALUE_SIZE, 10); dprintf("[ADSI] Adding large int value %ul", (UINT)col.pADsValues->Integer); break; } case ADSTYPE_INTEGER: { _itoa_s((UINT)col.pADsValues->Integer, valueTarget, VALUE_SIZE, 10); dprintf("[ADSI] Adding int value %u", (UINT)col.pADsValues->Integer); break; } case ADSTYPE_DN_STRING: { source = col.pADsValues->DNString; break; } case ADSTYPE_PRINTABLE_STRING: { source = col.pADsValues->PrintableString; break; } case ADSTYPE_NUMERIC_STRING: { source = col.pADsValues->NumericString; break; } case ADSTYPE_CASE_EXACT_STRING: { source = col.pADsValues->CaseExactString; break; } case ADSTYPE_CASE_IGNORE_STRING: { source = col.pADsValues->CaseIgnoreString; break; } case ADSTYPE_BOOLEAN: { source = col.pADsValues->Boolean == 0 ? L"False" : L"True"; break; } case ADSTYPE_OCTET_STRING: { char* s = bytes_to_string(col.pADsValues->OctetString.lpValue, col.pADsValues->OctetString.dwLength); if (s) { strncpy_s(valueTarget, VALUE_SIZE, s, VALUE_SIZE - 1); free(s); } break; } case ADSTYPE_UTC_TIME: { SYSTEMTIME* pt = &col.pADsValues->UTCTime; sprintf_s(valueTarget, VALUE_SIZE, "%4u-%02u-%02u %02u:%02u:%02u.%03u", pt->wYear, pt->wMonth, pt->wDay, pt->wHour, pt->wMinute, pt->wSecond, pt->wMilliseconds); break; } case ADSTYPE_PROV_SPECIFIC: { char* s = bytes_to_string(col.pADsValues->ProviderSpecific.lpValue, col.pADsValues->ProviderSpecific.dwLength); if (s) { strncpy_s(valueTarget, VALUE_SIZE, s, VALUE_SIZE - 1); free(s); } break; } case ADSTYPE_OBJECT_CLASS: { source = col.pADsValues->ClassName; break; } case ADSTYPE_CASEIGNORE_LIST: { // list of strings, yay! char* prefix = ""; PADS_CASEIGNORE_LIST list = col.pADsValues->pCaseIgnoreList; char* csr = valueTarget; size_t sizeLeft = VALUE_SIZE; while (list != NULL && sizeLeft > 0) { int printed = sprintf_s(csr, sizeLeft, "%s%S", prefix, list->String); sizeLeft -= printed; csr += printed; prefix = ", "; } break; } case ADSTYPE_PATH: { PADS_PATH path = col.pADsValues->pPath; sprintf_s(valueTarget, VALUE_SIZE, "Vol: %S, Path: %S, Type: %u", path->VolumeName, path->Path, path->Type); break; } case ADSTYPE_POSTALADDRESS: { PADS_POSTALADDRESS addr = col.pADsValues->pPostalAddress; char* prefix = ""; PADS_CASEIGNORE_LIST list = col.pADsValues->pCaseIgnoreList; char* csr = valueTarget; size_t sizeLeft = VALUE_SIZE; for (DWORD i = 0; i < sizeof(addr->PostalAddress) / sizeof(addr->PostalAddress[0]) && sizeLeft > 0; ++i) { if (!addr->PostalAddress[i] || lstrlenW(addr->PostalAddress[i]) == 0) { continue; } int printed = sprintf_s(csr, sizeLeft, "%s%S", prefix, addr->PostalAddress[i]); sizeLeft -= printed; csr += printed; prefix = ", "; } break; } case ADSTYPE_TIMESTAMP: { ADS_TIMESTAMP* pts = &col.pADsValues->Timestamp; sprintf_s(valueTarget, VALUE_SIZE, "Event: %S, Sec: %u", pts->EventID, pts->WholeSeconds); break; } case ADSTYPE_BACKLINK: { ADS_BACKLINK* pbl = &col.pADsValues->BackLink; sprintf_s(valueTarget, VALUE_SIZE, "Name: %S, ID: %u", pbl->ObjectName, pbl->RemoteID); break; } case ADSTYPE_TYPEDNAME: { PADS_TYPEDNAME ptn = col.pADsValues->pTypedName; sprintf_s(valueTarget, VALUE_SIZE, "Interval: %u, Level: %u, Name: %S", ptn->Interval, ptn->Level, ptn->ObjectName); break; } case ADSTYPE_NETADDRESS: { PADS_NETADDRESS pna = col.pADsValues->pNetAddress; // IP address octects won't be bigger than 3 chars (given that we can only have 255 as a max value char* s = bytes_to_string(pna->Address, pna->AddressLength, "%u", 3, "."); if (s) { strncpy_s(valueTarget, VALUE_SIZE, s, VALUE_SIZE - 1); free(s); } dprintf("[ADSI] %u network address of %u bytes added", pna->AddressType, pna->AddressLength); break; } case ADSTYPE_EMAIL: { source = col.pADsValues->Email.Address; break; } case ADSTYPE_NT_SECURITY_DESCRIPTOR: { ADS_NT_SECURITY_DESCRIPTOR* psd = &col.pADsValues->SecurityDescriptor; char* s = NULL; if(ConvertSidToStringSid((PSID)psd->lpValue, &s)) { dprintf("[EXTAPI ADSI] converted SID: %s", s); strncpy_s(valueTarget, VALUE_SIZE, s, VALUE_SIZE - 1); LocalFree(s); } else { s = bytes_to_string(psd->lpValue, psd->dwLength); if (s) { strncpy_s(valueTarget, VALUE_SIZE, s, VALUE_SIZE - 1); free(s); } } break; } case ADSTYPE_DN_WITH_BINARY: { PADS_DN_WITH_BINARY pdb = col.pADsValues->pDNWithBinary; sprintf_s(valueTarget, VALUE_SIZE, "DN: %S, Value: %S", pdb->pszDNString); size_t charsPrinted = lstrlenA(valueTarget); char* s = bytes_to_string(pdb->lpBinaryValue, pdb->dwLength, "%u", 3, "."); if (s) { strncpy_s(valueTarget + charsPrinted, VALUE_SIZE, s, VALUE_SIZE - 1); free(s); } break; } case ADSTYPE_DN_WITH_STRING: { PADS_DN_WITH_STRING pds = col.pADsValues->pDNWithString; sprintf_s(valueTarget, VALUE_SIZE, "DN: %S, Value: %S", pds->pszDNString, pds->pszStringValue); break; } case ADSTYPE_FAXNUMBER: case ADSTYPE_REPLICAPOINTER: default: { // this is a string of some kind dprintf("[ADSI] Unhandled ADSI type %u (%x), adding unknown", col.dwADsType, col.dwADsType); sprintf_s(valueTarget, VALUE_SIZE, "(unhandled ADSI type %u)", col.dwADsType); break; } } // if source is now non-null it means a value was added to it, // so we can just do a conversion directly into the value target if (source != NULL) { wcstombs_s(&charsConverted, valueTarget, VALUE_SIZE, source, VALUE_SIZE - 1); dprintf("[ADSI] Adding string value %s", valueTarget); } entries[dwIndex].header.length = lstrlenA(valueTarget) + 1; pDirSearch->FreeColumn(&col); } else { dprintf("[ADSI] Col read failed: %x", hr); valueTarget[0] = 0; entries[dwIndex].header.length = 1; } dwIndex++; } if (dwIndex > 0) { dprintf("[ADSI] Adding group packet of %u values", dwIndex); // Throw the user details together in a group, ready to return. packet_add_tlv_group(response, TLV_TYPE_EXT_ADSI_RESULT, entries, dwIndex); dprintf("[ADSI] Added group packet of %u values", dwIndex); } else { dprintf("[ADSI] Item found, but no fields extracted."); } ++rowsProcessed; } dprintf("[ADSI] Processed %u. Final result: %u (0x%x)", rowsProcessed, hResult, hResult); if (SUCCEEDED(hResult)) { hResult = S_OK; } free(entries); free(values); } while (0); if (hSearch != NULL) { pDirSearch->CloseSearchHandle(hSearch); } if (pDirSearch != NULL) { pDirSearch->Release(); } CoUninitialize(); } else { dprintf("[ADSI] Failed to initialize COM"); } return (DWORD)hResult; }