Coding in a debugger

Recently, I spent some time debugging all kinds of crazy, once-in-a-blue-moon type of bugs, usually MP related and often happening in final/release builds only. The annoying problem with these is they tend to “hide” when you try to repro them and then pop up 5 minutes later when you’re investigating something else. That’s why it’s often crucial to catch every chance you get and try to extract as much information as possible out of every case. It might not be your perfect scenario, you’re not prepared, you’ve just disabled your diagnostics, you might be running an optimized build, but it should still be possible to at least verify some theories. You can’t afford to shut the game down, modify the code and run again, bug happens too rarely to hope we’ll get it soon. In situations like this, being able to modify code/data “on the fly”, using debugger might prove priceless. I’ll focus on x86 here, but in most cases it’s just a matter of finding respective opcodes for your platforms. Here’s a small collection of tricks I like to employ on special occasions:

  • first things first – how do you modify the code in debugger? Two easiest methods are: memory window & immediate window. I use immediate mostly for NOPing instructions, it’s a little bit faster, for more complicated modifications memory window seems more handy. Switch to disassembly view first, note the code address, copy/paste it in the memory window and you’re good to go. Memory window now displays raw opcodes for instructions about to be executed, by modifying memory contents, you modify the code.

  • NOPing instructions. Most useful if you want to permanently disable that annoying assertion or deactivate certain code path - we can replace any unwanted instructions with NOP. Here’s quick assertion example:

    101363F2D 0F B6 C8         movzx       ecx,al
    201363F30 85 C9            test        ecx,ecx
    301363F32 75 01            jne         RunImpl+255h (1363F35h)
    401363F34 CC               int         3

We want to remove int 3 assertion, which breaks into debugger (opcode 0xCC). Copy-paste memory address (0x01363F34) to the memory window. You should see there’s 0xCC, replace it with 0x90 (NOP opcode). If you still have your disassembly view open, you’ll notice that int 3 changed to nop. As I mentioned, it might be quicker to use immediate window for easy, single byte modifications like this, just open it and type *(char *)0x01363F34 = 0x90.

  • Conditional jumps. Let’s use the same snippet, but try to find a way to avoid executing the assertion codepath. Now, our solution will be different depending if it’s supposed to be a one time modification or if we want to change the behavior permanently. If we only want to do it once, it’s easiest to change the value of ECX (or AL), just before the test. As you can see, jump will be taken if ECX (AL) is not zero. Put a breakpoint in 0x01363F30 and set the value of ECX to 1 (there are many ways to do it, watch window, immediate window, register window and probably others. I like immediate the most, it’s the fastest, just type ECX=1 there). Alternatively, you can set a breakpoint in the following line and modify the content of ZR. Sometimes, we want to change the code permanently, so that it takes the jump if ECX equals zero. In such case, it’s easiest to use different jump opcodes. I’ll admit I don’t know them by heart, I do know one is 0x74 and the other is 0x75 (can’t remember which is which), so I just ping pong between those. Copy-paste 0x01363F32 to the memory window, change 0x75 to 0x74 and you’ll see that jne changes to je. Sometimes you’ll need to use other jump types (like jae or jle), this site has a nice cheat sheet that’ll come handy.

  • Removing function call. Again, there are several possibilities here. Sometimes, we want to remove just one particular function call, in such case it’s best to just replace it with NOPs (you’ll need 5), easy. There are also cases when you want to “disable” function completely, regardless of who called it. We need to modify the code so that first instruction returns. Example:

    1013A3B7E 8B 44 24 08      mov         eax,dword ptr [esp+8]
    2013A3B82 2B 44 24 04      sub         eax,dword ptr [esp+4]
    3013A3B86 56               push        esi
    4013A3B87 8B F1            mov         esi,ecx
    5[...]
    6013A3BC5 89 46 04         mov         dword ptr [esi+4],eax
    7013A3BC8 5E               pop         esi
    8013A3BC9 C2 08 00         ret         8

