Snooping the Contents of a Password Edit Control
Did you ever get a chance to blankly stare at a screen similar to the above, trying to recollect what your password really was? Security is great, and so is "Save password"; you try snooping for the application's configuration file or the registry where the password might be stored, only to find the application is storing it encrypted.
If you're determined enough, you could start searching the process memory for strings to see if the password is stored somewhere in plaintext form. Or, if you're really determined, you could set a breakpoint in the window's window procedure, click OK and debug from there to get to the point where the password is actually being used (without symbols, of course, this is not going to be very easy).
So what's the alternative? Well, if we could write some code...
Clearly the application in question has access to the contents of the password box. How does it get access? Well, eventually it all boils down to the WM_GETTEXT window message being sent to the edit control. (Note: this isn't the case for every single edit control - for example, in WPF there's no window in the Windows sense of the word behind the control; in Internet Explorer the password input field is not a window either.)
So why don't we send that message to the window and get the password out as a result? (Preemptive disclaimer first: this is not a full-blown solution, just an attempt on my behalf to show a nice aspect of Win32.)
First of all, we'll need the window handle (HWND), which is easily obtained using a tool like Spy++ (part of Visual Studio tools). Just click Alt+F3 from within Spy++ and navigate to the window:
So now we could theoretically write a program that sends the WM_GETTEXT message and gets the password, right? (Dramatic suspense.)
Wrong. It would be too easy for malware to intercept passwords if this were the case (it's still easy, but at least not that easy), and therefore the password edit control will not give us the text if we're sending the message from another process. Here's what happens when we use SendMessage to send the edit control the WM_GETTEXT message:
So this clearly isn't going to work. But why doesn't it work? Because we're trying to send WM_GETTEXT across processes. What if we could somehow call WM_GETTEXT from within the target process?
That would be a totally different game. But how can we do it? Windows gives us the facilities to create a thread within another process, provided that we have the appropriate permissions to access that process. That other thread can send the WM_GETTEXT message within the context of the target process and retrieve the password for us.
So here's the code for the thread I'd like executed in another process:
DWORD WINAPI ExtractPassword(LPVOID fromWindow)
HWND hwndPassword = (HWND)fromWindow;
char* lpszBuffer = new char;
With the obvious limitation of not working on 64-bit (casting pointer to a DWORD), it seems all right. (I do care deeply about 64-bit; it's just that returning the pointer from the thread is the simplest alternative which will prove reusable later on. So this is for methodical purposes only.)
However, this still isn't that simple. When we're off creating a new thread in the target process, what would be that thread's start routine? You might be tempted to say, this "ExtractPassword" function I've just written earlier. But take a look at CreateRemoteThread's parameters:
It's fairly clear that we are passing a pointer to a thread routine. But that pointer is in another process' address space! So if ExtractPassword happens to be mapped at some virtual address (call it X) in my address space, it doesn't mean that it's mapped at address X in the other process, or that it's mapped there at all!
Attentive readers, you're right! The same can be argued about the use of the SendMessage function. It just happens to be part of USER32.DLL, which is very likely to be mapped in the target process, and even very likely to be mapped at the same address as in our process. (But this is not always true, for example on Vista the Address Space Layout Randomization feature might render this assumption invalid. Again, for methodical purposes we will forget about this limitation for now.)
So what we need to think about next is getting the code for ExtractPassword in the remote process. This can be accomplished by multiple ways, one of them being just writing the function's code into the process' memory. This will require us to know the function's size in advance (not very difficult, but requires some work). Another alternative is simply putting the function in a DLL and getting that DLL injected (loaded) into the target process. If we were worried about attack detection, we could unload that DLL later on (but of course, we aren't).
So, first things first, we need to put the function in a DLL and get the DLL loaded in the target process. I changed the function signature a little bit so that it's exported from the DLL and so that its name is not mangled:
extern "C" DWORD __declspec(dllexport) DoWork(LPVOID lParam)
HWND hwndPassword = (HWND)lParam;
char* lpszBuffer = new char;
Once we have this in place, we just need to load the DLL into the target process by using a combination of CreateRemoteThread and LoadLibrary (again, note that LoadLibrary is part of KERNEL32.DLL and therefore highly likely to be present at the same load address as in our process, except with ASLR). Note that LoadLibrary takes as a parameter the DLL name, so we have to write it into the target process' memory first.
//Open a handle to the process
HANDLE hInjecteeProcess = OpenProcess(PROCESS_ALL_ACCESS,
//Allocate memory for the DLL name
LPVOID lpszDllName = VirtualAllocEx(hInjecteeProcess,
NULL, 4096, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
//Write the DLL name into the process
const wchar_t InjectedDllName = L"InjectedDll.dll";
//Create a remote thread to load the DLL
HANDLE hRemoteThread = CreateRemoteThread(hInjecteeProcess,
NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary,
lpszDllName, 0, NULL);
//Wait for thread completion and get DLL base
LPVOID lpDllBase = (LPVOID)dwExitCode;
//Close the handle to the remote thread
Note the steps here: first of all, we open a handle to the process requesting PROCESS_ALL_ACCESS rights (we need the process id for that, but Process Explorer or Task Manager can help us discover it easily). Then we allocate memory within the target process to store the DLL name, which we proceed to write into that memory. Finally, we create a remote thread executing the LoadLibrary function for us (note that coincidentally, the signatures for LPTHREAD_START_ROUTINE and LoadLibrary are the same on 32-bit, even though the parameter types don't seem to be the same), and take that thread's exit code. The thread's exit code is the return value from LoadLibrary, which is an HMODULE. It's not really a handle to anything, it's just the base address at which the DLL was loaded in the target process.
So at this point we have the DLL loaded, now we need to create another thread which will execute the DoWork function (previously called ExtractPassword) for us. But this is another dangerous call, because DoWork in our process might be loaded at a completely different address than at the target process. However, we know where the DLL was loaded in the target process, and we can discover the offset of the DoWork function from the DLL's base address, so we can calculate the appropriate address within the target process, like so:
//Load the DLL in our process
HMODULE hDll = LoadLibrary(L"InjectedDll.dll");
//Get DoWork's address and calculate offset
FARPROC lpMyFunc = GetProcAddress(hDll, "DoWork");
DWORD_PTR dwOffset =
(DWORD_PTR)lpMyFunc - (DWORD_PTR)hDll;
//Calculate address in remote process
LPVOID lpRemoteProc =
(LPVOID)((DWORD_PTR)lpDllBase + dwOffset);
//Create thread with that address
hRemoteThread = CreateRemoteThread(hInjecteeProcess,
NULL, 0, (LPTHREAD_START_ROUTINE)lpRemoteProc,
(LPVOID)hwnd, 0, NULL);
//This time, the exit code is the password,
//so read the password from the target
(LPCVOID)dwExitCode, lpszPassword, 1024,
//And print it
printf("Password is: %S\n", lpszPassword);
Voila, we have the password!
If you'd like to, you can download the complete code for this post as a Visual Studio 2008 solution. Friendly reminder again: this is not production-ready code; several pitfalls were outlined along the way, the most significant ones being 64-bit compliance and ASLR.