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.
2 Responses to “Remotely Set a User’s Desktop Wallpaper”
Couldn’t you have just run psexec from sysinternals?
psexec would work for remotely executing chwp but not for actually changing the wallpaper. You need to be executing as the console user for the API call to work.
Also, with psexec, you’d need to install the service on the remote machine. I chose WMI because it didn’t require any additional software.