(Why) Does Windows "Calc.exe" lack a WndProc?

c, spy++, vb.net, winapi, windows

Solution

In short (about your code): `GetWindowLong()` fails because you're trying to read an address in target process address space.

EXPLANATION

When `GetWindowLong()` returns 0 it means there is an error, from MSDN:

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Check `Marshal.GetLastWin32Error()` and you probably see error code is `ERROR_ACCESS_DENIED` (numeric value is 0x5).

Why? Because `GetWindowLong()` is trying to get address (or handle) of window procedure (not in your code, but in target process, in theory it may even be default window procedure but I never saw an application main window that doesn't hanle at least few messages). You may use this trick (but I never tried!) to see if a window is using default procedure (you have an address or not), I don't know...someone should try.

Now think what `WNDPROC` is:

LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM);

An address (valid in process A) is not callable in process B (where it makes no sense at all). Windows DLLs code segments are shared across processes (I assume, I didn't check but it's reasonable in the game between safety and performance).

Moreover `CallWindowProc(NULL, ...)` will understand that `NULL` as a special value to invoke window procedure for that window class (on `HWND` owner). From MSDN:

...If this value is obtained by calling the GetWindowLong function ...the address of a window or dialog box procedure, or a special internal value meaningful only to CallWindowProc.

How Microsoft Spy++ does it (and maybe WinSpy++ does not)? Hard to say without WinSpy++ source code. For sure it's not such easy like `GetWindowLong()` and right way should involve `CreateRemoteThread()` and to do `LoadLibrary()` from that but both Microsoft Spy++ and WinSpy++ source code aren't available (AFAIK) for further inspection...

UPDATE

WinSpy++ inspection/debugging is pretty off-topic with the question (you should post a ticket to developers, your source code may fail for what I explained above, you should - always - check error codes) but we may take a look for fun.

In `InjectThread.c` we see it uses `WriteProcessMemory` + `CreateRemoteThread` then `ReadProcessMemory` to read data back (not relevant code omitted):

// Write a copy of our injection thread into the remote process
WriteProcessMemory(hProcess, pdwRemoteCode, lpCode, cbCodeSize, &dwWritten);

// Write a copy of the INJTHREAD to the remote process. This structure
// MUST start on a 32bit boundary
pRemoteData = (void *)((BYTE *)pdwRemoteCode + ((cbCodeSize + 4) & ~ 3));

// Put DATA in the remote thread's memory block
WriteProcessMemory(hProcess, pRemoteData, lpData, cbDataSize, &dwWritten);

hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, 
    (LPTHREAD_START_ROUTINE)pdwRemoteCode, pRemoteData, 0, &dwRemoteThreadId);

// Wait for the thread to terminate
WaitForSingleObject(hRemoteThread, INFINITE);

// Read the user-structure back again
if(!ReadProcessMemory(hProcess, pRemoteData, lpData, cbDataSize, &dwRead))
{
    //an error occurred
}

Window procedure in "General" tab and in "Class" tab differs (in "Class" tab it correctly display a value). From `DisplayClassInfo.c`:

//window procedure
if(spy_WndProc == 0)    
{
    wsprintf(ach, _T("N/A"));
}
else                    
{
    wsprintf(ach, szHexFmt, spy_WndProc);
    if(spy_WndProc != spy_WndClassEx.lpfnWndProc)
        lstrcat(ach, _T(" (Subclassed)"));
}

//class window procedure
if(spy_WndClassEx.lpfnWndProc == 0)
    wsprintf(ach, _T("N/A"));
else
    wsprintf(ach, szHexFmt, spy_WndClassEx.lpfnWndProc);

As you see they're different values (obtained in different ways). Code to fill `spy_WndProc` is in `WinSpy.c` and `GetRemoteWindowInfo.c`. Extracted code from `GetRemoteInfo()` in `WinSpy.c`:

GetClassInfoEx(0, spy_szClassName, &spy_WndClassEx);
GetRemoteWindowInfo(hwnd, &spy_WndClassEx, &spy_WndProc, spy_szPassword, 200);

Now in `GetRemoteWindowInfo()` we see a call to `GetClassInfoExProc` (injected in the other process):

pInjData->wndproc = (WNDPROC)pInjData->fnGetWindowLong(pInjData->hwnd, GWL_WNDPROC);
pInjData->fnGetClassInfoEx(pInjData->hInst,
    (LPTSTR)pInjData->szClassName, &pInjData->wcOutput);

As you can see (please follow using source code) `wcOutput` is what is displayed in "Class" tab and `wndproc` what is displayed in "General" tab. Simply `GetWindowLong()` fails but `GetClassInfoEx` does not (but they do not necessarily retrieve same value because (if I'm not wrong) what you have in `WNDCLASSEX` is what you registered with `RegisterClassEx` but what you get with `GetWindowLong()` is what you hooked with `SetWindowLong()`.

Problem

I am fiddling with wndprocs and WinSpy++ and i stumbled upon a strange thing with calc.exe. It appears to lack a WndProc. Here is my screenshot: a test program I made, the WinSpy++ window,, showing N/A, and the culprit. Maybe the tool is a bit outdated, but the empirical evidence proves no WndProc is there. I don't know if this is by design(this would be strange), or if I am missing something... Here is referenced code: ``` Function FindWindow(title As String) As IntPtr Return AutoIt.AutoItX.WinGetHandle(title) End Function Function GetWindowProc(handle As IntPtr) As IntPtr Return GetWindowLong(handle, WindowLongFlags.GWL_WNDPROC) End Function ```

Original source