Search code examples
cvb.netwindowswinapispy++

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


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.

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

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().