Learning Goals:
- Explore the behavior difference of debuggers on int 2dh.
- Debugging and modification of binary executable programs.
- Basic control flow constructs in x86 assembly.
Applicable to:
- Computer Architecture
- Operating Systems
- Operating Systems Security
- Software Engineering
Challenge of the Day:
- Find out as many ways as possible to make a program run differently in a debugged environment from a regular execution (using int 2d)?
1. Introduction
The behavior of int 2d instructions may be affected by many factors, e.g., the SEH handler installed by the program itself, whether the program is running under a ring 3 debugger, whether the OS is running in the debugged mode, the program logic of the OS exception handler (KiDispatch), the value of registers when int 2d is requested (determining the service that is requested).
In the following, we use an experimental approach to explore the possible ways to make a program behave differently when running in a virtual machine and debugged environment.
2. Lab Configuration
In addition the the immunity debugger, we are going to use WinDbg in this tutorial. Before we proceed, we need to configure it properly on the host machine and the guest XP.
If you have not installed the guest VM, please follow the instructions of
Tutorial 1. Pay special attention to Seciton 3.1 (how to set up the serial port of the XP Guest). In the following we assume that the pipe path on the
host machine is
\\.\pipe\com_11 and the
guest OS is using
COM1. The installation of WinDbg on the host machine can follow the instructions on
MSDN.
We need to further configure the XP guest to make it work.
(1) Revision of c:\boot.ini. This is to set up a second booting option for the debug mode. The file is shown as below, you can modify yours correspondingly. Note that we set COM1 as the debug port.
-------------------------------------
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="DEBUGGED VERSION" /noexecute=optin /fastdetect /debug /debugport=com1 /baudrate=115200
------------------------------------
(2) Manual configuration of COM ports. In some versions of XP, COM ports have to be manually configured. You can follow jorgensen's tutorial on "
How to Add a Serial Port in Windows XP and 7 Guest" (follow the XP part). It consists of two steps: (1) manually add a COM port in Control Panel and (2) manually configure COM1 as the port number.
(3) Test run of WinDbg.
Start your
XP guest in the
debug mode (2nd option).
Now in the
host machine, launch the "Windows SDK 7.1" command window coming with WinDbg. Change directory to "c:\Program Files\Debugging Tools for Windows(x86)" and type the following. You should be able to get a window as shown in Figure 1.
windbg -b -k com:pipe,port=\\.\pipe\com_11
You might notice that currently you are not able to access your XP Guest. This is because WinDbg stops its running, Simply type "
g" (standing for "go") in the WinDbg window, and let the XP Guest continue.
|
Figure 1: Screenshot of WinDbg |
3. Experiment 1: Int 2d on Cygwin
In the following, we demonstrate some of the interesting behaviors of Int 2d using a simple program Int2dPrint.exe. The C++ source of the program is shown in the following. The output of the program should be "AAAABBBB". We added a fflush(stdout) to enforce the output in an eager mode and before each printf() statement, there are
five integer operations to allow us insert additional machine code later.
----------------------------------------------
#include <stdio.h>
int main(){
int a = 0;
int b = 0;
int c = 0;
int d = 0;
int e = 0;
printf("AAAA");
fflush(stdout);
a = 0; b = 0; c = 0; d = 0; e = 0;
printf("BBBB");
fflush(stdout);
}
--------------------------------------------
Source code of Int2dPrint.exe
Figure 2 shows the assembly of the compiled code.Clearly,the 'MOV [EBP-xx], 0' instructions between 0x4010BA and 0x4010D6 correspond to the integer assignments "int a=0" etc. in the source program. The "MOV [ESP], 0X402020" at 0x4010DD is to push the parameter (the starting address of constant string "AAAA") into the stack, for the printf() call. Also note that before the fflush call at 0x4010F4, the program calls cygwin.__getreent. It is to get the thread specific re-entrant structure so that the stdout (file descriptor of the standard output) can be retrieved. In fact, you can infer that the stdout is located at offset 0x8 of the reentrant structure.
|
Figure 2. Compiled Binary of Int2dPrint.cc |
3.1 Patching Binary Code
Now let us roll our sleeves and prepare the patch the Int2dPrint.exe. The binary program is compiled using g++ under Cygwin. To run it, you need the cygwin1.dll in Cygwin's bin folder. You can choose to compile it by yourself, or use the one provided in the zipped project folder.
Make sure that your XP guest is
running in NON-DEBUG mode!
We now add the following assembly code at location 0x4010F9 of Int2dPrint.exe (the first "int a=0" before the printf("BBBB")). Intuitively, the code tests the value of EAX after the int 2d call. If EAX is 0 (here "JZ" means Jump if Zero), the program will jump to 0x401138, which skips the printf("BBBB").
Notice that this occurs only when the instruction "inc EAX" is skipped.
------------------------------------
xor EAX, EAX # set EAX=0;
int 2d # invoke the exception handler
inc EAX # if executed, will set EAX=1
cmp EAX, 0
JZ 0x401138 # if EAX=0, will skip printf("BBBB");
-----------------------------------
The assemble Code to Insert
The following shows you how to patch the code using IMM:
(1) Right click at 0x4010F9 in the CPU pane, and choose "
Assemble". (Or simple press Spacebar at the location). Enter the code as above.
(2) Right click in the CPU pane, choose "
Copy to Executable" --> "
All Modified", then click "
Copy All". A window of modified instructions will show up. Close that window and click "Yes" to save. Save the file as
Int2dPrint_EAX_0_JZ0.exe. The name suggests that the EAX input parameter to the int 2d service is 0, and we expect it to skip the printf("BBBB") if EAX=0, i.e., the output of the program should be "AAAA". (this, of course, depends on whether the "inc EAX" instruction is executed or not).
In Figure 3, you can find the disassembly of
Int2dPrint_EAX_0_JZ0.exe. Setting a breakpoint at 0x004010BA, you can execute the program step by step in IMM. You might find that the output is "AAAA" (i.e., "BBBB" is skipped). It seems to confirm the conclusion of byte scission of int 2d.You can also run the program in a command window, the output is the same.
|
Figure 3. Disassembly of Int2dPrint_EAX_0_JZ0.exe |
But wait, how about another experiment. Let's modify the instruction at 0x401101 and make it "
JNZ 0x401138" (name it as
Int2dPrint_EAX_0_JNZ0.exe). What is the expected output? "AAAABBBB"?
You might find that in IMM, the program outputs "AAAABBBB"; but if run in command window, it generates "AAAA" only!!! (Notice that we have ruled out the possibility that the I/O output was lost in buffer - because we call the fflush(stdout) to enforce all outout immediately). What does this mean? There could be two possibilities:
(1). Somehow, the instruction "INC EAX" is mysteriously executed (in the regular execution of Int2dPrint_EAX_0_JNZ0.exe). This makes no sense, because prior to 0x401101, the program is exactly the same as Int2dPrint_EAX_0_JZ0.exe.
(2). There is something tricky in the exception handler code (it could be the SEH of the program itself, or the KiDispatch in the kernel).
We will later come back to this strange behavior, and provide an explanation.
3.2 Experiments with Kernel-Debugging Mode
Now let's
reboot the guest OS into the DEBUG mode (but
without launching WinDbg in the host machine). Let's re-run the two programs, you might have some interesting finding.
Both programs hang the guest OS!
Now let's reboot the guest OS again into the DEBUG mode and launch WinDbg in the host machine (press "g" twice to let it continue). Now start the Int2dPrint_EAX_0_JNZ0.exe in command window. What is your observation? Figure 4 displays the result: the debugger stops at 0x4010fd (the "inc EAX" instruction) on exception 80000003 (the exception code "BREAKPOINT" in windows)! If you type "g", the program will proceed and
produce "AAAA"! (
while in the non-debugged windows mode and command window, it's producing "AAAABBBB"!)
|
Figure 4: Running Result of Int2dPrint_EAX_0_JNZ0.exe |
3.2 Discussion
Now let us summarize our observations so far in Table 1 (I did not discuss some of the experiments here but you can repeat them using the files provided).
|
Table 1: Summary of Experiment 1 |
To simply put:
int 2dh is a much more powerful technique to examine the existence of debuggers than people previously thought (see the reference list of
tutorial 3). It can be used to detect the existence of both ring 3 (user level) and ring 0 (kernel level) debuggers. For example, using Table 1, we can easily tell if Windows is running in DEBUG mode (i.e., kernel debugger enabled) or not, and if a kernel debugger like WinDbg is hooked to the debug COM port or not. We can also tell the existence of a user level debugger such as IMM, whether windows is running in non-debug or debug mode. The delicacy is that the final output of the int 2dh instruction is affected by many factors, and experiment 1 only covers a subset of them. The following is a re-cap of some of the important facts:
- EAX, ECX, EDX are the parameters to the int 2d service. EAX (1,2,3,4) represent the printing, interactive prompt, load image, unload image. See Almeida's tutorial for more details.Notice that we are supplying an EAX value 0, which is not expected by the service! (normal values should be from 1 to 4).
- Once the int 2d instruction is executed, CPU locates the interrupt vector and jumps to the handler routine, which is the part of OS.
- OS wraps the details of hardware exception, and generates kernel data structures such as Exception_Record, which contains Exception Code: 80000003 (represents a breakpoint exception).
- Then control is forwarded to kernel call KiDispatchException, which depending on if Windows is running in kernel mode, exhibits very sophisticated behavior. See details in G. Nebbett, "Windows NT/2000 Native API Reference" (pp 441 gives pseudo code of KiDispatchException). For example, in windows debug mode, this generally involves forwarding the exception to debugger first (calling DbgkForwardException), and then the invocation of user program installed SEH handlers, and then forward the exception to debugger a second time.
We now proceed to briefly explain all the behaviors that we have observed.
Case 1. Non-Debug Mode and Command Window (column 2 in Table 1): this is the only case that Int2dPrint_EAX_0_JZ0.exe and Int2dPrint_EAX_0_JNZ0.exe behave
the same way. There is only one explanation: the inc EAX is not executed - not because the exception handling behaves differently in a debugged environment, but because
the entire process is terminated. To illustrate the point, observe the two screenshots in Figure 5, which are generated by the IMM debugger via (
View->SEH Chain). Diagram (a) shows the SEH chain when the program is just started, you can see that the default handler kernel32.7C839AC0 (means the entry address of the handler is 7c839ac0 and it is located in kernel32). If you set a breakpoint right before the printf(), you might notice that the SEH chain now includes another handler from cygwin (in Fig 5(b))!
It's the cygwin handler which directly terminates the process (without throwing any error messages); if it is the kernel32 handler, it would pop a standard windows error dialog.
|
Figure 5: SEH Chain of Int2dPrint_EAX_0_JZ0.exe before and after reaching the main() |
Case 2. Non-Debug Mode and IMM Debugger (column 3 in Table 1): Based on the logic of the two programs, you can soon reach the conclusion that the byte instruction right after int 2dh is skipped! There are two observations here: (1) the Cygwin handler is
NEVER executed! This is because the Immunity Debugger takes the control first (Recall the logic of KiDispatchException and the KiForwardException to debugger port). (2) Immunity Debugger modifies the value of EIP register, because the exception is a breakpoint. See discussion in Ferrie's article about IMM's behavior [
1]. The result of shifting one byte, however, is also affected by the kernel behavior (look at the EIP-- operation in KiDispatchException (see pp. 439 of Nebbett's book [2]). The combined effect is to shift one byte. Note that if replacing IMM with another user level debugger such as IDA, you might have a different result.
Case 3. Debug Mode without WinDbg Attached and CMD shell (column 4 in Table 1): windows freeze! The reason is clear: no debuggers are listening to the debug port and the breakpoint exception is not handled (no one advances the EIP register).
Case 4. Debug Mode without WinDbg Attached and Run in IMM (column 5 in Table 1): This is similar to case 2. If you F9 (and run the program) in IMM, you might notice that IMM stops at the
SECOND instruction right after int 2dh (i.e, "
CMP EAX,0") first (because it's a breakpoint exception, but the kernel debugging service is actually not triggered). If you F9 (continue) the program, it continues and exhibits the same behavior as Case 2. Again, the byte scission is the combined result of IMM and the kernel behavior (on int exceptions).
Case 5. Debug Mode with WinDbg Attached and Run in CMD shell (column 6 in Table 1):In this case, WinDbg stops at the
instruction right after int 2dh (i.e., "
inc EAX") and if continues, executes the "inc EAX" instruction.
Case 6. Debug Mode with WinDbg Attached and Run in IMM (column 7 in Table 1):In this case, WinDbg never gets the breakpoint exception, it's the user level debugger IMM gets the breakpoint exception first and like case 4, IMM readjusts the EIP register so that it stops at the
SECOND INSTRUCTION after int 2d. It is interesting to note that, even when WinDbg is initiated, if you start a user debugger, it gets/overrides the WinDbg on the processing of breakpoints. This is of course understandable -- think about using Visual Studio in the debugged mode for debugging a program, it is natural to pass the breakpoint event to Visual Studio first. Once the user level debugger declares that the exception has been handled, there is no need to to pass to the kernel debugger for handling.
Clearly,
IMM debugger has a "defect" in its implementation. First, it blindly processes a breakpoint exception even if this is not a registered exception in its breakpoint list. Second, the kernel service handles the readjustment of EIP differently for int 3 and int 2d (even though both of them are wrapped as the 80000003 exception in windows). When IMM does not differentiate the cases, the combined effect is that the readjustment of EIP is "over-cooked" and we see the byte scission.
3.3 Challenges of the Day
All of the above discussion are based on the assumption that EAX is 0 when calling the int 2d service. Notice that this is a value unexpected by the windows kernel -- the legal values are 1, 2, 3, and 4 (debug print, interactive, load image, unload image). Your challenges today is to find out the cases when EAX is set to 1, 2, 3, 4, and other unexpected values and assess the system behavior. You will have interesting discoveries.
4. Experiment 2: notepad
There is another interesting area we have not explored: the user installed SEH. The Int2d programs are good examples. The preamble code before the main function installs an SEH handler offered by Cygwin. It immediately leads to the termination of the process. It is interesting to observe the behavior of the default kernel32 handler. The following experiment sheds some light.
4.1 Experiment Design
When we use File->Open menu of notepad, we will always see a dialog popped up.
Our plan is to insert the code in Section 3.1 before the call for popping dialog, and observe if there is any byte scission.
The first question is how to locate the code in notepad.exe that launches a file open dialog. We will again use some immunity debugger tricks. It is widely known that user32.dll provides important system functions that are related to graphical user interface. We could examine the visible functions by user32.dll using the following approach.
- Open notepad.exe (in c:\windows) using the Immunity Debugger
- View -> Executable Modules
- Right click on "user32.dll" and select "View->Names". This exposes the entry address of all externally visible functions of the dll. Browse the list of functions, we may find a collections of functions such as CreateDialogIndirectParamA and CreateDialogIndirectParamW. Press "F2" to set a software breakpoint on each of them.
- Now F9 to run the notepad program. Click File->Open and the IMM stops at 7E4EF01F. Go back to the View->Names window, you will find that it is the entry address of CreateDialogIndirectParamW.
- Now remove all other breakpoints (except CreateDialogIndirectParam), so that we are not distracted by others. You can do this in View->Breakpoints window to remove the ones you don't want.
- Restart the program (make sure that your BP is set), click file->open, now you are stopping at CreateDialogIndirectParamW. We will now take advantage of once nice feature in IMM. Click Debug-> Execute Till User Code (to allow us get to the notepad.exe code directly!). Note that since the dialog is a modal dialog (which sticks there until you respond), you have to go back to the running notepad and cancel the dialog. Then the IMM stops at instruction 0x01002D89 of notepad.exe! This is right after the call of GetOpenFileNameW, which we just returns from.
|
Figure 6. Disassembly of notepad.exe |
The disassembly of notepad.exe is quite straightforward. At 0x01002D27, it sets up the dialog file filter "*.txt", and then at 0x01002D3D, it calls the GetOpenFileW function. The return value is stored in EAX. At 0x01002D89, it tests the value of EAX. If it is 0 (meaning the file dialog is canceled), the program control jumps to 0x01002DE0 (which directly exists the File->open processing).
We now can insert our instructions (most from Section 3.1) at 0x01002D27 (the side-effect is that the dialog file filter is broken - but this is ok). The code is shown below (we call it
notepad_EAX_0_JZ0.exe. Similarly, we can generate
notepad_EAX_0_JNZ0.exe):
------------------------------------
xor EAX, EAX # set EAX=0;
int 2d # invoke the exception handler
inc EAX # if executed, will set EAX=1
cmp EAX, 0
JZ 0x01002D89 # if EAX=0, will skip printf("BBBB");
-----------------------------------
Run notepad_EAX_0_JZ0.exe in a command window (undebugged window), you will get the standard exception window thrown by windows. If you click the "details" link of the error dialog, you will be able to see the detailed information: note the error code 0x80000003 and the address of the exception (0x01002D2B!). I believe now you can easily draw the conclusion about the exception handler of kernel32.dll.
|
Figure 7: Error Report |
4.2 Challenge of the Day
Our question is:
are you sure that the error dialog is thrown by the handler of kernel32.7C839AC0? Prove your argument.
5. Experiment 3: SEH Handler Technique
Recall that the SEH handler installed by the user program itself can also affect the running behavior of int 2d. For example, Int2dPrint_EAX_0_JZ0.exe installed a handler in Cygwin1.dll, it leads to the termination of the process immediately; while the default kernel32.dll handler throws out an exception dialog that displays debugging information. In this experiment, we repeat Ferrie's example in [
3] and explore further potential possibilities of anti-debugging.
Figures 8 and 9 present our slightly adapted version of Ferrie's example in [
3] . The program is modified from the Int2dPrint.exe. The first part of the code is displayed in Figure 8, starting from 0x004010F9 and ending at 0x0040110E. We now briefly explain the logic.
Basically, the code is to install a new exception handler registration record (recall that SEH is a linked list and each registration record has two elements: prev pointer and the entry address of handler). So instruction at 0x004010FB is to set up the handler address attribute to
0x004016E8 (we'll explain later), and at 0x00401100 it is to set the prev pointer attribute. Then the instruction at 0x00401103 resets FS:[0], which always points to the first element in the SEH chain. The rest of the code does the old trick: it puts an "INC EAX" instruction right after the int 2d instruction and depending on whether the instruction is skipped, it is able to tell the existence of debugger.
|
Figure 8. Part I of Ferrie's Code |
We now examine the exception handler code at
0x004016E8. It is shown in Figure 9, starting at 0x004016E8 and ending at 0x004016F4. It has three instructions. At 0x004016E8, it puts a word 0x43434343 into address 0x00402025. If you study the instruction at 0x0040111c (in Figure 8), you might notice that at 0x00402025, it stores the string "BBBB". So this instruction is essentially to store "CCCC" into the RAM.
If the SEH handler is executed and if the second printf() statement is executed, you should see "AAAACCCC" in output, instead of "AAAABBBB". You might wonder, why not just change the value of a register (e.g., EBX) in the handler to indicate that the SEH is executed? Recall that interrupt handler of OS will recover the value of registers from kernel stack - no matter what value you set on a register (except for EAX), it will be wiped out by OS after return.
The last two instructions of the SEH handler simply returns 0. Notice that, as shown by Pietrek in [
1], "0" means
ExceptionContinueExecution, i.e., the exception has been handled and the interrupted process should resume. There are other values that you can play with, e.g., "1" means ExceptionContinueSearch, i.e., this handler could not solve the problem and the search has to be continued on the SEH chain to find the next handler. Note that these values are defined in the EXCEPT.h.
|
Figure 9. Part II of Ferrie's Code |
There could be another factor that affects your experimental results. The immunity debugger can be configured on whether or not to pass the exception to a user program. Click the "Debugger Options" menu in IMM, and then the "Exceptions" tab (shown in Figure 10). You can specify to pass all exceptions to user programs (by clicking the "add range" button and select all exceptions). After the configuration is done, running the program using "
Shift + F9" will pass the exceptions to user installed SEH (compared with F9).
|
Figure 10. Configuration of Exception Handling of IMM |
Similar to Section 4, we can run our program (Int2dprint_EAX0_RET0_JZ0.exe, meaning setting EAX to 0 when calling int 2d, and returning 0 in the SEH handler), under different environments, with debugging mode turned on or not. The results are displayed in Figure 11.
Non-debug mode: when running in command window, the output is "AAAACCCC". Clearly, the user installed SEH is executed and the byte scission did not occur (i.e., the "inc EAX" instruction is indeed executed). Compare it with the similar running environment in Table 1, you can immediately understand the effect of returning 0 in SEH: it tells the OS: "everything is fine. Don't kill the process!".
If you run the program in IMM, using F9 (without passing exceptions to user program), the result is "AAAA", where the "inc EAX" is skipped by IMM (similar to Table 1) and the user installed SEH is never executed; however, if you choose shift+F9 to pass exceptions to user program, the SEH is executed and the "inc EAX" is executed! It seems that in the "shift+F9" mode, IMM's does not re-adjust the EIP (as stated in Ferrie's article).
Debug-Mode with WinDbg Attached: Now when WinDbg is attached, the command line running of the program yields "AAAABBBB". This means that "inc EAX" is executed but the SEH is not executed! I believe, similarly, you can explain the IMM running result.
Now, the conclusion is:
the use of user installed SEH enables more possibilities to detect the existence of debuggers and how they are configured!
|
Figure 11. Experimental Results of Ferrie's Example |
5.1 Challenges of the Day
Play with the return values of your SEH handler, set it to 1, 2, and other values such as negative integers. What is your observation?
6. Conclusion
The int 2d anti-debugging technique is essentially an application of OS finger printing, i.e., from the behaviors of a system to tell its version and configuration. From the point of view of a program analysis researcher, it could be a very exciting problem to automatically generate such anti-debugging techniques, given the source/binary code of an operating system.
References
[1] M. Pietrek, "A Crash Course on the Depth of Win32Tm Structured Exception Handling," Microsoft System Journal, 1997/01. Available at
http://www.microsoft.com/msj/0197/exception/exception.aspxhttp://www.microsoft.com/msj/0197/exception/exception.aspx.
[2] G. Nebbett, "Windows NT/2000 Native API Reference", pp. 439-441, ISBN: 1578701996.
[3] P. Ferrie, "Anti-Unpacker Tricks - Part Three", Virus Bulletin Feb 2009. Available at
http://pferrie.tripod.com/papers/unpackers23.pdf, Retrieved 09/07/2011.