概述:windows 环境下获取域用户信息
0x01 前言
获取域用户信息首先需要一个登录到域环境中的账户,然后调用 IAds 和 IDirectorySearch 的相关接口执行查询和枚举即可。
[【AD】[【AD】用户属性|用户属性
[【AD】[【AD】相关函数调用|相关函数调用
[【AD】[【AD】RootDSE|RootDSE
微软相关文档:
- IADs 属性方法 (Iads.h) - Win32 apps | Microsoft Learn
- IDirectorySearch (iads.h) - Win32 apps | Microsoft Learn
0x02 函数与调用说明
-
打开默认的 rootDSE 获取
defaultNamingContext字段名调用 IAds 的
Get()方法获取域的信息,字段名及获取的属性参考 RootDSE (AD 架构) - Win32 apps | Microsoft Learn -
如果
defaultNamingContext获取成功,则打开对应 LDAP (示例:LDAP://defaultNamingContext),如果打开成功,就获取到了一个IDirectorySearch的对象 -
调用
IDirectorySearch::ExecuteSearch执行查询,然后遍历查询出来的结果即可。
IDirectorySearch::ExecuteSearch 说明
传入一个 LDAP 格式的搜索筛选器字符串,并传出一个 PADS_SEARCH_HANDLE 的句柄表示查询结果。
-
LDAP 格式的搜索字符串说明
类似于 SQL 语句,可以定义要查询的范围。书写格式参考文章:搜索筛选器语法 - Win32 apps | Microsoft Learn
// 一个 LDAP搜索字符串的示例 // 如下所示表示只查询域内启用的用户 LPCWSTR lpwFormat1 = L"(&(objectClass=user)(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(lockoutTime>=1))(!(badPwdCount>=%ls))(sAMAccountName=%ls))"; // Only enabled accounts -
PADS_SEARCH_HANDLE 句柄
查询到结果后,需要调用
IDirectorySearch的其他方法对结果进行枚举,ConvertColToUAStruct为自定义封装的查询结果处理函数,参考 【AD】相关函数调用// 枚举查询结果的大致流程 // Return specified properties hr = pContainerToSearch->ExecuteSearch(wcSearchFilter, (LPWSTR*)pszAttrFilter, sizeof(pszAttrFilter) / sizeof(LPWSTR), &hSearch); if (FAILED(hr)) { BeaconPrintf(CALLBACK_ERROR, "Failed to execute search.\n"); goto CleanUp; } if (SUCCEEDED(hr)) { // Call IDirectorySearch::GetNextRow() to retrieve the next row of data. hr = pContainerToSearch->GetFirstRow(hSearch); if (SUCCEEDED(hr)) { while (hr != S_ADS_NOMORE_ROWS) { UserAccount userAct; userAct.strAccountType = L"3"; userAct.strDomain = pdcInfo->DomainName; userAct.strIsDomain = "1"; userAct.strPwdExpireTime = "never"; userAct.bAdmin = FALSE; // Keep track of count. iCount++; // Loop through the array of passed column names. while (pContainerToSearch->GetNextColumnName(hSearch, &pszColumn) != S_ADS_NOMORE_COLUMNS) { hr = pContainerToSearch->GetColumn(hSearch, pszColumn, &col); if (SUCCEEDED(hr)) { ConvertColToUAStruct(col, userAct); pContainerToSearch->FreeColumn(&col); } if (pszColumn != NULL) { FreeADsMem(pszColumn); } } Users.emplace_back(userAct); // Get the next row hr = pContainerToSearch->GetNextRow(hSearch); } } // Close the search handle to clean up pContainerToSearch->CloseSearchHandle(hSearch); }
0x03 开源代码和项目
QueryADUser
holdyounger/QueryADUser: 查询AD域内用户信息
查询逻辑基本一直,参考了 SprayAD 项目。另外添加了判断 域管理员 的逻辑,只有在域管理员的机器上才获取域内所有用户。
判断当前机器是否域管理员,参数1为 IDirectorySearch, 需要传入打来的 LDAP 句柄。
BOOL IsDomainAdmin(_In_ IDirectorySearch* pContainerToSearch, _In_ LPCWSTR lpwFilterName)
{
BOOL bRet = FALSE;
HRESULT hr = S_OK;
WCHAR wcSearchFilter[BUF_SIZE] = { 0 };
LPCWSTR pszAttrFilter[] = {
L"isCriticalSystemObject", // boolean
L"sAMAccountName", // boolean
};
LPCWSTR lpwFormat = L"(&(objectClass=user)(objectCategory=person)((sAMAccountName=%ls)))"; // Only enabled accounts // (!(userAccountControl:1.2.840.113556.1.4.803:=2))
PUSER_INFO pUserInfo = NULL;
INT iCount = 0;
DWORD x = 0L;
LPWSTR pszColumn = NULL;
IADs* pRoot = NULL;
IID IADsIID;
ADS_SEARCH_COLUMN col;
DWORD dwAccountsFailed = 0;
DWORD dwAccountsSuccess = 0;
vector<UserAccount> uAccounts;
_ADsOpenObject ADsOpenObject = (_ADsOpenObject)
GetProcAddress(GetModuleHandleA("Activeds.dll"), "ADsOpenObject");
if (ADsOpenObject == NULL) {
return S_FALSE;
}
_FreeADsMem FreeADsMem = (_FreeADsMem)
GetProcAddress(GetModuleHandleA("Activeds.dll"), "FreeADsMem");
if (FreeADsMem == NULL) {
return S_FALSE;
}
if (!pContainerToSearch) {
return E_POINTER;
}
// Calculate Program run time.
LARGE_INTEGER frequency;
LARGE_INTEGER start;
LARGE_INTEGER end;
double interval;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
// Specify subtree search
ADS_SEARCHPREF_INFO SearchPrefs;
SearchPrefs.dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
SearchPrefs.vValue.Integer = 1000;
DWORD dwNumPrefs = 1;
// Handle used for searching
ADS_SEARCH_HANDLE hSearch = NULL;
// Set the search preference
hr = pContainerToSearch->SetSearchPreference(&SearchPrefs, dwNumPrefs);
if (FAILED(hr)) {
BeaconPrintf(CALLBACK_ERROR, "Failed to set search preference.\n");
goto CleanUp;
}
// Add the filter.
if (lpwFilterName == NULL) {
// lpwFilter = L"*";
BeaconPrintf(CALLBACK_ERROR, "Empty username!\n");
return hr;
}
swprintf_s(wcSearchFilter, BUF_SIZE, lpwFormat, lpwFilterName);
pUserInfo = (PUSER_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(USER_INFO));
if (pUserInfo == NULL) {
BeaconPrintf(CALLBACK_ERROR, "Failed to allocate UserInfo memory.\n");
goto CleanUp;
}
// Return specified properties
hr = pContainerToSearch->ExecuteSearch(wcSearchFilter, (LPWSTR*)pszAttrFilter, sizeof(pszAttrFilter) / sizeof(LPWSTR), &hSearch);
if (FAILED(hr)) {
BeaconPrintf(CALLBACK_ERROR, "Failed to execute search.\n");
goto CleanUp;
}
if (SUCCEEDED(hr)) {
// Call IDirectorySearch::GetNextRow() to retrieve the next row of data.
hr = pContainerToSearch->GetFirstRow(hSearch);
if (SUCCEEDED(hr))
{
while (hr != S_ADS_NOMORE_ROWS)
{
UserAccount uAccount;
// Keep track of count.
iCount++;
uAccount.bAdmin = FALSE;
// Loop through the array of passed column names.
while (pContainerToSearch->GetNextColumnName(hSearch, &pszColumn) != S_ADS_NOMORE_COLUMNS)
{
hr = pContainerToSearch->GetColumn(hSearch, pszColumn, &col);
if (SUCCEEDED(hr)) {
ConvertColToUAStruct(col, uAccount);
pContainerToSearch->FreeColumn(&col);
}
if (pszColumn != NULL) {
FreeADsMem(pszColumn);
}
}
uAccounts.emplace_back(uAccount);
// Get the next row
hr = pContainerToSearch->GetNextRow(hSearch);
}
}
// Close the search handle to clean up
pContainerToSearch->CloseSearchHandle(hSearch);
}
if (uAccounts.size() == 1)
{
if (uAccounts[0].bAdmin == 1 && uAccounts[0].UserName.CompareNoCase("Administrator") == 0)
{
bRet = TRUE;
}
}
if (SUCCEEDED(hr) && 0 == iCount) {
hr = S_FALSE;
}
if (dwAccountsSuccess == 0) {
BeaconPrintToStreamW(L"[-] Failed => %ls\\%ls (Skip!)\n", pdcInfo->DomainName, lpwFilterName);
}
CleanUp:
if (pUserInfo != NULL) {
HeapFree(GetProcessHeap(), 0, pUserInfo);
}
return bRet;
}测试:
BOOL GetCurUserName(CString &strUserName)
{
BOOL bRet = FALSE;
char username[BUF_SIZE];
DWORD usernameSize = sizeof(username);
if (FALSE == GetUserName(username, &usernameSize))
{
goto End;
};
strUserName = username;
bRet = TRUE;
printf("gethostname result: %S \n", username);
End:
return bRet;
}void test()
{
if (IsDomainAdmin(pContainerToSearch, CStringW("Administrator")))
{
}
CString DomainUserName;
// 获取用户名
if (FALSE == GetCurUserName(DomainUserName))
{
}
if (IsDomainAdmin(pContainerToSearch, CStringW(userName)))
{
}
}QueryADObject.cpp
//Reference:
//https://github.com/microsoft/Windows-classic-samples/tree/master/Samples/Win7Samples/netds/adsi/activedir/QueryUsers/vc
//https://github.com/outflanknl/Recon-AD
#include <stdio.h>
#include <objbase.h>
#include <activeds.h>
#include <sddl.h>
#pragma comment(lib, "ADSIid.lib")
#pragma comment(lib, "ActiveDS.Lib")
#pragma comment(lib, "Ole32.Lib")
#pragma comment(lib, "Advapi32.Lib")
#pragma comment(lib, "oleaut32.Lib")
int IS_BUFFER_ENOUGH(UINT maxAlloc, LPWSTR pszTarget, LPCWSTR pszSource, int toCopy = -1) {
if (toCopy == -1) {
toCopy = wcslen(pszSource);
}
return maxAlloc - (wcslen(pszTarget) + toCopy + 1);
}
HRESULT Search(IDirectorySearch *pContainerToSearch, // IDirectorySearch pointer to Partitions container.
LPOLESTR szFilter, // Filter for finding specific crossrefs. NULL returns all attributeSchema objects.
LPOLESTR *pszPropertiesToReturn, // Properties to return for crossRef objects found. NULL returns all set properties.
BOOL bIsVerbose //TRUE means display all properties for the found objects are displayed.
//FALSE means only the RDN
)
{
if (!pContainerToSearch)
return E_POINTER;
LPOLESTR pszNonVerboseList[] = { L"name",L"distinguishedName" };
unsigned long ulNonVbPropsCount = 2;
wprintf(L"%s\n", szFilter);
// Specify subtree search
ADS_SEARCHPREF_INFO SearchPrefs;
SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
SearchPrefs.vValue.Integer = ADS_SCOPE_SUBTREE;
DWORD dwNumPrefs = 1;
// COL for iterations
LPOLESTR pszColumn = NULL;
ADS_SEARCH_COLUMN col;
HRESULT hr;
// Interface Pointers
IADs *pObj = NULL;
IADs *pIADs = NULL;
// Handle used for searching
ADS_SEARCH_HANDLE hSearch = NULL;
// Set the search preference
hr = pContainerToSearch->SetSearchPreference(&SearchPrefs, dwNumPrefs);
if (FAILED(hr))
{
return hr;
}
LPOLESTR pszBool = NULL;
DWORD dwBool;
PSID pObjectSID = NULL;
LPOLESTR szSID = NULL;
LPOLESTR szDSGUID = new WCHAR[39];
LPGUID pObjectGUID = NULL;
FILETIME filetime;
SYSTEMTIME systemtime;
DATE date;
VARIANT varDate;
LARGE_INTEGER liValue;
LPOLESTR *pszPropertyList = NULL;
int iCount = 0;
DWORD x = 0L;
if (!bIsVerbose)
{
//Return non-verbose list properties only
hr = pContainerToSearch->ExecuteSearch(szFilter,
pszNonVerboseList,
ulNonVbPropsCount,
&hSearch
);
}
else
{
if (!pszPropertiesToReturn)
{
// Return all properties.
hr = pContainerToSearch->ExecuteSearch(szFilter,
NULL,
-1L,
&hSearch);
}
else
{
// Specified subset.
pszPropertyList = pszPropertiesToReturn;
// Return specified properties
hr = pContainerToSearch->ExecuteSearch(szFilter,
pszPropertyList,
sizeof(pszPropertyList) / sizeof(LPOLESTR),
&hSearch);
}
}
if (SUCCEEDED(hr))
{
// Call IDirectorySearch::GetNextRow() to retrieve the next row of data
hr = pContainerToSearch->GetFirstRow(hSearch);
if (SUCCEEDED(hr))
{
while (hr != S_ADS_NOMORE_ROWS)
{
// Keep track of count.
iCount++;
wprintf(L"--------------------------------------------------------------------\n");
// Loop through the array of passed column names, print the data for each column
while (pContainerToSearch->GetNextColumnName(hSearch, &pszColumn) != S_ADS_NOMORE_COLUMNS)
{
hr = pContainerToSearch->GetColumn(hSearch, pszColumn, &col);
if (SUCCEEDED(hr))
{
// Print the data for the column and free the column
// Get the data for this column
wprintf(L"[+] %s:\n", col.pszAttrName);
switch (col.dwADsType)
{
case ADSTYPE_DN_STRING:
for (x = 0; x< col.dwNumValues; x++)
{
if (wcscmp(L"member", col.pszAttrName) == 0) {
IADsNameTranslate *pNto;
BSTR bstr;
hr = CoCreateInstance(CLSID_NameTranslate,
NULL,
CLSCTX_INPROC_SERVER,
IID_IADsNameTranslate,
(void**)&pNto);
if (SUCCEEDED(hr)) {
hr = pNto->Set(ADS_NAME_TYPE_1779, col.pADsValues[x].DNString);
if (SUCCEEDED(hr)) {
hr = pNto->Get(ADS_NAME_TYPE_NT4, &bstr);
wprintf(L" %s\r\n", bstr);
SysFreeString(bstr);
}
pNto->Release();
}
}
else {
wprintf(L" %s\r\n", col.pADsValues[x].DNString);
}
}
break;
case ADSTYPE_CASE_EXACT_STRING:
case ADSTYPE_CASE_IGNORE_STRING:
case ADSTYPE_PRINTABLE_STRING:
case ADSTYPE_NUMERIC_STRING:
case ADSTYPE_TYPEDNAME:
case ADSTYPE_FAXNUMBER:
case ADSTYPE_PATH:
case ADSTYPE_OBJECT_CLASS:
for (x = 0; x< col.dwNumValues; x++)
{
wprintf(L" %s\r\n", col.pADsValues[x].CaseIgnoreString);
}
break;
case ADSTYPE_BOOLEAN:
for (x = 0; x< col.dwNumValues; x++)
{
dwBool = col.pADsValues[x].Boolean;
pszBool = dwBool ? L"TRUE" : L"FALSE";
wprintf(L" %s\r\n", pszBool);
}
break;
case ADSTYPE_INTEGER:
for (x = 0; x< col.dwNumValues; x++)
{
wprintf(L" %d\r\n", col.pADsValues[x].Integer);
}
break;
case ADSTYPE_OCTET_STRING:
if (_wcsicmp(col.pszAttrName, L"objectSID") == 0)
{
for (x = 0; x< col.dwNumValues; x++)
{
pObjectSID = (PSID)(col.pADsValues[x].OctetString.lpValue);
// Convert SID to string.
ConvertSidToStringSid(pObjectSID, &szSID);
wprintf(L" %s\r\n", szSID);
LocalFree(szSID);
}
}
else if ((_wcsicmp(col.pszAttrName, L"objectGUID") == 0))
{
for (x = 0; x< col.dwNumValues; x++)
{
// Cast to LPGUID
pObjectGUID = (LPGUID)(col.pADsValues[x].OctetString.lpValue);
// Convert GUID to string.
::StringFromGUID2(*pObjectGUID, szDSGUID, 39);
// Print the GUID
wprintf(L" %s\r\n", szDSGUID);
}
}
else
wprintf(L" Value of type Octet String. No Conversion.\n");
break;
case ADSTYPE_UTC_TIME:
for (x = 0; x< col.dwNumValues; x++)
{
systemtime = col.pADsValues[x].UTCTime;
if (SystemTimeToVariantTime(&systemtime,
&date) != 0)
{
// Pack in variant.vt
varDate.vt = VT_DATE;
varDate.date = date;
VariantChangeType(&varDate, &varDate, VARIANT_NOVALUEPROP, VT_BSTR);
wprintf(L" %s\r\n", varDate.bstrVal);
VariantClear(&varDate);
}
else
wprintf(L"[!] Could not convert UTC-Time.\n");
}
break;
case ADSTYPE_LARGE_INTEGER:
for (x = 0; x< col.dwNumValues; x++)
{
liValue = col.pADsValues[x].LargeInteger;
filetime.dwLowDateTime = liValue.LowPart;
filetime.dwHighDateTime = liValue.HighPart;
if ((filetime.dwHighDateTime == 0) && (filetime.dwLowDateTime == 0))
{
wprintf(L" No value set.\n");
}
else
{
// Check for properties of type LargeInteger that represent time
// if TRUE, then convert to variant time.
if ((0 == wcscmp(L"accountExpires", col.pszAttrName)) |
(0 == wcscmp(L"badPasswordTime", col.pszAttrName)) ||
(0 == wcscmp(L"lastLogon", col.pszAttrName)) ||
(0 == wcscmp(L"lastLogoff", col.pszAttrName)) ||
(0 == wcscmp(L"lockoutTime", col.pszAttrName)) ||
(0 == wcscmp(L"pwdLastSet", col.pszAttrName))
)
{
// Handle special case for Never Expires where low part is -1
if (filetime.dwLowDateTime == -1)
{
wprintf(L" Never Expires.\n");
}
else
{
if (FileTimeToLocalFileTime(&filetime, &filetime) != 0)
{
if (FileTimeToSystemTime(&filetime,
&systemtime) != 0)
{
if (SystemTimeToVariantTime(&systemtime,
&date) != 0)
{
// Pack in variant.vt
varDate.vt = VT_DATE;
varDate.date = date;
VariantChangeType(&varDate, &varDate, VARIANT_NOVALUEPROP, VT_BSTR);
wprintf(L" %s\r\n", varDate.bstrVal);
VariantClear(&varDate);
}
else
{
wprintf(L" FileTimeToVariantTime failed\n");
}
}
else
{
wprintf(L" FileTimeToSystemTime failed\n");
}
}
else
{
wprintf(L" FileTimeToLocalFileTime failed\n");
}
}
}
else
{
// Print the LargeInteger.
wprintf(L" high: %d low: %d\r\n", filetime.dwHighDateTime, filetime.dwLowDateTime);
}
}
}
break;
case ADSTYPE_NT_SECURITY_DESCRIPTOR:
for (x = 0; x< col.dwNumValues; x++)
{
wprintf(L" Security descriptor.\n");
}
break;
default:
wprintf(L"[!] Unknown type %d.\n", col.dwADsType);
}
pContainerToSearch->FreeColumn(&col);
}
CoTaskMemFree(pszColumn);
}
// Get the next row
hr = pContainerToSearch->GetNextRow(hSearch);
}
}
// Close the search handle to clean up
pContainerToSearch->CloseSearchHandle(hSearch);
}
if (SUCCEEDED(hr) && 0 == iCount)
hr = S_FALSE;
wprintf(L"--------------------------------------------------------------------\n");
return hr;
}
void wmain(int argc, wchar_t *argv[])
{
if (argc != 4)
{
printf("\nThis program queries for objects in the current user's domain.\n");
printf("Usage:\n");
printf(" %ws <ADS path> <search string> <outputdata>\n", argv[0]);
printf("Eg.\n");
printf(" %ws Current \"(&(objectClass=user)(objectCategory=person))\" ShortData\n", argv[0]);
printf(" %ws Current \"(&(objectCategory=computer)(objectClass=computer))\" ShortData\n", argv[0]);
printf(" %ws \"OU=Domain Controllers,DC=test,DC=com\" \"(&(objectCategory=computer)(objectClass=computer))\" ShortData\n", argv[0]);
printf(" %ws Current \"(&(objectCategory=group))\" ShortData\n", argv[0]);
printf(" %ws Current \"(&(objectCategory=organizationalUnit))\" ShortData\n", argv[0]);
printf(" %ws Current \"(&(objectClass=user)(objectCategory=person)(name=testa))\" AllData\n", argv[0]);
printf(" %ws Current \"(&(objectCategory=computer)(objectClass=computer)(name=COMPUTER01))\" AllData\n", argv[0]);
printf(" %ws Current \"(&(objectCategory=group)(name=Domain Admins))\" AllData\n", argv[0]);
return;
}
int maxAlloc = MAX_PATH * 2;
LPOLESTR pszBuffer = new OLECHAR[maxAlloc];
wcscpy_s(pszBuffer, maxAlloc, L"");
if (IS_BUFFER_ENOUGH(maxAlloc, pszBuffer, argv[2]) > 0)
{
wcscpy_s(pszBuffer, maxAlloc, argv[2]);
}
else
{
wprintf(L"Buffer is too small for the argument");
delete[] pszBuffer;
return;
}
BOOL bReturnVerbose = FALSE;
if (_wcsicmp(argv[3], L"AllData") == 0)
{
bReturnVerbose = TRUE;
}
CoInitialize(NULL);
HRESULT hr = S_OK;
// Get rootDSE and the current user's domain container DN.
IADs *pObject = NULL;
IDirectorySearch *pContainerToSearch = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
NULL,
NULL,
ADS_SECURE_AUTHENTICATION, // Use Secure Authentication
IID_IADs,
(void**)&pObject);
if (FAILED(hr))
{
wprintf(L"[!] Could not execute query. Could not bind to LDAP://rootDSE.\n");
if (pObject)
pObject->Release();
delete[] pszBuffer;
delete[] szPath;
CoUninitialize();
// Flush STDOUT
fflush(stdout);
return;
}
if (SUCCEEDED(hr))
{
hr = pObject->Get(L"defaultNamingContext", &var);
if (SUCCEEDED(hr))
{
wcscpy_s(szPath, MAX_PATH, L"LDAP://");
if (_wcsicmp(argv[1], L"Current") == 0)
{
// Build path to the domain container.
if (IS_BUFFER_ENOUGH(MAX_PATH, szPath, var.bstrVal) > 0)
{
wcscat_s(szPath, MAX_PATH, var.bstrVal);
}
else
{
wprintf(L"[!] Buffer is too small for the domain DN");
delete[] pszBuffer;
delete[] szPath;
CoUninitialize();
// Flush STDOUT
fflush(stdout);
return;
}
}
else
{
if (IS_BUFFER_ENOUGH(MAX_PATH, szPath, argv[1]) > 0)
{
wcscat_s(szPath, MAX_PATH, argv[1]);
}
}
wprintf(L"%s\n", szPath);
hr = ADsOpenObject(szPath,
NULL,
NULL,
ADS_SECURE_AUTHENTICATION, // Use Secure Authentication
IID_IDirectorySearch,
(void**)&pContainerToSearch);
if (SUCCEEDED(hr))
{
hr = Search(pContainerToSearch, //IDirectorySearch pointer to Partitions container.
pszBuffer,
NULL,
bReturnVerbose
);
if (SUCCEEDED(hr))
{
if (S_FALSE == hr)
wprintf(L"[!] No object could be found.\n");
}
else if (0x8007203e == hr)
wprintf(L"[!] Could not execute query. An invalid filter was specified.\n");
else
wprintf(L"[!] Query failed to run. HRESULT: %x\n", hr);
}
else
{
wprintf(L"[!] Could not execute query. Could not bind to the container.\n");
}
if (pContainerToSearch)
pContainerToSearch->Release();
}
VariantClear(&var);
}
if (pObject)
pObject->Release();
delete[] pszBuffer;
delete[] szPath;
// Uninitialize COM
CoUninitialize();
// Flush STDOUT
fflush(stdout);
}