ELF Binary Disassembly

Let us take a tour through a disassembly dump of an ELF binary and see if we can reverse engineer it. The following output is a result of:

mech@dev:$ gcc -o distut distut.c
mech@dev:$ objdump -d distut|grep main
push   %ebp                              // 0x55 
mov    %esp,%ebp                         // 0x89 0xe5 
sub    $0x14,%esp                        // 0x83 0xec 0x14
movl   $0x1,0xfffffff8(%ebp)             // 0xc7 0x45 0xf8 0x01 0x00 0x00 0x00
movl   $0x7a68,0xfffffff4(%ebp)          // 0xc7 0x45 0xf4 0x68 0x7a 0x00 0x00
cmpl   $0xa,0xfffffff8(%ebp)             // 0x83 0x7d 0xf8 0x0a
jne    0x80484d0 <main+32>               // 0x75 0x06
jmp    0x8048570 <main+192>              // 0xe9 0xa1 0x00 0x00 0x00
nop                                      // 0x90
add    $0xfffffff4,%esp                  // 0x83 0xc4 0xf4
push   $0x8048600                        // 0x68 0x00 0x86 0x04 0x08
call   0x80483b0 <printf>                // 0xe8 0xd3 0xfe 0xff 0xff 
add    $0x10,%esp                        // 0x83 0xc4 0x10
add    $0xfffffffc,%esp                  // 0x83 0xc4 0xfc
mov    0x8049768,%eax                    // 0xa1 0x68 0x97 0x04 0x08
push   %eax                              // 0x50
push   $0x8                              // 0x6a 0x08
lea    0xffffffec(%ebp),%eax             // 0x8d 0x45 0xec
push   %eax                              // 0x50 
call   0x8048380 <fgets>                 // 0xe8 0x8c 0xfe 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
add    $0xfffffffc,%esp                  // 0x83 0xc4 0xfc
push   $0x0                              // 0x6a 0x00
push   $0x0                              // 0x6a 0x00
lea    0xffffffec(%ebp),%eax             // 0x8d 0x45 0xec
push   %eax                              // 0x50
call   0x8048390 <strtol>                // 0xe8 0x89 0xfe 0xff 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
mov    %eax,%eax                         // 0x89 0xc0
mov    %eax,0xfffffffc(%ebp)             // 0x89 0x45 0xfc
mov    0xfffffffc(%ebp),%eax             // 0x8b 0x45 0xfc
cmp    0xfffffff4(%ebp),%eax             // 0x3b 0x45 0xf4
jne    0x8048538 <main+136>              // 0x75 0x21
add    $0xfffffff8,%esp                  // 0x83 0xc4 0xf8
mov    0xfffffff8(%ebp),%eax             // 0x8b 0x45 0xf8
push   %eax                              // 0x50
push   $0x8048620                        // 0x68 0x20 0x86 0x04 0x08
call   0x80483b0 <printf>                // 0xe8 0x88 0xfe 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
add    $0xfffffff4,%esp                  // 0x83 0xc4 0xf4
push   $0x0                              // 0x6a 0x00
call   0x80483c0 <exit>                  // 0xe8 0x8b 0xfe 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
mov    0xfffffffc(%ebp),%eax             // 0x8b 0x45 0xfc
cmp    0xfffffff4(%ebp),%eax             // 0x3b 0x45 0xf4
jle    0x8048550 <main+160>              // 0x7e 0x10
add    $0xfffffff4,%esp                  // 0x83 0xc4 0xf4
call   0x80483b0 <printf>                // 0xe8 0x63 0xfe 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
mov    0xfffffffc(%ebp),%eax             // 0x8b 0x45 0xfc
cmp    0xfffffff4(%ebp),%eax             // 0x3b 0x45 0xf4
jge    0x8048568 <main+184>              // 0x7d 0x10
add    $0xfffffff4,%esp                  // 0x83 0xc4 0xf4
push   $0x804864a                        // 0x68 0x4a 0x86 0x04 0x08
call   0x80483b0 <printf>                // 0xe8 0x4b 0xfe 0xff 0xff
add    $0x10,%esp                        // 0x83 0xc4 0x10
incl   0xfffffff8(%ebp)                  // 0xff 0x45 0xf8
jmp    0x80484c4 <main+20>               // 0xe9 0x54 0xff 0xff
xor    %eax,%eax                         // 0x31 0xc0 
jmp    0x8048574 <main+196>              // 0xeb 0x00
leave                                    // 0xc9
ret                                      // 0xc3
nop                                      // 0x90
nop                                      // 0x90 
nop                                      // 0x90

