Using objdump to disassemble code

The objdump utility can be used to get information from an object file. Much of its functionality duplicates that of readelf. One useful feature not found in readelf is the ability to disassemble object code (convert it from pure machine code to more humanreadable Assembly). There are two common formats for Assembly code: Intel and AT&T. The default is to use AT&T syntax, but this can be changed using the “-M intel” option. The Intel format seems to be preferred by PC programmers. It is important to realize that disassembly with a tool like this is not perfect, as any data in the sections normally containing code will be interpreted as machine code instructions.

The command objdump –disassemble -M intel xingyi_bindshell will disassemble the sections of the file normally expected to contain code. Partial results from running this command are shown in Figure 10.15. The snippet shown is part of the main method of the program. While a complete Assembly tutorial is well out of scope for this book, an introduction to the basics can be helpful in understanding this code. Assuming the code has not been intentionally obfuscated, only a basic understanding of Assembly is required to get the gist of what is going on.

Assembly language instructions are pretty basic. They mostly consist of moving things around; simple math like addition, subtraction, and multiplication; comparing numbers; jumping to new memory addresses; and calling functions. For commands that move things around, the source and target can be memory locations or high-speed storage areas in the CPU called registers.

Some of these registers have a special purpose, and the rest are used for performing calculations or passing variables to functions. Some of the registers have been around since the 16-bit processors of old (8086, 80286), others were added when 32-bit processors were released (80386 and newer), while still others were added when AMD released the first 64-bit processors. Standard 64-bit processor registers are shown in Figure 10.16. The legacy registers are named based on width. XX, EXX, and RXX denote 16-bit, 32-bit, and 64-bit wide registers, respectively. Some 16-bit registers can be further divided into XH and XL for the high and low bytes, respectively. Using the RAX register as an example AL, AX, EAX, and RAX represent the lowest byte, two bytes, four bytes, and all eight bytes of the register, respectively. When viewing Assembly code, you will often see different width registers used based on need.

The RIP (or EIP for 32-bit Assembly) register is known as the instruction pointer. It points to the address in memory where the next instruction to be run can be found. The RFLAGS register is used to keep track of status of comparisons, whether or not a mathematical operation resulted in a need to carry a bit, etc. The RBP register is known as the base pointer, and it points to the base (bottom) of the current stack frame. The RSP register is called the stack pointer, and it points to the top of the current stack frame. So what is a stack frame?

A stack frame is a piece of the stack which is associated with the currently running function in a program. So, what then is a stack? The stack is a special memory area that grows each time a new function is called via the Assembly CALL instruction and shrinks when this function completes. In C and similar languages you will hear people talk about stack variables that are automatically created when declared and go away at the end of their code block. These variables are allocated on the stack. When you think about what a function is, it is always a code block of some type. The local variables for functions are typically allocated on the stack.

When larger amounts of storage are required or variables need to live beyond a single function, variables may be created on the heap. The mechanism for getting heap space differs from one programming language to the next. If you look at the internals of how the operating system itself creates heaps and doles out memory to various processes, it is actually quite complex.

When reverse engineering applications in order to find vulnerabilities, the stack and heap play a central role in the process. For our purposes it is sufficient to understand that the stack is the most common place for functions to store their local variables. If you look back at Figure 10.15, you will see that the first instruction, push rbp, is used to save the current base pointer (bottom of the stack frame) to the stack. The current stack pointer is then moved to the base pointer with the command mov rbp,rsp, (recall that the target is on the left and source on the right in Intel notation). On the next line the current value stored in RBX is saved by pushing it on to the stack. On the next line 0x88 is subtracted from the stack pointer with the instruction sub rsp,0x88.

FIGURE 10.15

Partial results from disassembling xingyi_bindshell with objdump.

You might be thinking, “Hold on a second, why did I subtract to grow the stack?” The reason for this is that the stack grows downward (from high memory addresses to low memory addresses). By moving the old stack pointer (in RSP) to the base pointer (RBP), the old top of the stack frame has become the new bottom. Subtracting 0x88 from the stack pointer allocates 0x88 bytes for the current stack frame. This new storage is used by the current function. If you look at Figure 10.15, you will see several mov instructions that move values into this newly allocated stack buffer. The destinations for all of these moves are memory addresses contained in the square brackets, which are all of the form [rbp ].

There is also an odd instruction, xor eax,eax, among the move instructions. The bitwise exclusive-OR (XOR) operator compares each bit in two numbers, and the corresponding bit in the result is 1 if either, but not both, of the input values had a 1 in that position. The effect of XORing any number with itself is the same as setting that number to zero. Therefore, xor eax,eax is the same as mov eax,0x0. Readers who have done any shell coding will realize that use of XOR is preferred in that situation as it prevents a zero byte (which is interpreted as a NULL in many cases) from being present in code you are trying to inject.

Following the block of move instructions we see a call instruction. In high level languages calling a function and passing in a bunch of parameters is pretty simple. How does this work in Assembly? There needs to be a calling convention in place that dictates how parameters are passed into and out of a function. For various technical reasons, multiple calling conventions are used based on the type of function being called. 32-bit systems normally pass in parameters on the stack (ordered from right to left). 64-bit systems normally pass in parameters in the registers RDI, RSI, RDX, RCX, R8, R9, and place any additional parameters on the stack (also ordered right to left). Return values are stored in in EAX/EDX and RAX/RDX for 32-bit and 64-bit systems, respectively. One of the reasons that there are multiple calling conventions is that some functions (such as printf in C) can take a variable number of arguments. In addition to specifying where parameters are stored, a call convention defines who is responsible (caller or callee) for returning the stack and CPU registers to their previous state just before the function was called.

Armed with the knowledge from the previous paragraph, we can see the two lines, mov edi,0x100 and call are used to call the n_malloc function with a single parameter whose value is 0x100 (256). The return value, a pointer to memory allocated on the heap, is then stored on the stack frame on the next line, mov QWORD PTR [rbp-0x58],rax. On the lines that follow, the putchar and popen functions are called.

The return value from the call to popen is stored in the stack frame on the line mov QWORD PTR [rbp-0x70],rax. The next line compares the return value with zero. If the return value was zero, the line je 400fe7 causes execution to jump to 0x400FE7. Otherwise, execution continues on the next line at 0x400FB2. Note that the machine code is 0x74 0x35. This is known as a short conditional jump instruction (opcode is 0x74), that tells the computer to jump forward 0x35 bytes if the condition is met. Objdump did the math for you (0x400FB2 + 0x35 = 0x400FE7), and also told you this location was 0xBC bytes from the start of the main function. There are other kinds of jumps available, for different conditions or no condition at all, and for longer distances.

Other than the return instruction, ret, there is only one remaining Assembly instruction found in the main function that has not yet been discussed. This instruction is not in the snippet from Figure 10.15. This instruction is lea, which stands for Load Effective Address. This instruction performs whatever calculations you pass it and stores the results in a register. There are a few differences between this instruction and most of the others. First, you may have more than two operands. Second, if some of the operands are registers, their values are not changed during the operation. Third, the result can be stored in any register, including registers not used as operands. For example, lea rax,[rbp0x50] will load the value of the base pointer minus 0x50 (80) in the RAX register.

FIGURE 10.16

Registers for modern 64-bit processors.

The end of the disassembly of the main function by objdump is shown in Figure 10.17. The highlighted lines are cleanup code used to return the stack and registers to their previous state. Notice that we add back the 0x88 that was subtracted from RSP at the beginning of the function, and then pop RBX and RBP off the stack in the reverse order from how they were pushed on. Note that in the calling convention used here, it is the callee (main function in this case) that is responsible for restoring registers and the stack to their previous state. Functions (such as printf) that accept a variable number of parameters, require a different calling convention, in which the caller must perform the cleanup, as the callee does not know what will be passed into the function.

FIGURE 10.17

Cleanup code at the end of the main function. Note that the lines after the ret instruction are probably data, not instructions as depicted.

The bytes after the return instruction have been misinterpreted as machine code instructions by objdump. Later when we use the GNU Debugger (gdb) to disassemble the program, all of the program will be disassembled properly. For this reason, we will delay providing the full output from objdump here.

Up to this point we have been discussing what is known as static analysis. This is essentially dead analysis of a program that is not running. While using a tool like objdump to disassemble a program might lead to some minor errors, it is safe because the program is never executed on your forensics workstation. Often this is sufficient, if your primary goal is to determine if a file is malicious or benign. You certainly could use a tool such as gdb to get a more accurate disassembly of an unknown executable, but be careful not to run the program in the debugger on your forensics workstation!

results matching ""

    No results matching ""