Recreating Win32.Magistr's Conspicuous Payload
2348 words | 12 minutes
Preface
Old school malware has this certain charm to it. The amusing email bodies worms employed, edgy messages, destructive routines that bricks your BIOS or simply wipes your Windows folder (too many examples to cite) and at times, plain silly payloads which serve no purpose besides screwing with the user.
Win32.Magistr was an email worm which combined all these elements. A worm which targetted those in the law profession in particular, it would mass email fragments of confidential documents, call the user “another haughty bloodsucker” and “a chunk of shit”, delete a bunch of files and wipe the CMOS and BIOS to maximise its destruction.
Fig: Magistr’s less than friendly message |
But the real cherry on top was its non-destructive payload. Copying that of Win9x.Shoerec, if two months have elapsed since the initial infection, on odd days, it makes the icons on your screen run away from your cursor. That’s all.
Seems simple enough to implement, right? Right.
Mostly.
Fetching the Desktop
The idea is simple: get the cursor’s position, iterate through all the desktop icons positions, then if the icon is near the mouse, get the relative position to the mouse and then push it more in that direction.
We start with this useful blogpost from Raymond Chen’s The Old New Thing. It’d be nice to just blindly copy the code from this post (which we do, shamelessly), but let’s break it down a little.
If you were unaware, the Window’s desktop is a special shell window instance (the shell being explorer.exe
).
1CComPtr<IShellWindows> spShellWindows;
2HRESULT hResult = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
We start by fetching all available shell windows using CoCreateInstance
, passing in the CLSID (Class ID) for shell windows.
If we weren’t using a COM Object, we’d have to pass in a few more parameters such as the IID (Interface ID), with a function call more like CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**) &psw)
1CComVariant vtLoc(CSIDL_DESKTOP);
2CComVariant vtEmpty;
3
4long lhwnd;
5CComPtr<IDispatch> spdisp;
6
7hResult = spShellWindows->FindWindowSW (
8 &vtLoc, &vtEmpty,
9 SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
This next bit of code is where we actually isolate the desktop window itself. The vtLoc
Variant is initialized with the CSIDL (constant special item ID list, an anagram of the aforementioned CLSID) for the desktop. CSIDLs are a system independent way to ID certain special folders, because turns out you actually have the liberty to rename system folders such as C:\Windows
to something else.
The call to FindWindowSW
(as the name implies) is where the desktop window is searched for, with spdisp
being the corresponding IDispatch
interface. Passing SWC_DESKTOP
as the swClass
parameter restricts the search to only desktop windows.
It feels a little roundabout doing things like this as the SWC_DESKTOP
flag already narrows the search space to only desktop windows, yet we pass in a vtLoc
param to specify the window to find: the desktop.
Turns out, the CSIDL_DESKTOP
just maps to 0x0000
, and with a dual monitor setup, it doesn’t even serve as an index for the individual monitors. Changing this value also errors out the program, so we just roll with it.
1CComPtr<IShellBrowser> spBrowser;
2
3hResult = CComQIPtr<IServiceProvider>(spdisp)->
4 QueryService(SID_STopLevelBrowser,
5 IID_PPV_ARGS(&spBrowser));
6if (hResult != S_OK) return false;
7
8CComPtr<IShellView> spView;
9hResult = spBrowser->QueryActiveShellView(&spView);
10if (hResult != S_OK) return false;
QueryActiveShellView
is what will actually get us down to some sort of “desktop object” that we can subsequently pull interfaces from. To achieve this, we get an IShellBrowser
object, passing in the SID (Service ID) SID_STopLevelBrowser
. With this in place, we can finally query for an interface of choice. Sticking to the code we rip from Raymond Chen’s blog with some added error checking, we get the following function:
1bool FindDesktopFolderView(REFIID riid, void** ppv)
2{
3 // All open shell windows (explorer windows)
4 CComPtr<IShellWindows> spShellWindows;
5
6 HRESULT hResult = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
7 if (hResult != S_OK) return false;
8
9 CComVariant vtLoc(CSIDL_DESKTOP);
10 CComVariant vtEmpty;
11
12 long lhwnd;
13 CComPtr<IDispatch> spdisp;
14
15 // lhwnd is the corresponding handle, but what we really want is spdisp, the IDispatch interface
16 hResult = spShellWindows->FindWindowSW (
17 &vtLoc, &vtEmpty,
18 SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
19 if (hResult != S_OK) return false;
20
21 // IShellBrowser gives us some helpful methods, and the one we want is QueryActiveShellView. Quite self-explanatory
22 CComPtr<IShellBrowser> spBrowser;
23
24 // CComQIPtr is a smart pointer class and we use the QueryService method to get a reference to the browser
25 hResult = CComQIPtr<IServiceProvider>(spdisp)->
26 QueryService(SID_STopLevelBrowser, // GUID for the browser
27 IID_PPV_ARGS(&spBrowser));
28 if (hResult != S_OK) return false;
29
30 CComPtr<IShellView> spView;
31 hResult = spBrowser->QueryActiveShellView(&spView);
32 if (hResult != S_OK) return false;
33
34 // Lastly, we can apply QueryInterface to get our interface of choice from the Desktop object by passing in an interface
35 hResult = spView->QueryInterface(riid, ppv);
36 if (hResult != S_OK) return false;
37
38 return true;
39}
Now in wmain
, we can get our IShellFolder
interface.
1// Using our function, we get a reference to the desktop's IFolderView
2CComPtr<IFolderView> spView;
3FindDesktopFolderView(IID_PPV_ARGS(&spView));
4
5// The IShellFolder object will let us get attributes to do with the files in the folder
6CComPtr<IShellFolder> spFolder;
7spView->GetFolder(IID_PPV_ARGS(&spFolder));
IID_PPV_ARGS
is a macro that helps to expand the CComPtr
to fit the args we defined earlier, passing __uuidof(**(&spView)), IID_PPV_ARGS_Helper(&spView)
into the FindDesktopFolderView
function.
With this, we can now enumerate through all the items by calling for… Items
.
1CComPtr<IEnumIDList> spEnum;
2HRESULT hResult = spView->Items(SVGIO_ALLVIEW, IID_PPV_ARGS(&spEnum));
3
4for (CComHeapPtr<ITEMID_CHILD> spidl;
5 spEnum->Next(1, &spidl, nullptr) == S_OK;
6 spidl.Free()) {
7
8 POINT icon_pt;
9 spView->GetItemPosition(spidl, &icon_pt); // This gives us the coordinates of the icons!
10 // we fill in code here...
11}
Incorporating Mouse Position
Now this is the part where we fetch the mouse position. This is… surprisingly easy. Since we aren’t trying to get the mouse position relative to a window and instead just against the whole screen (the desktop should encompass the entire screen), all we have to do is call GetCursorPos
.
1POINT cursor_pt;
2while (true) {
3 if (GetCursorPos(&cursor_pt)){
4 // code ...
5 }
6}
Now, all we have to do is iterate through all the items and their coordinates, compare it against the mouse position and calculate their new positions!
Setting a new position is also simple, we just have to call SelectAndPositionItems
1PCITEMID_CHILD apidl[1] = { spidl };
2spView->SelectAndPositionItems(
3 1, apidl, &icon_pt, SVSI_POSITIONITEM
4);
We want there to be a “forcefield” around the mouse, so we calculate the Euclidean distance of each icon relative to the mouse. Depending on the current Euclidean distance, we scale the existing xy difference so that it equals to our forcefield radius.
Now this was where I ran into a minor issue: I have two monitors.
Mouse position has (0,0)
defined as the top middle of the screen, in between both monitors. However, the desktop icons has (0,0)
defined as the top left of the screen, so I have to minus off 1920
from the x coord of the icons. The reported icon positions also correspond to the top left of each icon, so we also account for the icon size, which appears to be 76x108
.
Since this is a toy program, I’m hardcoding these values to fit my own system, but this should be able to be dynamically done.
We first calculate the distance of an icon to the mouse, and if it is within the forcefield radius, we scale that distance to push the icon away.
1int pushRad = 300;
2
3double distance = pow(((icon_pt.x + 38 - 1920) - cursor_pt.x), 2) + pow(((icon_pt.y + 54) - cursor_pt.y), 2);
4
5if (distance <= pushRad * pushRad) {
6 double multiplier = pushRad / sqrt((distance)) - 1;
7 icon_pt.x += (int)(((icon_pt.x + 38 - 1920) - cursor_pt.x) * multiplier);
8 icon_pt.y += (int)(((icon_pt.y + 54) - cursor_pt.y) * multiplier);
9}
And this gives us the desired result!
Dealing With “Align Icons”
Problem: most people have Align Icons enabled by default.
Solution: turn it off. Easy.
The desktop icons behaviour is defined by a registry key found at HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Bags\1\Desktop\Fflags
, and it follows this spec for the various options. We want FWF_AUTOARRANGE
and FWF_SNAPTOGRID
disabled, leaving us with the flag combination of 0x40200220 = 1075839520
.
So we query the value at Fflags
, and if it isn’t equal to our target flag, we set it. We know that this field is a DWORD
, so we be explicit with our function calls, passing in RRF_RT_REG_DWORD
and REG_DWORD
.
1#define DESKTOP_KEY L"SOFTWARE\\Microsoft\\Windows\\Shell\\Bags\\1\\Desktop"
2
3DWORD desktopFflags{};
4DWORD pcbData = sizeof(desktopFflags);
5WCHAR targetFlag[11];
6swprintf_s(targetFlag, 11, L"%d", 1075839520);
7DWORD target = 1075839520;
8
9HKEY hKey = NULL;
10RegOpenKeyExW(HKEY_CURRENT_USER, DESKTOP_KEY, 0, KEY_WRITE, &hKey);
11RegGetValue(HKEY_CURRENT_USER, DESKTOP_KEY, L"FFlags", RRF_RT_REG_DWORD, NULL, &desktopFflags, &pcbData);
12if (desktopFflags != 1075839520) {
13 RegSetValueEx(hKey, L"FFlags", NULL, REG_DWORD, (const BYTE*)&target, sizeof(target));
14 RegCloseKey(hKey);
15}
But when testing this, we find that even though the registry key is changing, we can still reenable those options! In fact, enabling those options does not seem to affect the registry key itself…
Apparently, for the change to take effect, we have to restart the shell itself. Worse still, it seems non-trivial to detect when these options are being enabled during the payload loop itself.
This presents us with two options:
- Disable the ability to change Align Icons and Auto Arrange using Group Policy
- Debug explorer and find a way to hook onto the options being enabled
Neither are simple, and so we turn it into future work. For now, we at least want our set registry key operation to actually work, so we need to introduce a function that can restart explorer. Fortunately, another Microsoft engineer has written a nice blogpost detailing the use of Restart Manager to programmatically restart the shell.
For the sake of brevity, I won’t cover the GetExplorerApplication()
function in depth as it boils down to a simple iteration of EnumProcesses
, choosing the oldest process in the list with the name explorer.exe
1RM_UNIQUE_PROCESS GetExplorerApplication()
2{
3 RM_UNIQUE_PROCESS result = { 0 };
4 DWORD bytesReturned = 0;
5 DWORD processIdSize = 4096;
6 std::vector<DWORD> processIds;
7 processIds.resize(1024);
8 // Get the list of process identifiers.
9 EnumProcesses(processIds.data(), processIdSize, &bytesReturned);
10
11 while (bytesReturned == processIdSize)
12 {
13 processIdSize += processIdSize;
14 processIds.resize(processIdSize / 4);
15 EnumProcesses(processIds.data(), processIdSize, &bytesReturned);
16 }
17
18 std::for_each(processIds.begin(), processIds.end(), [&result](DWORD processId) {
19
20 HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
21 if (hProcess) {
22 std::wstring imageName;
23 imageName.resize(4096);
24 if (GetProcessImageFileName(hProcess, (LPWSTR)imageName.data(), 4096) > 0)
25 {
26 if (wcscmp(L"explorer.exe", PathFindFileName(imageName.data())) == 0)
27 {
28 //this is assmuing the user is not running elevated and won't see explorer processes in other sessions
29 FILETIME ftCreate, ftExit, ftKernel, ftUser;
30 if (GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel, &ftUser))
31 {
32 if (result.dwProcessId == 0)
33 {
34 result.dwProcessId = processId;
35 result.ProcessStartTime = ftCreate;
36 }
37 // we opt for the older process
38 else if (CompareFileTime(&result.ProcessStartTime, &ftCreate) > 0)
39 {
40 result.dwProcessId = processId;
41 result.ProcessStartTime = ftCreate;
42 }
43 }
44 }
45 }
46 CloseHandle(hProcess);
47 }
48 });
49 return result;
50}
Now, all we have to do is restart the process. Using Restart Manager, we first have to init a Restart Manager session:
1DWORD dwSession = 0;
2WCHAR szSessionKey[CCH_RM_SESSION_KEY + 1] = { 0 };
3DWORD dwError = RmStartSession(&dwSession, 0, szSessionKey);
If this succeeds, we register our isolated explorer process, shut it down then restart it:
1if (dwError == ERROR_SUCCESS) {
2 // isolate the explorer process
3 RM_UNIQUE_PROCESS rgApplications[1] = { GetExplorerApplication() };
4 dwError = RmRegisterResources(dwSession, 0, NULL, 1, rgApplications, 0, NULL);
5 DWORD dwReason;
6 UINT nProcInfoNeeded;
7 UINT nProcInfo = 10;
8 RM_PROCESS_INFO rgpi[10];
9 // get the current status of registered items
10 dwError = RmGetList(dwSession, &nProcInfoNeeded,
11 &nProcInfo, rgpi, &dwReason);
12 if (dwReason == RmRebootReasonNone) // now free to restart explorer
13 {
14 RmShutdown(dwSession, RmForceShutdown, NULL);
15 RmRestart(dwSession, 0, NULL);
16 }
17}
18RmEndSession(dwSession);
Now, all we have to do is call this after we set our registry key, and it should turn off Align Icons and Auto Arrange the first time the payload is run.
Annoyingly enough, the code isn’t particularly stable. At times, the RestartExplorer
function hangs for some inexplicable reason. At times we hit these sort of assertion errors as well:
Fig: An annoyingly nondescript error message |
Apparently, this can be attributed to improper handling of the CComPtr
, so we throw in a few calls to Release
to hopefully compensate. We also shift the registry key assignment into the RestartExplorer
function, overloading it when we want to pass in the HKEY
. Adding a few calls to sleep also seems to help (and the program was relatively stable during step by step debugging), and given that this is a toy project, I don’t really want to spend the time debugging the hell out of it.
We also include CCoInitialize
, a nice piece of code mentioned in the Raymond Chen post we were referencing that has its own explanatory post. Uninitializing the CComPtr
seems to be a pretty big deal, so we place it in the destructor of the CCoInitialize
object and initialize an instance of it at the very beginning of our code.
1class CCoInitialize {
2public:
3 CCoInitialize() : m_hr(CoInitialize(NULL)) { }
4 ~CCoInitialize() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
5 operator HRESULT() const { return m_hr; }
6 HRESULT m_hr;
7};
8
9...
10
11int __cdecl wmain(int argc, wchar_t** argv)
12{
13 CCoInitialize init;
14...
And lastly, we don’t want a console window hanging around, so we make a call to FreeConsole()
at the very beginning of wmain.
With all this in place, our final product:
It can’t deal with the user switching on Align Icons or Auto Arrange when the payload is actually being run (previoulsy discussed, although technically we can just force more restarts of explorer), but if you have a dual monitor, it actually makes the icons fly over to the other screen, which is amusing in its own right.
You can take a look at the crappy source code in its entirety here.