The reader is assumed to be familiar with the above output. I have removed the memory addresses so each line can fit on a 80 character wide screen. The assembly dump is in AT&T syntax. For more in depth information on each of the opcodes see the references at the end of this tutorial.

Let us take a look at what this binary is doing.
==============================================================================

After libc does a call to main() a stack frame or an area in memory to store such things as function arguments, local variables, and the address where main() is executing needs to be constructed. The prolog marks off this area.

push %ebp     // 0x55
mov %esp,%ebp // 0x89 0xe5

The current frame pointer %ebp(extended base pointer) is being saved, which belongs to the caller(libc), by pushing it onto the stack. Then the location of the stack pointer %esp(extended stack pointer) is copied into %ebp. %esp is pointing to the end of libc’s stack frame four bytes away from the saved %ebp. This position is used as the base for main()’s stack frame.

Note: Initial distance between %ebp and %esp is arbitrary. Also remember that each register has its own address and an address it points to.

Before %ebp is pushed onto the stack.

(stack grows downward)

3    2    1    0

|--------------| << bottom of stack
| junk         |
|--------------|
| junk         |
|--------------|
| 0x00000000   | <+++( ebp:0xbffff680 )
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x40038836   | <+++( esp:0xbffff670 ) && top of stack
 --------------

After %ebp is pushed onto the stack.

3    2    1    0

|--------------|
| junk         |
|--------------|
| junk         |
|--------------|
| 0x00000000   | <+++( ebp:0xbffff680 )
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x40038836   | <+++( esp:0xbffff670 )[old]
|--------------|
| 0xbffff680   | <+++( esp:0xbffff66c )[new]
 --------------

Keep in mind that memory addresses are 4 bytes long. So the instruction moves %esp down (to lower memory) 4 bytes from its location in the callers stack frame. %esp now points to the last byte in the saved %ebp.

This is how the stack looks after %esp is copied to %ebp.

3    2    1    0

|--------------|
| junk         |
|--------------|
| junk         |
|--------------|
| 0x00000000   |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x40038836   |
|--------------|
| 0xbffff680   | <+++( esp:0xbffff66c & ebp:0xbffff66c )
 --------------

%esp and %ebp both have the same address so they overlap until the next instruction which moves %esp down away from %ebp.

Note: a stack frame is created for every function call. For recursive functions a new stack frame is created with each iteration.

sub $0x14,%esp // 0x83 0xec 0x14

Here space is being allocated for the program variables by subtracting 0x14(20 bytes) from the address stored in %esp. If we scan the disassembly output we see four areas of storage referenced.

0xfffffffc(%ebp) & 0xfffffff8(%ebp) & 0xfffffff4(%ebp) & 0xffffffec(%ebp)

Here is how it looks:

3    2    1    0

| 0xbffff680   | <+++( ebp:0xbffff66c )
|--------------|
| junk         | var1:0xfffffffc
|--------------|
| junk         | var2:0xfffffff8
|--------------|
| junk         | var3:0xfffffff4
|--------------|
|              |
| junk         |
|              | var4:0xffffffec & ( esp:0xbffff658 )
 --------------

The compiler allocates space for variables based on the default stack alignment of the system.

For more information on modifying stack alignment during compilation refer to the compiler (gcc) manual or info page and search for: -mpreferred-stack-boundary

movl $0x1,0xfffffff8(%ebp)    // 0xc7 0x45 0xf8 0x01 0x00 0x00 0x00
movl $0x7a68,0xfffffff4(%ebp) // 0xc7 0x45 0xf4 0x68 0x7a 0x00 0x00

Some assignments are taking place here. A numeric constant following an opcode is prefixed with a ‘$’.

0xfffffff8(%ebp) == (%ebp+(-8))

add a negative 8 (0xfffffff8 is a signed integer) to the address stored in %ebp and have the resulting address point to the value 0x1(1)

