I’ve spent some time recently figuring out how function hooking works. There are tons of great resources available about it, but I’ve noticed that a lot of them are really light on providing example code, and the ones that do provide code tend to link to fully mature hooking frameworks. Usually the linked projects are really impressive, but they aren’t the easiest places to learn the basics from.
Now that I know enough to be dangerous, it seemed like fun to rectify this lack of sample code by building some hooking code from the ground up and walking through how to use that code to hook a running program. My past two blog posts were about making Notepad do weird stuff, so for the sake of variety, this post is going to pick on MSPaint instead.
I’m going to explain how to build 4 example programs. Two of them will show off fundamental hooking concepts by hooking functions in the example code itself. The other two will use those same concepts to hook MSPaint and make it disable the “Edit With Paint3D” button in a running MSPaint instance and force it to always draw with my favourite color (orange).
If you’re only interested in sample code, I’ve published a github repo called Hooking-by-Example which has 14 increasingly complex example programs that demonstrate how function hooking works (or at least, the bits of it that I’ve figured out). Everything that I talk about here (and more) is also demonstrated by the programs in that repo.
WTF is Function Hooking?
Function Hooking is a programming technique that lets you to intercept and redirect function calls in a running application, allowing you to change that program’s runtime behaviour in ways that may not have been intended when the program was initially compiled. It’s a little bit like when a dog gets into a car thinking they’re going to the park and ends up at the vet instead. The dog called goToPark(), but instead unexpectedly ended up inside goToVet() instead. This example isn’t great.
The real fun of function hooking is that you can use it to change the behaviour of programs that you don’t have the source code to, or otherwise can’t recompile. Combined with process injection (which I explained a bit in my last post), you can use function hooks to add entirely new behaviour to any program that you can run on your pc. For example, ReShade uses function hooking to add new postprocessing effects to games, and RenderDoc uses a form of hooking (although not the kind covered here) to allow you to debug graphics code in running applications.
More examples of things you might want to do with function hooking include:
Logging or replacing function arguments
Measuing the execution time of a function
Monitoring or replacing data before it gets sent over a network
The only limits are your imagination and ability to read assembly!
How Does It Work?
Let’s say we have a function that adds two Gdiplus::ARGB values together, and we want to use a hook to bypass the addition logic and always return red. The ARGB type is a DWORD that uses a byte for Alpha, Red, Green, and Blue, respectively. Adding two of them together might look like this:
The function that we want to replace it with (which I’ll call that “payload” function), looks like this:
If this was in your own code, you’d add a “return ReturnRed(left, right)” call to the beginning of AddColors(), recompile and call it a day, but what if you couldn’t recompile it? For example, what if it’s part of a closed source third party library, or the program that calls AddColors() is already running?
Rather than recompiling, we can use hooking to modify its instruction bytes instead, and replace the first instruction in AddColors() with a jmp to the beginning of the ReturnRed() function. This works even if the function we want to hook comes from a system dll, since DLL code segments are copy-on-write, so there’s no chance of a hook interfering with other processes.
Imagine that the first instruction in ReturnRed() is located 1024 bytes after AddColors() in memory. In assembly, replacing AddColors’ instructions with a jump will look like this:
The jump instruction used here is a relative jump with a 32 bit operand. The opcode is E9, and that’s followed by a 4 byte value that represents how many bytes to jump.
Notice that after the jmp instruction, we’re left with garbage. This is because the process of overwriting the first 5 bytes of AddColors() left a partial instruction in its wake. The first byte of the second instruction was overwritten, but the rest of the bytes are still there, and who knows what instructions those map to. That leaves the rest of the function in an unknown (and likely invalid) state. This doesn’t matter for the example, because the program is going to jump to ReturnRed() before it ever gets to the garbage we just created, but it’s important to keep in mind.
We’ll write some hooks that preserve the hooked function’s original logic later in this post, so don’t worry about that too much right now. For our first example, we’ll build a program that destructively hooks a function, exactly like what’s shown in the diagram above (with some extra sauce to handle 64 bit code).
Example 1: Our First Function Hook
Let’s roll with the example code already provided and write a program that actually redirects program flow from AddColors() to ReturnRed(). The game plan here is to end up with a main() function that looks like this:
In a 32 bit program, the logic for InstallHook() can be implemented pretty much exactly how the diagram above suggests it would be:
Things are a bit trickier in 64 bit, because functions can be located so far away from each other in memory that a 32 bit jmp instruction can’t jump that far, meaning that the 5 byte jump written by InstallHook() might be unable to reach the payload function from the hooked function.
There’s no such thing as a 64 bit relative jmp instruction, so the next best option is to jmp to an address stored in a register, like the assembly shown below. Note that this snippet uses the r10 register because it’s one of the few volatile registers that isn’t used for passing function arguments in the Windows x64 calling convention (msdn link)
If we throw this in the beginning of hooked functions instead of the 5 byte jump from before, we’d limit the number of functions that we could hook to those with 13 or more bytes. That’s a singificantly bigger limitation than our 32 bit code, so we’re instead going to write the bytes for this absolute jump somewhere in memory that’s close to the function we’re hooking. Then we’ll have the 5 byte jump we install in that function jump to this absolute jump, instead of straight to the payload function. Minhook refers to this absolute jump as the “relay function,” and I’m going to use that terminology as well.
Writing code to do this little dance is similar to the InstallHook() function shown above, but with a few more steps. The trickiest part of the process is allocating memory for the relay function that’s close enough to the target function to be reachable by a 5 byte jump. I’ve implemented logic for this in a function called AllocatePageNearAddress(). This function is a bit long, so I’ve included it’s implementation in the (expandable) box below, and omitted it from the sample code snippet immediately after that.
AllocPageNearAddress() implementation (click to expand)
With a bit of copy and paste magic, all the code snippets until now can be combined into our first example program. The end result is a small program that ends up calling ReturnRed() whenever we try to call AddColors(). The full code for this example is included in the expandable box below. Note that since this example creates x64 specific instructions for the relay function, it won’t work if it’s built as a 32 bit application. This will be the same for every example we build in this post.
Full Code For Example 1 (click to expand)
This is all we need to know to start installing hooks in programs we have source access to, but there’s an annoying gap between that and being able to hook a running instance of a program. We’ll bridge that gap with the next example.
Example 2: Hooking Functions in a Running Program
The second example program we’re going to build will disable the “Edit With Paint3D” button in a running instance of mspaint.exe.
There are 2 new hurdles we have to overcome in order to install a hook in a running program: getting the target program to execute our hooking logic, and figuring out the address of the function we want to hook. We’ll tackle these in order.
Our mission is to keep the Paint3D button from accomplishing its mission.
Getting Code Into a Running Process
The simplest way to get an arbitrary process to execute hooking logic is to build that logic into a DLL and use DLL injection to get that code into the target process’ memory.
The nuts and bolts of how DLL injection work are beyond the scope of this blog post, but if you want to learn more, check out this article. I’ve included the code for a basic DLL injection program in the collapsable box below. This is the code that the example program will use to inject its dll into mspaint.exe.
Full DLL Injection Code (click to expand)
The code for the dll we’re going to inject is basically identical to the last example except that main() will be replaced by DllMain(), and we need to do some extra work to get a pointer to the function we want to hook. With those concerns in mind, the skeleton of Example 2’s dll looks like this:
What Function Do We Need to Hook?
Since our goal is to disable the “Edit With Paint3D” button, we need to find the mspaint.exe function that handles that button press. We know that the “Edit With Paint3D” button eventually launches a Paint3D process, so we can be reasonably sure that a function like CreateProcessA() or OpenProcess() gets called at some point during the button handling function. Blindly hooking either of these functions and redirecting them to an empty function doesn’t work (I tried), but throwing some breakpoints on them is as good a place to start as any.
If we look at the functions imported by mspaint in a debugger (like x64dbg), we can see that it is in fact importing OpenProcess(), so our first step is to throw a breakpoint there and then see what happens when we press the paint3d button.
It turns out that our breakpoint does get hit in response to the button click, which is fantastic. If we switch over to the callstack view while we’re stopped at the breakpoint, we can see a couple of mspaint.exe functions much higher up in the stack. It’s possible that the one of these that’s highest in the callstack is the button handler function we’re after.
Going to the address shown for that function brings us into middle of a function body. What we’re after is the relative virtual address (RVA) of the beginning of this function. x64dbg makes this really easy. All we need to do is scroll up until we find the first instruction for the function, then right click on the address of that instruction and select “Copy->RVA.” In my version of mspaint.exe, the RVA of this function is 0x4AA40.
I’m going to save us some trial and error here and reveal that 0x4AA40 ends up not being the address we need. The real button handler runs on a different thread. Hooking 0x4AA40 and redirecting it to an empty function disables the Paint3D button, but only if the current document is empty.
I wish I had a better procedure to share, but my next step after realizing the above was to retry the same procedure except draw something in paint before I clicked the Paint3D button. The callstack I got then had a number of calls inside uiribbon.dll, and the highest mspaint.exe function in that stack ended up being the button handler. Its RVA was 0x6C6F0.
Turning an RVA Into a Runtime Memory Address
RVAs are addresses which are relative to the base address of the module they’re located in. Since programs (and individual modules, thanks to ASLR) can be loaded into memory at different locations across multiple runs of the same program, having the RVA of a function means that we can reliably get that function’s address, no matter where the process is loaded in memory.
In this case, our target function is implemented inside the base module of the process (since it isn’t imported from a dll), so we need to find the base address of the mspaint.exe module. We can do this with the function below.
HMODULES are actually pointers to the location of a module in memory, so the cast to a uint64_t in the above example is mostly for convenience. In order to get the address of our target function, we’ll need to add the function’s RVA to this base module address.
If we were hooking a function that was imported from a dll, we’d need to modify the GetBaseMdouleForProcess() function to let us specify the name of the module that we were after, rather than being hardcoded to find the base. We’ll do this in the fourth example in this post, but you can also see an example of this in the code for my hooking-by-example repo here.
Putting It All Together
Now that we have a function to hook, we need to do is to redirect it to an empty payload function to disable it. This is straightforward as it sounds:
We got a bit lucky here because the button handling function doesn’t have a significant return value (or at least, returning 0 from it is valid). The smart way to approach this would probably be to spend some time in the debugger really understanding what this button handling function does, so that we could write a payload that we knew wasn’t going to break anything, but sometimes it’s better to be lucky than smart.
All we need to do to finish things off is add the implementation for GetFunc2HookAddr() and the payload function into our example dll. The end result is a dll that disables the “Edit with Paint3D” button when injected into mspaint, exactly as we planned. The full source for this example is in the collapsable bow below.
Full Code for Example 2 (click to expand)
Function Hooking for Big Kids
The previous examples technically hooked a couple functions, but did so at the cost of destroying their original functionality. This meant that we couldn’t do things like modify function arguments being passed to the original functions, or add logging while preserving the original logic of the hooked programs. Real function hooking doesn’t have to make this trade, and our next two examples won’t either.
So far, the hooks we’ve created have had 3 parts: the hooked function, the relay function, and the hook payload. Now we need to add another step in this process, called a trampoline. With this new step, our hook process looks like this:
Rather than simply replace the initial instructions in the hooked function, we’re going to use those instructions to build a trampoline that we can call from a payload function when we want to execute the original version of the hooked function. A hook payload that uses a trampoline might look like this:
At a super high level, trampolines need to do two things:
Execute the instructions that were overwritten when the hook jmp was installed in the hooked function.
Jump back to the body of the hooked function AFTER the installed jump instruction, so that the rest of the function can continue like normal.
The first item on this list is really easy to get working for contrived cases, but really difficult to get right for real world use. Consider the following assembly (shown with the addresses of the instructions on the left):
This is an example of the “easy” case for creating a trampoline. The first 5 bytes of the function belong to one instruction, and that instruction doesn’t rely on any rip-relative addressing. All we need to do to make a trampoline for this function is copy the first 5 bytes to a buffer before we overwrite them with our hook, and then add a jump to 00007FF6E3521FF5 immediately after it. In assembly, this might look like this:
Functions that are harder to hook with a trampoline might have multiple instructions contained in their first 5 bytes, or use instructions with relative operands, like jumps or rip-relative addresses. The snippet below shows an example of a function that has some of these issues.
In order to build a trampoline for this function, we’re going to have to get our hands dirty. First of all, we’re going to need to steal the first 7 bytes of this function instead of the first 5, so that we can execute whole instructions in our trampoline. Second, we’re going to need to do something about the je at 00007FF72B1F1392h, since it won’t make sense to do a relative jump once we relocate the instruction.
The next section of this post is going to walk through how to write code that deals with these “hard” issues, but as a bit of a teaser, here’s what the trampoline for this will look like:
This trampoline can be thought of as being made up of three sections (like a “jump sandwich”, which I thought was very funny when I wrote this at 5 am). It starts with the stolen bytes from the hooked instruction, with the relative instructions rewritten to jump to a later part of the trampoline. The meat of the sandwich is an absolute jump that goes back to the body of the hooked function (to an address after the jmp we installed for the hook). Finally, the bottom of the trampoline are absolute jumps (or calls, if we had any) that go to the addresses that the relative jumps/calls in the stolen bytes actually want to go.
Other sources refer to the absolute instruction table as a jump table, but I’m giving it a fancy name because it’s not going to contain jump instructions exclusively.
Example 3: Building a Trampoline For Code We Can Recompile
We just saw the rough skeleton of the trampoline we’re going to build, now it’s time to write the code to build it. Roughly speaking, our plan of attack looks like this:
“Steal” the first 5+ bytes (rounded up to the nearest whole instruction) of the function we want to hook.
Fixup any rip-relative addressing (like lea rcx,[rip+0xbeef])
For each relative jump or call instruction, calculate the address that it originally intended to reference, and add an absolute jmp/call to that address in the Absolute Instruction Table.
Rewrite the relative instructions in the stolen bytes to jump to their corresponding entry in the Absolute Instruction Table.
Write a jump back to the 6th byte of the hooked function immediately after the stolen instruction bytes, to continue executing the hooked function once the trampoline ends.
These steps won’t be completed sequentially in our final program, but I’ve split them out into discrete steps to make explaining things easier.
For a bit of context, here’s what our final InstallHook() function is going to look like when we’re done. We’re going to be constructing a BuildTrampoline() function which will be given a pointer to some memory to write a trampoline into, and not much else. BuildTrampoline() is going to be called from a modified version of the InstallHook() function we had in our earlier example. Notice that BuildTrampoline() will also return the size, in bytes, of the trampoline that it creates.
The intended use case for the trampoline pointer is to allow payload functions to call trampolines like regular functions, as shown in the snippet below.
Notice that we’re going to build the trampoline in the same “near” memory that the relay function is currently being constructed in. That’s going to make dealing with the rip-relative addressing a lot easier when we get it to it.
Step 1: Stealing Instruction Bytes
In order for our trampoline to work at all, it needs to execute the instructions that are overwritten when we install our hook. To do this, we need to “steal” these instruction bytes from our target function before overwriting them. The verb “steal” is important here - we’re not only going to copy these instruction bytes, we’re also going to replace them with 1 byte NOPs in the target function. That way won’t wind up with any partial instructions when we install the hook jump.
To make sure we steal whole instructions, we need to use a disassembly library. The rest of this article is going to use the Capstone library for all disassembly tasks. Any disassembler will do, but Capstone has some features that are going to make our life easier later on.
This snippet shos how to steal the instructions contained within the first 5 bytes of a target function using Capstone. The StealBytes() function returns a struct with some additional data about the stolen instructions which we’ll use later.
We’ll call this function right at the start of BuildTrampoline(), so it’s about time we started writing that function too. I’ve found the most intuitive way to structure BuildTrampoline() is to create 3 pointers at the start, each pointing to the next available location in each of the three sections of our trampoline memory. Whenever we write to a location pointed to by one of these pointers, we’ll then increment the pointer by that many bytes, so each of them is always pointing to an available memory address.
If we only ever needed to hook “easy” functions (as defined earlier), we could skip down to the last step in our trampoline creation procedure now. There’s a lot more legroom required to support less-than-easy functions though.
Step 2: Fixing up RIP-Relative Addressing
One case where our naiive trampoline building function will fail is if any of the stolen instructions contain rip-relative addressing. In x64, there are a lot of instructions that do this, but the easiest example is a function that calls printf.
On my machine, the generated assembly uses an lea instruction to load the string location before the call to printf. The assembly string generated by visual studio makes it look like the lea call is grabbing an absolute address, but the instruction bytes reveal that we’re actually computing the address of the “Haha\n” string by adding an offset to the current value of the instruction pointer.
If we steal the lea instruction verbatim, we’ll get garbage data when we executed the stolen instruction because our instruction pointer will be at a different address. In order to actually use instructions that have rip-relative addressing in our trampoline, we need to fix up the offsets they use to be relative to our trampoline memory.
The first step of this is to detect when an instruction contains a rip-relative operand. Capstone makes this easy.
Relocating an instruction that’s been identified as having a rip-relative operand is a bit more of a bear. Remember how I mentioned that we’re going to put our trampoline in memory that’s within a 32 bit jump of our target function? That’s to try to avoid cases where the new offset we compute is too large to be stored in the existing instruction’s operand.
Plugging these functions into the BuildTrampoline() logic requires adding a check and a function call to the for loop that processes our stolen instructions.
Now we can hook our little printf function with wild abandon!
Step 3: Building the Absolute Instruction Table
Next we need to deal with any relative jump or call instructions in our stolen bytes. After all “jump 10 bytes from here” doesn’t mean very much when the instruction has been moved to a new “here.” I have no idea how to handle loop instructions, so the example code will only deal with jmp and call instructions.
Like with the rip-relative operands, the first thing we need to do is identify whether an instruction is one of the flavors of jmp or call that we care about. Identifying relative calls is pretty easy, because there aren’t that many varieties of call instructions, and all the relative versions have opcodes that start with 0xE8.
Identifying jmps is a little harder because there are lots of different types of jmp instructions. Since conditional jumps only come in relative versions, if an instruction’s id says it’s a conditional jump, we know it uses relative addressing. The unconditional “jmp” instruction can use relative addressing, but it can also do things like jump to an address in a register. Thankfully, the behaviour of a jmp is dictated by it’s opcode bytes. Relative jmps start with 0xEB and 0xE9.
We can use these two functions to quickly identify any stolen instructions that are going to require extra attention:
Next We need to figure out the address that the original instruction wanted to go to, and add an absolute jump (or call) to that address to our Absolute Instruction Table. The Capstone library handles calculating the target address of relative instructions for us automatically, which is handy.
Jumps are easier to handle than calls, so we’ll start there. We’ll reuse the WriteAbsoluteJump64 function from earlier in this post to make the code a bit more concise.
Note that this function doesn’t rewrite the existing jump instruction, it only adds an absolute version of it to the absolute instruction table (AIT). We’ll handle pointing the original jump to this AIT entry later in this post.
Dealing with calls is a bit different. If we just add an absolute call instruction to our AIT, when that call returns, we’ll wind up at the next jump in the table. That would be bad, so instead we also need to add a jump instruction after our absolute calls to redirect program flow to somewhere more helpful. In this case, we’ll jump to the middle of our trampoline, which is the jump back to the hooked function’s body.
You’ve probably noticed that both of these functions return the number of bytes that they wrote to the AIT. This is so we can increment the absTableMem pointer in BuildTrampoline(). These calls should be added inside the IsRelativeJump()/IsRelativeCall() conditionals in the BuildTrampoline() function.
Step 4: Rewriting Jumps/Calls to Use the AIT.
Adding instructions to the Absolute Instruction Table is great and all, but in order for any of that work to matter, we also need to rewrite our stolen relative instructions to actually go to the AIT. Similar to the last step, this needs to be handled differently for jumps vs calls.
Calls are the easier of the two to rewrite, so we’ll start with them. Since all call instructions are unconditional, we can replace any relative calls with jumps to the appropriate address inside the AIT. We know that our trampoline won’t be larger than 255 bytes, so we can use a 2 byte jmp instruction for this. We don’t want to change the size of the call instruction we’re rewriting, so we’ll first replace all the bytes for that instruction with NOPs. That way, if we rewrite a 4 byte call with a 2 byte jmp, we haven’t added garbage instructions to the trampoline.
Jumps are more of a pain. There are a lot of different jump instructions that we might encounter, many of which are some flavor of a conditional jump. We can’t replace these instructions with a normal jmp because that could change the execution logic of our stolen bytes. Instead we need to rewrite the operands directly, so that these jumps will conditionally jump to the Absolute Instruction Table.
The snippet below shows how these new functions should be added to BuildTrampoline(). Notice that we need to wait until after calling these new rewrite functions before we can increment the absTableMem pointer.
Step 5: Write the Jump Back to the Hooked Function’s Body
This has been a long process, but we’re almost there. Now we need to fill in the middle of the jump sandwich, and return our trampoline’s size. After all the work we’ve done so far, this last step doesn’t need much explanation. All we need to do is replace the comment in the snippet above with the following:
When we stole the bytes from func2hook, we also replaced them with NOP instructions. This makes our life easier here, since the jump back to our hooked function doesn’t have to care about the number of bytes we stole. Jumping to the byte immediately after the hook’s jump is guaranteed to be safe.
Finally we return the byte count of our trampoline, so that InstallHook() can write the relay function into memory right after our trampoline bytes.
aaaaand we’re done! The collapsebox below shows the full source for a program that uses this trampoline to hook a function. We’ve already talked about all the fun parts, so I’m going to leave it here without comment and move on to the grand finale.
Full Example of Trampoline Hooking a Function In The Same Process As The Hook Code
Example 4: Using a Trampoline to Hook a Running Program
Like Po at the end of Kung Fu Panda, it’s time to put all our newfound skills to use and fulfill our destiny of becoming the dragon warrior.
The last example is going to use a trampoline to force mspaint to always use the color orange, no matter what color the user tries to select. This was shown in the gif at the start of article, but it’s been a long time since then, so here that gif is again:
Mercifully for us, we don’t need to go on an RVA fishing trip this time, because the function we want to hook is exported from a DLL. We’re going to install a hook into gdiplus.dll’s GdipSetSolidFillColor() function. Finding out that this was the right function to hook was pretty much the same process as the last mspaint example: lots of trial and error with breakpoints in x64dbg. A reverse engineer I am not.
So, here’s the plan:
Write a hook payload function that intercepts calls to GdipSetSolidFillColor and replaces the incoming function arguments with the color orange.
Put that payload in a DLL, along with all the hooking logic required to make it happen
Inject that DLL into a running instance of mspaint
Make beautiful artwork with the best color ever.
We’ve already exhaustively walked through a code example that used the same hooking logic that we need to use here. Rather than do that again, let’s focus on what’s different this time. Looking up GdipSetSolidFillColor() gives us this function signature:
Recall that the ARGB type is a uint32 with each byte representing a color channel. This means that all our payload need to do to make things orange is set some bits and pass the new ARGB value to the trampoline:
This isn’t going to be enough to make ALL the possible painting tools spit out orange all the time. The paint can tool, spray paint brushes, etc will still use the colors selected. Our dll will just make most brushes always paint orange, which is good enough for me. It’ll also totally mess with the output of some brushes and make them operate weirdly too, which is fun in its own way.
Here’s a gif demonstrating some of the tools not painting orange, despite our dll being injected into paint:
The hooking logic that we include in the DLL is going to similar to the trampoline code we wrote for Example 3. The main difference is how we get a pointer to the function we want to hook.
The FindModuleInProcess() function called above is similar to the GetBaseModuleForProcess() function that we used in a previous example, except that it can look for any loaded module by string name. The function is a bit long, so rather than paste it here, I’ve included it in the complete source for this example. The program used to inject this dll into paint is the same as the one we used before, but it’s also included below.
It took a while to get here, but we’re finally done Example 4! Go celebrate by making beautiful orange artwork!
Full Source For DLL Injector Program (click to expand)
Full Source For Example 4 (click to expand)
Where to Go Next
Despite being by far the longest post I’ve written to date, this rabbit hole goes a whole lot deeper than what I’ve written about here.
First of all, there are significant issues with the code written in this post:
There’s no way to uninstall hooks
Hooking 32 bit applications isn’t supported at all
Everything breaks if 2 hooked functions share a payload
Stuff also breaks if a thread is executing instructions while they’re being stolen
More stuff breaks if the stolen instructions for a function use the r10 register
There are at least 3 additional scary problems I don’t know about yet
I solve some of these problems (at least in a “good enough” sorta way) in my hooking-by-example repo, but others are left, I suppose, as an exercise for the reader. If you want to learn more, the sources for Detours, Minhook, Easyhook and Polyhook might be of interest. I found the Polyhook code the easiest to read, for whatever that’s worth.
There’s also some really cool approaches to function hooking that don’t require you to know the function signature of what you’re hooking. I haven’t delved into this at all, but I’ve had this github repo starred for awhile now.
Lastly, there’s a whole world of other hooking techniques out there. One that seems particularly interesting to me is import address table hooking, which RenderDoc uses. I expect I’ll lose several weekends to this very soon.
I’ve written a lot already, so I’ll keep my sign off short. There are two things that I didn’t find room to mention in the ocean of text above that I think warrant a mention:
If you try to disassemble a function that you have breakpoints set in, you’re going to have a bad time.
To debug an injected dll, attach your debugger to the process the dll was injected into.
Finally, my twitter handle is @khalladay. Send me questions or comments or whatever there. I’ll probably respond, unless I’m tired that day and forget to come back to it later.