1. 前言 在使用 Sysinternals 出品的 Process Explorer 過程中,對 “Run as Limited User” 功能的實現方式頗感興趣,一番搜尋之下發現Mark大神在《Running as Limited User – the Easy Way》中對該功能的實現做了相 ...
1. 前言 |
在使用 Sysinternals 出品的 Process Explorer 過程中,對 “Run as Limited User” 功能的實現方式頗感興趣,一番搜尋之下發現Mark大神在《Running as Limited User – the Easy Way》中對該功能的實現做了相關的闡述:
use the CreateRestrictedToken API to create a security context, called a token, that’s a stripped-down version of its own, removing administrative privileges and group membership. After generating a token that looks like one that Windows assigns to standard users Process Explorer calls CreateProcessAsUser to launch the target process with the new token.
使用 CreateRestrictedToken API來創建安全上下文,降低令牌(Token)的管理員許可權和組成員資格,使其創建的令牌看起來像Windows賦予普通用戶時一樣,然後使用此令牌作為傳入參數調用CreateProcessAsUser來創建新的子進程。
Process Explorer queries the privileges assigned to the Users group and strips out all other privileges, including powerful ones like SeDebugPrivilege, SeLoadDriverPrivilege and SeRestorePrivilege.
查詢賦予用戶組的特權並從當前進程許可權中剔除這些許可權比如SeDebugPrivilege、SeLoadDriverPrivilege和SeRestorePrivilege。
剛好最近有個項目需要實現降低進程許可權的功能,在一翻折騰下就將其實現了,下麵將談談實現的歷程,如果紕漏之處,不吝指出。
2. 知識背書 |
在列出代碼前需要瞭解一下一些實現原理,下麵是一些相關的知識點,如果無耐心往下看,可以直接點擊這裡跳到代碼實現處。
安全對象
有資格擁有安全描述符的對象如文件、管道、進程、進程間同步對象等。所有已命名的Windows對象都是安全的,那些未被命名的對象比如線程或進程對象也可以擁有安全描述符。
對於大多數的安全對象,當創建該對象時可以指定它的安全描述符。當一個安全對象被創建時,系統會對其賦予一個安全描述符,安全描述符包含由其創建者指定的安全信息,或者預設的安全信息(如果沒有特意進行指定的話)。
- 應用程式可以使特定的函數對已有的對象進行操作以來獲取和設置安全信息。
- 每種類型的安全對象定義了它自身的訪問許可權和自身映射的通用訪問許可權。
更詳細內容見:https://msdn.microsoft.com/en-us/library/windows/desktop/aa379557(v=vs.85).aspx
安全描述符(security descriptor)
包含用於保護安全對象的安全信息。
安全描述符描述 對象的所有者(SIDs) 和 以下的訪問控制列表:
- 自由訪問控制列表(DACL):指明特定用戶或組對該對象的訪問是允許或拒絕;
- 安全訪問控制列表(SACL):控制系統審計如何訪問對象。
安全標識(Security Identifiers)
一定長度用來表示托管的唯一值。
安全標識主要運用於如下幾個方面:
- 在安全描述符中定義對象的所有者和基本組;
- 在訪問控制項中定義托管的許可權是允許、拒絕或是審計;
- 在訪問令牌中定義用戶和用戶所在的組。
訪問令牌
包含登錄用戶的信息。用來描述一個進程或線程的安全上下文的對象,令牌的信息包含關聯到進程或線程的賬號的標識和特權。
當一個用戶登錄時,系統對用戶的賬號和密碼進行認證,如果登錄成功,系統則創建一個訪問令牌,每個進程運行時都有一個訪問令牌代表當前的用戶,訪問令牌中的安全描述符指明當前用戶所屬的賬號和所屬的組賬號,令牌也包含一系列由用戶或用戶所在組進行維護的許可權,在一個進程試圖進行訪問安全對象或執行系統管理員任務過程中需要許可權時,系統通過這個令牌來確認關聯的用戶。
訪問控制列表及其訪問控制項
自由訪問控制列表(DACL)包含若幹個訪問控制項(ACEs)。
約定的執行規則如下:
- 如果對象沒有自由訪問控制列表(DACL),則任何用戶對其均有完全的訪問許可權;
- 如果對象擁有DACL,那麼系統僅允許那些在訪問控制項(ACE)顯式指明的訪問許可權;
- 如果在訪問控制列表(DACL)中不存在訪問控制項(ACE),那麼系統不允許任何用戶對其進行訪問;
- 如果訪問控制列表中的訪問控制項對准許訪問的用戶或組數目有限,那麼系統會隱式拒絕那些不在訪問控制項中的其他托管的訪問
需要註意的是訪問控制項的排序很重要。因為系統按照隊列的方式讀取訪問控制項,直到訪問被拒絕或允許。用戶的訪問拒絕ACE必須放在訪問允許ACE的前頭,否則當系統讀到對組的訪問允許ACE時,它會給當前限制的用戶賦予訪問的許可權。系統在檢測到請求訪問被允許或拒絕後就不再往下檢查。
你可以通過標識允許訪問的ACE來控制對對象的訪問,你無需顯式地拒絕一個對象的訪問。
線程和安全對象間的交互
當一個線程想要使用一個安全對象時,系統線上程執行前會進行訪問審核,在訪問審核中,系統將線程訪問令牌中的安全信息與對象安全描述符中的安全信息進行比對。
訪問令牌中包含的安全標識(SIDs)可以指明與線程關聯的用戶,系統查看線程訪問令牌中用戶或組的SID,同時檢查對象的自由訪問控制列表(DACL),自由訪問控制列表(DACL)中包含存儲有指明對指定的用戶或組的訪問許可權是允許或拒絕信息的訪問控制項(ACE),系統檢查每個訪問控制項(ACE)直至出現指明針對此線程(的用戶或組的SID)的訪問許可權是允許還是拒絕的ACE,或者到最終都沒有對應的ACEs可以檢查。
(圖片出處:https://msdn.microsoft.com/en-us/library/windows/desktop/aa378890(v=vs.85).aspx)
系統按照序列檢查每個ACE,查詢ACE中的托管與定義線上程中的托管(根據托管的SID)一致的ACE,直到如下的情況出現:
- 表明訪問拒絕的ACE顯式拒絕線上程的訪問令牌中的一個托管的任何訪問許可權;
- 一個或多個表明訪問允許的ACEs顯式地為線程訪問令牌中的托管提供所有訪問許可權;
- 所有ACEs已經比對審核完但至少有一個請求訪問許可權沒有顯式允許,這種情況下該訪問許可權則被隱式拒絕。
一個訪問控制列表(ACL)可以有多個的訪問控制項(ACE)針對令牌的(同一個)SIDs,這種情況下每個ACE授予的訪問許可權是可以進行累積疊加,比如,如果一個ACE對一個組允許讀的訪問許可權,而另一個ACE對該組內的一個用戶允許寫的訪問許可權,那麼該用戶對於當前對象就擁有了讀和寫的訪問許可權。
(圖片出處:https://msdn.microsoft.com/en-us/library/windows/desktop/aa446597(v=vs.85).aspx)
如上圖所示,對於線程A,儘管在ACE@3中允許寫許可權,但因為在ACE@1中已經顯式拒絕“Andrew”用戶的訪問許可權,所以該安全對象對於線程A是不可訪問的;對於線程B,在ACE@2中顯式指明A組用戶可以有寫的許可權,並且在ACE@3中允許任何用戶讀和執行的許可權,所以線程B對這個安全對象擁有讀、寫以及執行的許可權。
完整性級別
Windows完整性機制是對Windows安全架構的擴展,該完整性機制通過添加完整性等級到安全訪問令牌和添加強制性標記訪問控制項到安全描述符中的系統訪問控制列表(SACL)
進程在安全訪問令牌中定義完整性等級,IE在保護模式下的完整性等級為低,從開始菜單運行的應用程式的等級為中等,需要管理員許可權並以管理員許可權運行的應用程式的等級為高。
保護模式能夠有效地減少IE進程附帶的攻擊行為如篡改和摧毀數據、安裝惡意程式;相比其他程式,連接網路的程式更容易遭受網路的攻擊因為它們更可能從未知源地址下載未受信任的內容,通過降低完整性等級或限制對其的允許許可權,可以減少篡改系統或污染用戶數據文件的風險。
在系統訪問控制列表(SACL)中有一個稱為強制標識的訪問控制項(ACE),該控制項的安全描述符定義完整性等級或允許訪問當前對象需要達到的等級,安全對象如果沒有該控制項則預設擁有中等的完整性等級;
即使用戶在自由訪問控制列表(DACL)中已經明確授予相應的寫許可權,低等級的進程也不能獲取比其高等級的安全對象的寫許可權,完整性等級檢驗在用戶訪問許可權審查之前完成。
所有的文件和註冊表鍵在預設下的完整性等級為中,而由低等完整性進程創建的安全對象,系統會自動地賦予其低等完整性強制標誌,同樣,由低等完整性進程創建的子進程也是在低完整性等級下運行。
完整性訪問等級(IL) |
系統許可權 |
安全標識 |
|
System |
System |
S-1-16-16384 |
|
High |
Administrative |
S-1-16-12288 |
可安裝文件到Program Files文件夾、往敏感的註冊表中如HKEY_LOCAL_MACHINE寫數據 |
Medium |
User |
S-1-16-8192 |
創建和修改用戶文檔中的文件、往特定用戶的註冊表如HKEY_CURRENT_USER中寫數據 |
Low |
Untrusted |
S-1-16-4096 |
僅能往低等級位置寫數據如臨時網路文件夾和註冊表 HKEY_CURRENT_USER\Software\LowRegistry |
低完整性進程可以往用戶存檔文件下寫文件,通常為%USER PROFILE%\AppData\LocalLow,可以通過調用SHGetKnownFolderPath 函數並傳入FOLDERID_LocalAppDataLow參數來獲取完整的路徑名稱
SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, NULL, szPath, ARRAYSIZE(szPath));
同樣低完整性進程可以往指定的註冊表下創建和修改子鍵,該註冊表路徑通常為HKEY_CURRENT_USER\Software\AppDataLow
3. 代碼實現 |
實現思路
- 創建新的普通用戶組和系統管理員的安全描述符標識;
- 獲取當前進程的令牌,並根據令牌句柄獲取當前進程所擁有的特權;
- 通過已創建的普通用戶組的安全描述符標識獲取普通用戶組所擁有的特權;
- 給當前進程令牌中的管理員安全描述符添加Deny-only屬性,以此達到避免新創建的進程以管理員作為其所有者;
- 從當前進程擁有的特權中剔除普通用戶組所沒有的特權;
- 從新的受限令牌中複製為模擬令牌;
- 將模擬令牌的完整性特權設為低級,以限制新創建的進程對普通文檔、可執行程式的寫、執行等訪問許可權;
代碼實現
1 void CreateRestrictedProcess() 2 { 3 SECURITY_ATTRIBUTES sa; 4 sa.nLength = sizeof(SECURITY_ATTRIBUTES); 5 sa.bInheritHandle = TRUE; 6 sa.lpSecurityDescriptor = NULL; 7 8 TCHAR szCmdLine[CMDLINE_SIZE] = {0}; 9 HANDLE hToken = NULL; 10 HANDLE hNewToken = NULL; 11 HANDLE hNewExToken = NULL; 12 13 CHAR szIntegritySid[20] = "S-1-16-4096"; 14 PSID pIntegritySid = NULL; 15 PSID pUserGroupSID = NULL; 16 PSID pAdminSID = NULL; 17 18 TOKEN_MANDATORY_LABEL tml = {0}; 19 20 PROCESS_INFORMATION pi; 21 STARTUPINFO si; 22 23 BOOL bSuc = FALSE; 24 ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); 25 26 ZeroMemory(&si, sizeof(STARTUPINFO)); 27 si.cb = sizeof(STARTUPINFO); 28 GetStartupInfo(&si); 29 DWORD fdwCreate = 0; 30 31 __try 32 { 33 34 if (!OpenProcessToken(GetCurrentProcess(), 35 //MAXIMUM_ALLOWED, 36 TOKEN_DUPLICATE | 37 TOKEN_ADJUST_DEFAULT | 38 TOKEN_QUERY | 39 TOKEN_ASSIGN_PRIMARY, 40 &hToken)) 41 { 42 char szMsg[DEFAULT_MSG_SIZE] = {0}; 43 Dbg("OpenProcessToken failed, GLE = %u.", GetLastError()); 44 __leave; 45 } 46 47 Dbg("Using RestrictedTokens way !!!"); 48 DWORD dwSize = 0; 49 DWORD dwTokenInfoLength = 0; 50 SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY; 51 SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; 52 if(!AllocateAndInitializeSid( 53 &SIDAuthNT, 54 0x2, 55 SECURITY_BUILTIN_DOMAIN_RID/*0×20*/, 56 DOMAIN_ALIAS_RID_USERS, 57 0, 0, 0, 0, 0, 0, 58 &pUserGroupSID)) 59 { 60 Dbg("AllocateAndInitializeSid for UserGroup Error %u", GetLastError()); 61 __leave; 62 } 63 64 // Create a SID for the BUILTIN\Administrators group. 65 if(! AllocateAndInitializeSid( &SIDAuth, 2, 66 SECURITY_BUILTIN_DOMAIN_RID, 67 DOMAIN_ALIAS_RID_ADMINS, 68 0, 0, 0, 0, 0, 0, 69 &pAdminSID) ) 70 { 71 Dbg("AllocateAndInitializeSid for AdminGroup Error %u", GetLastError()); 72 __leave; 73 } 74 75 SID_AND_ATTRIBUTES SidToDisable[1] = {0}; 76 SidToDisable[0].Sid = pAdminSID; 77 SidToDisable[0].Attributes = 0; 78 79 PTOKEN_PRIVILEGES pTokenPrivileges = NULL; 80 PTOKEN_PRIVILEGES pTokenPrivilegesToDel = NULL; 81 if(!GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwSize)) 82 { 83 if(GetLastError() == ERROR_INSUFFICIENT_BUFFER) 84 { 85 pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(0, dwSize); 86 pTokenPrivilegesToDel = (PTOKEN_PRIVILEGES)LocalAlloc(0, dwSize); 87 if(pTokenPrivileges != NULL && pTokenPrivilegesToDel != NULL) 88 { 89 if(!GetTokenInformation(hToken, TokenPrivileges, pTokenPrivileges, dwSize, &dwSize)) 90 { 91 Dbg("GetTokenInformation about TokenPrivileges failed GTE = %u.", GetLastError()); 92 __leave; 93 } 94 } 95 else 96 { 97 char szMsg[DEFAULT_MSG_SIZE] = {0}; 98 Dbg("LocalAlloc for pTokenPrivileges failed GTE = %u.", GetLastError()); 99 __leave; 100 } 101 } 102 } 103 104 LUID_AND_ATTRIBUTES *pTokenLUID = pTokenPrivileges->Privileges; 105 Dbg("CurrentToken's TokenPrivileges Count: %u", pTokenPrivileges->PrivilegeCount); 106 DWORD dwLuidCount = 0; 107 PLUID pPrivilegeLuid = NULL; 108 if(!CTWProcHelper::GetPrivilegeLUIDWithSID(pUserGroupSID, &pPrivilegeLuid, &dwLuidCount)) 109 { 110 Dbg("GetPrivilegeLUIDWithSID failed GTE = %u.", GetLastError()); 111 if(pPrivilegeLuid) 112 { 113 //HeapFree(GetProcessHeap(), 0, pPrivilegeLuid); 114 LocalFree(pPrivilegeLuid); 115 pPrivilegeLuid = NULL; 116 } 117 __leave; 118 } 119 Dbg("UserGroup's TokenPrivileges Count: %u", dwLuidCount); 120 121 DWORD dwDelPrivilegeCount = 0; 122 for(DWORD dwIdx=0; dwIdx<(pTokenPrivileges->PrivilegeCount); dwIdx++) 123 { 124 BOOL bFound = FALSE; 125 DWORD dwJdx = 0; 126 for(; dwJdx<dwLuidCount; dwJdx++) 127 { 128 //if(memcmp(&(pTokenLUID[dwIdx].Luid), &(pPrivilegeLuid[dwJdx]), sizeof(LUID)) == 0) 129 if((pTokenLUID[dwIdx].Luid.HighPart == pPrivilegeLuid[dwJdx].HighPart) 130 && 131 (pTokenLUID[dwIdx].Luid.LowPart == pPrivilegeLuid[dwJdx].LowPart)) 132 { 133 bFound = TRUE; 134 break; 135 } 136 } 137 if(!bFound) 138 { 139 char szPrivilegeName[MAX_PATH] = {0}; 140 DWORD dwNameSize = MAX_PATH; 141 if(!LookupPrivilegeName(NULL, &(pTokenLUID[dwIdx].Luid), szPrivilegeName, &dwNameSize)) 142 { 143 Dbg("LookupPrivilegeName failed GTE = %u.", GetLastError()); 144 //Dbg("NoFound[%u]: i=%u, j=%u", dwDelPrivilegeCount, dwIdx, dwJdx); 145 } 146 //else 147 //{ 148 // Dbg("NoFound[%u]: i=%u, j=%u -> %s", dwDelPrivilegeCount, dwIdx, dwJdx, szPrivilegeName); 149 //} 150 pTokenPrivilegesToDel->Privileges[dwDelPrivilegeCount++].Luid = pTokenLUID[dwIdx].Luid; 151 } 152 } 153 pTokenPrivilegesToDel->PrivilegeCount = dwDelPrivilegeCount; 154 Dbg("TokenPrivileges to delete Count: %u", dwDelPrivilegeCount); 155 if(pPrivilegeLuid) 156 { 157 //HeapFree(GetProcessHeap(), 0, pPrivilegeLuid); 158 LocalFree(pPrivilegeLuid); 159 pPrivilegeLuid = NULL; 160 } 161 162 if(!CreateRestrictedToken(hToken, 163 0, 164 1, SidToDisable, 165 //0, NULL, 166 dwDelPrivilegeCount, pTokenPrivilegesToDel->Privileges, 167 0, NULL, 168 &hNewToken 169 )) 170 { 171 char szMsg[DEFAULT_MSG_SIZE] = {0}; 172 Dbg("CreateRestrictedToken failed GTE = %u.", GetLastError()); 173 __leave; 174 } 175 176 // Duplicate the primary token of the current process. 177 if (!DuplicateTokenEx(hNewToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, 178 TokenPrimary, &hNewExToken)) 179 { 180 Dbg("DuplicateTokenEx failed GTE = %u.", GetLastError()); 181 hNewExToken = NULL; 182 //__leave; 183 } 184 else 185 { 186 if (ConvertStringSidToSid(szIntegritySid, &pIntegritySid)) 187 { 188 tml.Label.Attributes = SE_GROUP_INTEGRITY; 189 tml.Label.Sid = pIntegritySid; 190 191 // Set the process integrity level 192 if (!SetTokenInformation(hNewExToken, TokenIntegrityLevel, &tml, 193 sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid))) 194 { 195 Dbg("SetTokenInformation failed GTE = %u.", GetLastError()); 196 //__leave; 197 } 198 else 199 { 200 CloseHandle(hNewToken); 201 hNewToken = hNewExToken; 202 hNewExToken = NULL; 203 Dbg("Assign Low Mandatory Level to New Token which used to CreateProcessAsUser."); 204 } 205 } 206 207 } 208 209 if(!(bSuc = CreateProcessAsUser(hNewToken, NULL, 210 szCmdLine, // command line 211 NULL, // TODO: process security attributes 212 NULL, // TODO: primary thread security attributes 213 TRUE, // handles are inherited ?? 214 fdwCreate, // creation flags 215 NULL, // use parent's environment 216 NULL, // use parent's current directory 217 &si, // STARTUPINFO pointer 218 &pi))) // receives PROCESS_INFORMATION 219 { 220 Dbg("CreateProcessAsUser failed GTE = %u.", GetLastError()); 221 __leave; 222 } 223 224 if(pTokenPrivileges) 225 { 226 LocalFree(pTokenPrivileges); 227 } 228 if(pTokenPrivilegesToDel) 229 { 230 LocalFree(pTokenPrivilegesToDel); 231 } 232 } 233 __finally 234 { 235 if(pIntegritySid) 236 { 237 LocalFree(pIntegritySid); 238 } 239 if(pUserGroupSID) 240 { 241 LocalFree(pUserGroupSID); 242 } 243 if(pAdminSID) 244 { 245 LocalFree(pAdminSID); 246 } 247 // 248 // Close the access token. 249 // 250 if (hToken) 251 { 252 CloseHandle(hToken); 253 } 254 if(hNewToken) 255 { 256 CloseHandle(hNewToken); 257 } 258 if(hNewExToken) 259 { 260 CloseHandle(hNewExToken); 261 } 262 if(!bSuc) 263 { 264 Dbg("Retry to Create process in normal way."); 265 //Create process. 266 bSuc = CreateProcess(NULL, 267 szCmdLine, // command line 268 NULL, // TODO: process security attributes 269 NULL, // TODO: primary thread security attributes 270 TRUE, // handles are inherited ?? 271 fdwCreate, // creation flags 272 NULL, // use parent's environment 273 NULL, // use parent's current directory 274 &si, // STARTUPINFO pointer 275 &pi); // receives PROCESS_INFORMATION 276 } 277 } 278 }
其中 GetPrivilegeLUIDWithSID 函數的實現如下:
1 BOOL GetPrivilegeLUIDWithSID(PSID pSID, PLUID *pLUID, PDWORD pDwCount) 2 { 3 LSA_OBJECT_ATTRIBUTES ObjectAttributes; 4 NTSTATUS ntsResult; 5 LSA_HANDLE lsahPolicyHandle; 6 7 // Object attributes are reserved, so initialize to zeros. 8 ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes)); 9 10 // Get a handle to the Policy object. 11 ntsResult = LsaOpenPolicy( 12 NULL, //Name of the target system. 13 &ObjectAttributes, //Object attributes. 14 POLICY_ALL_ACCESS, //Desired access permissions. 15 &lsahPolicyHandle //Receives the policy handle. 16 ); 17 18 if (ntsResult != STATUS_SUCCESS) 19 { 20 printf("OpenPolicy failed returned %lu", LsaNtStatusToWinError(ntsResult)); 21 return FALSE; 22 } 23 24 PLSA_UNICODE_STRING UserRights = NULL; 25 ULONG uRightCount; 26 ntsResult = LsaEnumerateAccountRights(lsahPolicyHandle, pSID, &UserRights, &uRightCount); 27 if (ntsResult != STATUS_SUCCESS) 28 { 29 printf("LsaEnumerateAccountRights failed returned %lu", LsaNtStatusToWinError(ntsResult)); 30 LsaClose(lsahPolicyHandle); 31 return FALSE; 32 } 33 34 printf("LsaEnumerateAccountRights returned Right count: %lu", uRightCount); 35 36 (*pDwCount) = 0; 37 //pLUID = (PLUID)HeapAlloc(GetProcessHeap(), 0, uRightCount*sizeof(LUID)); 38 (*pLUID) = (PLUID)LocalAlloc(LPTR, uRightCount*sizeof(LUID)); 39 if((*pLUID) == NULL) 40 { 41 printf("HeapAlloc for PLUID failed returned %u", GetLastError()); 42 LsaClose(lsahPolicyHandle); 43 return FALSE; 44 } 45 46 for(ULONG uIdx=0; UserRights != NULL && uIdx<uRightCount; uIdx++) 47 { 48 int nLenOfMultiChars = WideCharToMultiByte(CP_ACP, 0, UserRights[uIdx].Buffer, UserRights[uIdx].Length, 49 NULL, 0, NULL, NULL); 50 PTSTR pMultiCharStr = (PTSTR)HeapAlloc(GetProcessHeap(), 0, nLenOfMultiChars*sizeof(char)); 51 if(pMultiCharStr != NULL) 52 { 53 WideCharToMultiByte(CP_ACP, 0, UserRights[uIdx].Buffer, UserRights[uIdx].Length, 54 pMultiCharStr, nLenOfMultiChars, NULL, NULL); 55 LUID luid; 56 if(!LookupPrivilegeValue(NULL, pMultiCharStr, &luid)) 57 { 58 printf("LookupPrivilegeValue about %s failed, GLE=%u.", pMultiCharStr, GetLastError()); 59 HeapFree(GetProcessHeap(), 0, pMultiCharStr); 60 continue; 61 } 62 (*pLUID)[(*pDwCount)++] = luid; 63 HeapFree(GetProcessHeap(), 0, pMultiCharStr); 64 } 65 } 66