Exceptions In Injected Code
Injection and API Hooking - When you don't know enough to know that you are getting it wrong...
Code Injection and API hooking techniques are gaining in popularity. Whether they are used in anti-malware products, malware itself, or in even more common places like the application compatibility layer provided by Windows, code injection and hooking has a vital place in modern software. Some may even be surprised to find that new commercial products are emerging that use these techniques on platforms such as iOS where Apple has locked down all but the most rudimentary techniques on non-jail broken devices.
What should you learn next?
My first exposure to using code injection and API hooking in a commercial product, many years ago, was for a multi-platform product that ran on MVS (OS/390), Windows, and at least 4 Unix variants. In the years that have followed I've gained substantial insight into what works well, and into what not to do.
This article will briefly explore one area that is often overlooked: Exception Unwinding. Frankly, this is an area that even many seasoned engineers simply do not understand. They don't know enough to know that they are getting it wrong! This is bad enough in regular every day coding, but it is magnified many times over when your code is injected into a 3rd party process and may even be running in the context of a hooked API, a hooked private function, or even just a subclassed window procedure.
I've seen engineering teams that have decided to write their injected code in straight-C. Another group even decided to do it all in x86 / x64 assembly language. In both cases they thought that this would give them the tightest code and best control over the environment in which they were injected. This is an example of not even knowing enough to know that you are getting it wrong! The best language in which to write injected code is C++.
There are many reasons I could make that statement, but none are as important as how you write code that deals with exception unwinding. Chances are, if your code runs in the context of a 3rd party process (especially on Windows), that process is written in C++, or otherwise has exception handlers which are designed to unwind from an exception. As the author of injected code you must especially be aware of what those exception handlers -- including the ones you aren't aware of, may do to your code.
To make this obvious, consider the following code:
static CRITICAL_SECTION g_csLock;
void Initialize() // Assume this is called before we subclass a window
{
InitializeCriticalSection(&g_csLock);
void Lock()
{
EnterCriticalSection(&g_csLock);
void Unlock()
{
LeaveCriticalSection(&g_csLock);
LRESULT MySubclassWndProc(HWND hWnd, UINT uiMsg, WPARAM wp, LPARAM lp)
{
LRESULT lr;
switch(uiMsg)
{
case WM_USER + 101: // Our special message...
Lock(); // Another thread we have running will use this lock too
lr = DoSomething(hWnd, uiMsg, wp, lp); // This will call other APIs
Unlock(); // The lock is protecting a shared resource
return lr;
case WM_USER + 102: // Something else...
// ...
break;
default:
break;
}
return DefSubclassProc(hWnd, uiMsg, wp, lp);
I'm sure you've seen code just like this many times. Our focus will be on the CRITICAL_SECTION. It looks okay, right? Let's say that for simplicity, DoSomething() is going to put some data on a shared queue -- maybe even a good old C linked list. Other worker threads will then lock the CRITICAL_SECTION and remove data from the queue to do some special processing outside of the windows loop. In the code above g_csLock is locked while you run DoSomething(), allowing you to safely add an entry to your queue. If you are really good, you've learned to limit the time that the lock is held. That will help, but it isn't enough.
You see, you've just set your code up for a deadlock. Worse than that, you are going to struggle to find out why it deadlocked. The lock looks tight, right? Especially if DoSomething() is limited to just adding data to your queue. You are only holding the lock while it really needs to be held, what could go wrong?
What you may not know is that while you were inside of DoSomething(), you hit an odd error condition. You went down a code path the isn't often tested because it is the abnormal case -- the one your unit test doesn't bother to test. Even worse, maybe you are using a 3rd party library to queue your data, and it threw an exception because of a temporary but abnormal condition. Or maybe it is the result of another bug -- maybe you have code elsewhere that overruns a buffer which messes up your linked list so that when you try to add data to it inside of DoSomething() you get an access violation. Under normal circumstances this would be okay. You'd crash, get a crash dump, and then if lucky, know where to place break-on-write breakpoints to try to catch the offending code. But in this case -- in any of the cases I've suggested, you may be out of luck. In fact, your unit test might be really good and it still might not show the problem depending on the context in which it was tested. Your buffer overwrite may never be identified before your code ships because you will not get the crash you'd expect. So, how could your test look okay, but still end up ignoring an access violation or another type of exception? And how could that lead to your code causing a deadlock? It happens because the exception was masked by a higher function -- one that invoked your function. This was most likely done using __try/__except, or a try/catch(...).
Most experienced engineers have learned by hard experience the dangers of masking exceptions (e.g. catch(...)). When you produce injected code however, you can't assume your target was written by an experienced engineer. Or that there wasn't a justified reason for masking exceptions when they wrote the code. What is worse, many Windows applications in particular mask exceptions without the engineer even knowing it. For example, if you produce a sample MFC application using the Visual Studio Wizard, you can set a breakpoint inside of one of your methods that is called by the Window Procedure, such as your OnPaint() method. From there walk the stack back to the frame which inside of AfxCallWndProc. The code for this is inside of wincore.cpp. If you open up wincore.cpp you'll discover that several frames above your code -- your callback method, is a TRY/CATCH_ALL (which resolves to something very close to try/catch(...). This wraps the call to another WndProc method, which as you know eventual calls your OnPaint() method.
That TRY/CATCH_ALL, is an example of exception masking. When the CATCH_ALL runs, everything will unwind to the point of the catch handler code. It is very convenient -- because you will not crash, right? No, it is very evil, because you will not crash! You will not see the access violation, etc. But what is worse than this being done to you, is that you've provided the system no way of knowing how to unwind your CRITICAL_SECTION. Since the exception occurs after you call your Lock() function, the CRITICAL_SECTION is now "entered" by this thread. In its benovolence the exception handler will unwind everything it knows about, but it will skip right over your lock. It will probably also leak any memory you allocated for queue entries, etc. Anything that is not designed to unwind will be leaked or lost. In the code above, this will manifest first in a deadlock.
It will not be until you run !locks in the debugger (windbg.exe, ntsd.exe, or cdb.exe) that you will discover that the RecursionCount on your CRITICAL_SECTION is still 1 after you've returned from LeaveCriticalSection. If you try stepping through it in the debugger you will see that you can successfully complete the call to EnterCriticalSection in your MySubclassWndProc. On the way out you will still call LeaveCriticalSection, just as expected. Unfortunately though, when one of your worker threads tries to acquire the lock, to pull some data off the queue, it willl be blocked. Eventually all of your worker threads will be blocked on this CRITICAL_SECTION, and not just waiting to be signaled to process data. Your MySubclassWndProc thread however will not be blocked, because even though it already has the CRITICAL_SECTION locked, it is still allowed to re-enter. The recursion count on the lock will increase, but will never go back to 0. So you'll still get more and more data on the queue -- assuming that the condition that led to the exception was either temporary, or specific to a certain condition that doesn't prevent other data from entering the queue. Your mechanism for signalling the worker threads will still get called in those cases. And each worker thread that awakes and attempts to enter the CRITICAL_SECTION will end up blocked. It will look something like this:
0:049> !locks
CritSec example!___PchSym_+4 at 001433c0
WaiterWoken No
LockCount 48
RecursionCount 1
OwningThread 366c
EntryCount 0
ContentionCount 30
*** Locked
Scanned 51 critical sections
Beyond leaking the lock, you've probably leaked a half a dozen other things as well. If the problem that led to the exception was not temporary, you will continue to leak the lock and the recursion count will increase, but you will also most likely be leaking more and more memory used for your queue entries. Unfortunately I can't tell you of any good way of solving that problem if your injected code is written in straight-C or even in assembly language. I can tell you it will be a lot of work to get it right. And it will probably be very ugly. You may have to resort to wrapping everything in __try/__finally blocks. That will probably work, but it is error prone (especially when compared to the C++ alternative) and it looks really ugly! Not to mention the fact that x86 SEH exception handling -- which is what you will be generating with a __try/__except block is subject to other forms of attack.
You will also find situations in which the OwningThread for the lock is gone. This is especially likely if it was one of your worker threads that caught and masked the exception without proper unwinding. Often times you'll see code where the exception handler just exits the thread, because it seems the only safe thing to do. In that case, your OwningThread (as shown by !locks) will no longer be present.
So, what is so great about C++? RAII. RAII stands for Resource Acquisition Is Initialization. What it means however is that you can wrap your resources, such as your locks in objects that release those resources properly on unwind! Every resource, especially in injected code, should be managed by RAII. It will make your life much easier! Unlike a __try/finally, it also binds the clean-up (unwinding) of the resource to the resource itself. This will be less error prone than the __try/__finally approach.
Below is an example of how we might write our lock code using RAII. We will create a class with the purpose of managing a CRITICAL_SECTION. It will be very light weight. In fact, if you look at the disassembly of the generated code you probably will not be able to detect the presence of the object in a release build. Proposals now being considered for the C++ Standard Library will make this very simple for any type of resource, but you can already use std::shared_ptr and std::unique_ptr to ensure that your pointer types, including Windows HANDLEs, gets cleaned up, both when leaving scope and on unwind from an exception. A lock however is important enough to justify it's own class, because it will probably get reused a lot. (In fact, if you are using Boost, or happen to be reading this late enough for C++14, you will have class that manage locks already, such as boost::lock_guard.)
The solution presented here will produce two classes. The first manages the CRITICAL_SECTION as a resource. Though it has members for Lock() and Unlock() they can only be accessed by the second class: LockCriticalSection. LockCriticalSection will then engage the lock when it is instanciated, and it will release the lock on scope-exit, which includes unwinding from exceptions!
Most RAII objects are less involved than that of a CRITICAL_SECTION. By calling DeleteCriticalSection when it is no longer in use, we ensure correct output for !locks in the debugger. While initialization of a CRITICAL_SECTION can't fail, it is still kept in a process-wide linked-list. This is why the first class is needed -- for the clean-up of the CRITICAL_SECTION. The second class is solely focused on the careful management of holding the lock. It limits the time the lock is held to the scope in which the LockCriticalSection object will exist. Therefore, this solution produces two (very simple) RAII objects:
class CriticalSection
{
private:
CRITICAL_SECTION m_cs;
void Lock()
{
EnterCriticalSection(&m_cs);
void Unlock()
{
LeaveCriticalSection(&m_cs);
}
public:
CriticalSection()
{
InitializeCriticalSection(&m_cs);
~CriticalSection()
{
DeleteCriticalSection(&m_cs);
}
class LockCriticalSection
{
private:
LockCriticalSection();
LockCriticalSection(const LockCriticalSection &);
void operator=(const LockCriticalSection &);
public:
LockCriticalSection(CriticalSection &cs) : m_cs(cs)
{
m_cs.Lock();
~LockCriticalSection()
{
m_cs.Unlock();
}
Now, with these classes we can fix our MySubclassWndProc function as follows:
static CriticalSection g_csLock;
// The CRITICAL_SECTION is usable until scope-exit, which here will be until
// the CRT (runtime) shutdown code destroys our g_csLock object on DLL unload
LRESULT MySubclassWndProc(HWND hWnd, UINT uiMsg, WPARAM wp, LPARAM lp)
{
switch(uiMsg)
{
case WM_USER + 101: // Our special message...
{
LockCriticalSection lock(g_csLock); // Hold the lock for duration of scope
return DoSomething(hWnd, uiMsg, wp, lp); // This will call other APIs
}
case WM_USER + 102: // Something else...
// ...
break;
default:
break;
}
return DefSubclassProc(hWnd, uiMsg, wp, lp);
Doing this, you have ensured that when an exception occurs and is swallowed (by that exception handler you didn't even know was there), your lock will not be leaked. You have also neatly limited the time that the lock is held, by limiting it's scope. RAII is a powerful feature of C++. Failure to use it can lead to the subtlities I've just discribed. For those interested in an early preview of the types of resource-managing RAII objects being proposed for the next C++ standard library, please see http://www.andrewlsandoval.com/scope_exit/. Links to a reference implementation are provided in the proposal, these may be adapted and use as needed.
(NOTE: CRITICAL_SECTIONs are preferred to mutexes on Windows for two reasons: First, they can be examined in the debugger using !locks. This is substantially easier than tracking down usage of a mutex. And second, they are a no-fail resource. They do not fail on initialization, and they do not fail on entry with EnterCriticalSection. With a mutex on the other hand, there are a number of cases where you can get an error code back from WaitForSingleObject. One of those, which is often overlooked is WAIT_ABANDONED -- in which case you did successfully acquire the mutex, but you may need to clean-up after the abandoning thread! Far too often those using mutexes will simply check for a return code of WAIT_OBJECT_0 and follow the failure case if it is not set... This can lead to an additional lock leak.)
Summary
There are plenty of good articles out there that can explain how exactly exception handling works on Windows. I highly recommend that anyone working on injected code fully study the topic. Just be aware that the way it works in 64-bit Windows is entirely different than the way it works in 32-bit Windows. This difference becomes very important when you are producing injected code, whether it be shell code, or hooking functions.
If you use a good compiler (such as Visual Studio), it will generate the proper unwind data for your code. If you build and inject a DLL, you will get exception handling and unwinding that works automatically (assuming you use RAII objects). But if you are producing shell code, or trampolines -- or if you are hand loading DLLs or their equivalent, you must ensure that you not only provide but also register the unwind data for any code that will ever run as a non-leaf node on 64-bit Windows. This is not difficult, but rest assured, if you fail to provide unwind data in a trampoline, or in shell code, many things can cause it to blow up (or worse). In fact, if your code runs in Kernel Mode, a simple call to GetThreadContext by a user-mode debugger, against certain processes may lead to a BSOD, if unwind data is missing for non-leaf node! And the BSOD, while very difficult to trace to missing unwind data is still probably going to be easier to deal with than figuring out how you leaked CRITICAL_SECTION, mutex, or ERESOURCE, etc.
When you inject code, and when you hook 3rd party applications -- or even the kernel itself, it is crucial that you understand how exception handling works, where exception handlers that might affect your code exist, and how to code defensively so that you don't do unintended damage. Such damage may lead to your code "getting caught", or it may lead to it being uninstalled as would be the case in a poorly written anti-malware application.
And finally, understanding how exception handling works on x64 Windows, how the PsInvertedFunctionTable is used, and how to produce and register unwind data (see RtlAddFunctionTable) will keep your trampolines and other injected code functioning properly. Protecting all resources with RAII wrappers to ensure proper unwinding will make your code resilient in the face of unexpected unwinding. Then you will know a little more of what you need to know to be successful in writing injected code!
Additional Information
!exchain in the debugger will help you to find existing exception handlers. Below is an example of how to find and examine the exception handlers which wrap the top-level WndProc in calc.exe: (Entered commands are in bold. You will need the RevEngX debugger extension, which can be found here: http://www.revengx.com/).
Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: C:WindowsSystem32calc.exe
Symbol search path is: srv*c:sym*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00000000`ff940000 00000000`ffa23000 calc.exe
ModLoad: 00000000`77c00000 00000000`77da9000 ntdll.dll
ModLoad: 00000000`77ae0000 00000000`77bff000 C:Windowssystem32kernel32.dll
ModLoad: 000007fe`fe130000 000007fe`fe19c000 C:Windowssystem32KERNELBASE.dll
ModLoad: 000007fe`feba0000 000007fe`ff928000 C:Windowssystem32SHELL32.dll
ModLoad: 000007fe`ffa50000 000007fe`ffaef000 C:Windowssystem32msvcrt.dll
ModLoad: 000007fe`ffb30000 000007fe`ffba1000 C:Windowssystem32SHLWAPI.dll
ModLoad: 000007fe`feb30000 000007fe`feb97000 C:Windowssystem32GDI32.dll
ModLoad: 00000000`77680000 00000000`7777a000 C:Windowssystem32USER32.dll
ModLoad: 000007fe`ffb10000 000007fe`ffb1e000 C:Windowssystem32LPK.dll
ModLoad: 000007fe`fe640000 000007fe`fe709000 C:Windowssystem32USP10.dll
ModLoad: 000007fe`fc4a0000 000007fe`fc6b6000 C:WindowsWinSxSamd64_microsoft.windows.gdiplus_6595b64144ccf1df_1.1.7601.17825_none_2b253c8271ec7765gdiplus.dll
ModLoad: 000007fe`fe430000 000007fe`fe633000 C:Windowssystem32ole32.dll
ModLoad: 000007fe`fe710000 000007fe`fe83d000 C:Windowssystem32RPCRT4.dll
ModLoad: 000007fe`fe8a0000 000007fe`fe97b000 C:Windowssystem32ADVAPI32.dll
ModLoad: 000007fe`ffaf0000 000007fe`ffb0f000 C:WindowsSYSTEM32sechost.dll
ModLoad: 000007fe`ffd90000 000007fe`ffe67000 C:Windowssystem32OLEAUT32.dll
ModLoad: 000007fe`fc6f0000 000007fe`fc746000 C:WindowsSystem32UxTheme.dll
ModLoad: 000007fe`fca70000 000007fe`fcc64000 C:WindowsWinSxSamd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.7601.17514_none_fa396087175ac9acCOMCTL32.dll
ModLoad: 000007fe`fbd60000 000007fe`fbd9b000 C:WindowsSystem32WINMM.dll
ModLoad: 000007fe`fd050000 000007fe`fd05c000 C:WindowsSystem32VERSION.dll
(bbc.1208): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00000000`77cacb60 cc int 3
0:000> g
ModLoad: 000007fe`fea90000 000007fe`feabe000 C:Windowssystem32IMM32.DLL
ModLoad: 000007fe`fe980000 000007fe`fea89000 C:Windowssystem32MSCTF.dll
ModLoad: 000007fe`fbf20000 000007fe`fc04a000 C:WindowsSystem32WindowsCodecs.dll
ModLoad: 000007fe`fc120000 000007fe`fc138000 C:WindowsSystem32dwmapi.dll
ModLoad: 000007fe`fdf90000 000007fe`fdf9f000 C:WindowsSystem32CRYPTBASE.dll
ModLoad: 000007fe`ffe70000 000007fe`fff09000 C:Windowssystem32CLBCatQ.DLL
ModLoad: 000007fe`fab70000 000007fe`fabc4000 C:Windowssystem32oleacc.dll
(bbc.ff0): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
00000000`77c50530 cc int 3
0:004> .load RevEngX
RevEngX Reverse Engineering Extensions version: 1.0.0.12
Copyright © 2012, Andrew L. Sandoval. All rights reserved.
Redistribution and use of this software (RevEngX.dll and any accompany documentation)
in binary form, without modification, is permitted provided that the following conditions
are met:
1.Redistributions must retain or reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
2.The name of the copyright holder may NOT be used to endorse or promote products
that incorporate or bundle this software without specific prior written permission.
3.As this software may be used for analysis and reverse engineering of third party
software, binary files, and object code, the user of this software assumes full
responsibility for compliance with all applicable license agreements and copyright
laws pertaining to such software and agrees to indemnify the copyright holder of this
software, for any and all damages and legal fees arising from failure to abide by
applicable copyright laws and license agreements while utilizing (or for the misuse of)
this software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCURMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0:004> !windows
THREAD-ID HWND VISIBLE PARENTHWND 'CLASS' 'REALCLASS' WINDOWTEXT
1208 0000000000201372 FALSE 0000000000000000 'Edit''Edit'
1208 000000000DCF0822 TRUE 0000000000000000 'CalcFrame''CalcFrame' Calculator
1208 00000000001A139C TRUE 000000000DCF0822 'CalcFrame''CalcFrame'
1208 00000000060A11B2 TRUE 00000000001A139C 'Static''Static'
1208 00000000002112DA TRUE 00000000001A139C '#32770''#32770'
1208 00000000001C138C TRUE 00000000002112DA 'Static''Static'
1208 00000000002E131E TRUE 00000000002112DA 'Static''Static'
1208 00000000001A135C TRUE 00000000002112DA 'Static''Static' 0
1208 0000000000DB0766 TRUE 00000000002112DA 'Static''Static'
1208 000000000013031C TRUE 00000000001A139C '#32770''#32770'
1208 00000000006B078A TRUE 000000000013031C 'Static''Static'
1208 000000001CF313EC TRUE 000000000013031C 'Button''Button' Hex
1208 00000000002B1326 TRUE 000000000013031C 'Button''Button' Dec
1208 00000000000B07C8 TRUE 000000000013031C 'Button''Button' Oct
1208 0000000000171286 TRUE 000000000013031C 'Button''Button' Bin
1208 00000000005011CE TRUE 000000000013031C 'Static''Static'
1208 00000000000907CE TRUE 000000000013031C 'Button''Button' Qword
1208 0000000000220776 TRUE 000000000013031C 'Button''Button' Dword
1208 00000000002F075E TRUE 000000000013031C 'Button''Button' Word
1208 000000000AFE07C0 TRUE 000000000013031C 'Button''Button' Byte
1208 00000000001E13F6 TRUE 000000000013031C 'Static''Static'
1208 0000000000430D44 TRUE 000000000013031C 'Button''Button'
1208 000000000060135E TRUE 000000000013031C 'Button''Button'
1208 0000000000121296 TRUE 000000000013031C 'Button''Button'
1208 0000000000F90756 TRUE 000000000013031C 'Button''Button'
.
.
.
1208 0000000010040736 TRUE 000000000013031C 'Button''Button'
1208 000000000080071E TRUE 000000000013031C 'Static''Static'
1208 0000000001DA0716 TRUE 000000000013031C 'Static''Static' 0
.
.
.
1208 00000000004E0962 TRUE 000000000013031C 'Static''Static' 0
1208 000000000014127E FALSE 00000000001A139C '#32770''#32770'
1208 0000000009BD1236 FALSE 000000000DCF0822 'IME''IME' Default IME
1208 000000000035074E FALSE 0000000009BD1236 'MSCTFIME UI''MSCTFIME UI' MSCTFIME UI
1c74 00000000001D13D6 FALSE 0000000000000000 'GDI+ Hook Window Class''GDI+ Hook Window Class' GDI+ Window
1c74 00000000002512E8 FALSE 00000000001D13D6 'IME''IME' Default IME
0:005>!hwnd 000000000DCF0822
HWND 000000000DCF0822
Class: CalcFrame
Real Class: CalcFrame
Parent: 0000000000000000
Owner: 0000000000000000
Visible: Yes
Enabled: Yes
IsUnicode: Yes
Thread Id: 0000000000001208
Process Id: 0000000000000BBC
Class WndProc(A): 00000000`ffff0381
Class WndProc(W): calc!WndProc (00000000`ff941c58)
Window Long Values:
GWLP_USERDATA: 00000000`00000000 [32-bit]: 00000000
GWL_EXSTYLE: 00000000`00000100 [32-bit]: 00000100
GWL_STYLE: 00000000`14ca0000 [32-bit]: 14ca0000
GWLP_ID: 00000000`0ec10e53 [32-bit]: 0ec10e53
GWLP_HINSTANCE: calc!CCalculatorController::CCalculatorController (calc+0x0) (00000000`ff940000) [32-bit]: 00000000
GWLP_WNDPROC: Error 0n5 use !callfn GetWindowLongPtrW(000000000DCF0822, GWLP_WNDPROC) to obtain from target process.
Props:
Prop (00000000`0000a918) <--ATOM: #43288=00000000`01e83b30 Text: Calculator 0:005> !callfn -quiet -nosuspend tmp_gwl_value = GetWindowLongPtrW(000000000DCF0822, GWLP_WNDPROC);!boldon;!disp HWND 000000000DCF0822 GWLP_WNDPROC: @tmp_gwl_value;!boldoff
(bbc.1524): Break instruction exception - code 80000003 (first chance)
HWND 000000000DCF0822 GWLP_WNDPROC: calc!WndProc (00000000`ff941c58)
0:004>bp calc!WndProc
0:004>g
Breakpoint 0 hit
calc!WndProc:
00000000`ff941c58 fff3 push rbx
0:000>!exchain /f
11 stack frames, scanning for handlers...
Frame 0x00: calc!WndProc (00000000`ff941c58)
ehandler calc!_GSHandlerCheck (00000000`ff99d2dc)
Frame 0x02: USER32!DispatchClientMessage+0xc3 (00000000`776972cb)
ehandler USER32!_C_specific_handler (00000000`7769d590)
Frame 0x04: ntdll!KiUserCallbackDispatcherContinue (00000000`77c51225)
ehandler ntdll!KiUserCallbackDispatcherHandler (00000000`77c51173)
Frame 0x06: USER32!DispatchMessageWorker+0xdb (00000000`776a84c6)
ehandler USER32!_C_specific_handler (00000000`7769d590)
Frame 0x07: calc!WinMain+0x1db4 (00000000`ff941a76)
ehandler calc!_GSHandlerCheck (00000000`ff99d2dc)
Frame 0x08: calc!LDunscale+0x1ea (00000000`ff95a00f)
ehandler calc!_C_specific_handler (00000000`ff99d25c)
Frame 0x0a: ntdll!RtlUserThreadStart+0x1d (00000000`77c2c521)
ehandler ntdll!_C_specific_handler (00000000`77c1850c)
What should you learn next?
Note that this stack of exception handlers is wrapping the call to the main window's wndproc function. If anyone one of them masks an exception and allows the thread to continue, resources that are not unwound will be leaked. Compare this to the stack of an MFC application when it is in a callback method for a WndProc.