example: var2 = 1;

0xfffffff4(%ebp) == (%ebp+(-12))

add a negative 12 (0xfffffff4 is a signed integer) to the address stored in %ebp and have the resulting address point to the value 0x7a68(31336).

example: var3 = 31336;

%ebp isn’t affected by these operations.

cmpl $0xa,0xfffffff8(%ebp) // 0x83 0x7d 0xf8 0x0a
jne 0x80484d0 <main+32>    // 0x75 0x06
jmp 0x8048570 <main+192>   // 0xe9 0xa1 0x00 0x00 0x00

This is the beginning of a loop construct (i.e. do(), while(), for()).

example:

while(var2 != 0xa)
jne 0x80484d0 <main+32>
jmp 0x8048570 <main+192>
{
  statements to be executed: <main+32>
}
outside while loop: <main+192>

0xa(10) is compared with the value pointed to by (%ebp-8) if they are equal then the jmp instruction is executed otherwise jne is executed. jne and jmp execute based on the condition of the EFLAGS (CF,OF,SF,ZF,AF,PF) status registers. jne and jmp simply pass execution on to the next line if the necessary conditions are not met.

for example when execution reaches:

 cmpl $0xa, 0xfffffff8(%ebp)

if (%ebp-8) == 10 then the status registers will hold the following values:

CF [0] OF [0] SF [1] ZF [1] AF [0] PF [1]

otherwise they will hold these values:

CF [1] OF [0] SF [1] ZF [0] AF [1] PF [0]

The zero flag(ZF) is set when the two operands are congruent.

basically:

if (ZF == 1) then jmp
else
jne
add $0xfffffff4,%esp // 0x83 0xc4 0xf4
push $0x8048600      // 0x68 0x00 0x86 0x04 0x08
call 0x80483b0 <printf> // 0xe8 0xd3 0xfe 0xff 0xff
add $0x10,%esp       // 0x83 0xc4 0x10
add $0xfffffffc,%esp // 0x83 0xc4 0xfc

Before the call to printf(), a stack adjustment is made and a pointer to a string is pushed onto the stack. After the call to printf() the stack is readjusted. The last two adjustments could have been done once, but this is how the compiler has chosen to do it.

example: printf(0x8048600);

mov 0x8049768,%eax     // 0xa1 0x68 0x97 0x04 0x08
push %eax              // 0x50
push $0x8              // 0x6a 0x08
lea 0xffffffec(%ebp),%eax // 0x8d 0x45 0xec
push %eax                 // 0x50
call 0x8048380 <fgets> // 0xe8 0x8c 0xfe 0xff 0xff
add $0x10,%esp         // 0x83 0xc4 0x10
add $0xfffffffc,%esp   // 0x83 0xc4 0xfc

The parameters to fgets() are being pushed onto the stack.

example: fgets(0xffffffec(%ebp),0x8,0x8049768)

after the call to fgets() the stack is readjusted.

push $0x0                 // 0x6a 0x00
push $0x0                 // 0x6a 0x00
lea 0xffffffec(%ebp),%eax // 0x8d 0x45 0xec
push %eax                 // 0x50
call 0x8048390 <strtol>   // 0xe8 0x89 0xfe 0xff 0xff 0xff
add $0x10,%esp            // 0x83 0xc4 0x10

Again parameters are being pushed onto the stack for strtol().

example: strtol(0xffffffec(%ebp),0x0,0x0)

after the call to strtol() the stack is readjusted and the return value is stored in %eax.

mov %eax,%eax             // 0x89 0xc0
mov %eax,0xfffffffc(%ebp) // 0x89 0x45 0xfc
mov 0xfffffffc(%ebp),%eax // 0x8b 0x45 0xfc
cmp 0xfffffff4(%ebp),%eax // 0x3b 0x45 0xf4
jne 0x8048538 <main+136>  // 0x75 0x21
add $0xfffffff8,%esp      // 0x83 0xc4 0xf8
mov 0xfffffff8(%ebp),%eax // 0x8b 0x45 0xf8
push %eax                 // 0x50
push $0x8048620           // 0x68 0x20 0x86 0x04 0x08
call 0x80483b0 <printf>   // 0xe8 0x88 0xfe 0xff 0xff
add $0x10,%esp            // 0x83 0xc4 0x10
add $0xfffffff4,%esp      // 0x83 0xc4 0xf4
push $0x0                 // 0x6a 0x00
call 0x80483c0 <exit>     // 0xe8 0x8b 0xfe 0xff 0xff

