Just Let It Flow

January 3, 2012

Bagging Some Property – Getting A User’s Picture Tile

Filed under: Code,Windows — adeyblue @ 3:50 am

Contents

  1. Introduction
  2. Enter the Shell
  3. A Better Alternative
  4. Grabbing the Blob
  5. 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.

1 Comment »

  1. Please, don’t do this. Ever. Yes, saving a picture to a temporary file and then reading it sucks – but just do it. Otherwise once the internal format changes your program stops working, and Raymond Chen will have to write another compatibility shim (or not, if your program is not popular enough) and a blog post.

    Comment by Arseny Kapoulkine — January 5, 2012 @ 2:52 am

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress