In my previous post I explained how I built a little ASP.NET Core logger that writes log messages to an active Notepad window. In this post I'll go into how it actually does that, and how it was extended for Notepad++ and Notepad2.
The core of the original code, as featured on Reddit, was these three function calls:
notepad = FindWindow(NULL, "Untitled - Notepad");
edit = FindWindowEx(notepad, NULL, "EDIT", NULL);
SendMessage(edit, EM_REPLACESEL, TRUE, (LPARAM)buf);
This performs its arcane magic using Windows' window messaging.
The way Windows works is that every window* has a message queue. It constantly checks the queue for new messages, and handles them. These are usually fairly benign messages such as "the user moved their mouse" or "there was a mouse click here", and is usually handled for you by your underlying framework such as Windows Forms or WPF.
*Some things that are windows don't look like them. You can create a hidden window and get a window handle without actually having a visible panel that users can interact with.
In our case, we first have to find the Notepad window. We do this by searching for any window with the title "Untitled - Notepad"
.
(On recent versions of Windows 10, we also need to search for "*Untitled - Notepad"
as Notepad now uses a leading asterisk to denote a document with unsaved changes.)
We then find a child window with the "EDIT"
class. This represents the standard windows Edit control that makes up 99% of Notepad's interface.
The last thing we do is send it a message EM_REPLACESEL
, which says "replace whatever the currently selected text is with this stuff I'm sending you." If there is no text currently selected, then the new text is inserted wherever the editor caret (cursor) is.
(The third parameter doesn't really matter for our use case - TRUE
means that the operation can be undone (e.g. with Ctrl+Z), FALSE
means that it can't be undone.)
So that's how it works in Notepad - quite a lot of work under the hood that is bundled up into just 3 short lines of C/C++ code.
Getting it to work with Notepad++ and Notepad2 was a bit of a different story - fortunately though, they function identically in this regard.
First, we need to find the window. For "Notepad2" we can look for "Untitled - Notepad2"
, but for Notepad++ it uses a counter, e.g. "new 1 - Notepad++"
, "new 2 - Notepad++"
etc. To find this window I had to switch from FindWindow
, which find the first window with a given name, to EnumWindows
, which loops through all active windows, so that I could match one with a regular expression.
Next, we need the editor control's window handle. Unlike Notepad, Notepad++ and Notepad2 do not use the standard Windows Rich Edit control. Instead, they are built on top of the Scintilla open-source project. To find this control, we can just substitute "EDIT"
for "Scintilla"
in our call to FindWindowEx
.
Lastly, we need to write our message to Scintilla. This is not as simple as with Notepad, however. Scintilla does have an equivalent window message, SCI_ADDTEXT
, that lets you insert text into the document at the caret/cursor position, however if you try and use this directly, the target application (Notepad++/Notepad2) just crashes with a memory error.
This seems to be due to memory isolation. In Windows, two programs cannot (by default) read or write each-others memory addresses. The best reason I can piece together for why this worked for Notepad is that Windows innately understands the EM_REPLACESEL
message, and when it sees this, it copies our text into Notepad's memory on our behalf. Windows doesn't have the same knowledge about Scintilla, so the same doesn't occur. Scintilla therefore gets a pointer into our memory space, and when it tried to read it, Windows blocks the read and Scintilla crashes.
To work around this, I had to invoke some fun magic that I've only done once before when building a memory injector. The plan here is to write the message into Scintilla's memory, give it a pointer to its own address space, then free that memory.
From our window handle, we can get the process ID by calling GetWindowThreadProcessId
. Then we can use VirtualAllocEx
with the process handle and MEM_COMMIT | MEM_RESERVE
to allocate memory inside the other process's memory space.
Since that's not our own memory space, we can't just write directly to it. We have to call WriteProcessMemory
to copy the bytes across.
Now we can finally call SendMessage
to insert our text into the Notepad++/Notepad2 editor, and it doesn't crash!
Lastly, we need to clean up, otherwise we've now injected a memory leak into another program. To do this we call VirtualFreeEx
with MEM_RELEASE
to free the memory, and poof it's now gone.
That's how this all works under the hood, and how we managed to log ASP.NET Core messages to Notepad, Notepad++, and Notepad2. If you want to give it a whirl for yourself, the package is available on NuGet.