Here the compiler is doing some extra unnecessary things, the first instruction is equivalent to a ‘nop'(no operation). The next two copy the return value of strtol() (in %eax) to a location in memory (variable assignment [ var1 ]) and then reverses the process. Again compiler issues :

The return value from strtol() stored in %eax and var1 is compared to the value currently at (%ebp-12). If the value stored in %eax is not 31336 then program execution continues at memory location 0x8048538. However, if the values in both locations are congruent then the jne instruction is not executed and a stack adjustment takes place. Next we have the parameters for printf() being pushed onto the stack. After the call to printf() the stack is readjusted.

example:

if( var1 == var3)
jne 0x8048538 <main+136>
{
  printf(0x8048620,var1);
  exit( 0x0 );
}

execution continues here: <main+136>

The program terminates after the parameter is pushed onto the stack and the call to exit() is made.

add $0x10,%esp            // 0x83 0xc4 0x10
mov 0xfffffffc(%ebp),%eax // 0x8b 0x45 0xfc
cmp 0xfffffff4(%ebp),%eax // 0x3b 0x45 0xf4
jle 0x8048550 <main+160>  // 0x7e 0x10
add $0xfffffff4,%esp      // 0x83 0xc4 0xf4
push $0x8048640           // 0x68 0x40 0x86 0x04 0x08
call 0x80483b0 <printf>   // 0xe8 0x63 0xfe 0xff 0xff
add $0x10,%esp            // 0x83 0xc4 0x10

Note: These instructions are only executed if the return value from strtol() is not equal to 31336.

First a stack adjustment is made and then a comparison with var3 and the data stored in %eax ( var1 ).

example:

if( %eax > var3 )
jle 0x8048550 <main+160>
{
printf(0x8048640); // "Too high"
}
execution continues here: <main+160>

If the value stored in %eax is less then 31336 then program execution continues at memory location 0x8048550. However, if the reverse is true jle is not executed and a stack adjustment is made. printf() is then called after its parameter is pushed onto the stack. Then as usual the stack is readjusted.

mov 0xfffffffc(%ebp),%eax // 0x8b 0x45 0xfc
cmp 0xfffffff4(%ebp),%eax // 0x3b 0x45 0xf4
jge 0x8048568 <main+184>  // 0x7d 0x10
add $0xfffffff4,%esp      // 0x83 0xc4 0xf4
push $0x804864a           // 0x68 0x4a 0x86 0x04 0x08
call 0x80483b0 <printf>   // 0xe8 0x4b 0xfe 0xff 0xff
add $0x10,%esp            // 0x83 0xc4 0x10
incl 0xfffffff8(%ebp)     // 0xff 0x45 0xf8
jmp 0x80484c4 <main+20>   // 0xe9 0x54 0xff 0xff

Again var1 is copied into %eax and compared with var3. If var1 is greater than 31336 program execution jumps to 0x8048568. Otherwise, a stack adjustment is made and printf() is called after its parameter is pushed onto the stack. The stack is then readjusted.

example:

if( var1 < 31336 )
jge 0x8048468 <main+184>
{
  printf(0x8048568); // "Too lown"
}
execution continues here: <main+184>

var2 is incremented by 1 and after the jump to memory address 0x80484c4 its value is evaluated in the test condition. So we know that var2 is the counter that controls this loop. The loop will not exit until execution jumps past this point which occurs when var2 == 10.

xor %eax,%eax // 0x31 0xc0
jmp 0x8048574 <main+196> // 0xeb 0x00
leave  // 0xc9
ret    // 0xc3
nop    // 0x90
nop    // 0x90
nop    // 0x90

Execution jumps here when the test condition fails. (var2 == 10)

xor %eax, %eax

is equivalent to

mov $0x0, %eax

so we derive that %eax is being cleared and then the jmp statement jumps to the instruction below it, and returns control to the caller which in turn returns control to the operating system.

