Remotely Set a User's Desktop Wallpaper

I recently discussed, with a coworker, the feasibility of changing a logged on user's desktop wallpaper remotely. It was an interesting problem with quite a number of challenges. The Win32 security model is very complex and I had to jump through a lot of hoops for what I thought would be a fairly simple task. Overall it was a very enlightening experience but I did learn a new appreciation for seteuid(0).

My journey began (as these journeys often do) by perusing the Win32 API on MSDN. I knew it was possible to change the wallpaper path in the registry, but that change would only take effect the next time the user logged in. What I needed was a way to set the user's wallpaper and force their desktop to refresh. I wrote a simple prototype to set my wallpaper by calling SystemParametersInfo with SPI_SETDESKWALLPAPER and SPIF_SENDCHANGE, and it worked as expected. But how could I do this remotely?

I wrote a simple batch script to copy my prototype to the target machine and then start the process with WMI.

mkdir \\%1\c$\tmp
copy chwp.exe \\%1\c$\tmp
copy cat-owned.bmp \\%1\c$\tmp
wmic /node:"%1" /user:bob.carroll process call create ^
"cmd.exe /c c:\tmp\chwp.exe \tmp\cat-owned.bmp > c:\tmp\out.txt"



Initially, the call kept failing with ERROR_INSUFFICIENT_BUFFER. I'm a domain administrator so I had the necessary rights, but logging in at the console before running my script seemed to fix the issue. That suggests the problem was caused by not having a profile, but I didn't look into it further.

At this point I was able to remotely launch my application, but it was executing in a service window station. I figured that my application would need to run in the context of the interactive desktop before I could change the user's wallpaper. It was easy enough to attach to WinSta0\Default, but I had to adjust the window station's DACL in order to open it for WINSTA_ALL_ACCESS.

With my application running in the correct desktop context, I attempted to call SystemParametersInfo but it failed with ERROR_ACCESS_DENIED. This actually made sense because I was still executing as myself but I didn't own the desktop session. I thought about impersonating the console user, but I needed an access token and LogonUser requires a password. Calling NtCreateToken might work, but I'd have to fill the token myself. If only I could steal the console user's token somehow...

Since I was executing in the console user's desktop session, I was able to locate the Program Manager window and then get the EXPLORER.EXE process ID. Ideally, I could copy the access token from EXPLORER.EXE and use it to impersonate the console user. I enabled SeDebugPrivilege and opened the process for all access, but I was unable to call OpenProcessToken with TOKEN_DUPLICATE. Apparently the token itself has a DACL and I was implicitly not allowed to read it.

After hours of reading, I couldn't find a way around this without stomping on the token and granting myself access. So I switched on SeTakeOwnershipPrivilege and did just that. Interestingly, calling SetTokenInformation failed with ERROR_ACCESS_DENIED, but calling SetKernelObjectSecurity succeeded. Once I had rights to read the token, I copied it and called ImpersonateLoggedOnUser. Now I was running in the console user's desktop session as that user.

And for the moment of triumph: running my script from machine A caused the desktop wallpaper to change on machine B! I made a video to demo the tool.

You can find the sources here.