We want to “move” ret 8 to be the first executed instruction. Conveniently, we even have the correct code here, so all you need to do is to modify memory at 013A3B7E to contain C2 08 00 (ret 8).

  • Skipping parts of function. That’s a little bit more complicated (and probably quite rare) situation, where you need to skip part of the function, but not eliminate it completely. In such case, you’ll have to inject jump instruction + address. There are two ways, you can either use relative jump (0xE9 + 4 byte offset) or an absolute jump (0xEA + CS:offset). I usually prefer the second one as I don’t have to calculate the offset, but it’s a matter of taste. Quick example:
    1013A3B7E 8B 44 24 08      mov         eax,dword ptr [esp+8]
    2013A3B82 2B 44 24 04      sub         eax,dword ptr [esp+4]
    3013A3B86 56               push        esi
    4013A3B87 8B F1            mov         esi,ecx
    5013A3B89 8B 0E            mov         ecx,dword ptr [esi]
    6013A3B8B C1 F8 02         sar         eax,2
    7013A3B8E 57               push        edi
    8013A3B8F 8B F8            mov         edi,eax

Now, imagine we want to skip those 2 instructions at 0x013A3B87 and jump directly to 0x013A3B8E (that’s just so that example doesn’t take too much space, normally it’d be quicker to NOP them). Let’s try absolute jump first, we replace it with EA 8E 3B 3A 01 (absolute jump address, little endian). Now, we’ll need a code segment as well, just open the registers window and see what’s in the CS register. In my case it was 0x0023, so the complete code is: EA 8E 3B 3A 01 23. If we wanted to use a relative jump, you’d have to calculate the offset first. In this case it’d be (0x013A3B8E - 0x013A3B8C) = 2 (0x013A3B8C because it’s the offset from the instruction following the jump instruction). Replace code at 0x013A3B87 with E9 02 00 00 00 and you’re done. (Please note: sometimes when modifying the instruction, it causes the following code to change as well, if the new instruction is of different length, in such case just fill the gaps with NOPs).

  • Data modifications. Most of the time, you can get away with using the watch window, but sometimes you’ll have to modify immediate values. Simple example:
    1if (mode == MODE_A) { DoSomething(); }
    2...
    300AAE45D A1 2C 0F B1 00   mov         eax,dword ptr [mode (0B10F2Ch)]
    400AAE462 83 F8 01         cmp         eax,1
    500AAE465 75 05            jne         ::RunImpl+2Eh (0AAE46Ch)
    600AAE467 E8 14 38 FE FF   call        ::DoSomething (0A91C80h)

Now, we’d like to change it so that it DoesSomething, but only if mode == MODE_B = 2 (there’s also MODE_C == 3). The value we’re testing against is a part of the code. You can easily see it yourself, notice the 01 byte. Copy-paste 0x00AAE462 to the memory window, change 01 to 02 and voila. Things get a little bit more hairy if we want to modify floating-point value. Imagine the following code trying to check if the bad guy is on another floor:

 1bool differentFloor = (myY - otherY) > 15.f;
 2...
 300F68B7E D9 05 34 0F FE 00 fld         dword ptr [myY (0FE0F34h)]
 400F68B84 D8 25 2C 0F FE 00 fsub        dword ptr [otherY (0FE0F2Ch)]
 500F68B8A D9 05 50 AB FB 00 fld         dword ptr [__real@41700000 (0FBAB50h)]
 600F68B90 D9 C9            fxch        st(1)
 700F68B92 DF F1            fcomip      st,st(1)
 800F68B94 DD D8            fstp        st(0)
 900F68B96 76 03            jbe         ::IsDifferentFloor+1Dh (0F68B9Bh)
1000F68B98 B0 01            mov         al,1

We’d like to see if perhaps the floor height wasn’t selected correctly and we’d like to modify it. Floating-point values are not part of the instruction, they’re stored in memory. Fortunately, the debugger is helpful enough to give us the exact address. Again, copy-paste 0x0FBAB50 to the memory window. First four bytes is our floor height (15.0f). If we want to modify it, we’ll need to know a hexadecimal representation of the new value. Unless it’s a well known number as 1.0f or FLT_MAX you’ll probably need some help, here’s the online converter I use. Assuming we’d like to bump floor height to 20m, we convert it to hex first (0x41c80000), then replace 00 00 70 41 with 00 00 c8 41.

Old comments

jiaolu 2012-10-29 06:38:01

real ,real nice gem article, thanks for sharing

insominx 2012-10-30 18:12:43

Golden… this is the sort of voodoo I want to see more of. Thanks!

Karl Schmidt 2012-11-18 20:27:48

Thanks very much for writing this up - I would have found these tips VERY useful in the past :)

Hern 2012-12-08 18:50:52

Great post. That kind of magic for sure will be helpful :)

More Reading
Newer// Junkers
Older// Copy or move