This would be a good time to alert you to the fact that after the saved frame pointer is pop’d off the stack there are other things located on the stack such as the return address (instruction pointer), arguments to the function, and if you go far enough you will hit the environment variables. I referred vaguely, to this data as ‘junk’ in the diagrams. This extra data is used by the epilog:

leave
ret

leave cleans up the current stack frame. It restores %esp and pops the saved %ebp off the stack and copies it to %ebp. Remember the address we are popping off the stack is the same address we pushed in the very beginning. *cough* prolog *cough*

example:

mov ebp,esp
pop ebp

pop will copy whatever is on top of the stack into its operand. Therefore after %ebp is copied to %esp we are pointing to the last byte in the saved %ebp.

Lets look at a visual example:

(before any operation)

3    2    1    0

|--------------| << bottom of stack
| junk         |
|--------------|
| junk         |
|--------------|
| 0xbffff680   | <+++( ebp:0xbffff66c )
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x08048456   | <+++( esp:0xbffff65c ) && top of stack (random location)
 --------------

mov %ebp, %esp

3    2    1    0

|--------------|
| junk         |
|--------------|
| junk         |
|--------------|
| 0xbffff680   | <+++( ebp:0xbffff66c && esp: 0xbffff66c )
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x40038836   |
|--------------|
| 0xbffff680   |
 --------------

After pop

3 2 1 0

|--------------| << bottom of stack
| junk         |
|--------------|
| junk         |
|--------------|
| 0x00000000 | <+++( ebp:0xbffff680 )
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| ....         |
|--------------|
| 0x40038836   | <+++( esp:0xbffff670 ) && top of stack
|--------------|
| 0xbffff680   |
|--------------|

pop moves %esp up to higher memory four bytes, or more correctly push/pop and family move %esp up/down the stack based on the size of the operand.

ret (return from procedure) basically restores the program environment to the state prior to a function call. It pops the return address off the stack and copies it to %eip(extended instruction pointer).

A CALL instruction saves the return address, or the address of the instruction that succeeds the function call to the stack, then jumps to the function which is its operand. So ret just pops that same address back into %eip and execution continues where it was before, in this case, the call to main().

example:

pop %eip

or

jmp 0xdeadbeef

so leave followed by ret is equivalent to:

mov %ebp, %esp
pop %ebp
pop %eip

or

mov %ebp, %esp
pop %ebp
jmp 0xdeadbeef

Putting it all together we get:

int
main( void )
{
long var1,
var2 = 0x1,
var3 = 31336;

char var4[8];

while(var2 != 0xa) /* MAIN+20
* jne 0x80484d0 <main+32>
* jmp 0x8048570 <main+192>
*/
{

printf(0x8048600); /* MAIN+32
*/

fgets(var4,0x8,0x8049768);
var1 = strtol(var4,NULL,0x0);

if( var1 == var3) /* jne 0x8048538 <main+136>
*/
{
printf(0x8048620,var1);
exit( 0x0 );
}
if( var1 > var3 ) /* MAIN+136
* jle 0x8048550 <main+160>
*/
{
printf(0x8048640);
}
if( var1 < var3 ) /* MAIN+160
* jge 0x8048568 <main+184>
*/
{
printf(0x8048568);
}
var2++; /* MAIN+184
* jmp 0x80484c4 <main+20>
*/
}
/* MAIN+192
* jmp 0x8048574 <main+196>
*/
return( 0x0 ); /* MAIN+196
*/
}

End Note:

This tutorial is a very trivial example of a disassembly or ‘reversing’, it is in no way this simple with a real world binary. I didn’t go into disassembling the libc functions above because they are well known functions that we already understand, and it is left as an exercise for the reader. This is only the tip of the iceberg. >;]

References:

http://developer.intel.com/design/pentium/manuals/
http://linuxassembly.org/
https://github.com/wtsxDev/reverse-engineering
https://www.google.com
http://www.acm.uiuc.edu/sigmil/RevEng/
http://www.gnu.org/software/gdb/gdb.html
https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
http://www.securityfocus.com/infocus/1641
http://www.ouah.org/RevEng/
http://www.amazon.com/exec/obidos/tg/detail/-/1931769222/103-1059424-6329447?v=glance

Thanks for reading.

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: