Contents
- Introduction
- Enter the Shell
- A Better Alternative
- Grabbing the Blob
- Outro – More Than You Bargained For
Introduction
It doesn’t sound like it should be so hard. I mean, the shell has managed to produce it every time you’ve logged on since Windows XP. MSDN even has a page dedicated to user profiles that includes a section on where it is and how its treated. It details that the users picture lives in their temp directory, except for most times when it doesn’t. It’s not wrong in its description. The picture will turn up if you open the User Account control panel, but if you’re trying to grab it programatically, asking the user to open Control Panel and all that or even worse, opening it from your own code and killing the window just as quick aren’t fantastic solutions.
Enter The Shell
If you’ve searched for this before or being otherwise snooping through the shell’s exported functions, you may have seen something called SHGetUserPicturePath or its ex versionSHGetUserPicturePathEx. Just the sound of their names elicit sounds of joy, a joy that the long search is over. And it should be, except that from the position of the vertical scrollbar on your browser you can tell it’s not that easy. For one thing, up until a few weeks ago there was no public record of how to use them or what they do, at least not one picked up by Google. Now that’s been rectified and given the MSDN page, 2+2 would suggest these are the functions called upon opening the control panel.
That’s fine and all and will allow you to use the temp folder to grab the picture. If you wanted a copy of the picture anyway, then that’s all you need to be concerned about. But if it’s a copy, where does the original reside? If you took the opportunity above to visit the …PathEx function’s page, you’ll have noticed the pwszPicPath. This parameter does indeed receive the current location of the original copy the control panel made when the user selected an avatar.
Again, that’s great except if all that’s wanted are the bits of the image or some other metadata a copy of the file isn’t necessary. Unfortunately, SHGetUserPicturePath or its Ex brother have no option or combination of parameters that ellide the copy operation. If we were mere programming monkeys, we may be content with deleting the copy every time treating it as if it were collateral damage. The good news is, we’re not. We’re analytical and prepared to go deeper because we know the function must get its information from somewhere.
A Better Alternative
In the case we’re interested in, SHGetUserPicturePathEx is a simple wrapper around GetTemporaryFileName and a function it passes the resulting path to, NetGetUserPicture. This, in turn is a thin wrapper a function called SHCopyUserTile which does the actual donkey work of copying the picture.
Its first port of call is SHGetUserPictureBytes which, after validating the user isn’t the guest account, executes this code sequence:
.text:7241FB42 lea eax, [ebp+pPropStore] .text:7241FB45 push eax ; these 2 are the equiv of &pPropStore .text:7241FB46 push [ebp+pUserName] ; passed in user name .text:7241FB49 call ?_GetAccountProps@@YGJPBGPAPAUIPropertyStore@@@Z ; _GetAccountProps(ushort const *,IPropertyStore * *) .text:7241FB4E mov edi, eax .text:7241FB50 cmp edi, ebx .text:7241FB52 jl loc_7241FC99 ; jump if it failed .text:7241FB58 xor eax, eax .text:7241FB5A mov [ebp+pvar.vt], ax ; all these clear out the propvariant data fields .text:7241FB5E lea edi, [ebp+pvar.wReserved1] .text:7241FB61 stosd ; these 4 clear out the PropVariant fields .text:7241FB62 stosd ; 4 bytes at a time .text:7241FB63 stosd .text:7241FB64 stosw ; clear the final two bytes .text:7241FB66 mov eax, [ebp+pPropStore] ; these set up the COM call .text:7241FB69 mov ecx, [eax] .text:7241FB6B lea edx, [ebp+pvar] ; again, these 2 equivalent to &pvar .text:7241FB6E push edx .text:7241FB6F push offset _PKEY_SAM_UserPicture .text:7241FB74 push eax .text:7241FB75 call dword ptr [ecx+14h] ; IPropertyStore::GetValue .text:7241FB78 mov edi, eax .text:7241FB7A mov [ebp+savedHR], edi .text:7241FB7D cmp edi, ebx .text:7241FB7F jl loc_7241FC7D ; jump if failed .text:7241FB85 cmp [ebp+pvar.vt], 41h ; See if the propvariant is of VT_BLOB type .text:7241FB8A jz short loc_7241FBEF .text:7241FB8C cmp [ebp+pvar.vt], 46h : or of VT_BLOBOBJECT type .text:7241FB91 jz short loc_7241FBEF .text:7241FB93 cmp [ebp+pvar.vt], bx ; or VT_EMPTY .text:7241FB97 jz short loc_7241FBEF
This code somehow retrives an IPropertyStore pertinent to the user specified by pUserName, and then queries it for its UserPicture property which returns as a blob type. It’s a promising lead given the names involved, a lead made all the more promising when we see the function and parameters passed to the blob parsing function.
.text:7241FC02 lea eax, [ebp+pOriginalPictureFileName] .text:7241FC05 push eax .text:7241FC06 lea eax, [ebp+numOfPictureBytes] .text:7241FC09 push eax .text:7241FC0A lea eax, [ebp+pointerToPictureBytes] .text:7241FC0D push eax .text:7241FC0E push dword ptr [ebp+pvar.data] ; size of blob data .text:7241FC11 push dword ptr [ebp+pvar.data+4] ; blob data pointer .text:7241FC14 push [ebp+pwszDesiredSrcExt] ; the desired filetype of the bytes .text:7241FC17 push ebx ; unused .text:7241FC18 call ?_GetPicture@@YGJHPBGPBU_USER_PICTURE_ELEMENTS@@KPAPBEPAKPAPBG@Z ... ... .text:7241FC50 push [ebp+pOriginalPictureFileName] ; parsed filename .text:7241FC53 push [ebp+origPathLen] ; passed in string len .text:7241FC56 push [ebp+pszOrigPath] ; passed in filename buffer .text:7241FC59 call ?StringCchCopyW@@YGJPAGIPBG@Z
The only external function GetPicture calls is CompareString so the original filename is most definitely within the blob, and we know this is the original file name as it is copied to the passed in buffer a few instructions later. It also seems from further down in SHCopyUserTile that, perhaps surprisingly, the picture data is embedded within the blob.
GetPicture is quite large in terms of instructions so instead of a dissection, here’s the data format of the blob, which is the same in both 64 and 32-bit programs and between Vista and the 8 Developer Preview.
dword 1 dword 2 (3 if the original filename is present) dword 1 dword filesize - rounded to nearest 4 filedata - BMP format dword 0 dword extensionLenInbytes - rounded to nearest 4 extension - Unicode text - without dot - null terminated dword 2 dword originalFileNameLen - in bytes rounded to nearest 4 Original file name - unicode text - null terminated
Grabbing the Blob
Now we have the blob, its format, and the magic incantation of how to get it, all we need to grab now is the property store. We’ve seen a function, GetAccountProps, that can translate a user name into one, so let’s take a gander at how it works:
.text:7241EC7F lea eax, [ebp+pIComputerAccounts] .text:7241EC85 push eax ; &pInterfacePointer .text:7241EC86 push offset IID_IComputerAccounts ; IID .text:7241EC8B push 1 ; CLSCTX_INPROC_SERVER .text:7241EC8D push edi ; NULL .text:7241EC8E push offset _CLSID_LocalUserAccounts ; CLSID .text:7241EC93 call _CoCreateInstance@20 .text:7241EC98 mov esi, eax .text:7241EC9A cmp esi, edi .text:7241EC9C jl short loc_7241ECC5 ; exit if FAILED(hr) .text:7241EC9E mov eax, [ebp+pIComputerAccounts] .text:7241ECA4 mov ecx, [eax] .text:7241ECA6 push [ebp+pIPropStore] ; IPropertyStore ** .text:7241ECAC lea edx, [ebp+userNameBuf] .text:7241ECB2 push edx ; pUserName .text:7241ECB3 push eax ; the This pointer .text:7241ECB4 call dword ptr [ecx+24h] ; Call one of the interface methods .text:7241ECB7 mov esi, eax .text:7241ECB9 mov eax, [ebp+pIComputerAccounts] .text:7241ECBF mov ecx, [eax] .text:7241ECC1 push eax ; the This pointer .text:7241ECC2 call dword ptr [ecx+8] ; IComputerAccounts::Release
That’s the entirety of the function, about 5 lines of C. An interface is created, one method is called on it, and then it’s disposed of with Release(). Doing this ourselves is easy, the dissassembly will give us the make up of both GUIDs, we know the function is the 10th one in the vtable (the 3 IUnknown ones, and then 6 others we don’t care about before ours), what arguments it takes (and LPCWSTR and an IPropertyStore**) and what it returns (HRESULT).
#include <windows.h> #include <ole2.h> #include <cassert> #include <cstdio> #include "accountprops.h" // see below the code for what's in this struct PicElementsHeader { DWORD signature; DWORD flags; DWORD unk2; DWORD fileDataSize; }; void MakeSenseOfPictureBlock(const BLOB* pBlob, LPCWSTR* ppExt, LPCWSTR* ppOriginalName, PBYTE* ppData, ULONG* pDataSize) { PBYTE pBlock = pBlob->pBlobData; PicElementsHeader* pPic = (PicElementsHeader*)pBlock; assert(pBlob->cbSize > (sizeof(*pPic) + pPic->fileDataSize)); assert(pPic->signature == 1); assert(pPic->unk2 == 1); if(pDataSize) { *pDataSize = pPic->fileDataSize; } pBlock += sizeof(*pPic); if(ppData) { *ppData = pBlock; } pBlock += pPic->fileDataSize; DWORD extByteLen = *(DWORD*)(pBlock + sizeof(DWORD)); pBlock += 2 * sizeof(DWORD); WCHAR* pExt = (WCHAR*)pBlock; if(ppExt) { *ppExt = pExt; } if(!((pPic->flags & 1) && ppOriginalName)) { return; } pBlock += extByteLen; assert(*(PDWORD)pBlock == 2); pBlock += sizeof(DWORD); // Unneeded - would be here // DWORD fullNameByteLen = *(PDWORD)pBlock; pBlock += sizeof(DWORD); LPCWSTR fullOrigName = (PCWSTR)pBlock; *ppOriginalName = fullOrigName; assert(GetFileAttributes(fullOrigName) != INVALID_FILE_ATTRIBUTES); } int main() { CoInitialize(NULL); IComputerAccounts* pAccs = NULL; HRESULT hr4 = CoCreateInstance(CLSID_LocalUserAccounts, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pAccs)); if(SUCCEEDED(hr4)) { IPropertyStore* pPropStore = NULL; if(SUCCEEDED(hr4 = pAccs->FindByName(argv[1], &pPropStore))) { PROPVARIANT prop = {0}; if(SUCCEEDED(hr4 = pPropStore->GetValue(PKEY_SAM_UserPicture, &prop))) { LPCWSTR pExt = NULL, pOrigName = NULL; PBYTE pData = NULL; ULONG dataSize = 0; MakeSenseOfPictureBlock(&prop.blob, &pExt, &pOrigName, &pData, &dataSize); wprintf(L"Data at %p, size %lu\nExtension is %s\nFull path is %s\n", pData, dataSize, pExt, pOrigName); PropVariantClear(&prop); } else { wprintf(L"PropStore->GetValue failed with error %#x\n", hr4); } pPropStore->Release(); } else { wprintf(L"FindAccName failed with error %#x\n", hr4); } pAccs->Release(); } else { wprintf(L"CCI failed with error %#x\n", hr4); } CoUninitialize(); return 0; }
More Than You Bargained For
As the method is named FindByName, you can use it to query for any users property store and thus picture details. Of course, there could be other properties you can query apart from the picture details, and there are at least 6 other methods in the interface that could potentially be useful, so I checked them out. And there are quite a number of useful things. accountprops.h for the above code should be populated with the various propertykeys, guids and interfaces from that link.
So we’ve gone from manually opening the control panel to get at the users picture tile, to a function that unaviodably copies it to the temp directory, to a function/program which queries where it resides straight out. Quite the improvement I must say. Happy property bagging.