- Apply the techniques presented in Tutorials 3 and 4 to analyzing Max++ anti-debugging trick.
- Practice reverse engineering/interpretation of Intel x86 assembly.
- Computer Architecture
- Operating Systems Security
- Software Engineering
Challenge of the Day:
- Write a Python snippet for Immunity Debugger that executes Max++ and generates a log message for each INT 2D instruction executed.
[Lab Configuration: we assume that you are running the VM instance using NON-DEBUG mode. We will use the Immunity Debugger in this tutorial.]
We now revisit Max++ and apply the knowledge we have obtained in Tutorial 3 and Tutorial 4. Figure 1 presents the disassembly of the first 20 instructions of Max++. The entry point is 0x00403BC8. Execute the code step by step until you reach 0x403BD5, now you are facing your first challenge: How do you deal with the INT 2D instruction?
There are several choices you could take: (1) Simply press F8 and IMM will SKIP the RETN instruction and directly jumps to 0x413BD8 (by executing the CALL 0x413BD8 instruction right after the RETN instruction); (2) Execute the RETN instruction by readjusting the EIP register to enforce its execution. In IMM, you can readjust the value of EIP by launching the Python window (clicking the 2nd button on the toolbar, on the right of the "open_file" button), and then executing the following Python command: "imm.setReg("EIP", 0x00413BD7);".
|Figure 1. Entry Point of Max++|
Which action to take will depend on the behavior of IMM -- if we press F8, will its behavior be the same as running the program without any debuggers attached? Following a similar approach taken in Tutorial 4 we can do an experiment for the case of EAX=1 (i.e., calling the debug print service of INT 2D). The conclusion is:
When the DEBUG-MODE is NOT enabled at booting, the behavior of IMM is the same as regular execution, given EAX=1 (note that in tutorial 4, our experiments explore the case when EAX=0). Hence, we can feel safe about stepping over (and skipping the RETN instruction) in IMM!
2.Diverting Control Flow using Int 2D
As shown in Figure 2, now we are at 0x00413A38. In this function, we only have four instructions: STD, MOV EDI, EDI, CALL 0x00413BB4, and IRETD.
The purpose of the STD instruction is to set the growth direction of EDI (i.e., direction flag) to -1. EDI/ESI registers are frequently used in RAM copy instructions such as "REP STOSB" (to repeatedly copy from memory address pointed by ESI to the destination address pointed by EDI). Later we'll see the use of these instructions in the decoding of encrypted malicious code in Max++.
The MOV EDI,EDI instruction does nothing (no impacts on any flag registers) and then we are calling the function at 0x00413BB4.
Note that it looks like once we are back from 0x00413BB4, the next immediate instruction is to return (IRETD), however, it is not the case. Function 0x413BB4 will retrieve a section of encrypted code, decrypt them, and deploy it from the location of IRETD. So if a static analysis tool is used to analyze the program, e.g., draw the control flow graph of Max++, it will mislead the malware analyzers. We'll get to the decoding function in the next tutorial.
|Figure 2. Function 0x413A38|
Press "F7" to step into Function 0x00413BB4. Now we are getting to the interesting point. Look at instruction CALL 0x00413BB9 at 0x413BB4 in Figure 3!
The CALL instruction basically does two things: (1) it pushes the address of the next instruction to the stack (so when the callee returns, the execution will resume at the next instruction); If you observe the stack content (the pane on the right-bottom on IMM), you will notice that 0x413BB9 is pushed into stack. (2) It then jumps to the entry address of the function, which is 0x00413BB9.
Now the next two instructions is to call the INT 2D service. Notice that the input parameter EAX is 3 (standing for the load image service). Using an approach similar to Tutorial 4, you can design an experiment to tell what is the next action you would take. The conclusion is: when EAX is 3, in the non-kernel-debug mode, the IMM behavior is the same as normal execution, which is: the next immediate byte instruction after INT 2D will be skipped.
Now, what if the RETN instruction is executed (i.e., the byte instruction is not skipped, assume that an automatic analyzer does not do a good job at handling INT 2D)? You will jump directly and return. The trick is as follows: Recall that RETN takes out the top element in stack and jump to that address. The top element in stack is now 0x00413BB9. So what happens is that the execution comes back to 0x00413BB9 again. Then doing the INT 2D again and RETN again will force the execution to 0x00413A40 (the IRETD instruction, which is right after the CALL 0X00413BB4 in function 0x00413A38 (see Figure 2)). It then returns to the main program and exits. So the other malicious activities will not be performed in this scenario. To this point, you can see the purpose of the int 2d trick: the malware author is trying to evade automatic analysis tools (if they did not handle int 2dh well) and certain kernel debuggers such as WinDbg.
Challenge of the day: use WinDbg instead of Immunity Debugger to debug through Max++ (with DEBUG-MODE enabled at booting). What is your observation?
|Figure 3. Trick: Infinite Loop of Call|
We have shown you several examples of the use of INT 2D in Max++ to detect the existence of debugger and change malware behavior to avoid being analyzed by a debugger. For debugger to cope with INT 2D automatically will not be an easy job. First, there are many scenarios to deal with (affected by the type of debugger, existence of kernel debugger, and the booting options). Second, don't expect to catch all INT 2D instructions when the program is loaded, because a program can be self-extracting (modifying its code segment at run time).
4. Challenge of the Day
It is beneficial to write a Python script that drives the Immunity Debugger to cope with INT 2D automatically. We provide some basic ideas here:
In IMM, there is a global variable "imm" for you to drive the debugger. You can use "imm" to inspect all register values, set all register values (thus including modifying EIP to change control flow), examine and modify RAM. Your program will be a simple loop, which executes instructions one by one (to do this, you can take advantage of breakpoint functions available in the Python API of IMM). Before executing an instruction, you can examine its opcode (using libanalyze.opcode, check the IMM documentation), and take proper actions for INT 2D (skipping the next byte based on the value of EAX, to simulate a normal non-debug environment).