Great article, Vlastimil!
-- kyrathaba on DC
Published by Vlasta on February 27th 2012.
In the past years, I have received numerous emails from users, who were having trouble saving their files. In most cases, the users have opened one of the stock Windows cursors from C:\Windows\Cursors and were attempting to save a modified cursor into the same folder. Because this folder is protected by UAC in Vista and Win7, the operation failed.
The next version of RWCursorEditor will finally address this problem and users will see the the standard UAC prompt when attempting to save to a protected location.
The rest of this post is aimed at fellow programmers, who are considering adding the same functionality to their applications and want to do it with minimum effort.
So, you have created a nifty software tool and your users want to save files to protected folders. What are your options if you want to fulfill the request? According to Microsoft:
Both of these methods are problematic and require a lot of work.
Starting an elevated process can be done via a manifest snippet (that may cause a BSOD on WinXP SP2) or by using an undocumented option of the ShellExecuteEx API function.
If you go the manifest way, you need to ship another .exe file with your tool. If your tool is portable (possibly a standalone .exe), having 2 .exe files will confuse the users. If you use ShellExecuteEx API, you must be in a single-threaded apartment or it will not work. Then you can launch the same .exe file (the one that is running) with admin permissions and some kind of command line argument that will allow it to communicate with your original process.
In either case, after you succeed in creating the elevated process, you must transfer the data that needs to be saved to that process and instruct it to actually save the data and report the result. Hooray, you'll have some inter-process-communication fun. I bet working with pipes and synchronization objects is what you like best.
If you want to know more, read http://www.codeproject.com/Articles/19165/Vista-UAC-The-Definitive-Guide
In short, using an elevated process is unwieldy, complicated and work-intensive. It (the saving function) cannot be implemented as an isolated component that you just plug into your tool.
In order to start a COM object with elevated permissions, it needs to be registered in the HKLM branch of the registry. You need to have admin permissions to modify entries under HKLM (putting the entries to HKCU will not work). So, if your tool is portable (as is mine), this solution is out of the question.
The recommended ways failed, but fortunately, there is something that can be used. When Microsoft was designing Windows Explorer (the file manager) in Vista, they had to workaround the UAC somehow and created a COM object that can do basic stuff with files.
We can use this Explorer's COM object to our advantage. It cannot save data to files, but it can copy or move files around.
So, here is the plan:
While this is not an ideal solution (because of the temporary file), it has several advantages:
We'll need these functions (some of the code was taken from VistaTools.cxx):
typedef WINGDIAPI BOOL WINAPI fnOpenProcessToken(HANDLE,DWORD,PHANDLE);
typedef WINADVAPI BOOL WINAPI fnGetTokenInformation(HANDLE,TOKEN_INFORMATION_CLASS,LPVOID,DWORD,PDWORD);
HRESULT GetElevationType(TOKEN_ELEVATION_TYPE* ptet)
{
static TOKEN_ELEVATION_TYPE tTET = TokenElevationTypeDefault;
static HRESULT hRes = S_FALSE;
if (hRes == S_FALSE)
{
HMODULE hMod = LoadLibrary(_T("Advapi32.dll"));
fnOpenProcessToken* pfnOpenProcessToken = (fnOpenProcessToken*)GetProcAddress(hMod, "OpenProcessToken");
fnGetTokenInformation* pfnGetTokenInformation = (fnGetTokenInformation*)GetProcAddress(hMod, "GetTokenInformation");
hRes = E_FAIL;
if (pfnOpenProcessToken && pfnGetTokenInformation)
{
HANDLE hToken = NULL;
DWORD dwReturnLength = 0;
if (pfnOpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &hToken) &&
pfnGetTokenInformation(hToken, TokenElevationType, &tTET, sizeof tTET, &dwReturnLength) &&
dwReturnLength == sizeof tTET)
hRes = S_OK;
if (hToken)
::CloseHandle(hToken);
}
FreeModule(hMod);
}
*ptet = tTET;
return hRes;
}
bool IsVista()
{
static OSVERSIONINFO tVersion = { sizeof(OSVERSIONINFO), 0, 0, 0, 0, _T("") };
if (tVersion.dwMajorVersion == 0)
GetVersionEx(&tVersion);
return tVersion.dwMajorVersion >= 6;
}
In your code, you will use:
if (ordinary saving failed)
{
TOKEN_ELEVATION_TYPE tTET;
if (IsVista() && S_OK == GetElevationType(&tTET) && tTET == TokenElevationTypeLimited)
{
// here goes the saving code discussed next
}
}
BIND_OPTS3 tBO3;
ZeroMemory(&tBO3, sizeof tBO3);
tBO3.cbStruct = sizeof tBO3;
tBO3.dwClassContext = CLSCTX_LOCAL_SERVER;
CComPtr<IFileOperation> pFO;
HRESULT hRes = CoGetObject(L"Elevation:Administrator!new:{3ad05575-8857-4850-9277-11b85bdb8e09}", &tBO3, __uuidof(IFileOperation), reinterpret_cast<void**>(&pFO));
If everything succeeded (user confirmed your UAC prompt), you'll have a valid pointer in pFO.
I'll leave this part to you...
This is a bit trickier, because the object does not accept paths. It wants its special IShellItem objects. We need to convert the paths to these objects with SHCreateItemFromParsingName API function. Since this function is not available on older Windows, I am loading it dynamically.
This piece of code assumes:
TCHAR* pszTarget; // is the path to the file in the UAC-protected location
TCHAR* pszTemp; // is the path to the temporary file, where you have saved the data
LPTSTR pszNewName = _tcsrchr(pszTarget, _T('\\'));
LPTSTR pszNewName2 = _tcsrchr(pszTarget, _T('/'));
if (pszNewName < pszNewName2) pszNewName = pszNewName2;
if (pszNewName)
{
*pszNewName = _T('\0');
++pszNewName;
}
if (FAILED(pFO->SetOperationFlags(FOF_NO_UI)))
return E_FAIL;
HMODULE hMod = LoadLibrary(_T("Shell32.dll"));
fnSHCreateItemFromParsingName* pfnSHCreateItemFromParsingName = (fnSHCreateItemFromParsingName*)GetProcAddress(hMod, "SHCreateItemFromParsingName");
CComPtr<IShellItem> psiFrom;
CComPtr<IShellItem> psiTo;
if (pfnSHCreateItemFromParsingName == NULL ||
FAILED(pfnSHCreateItemFromParsingName(pszTemp, NULL, IID_PPV_ARGS(&psiFrom))) ||
FAILED(pfnSHCreateItemFromParsingName(pszTarget, NULL, IID_PPV_ARGS(&psiTo))))
{
FreeModule(hMod);
return E_FAIL;
}
FreeModule(hMod);
if (FAILED(pFO->MoveItem(psiFrom, psiTo, pszNewName, NULL)) ||
FAILED(pFO->PerformOperations()))
{
DeleteFile(pszTemp);
return E_FAIL;
}
There you go, that is all you need to make your application capable of saving to an UAC-protected folder. Less than 100 lines of code.
Microsoft annoyed quite a lot people with the frequent UAC prompts back in 2007 when Windows Vista was released. Not me, I actually liked the improved security. It was a nice idea, but the implementation sucked. The first half was good, the second needed some work.
What happened in Windows 7 was a disaster. Instead of properly implementing the user-interaction part in Windows tools, Microsoft gave itself an exception from the UAC, and thus rendered UAC completely useless (due to many security holes in Explorer and other Microsoft tools).
From the programmer perspective, UAC is a pain to work with. The worst part is that every software developer has to implement the same (and quite complex) algorithm. Would it be really so hard if Microsoft officially published a couple of elevation-compatible COM objects for the usual tasks like saving a file? That would save countless software developers countless hours of their time... To simply save a file, one should not be forced to resort to hacks or to delve into the intricacies of IPC on Windows.
Great article, Vlastimil!
-- kyrathaba on DC
Find out how Vista icons differ from XP icons.
See how RealWorld Icon Editor handles Vista icons.