64-bit and 32-bit

Today we are going to do a little discussion about the differences between a 32-bit machine and a 64-bit machine with x86 architecture.  The differences in the architectures have implications for how we go about exploiting programs. I am going to be doing some comparisons between the 32-bit Debian I have been using and 64-Bit Mint.

What do those bits mean?

The first thing we are going to talk about is what it actually means to be 32-bit or 64-bit.  These numbers refer to the number of bits that an address uses.  For example we have been seeing addresses that have the form 0xbffffc24.  This is a 32-bit address and there are {2}^{32} of them which is where we get the 4GB number for memory.  A 64-bit machine has {2}^{64} memory addresses. However the address space is not as straightforward as the 32-bit space.

Address Space.

The address space for the 32-bit systems and the 64-bit systems are obviously different by size.  However they are also different in layout.  In a 32-bit system we haven’t worried about what addresses we are allowed to use.  (in some 32-bit systems there are restricted addresses for user land processes but we haven’t had to worry about it at this point)  In a 64-bit system we have to keep our addresses below the 0x00007fffffffffff cutoff or we will cause an exception error in the system.  An exception error means a fatal error causing the program to crash, you can read more about it here.

Lets Take a Look.

Lets take a look at the differences in the systems from the perspectives of our simple buffer overflow program.  Remember back in the beginning we had written this basic example

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int value = 5;
    char buffer_one[8], buffer_two[8];

    strcpy(buffer_one, "one"); //Put "one" in buffer one
    strcpy(buffer_two, "two"); //Put "two" in buffer two

    printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
    printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
    printf("[BEFORE] value is at %p and is %d (0x%08x)\n", &value, value, value);

    printf("\n[STRCPY] copying %d bytes into buffer_two\n\n", strlen(argv[1]));
    strcpy(buffer_two, argv[1]); //copy first command line argument into buffer two.

    printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
    printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
    printf("[AFTER] value is at %p and is %d (0x%08x)\n", &value, value, value);
    
}

When we run the 32-bit buffer overflow we get the output we expected.

$ ./overflow $(perl -e 'print "A"x18')
[BEFORE] buffer_two is at 0xbf8c819c and contains 'two'
[BEFORE] buffer_one is at 0xbf8c81a4 and contains 'one'
[BEFORE] value is at 0xbf8c81ac and is 5 (0x00000005)

[STRCPY] copying 18 bytes into buffer_two

[AFTER] buffer_two is at 0xbf8c819c and contains 'AAAAAAAAAAAAAAAAAA'
[AFTER] buffer_one is at 0xbf8c81a4 and contains 'AAAAAAAAAA'
[AFTER] value is at 0xbf8c81ac and is 16705 (0x00004141)

Now lets compare to what happens on the 64-bit machine:

$ ./overflow $(perl -e 'print "A"x18')
[BEFORE] buffer_two is at 0x7ffd40e26960 and contains 'two'
[BEFORE] buffer_one is at 0x7ffd40e26970 and contains 'one'
[BEFORE] value is at 0x7ffd40e2697c and is 5 (0x00000005)
[STRCPY] copying 18 bytes into buffer_two

[AFTER] buffer_two is at 0x7ffd40e26960 and contains 'AAAAAAAAAAAAAAAAAA'
[AFTER] buffer_one is at 0x7ffd40e26970 and contains 'AA'
[AFTER] value is at 0x7ffd40e2697c and is 5 (0x00000005)

We got a similar result but we didn’t overflow the same amount. Lets take a look at what’s going on in the assembly for each program and compare them.

Time to Disassemble Things.

First lets disassemble the main function on each type of architecture to see what we have going on.

On the 32-bit Debian we have:

(gdb) disass /m main
Dump of assembler code for function main:
6       {
   0x0804845b :     lea    ecx,[esp+0x4]
   0x0804845f :     and    esp,0xfffffff0
   0x08048462 :     push   DWORD PTR [ecx-0x4]
   0x08048465 :    push   ebp
   0x08048466 :    mov    ebp,esp
   0x08048468 :    push   ebx
   0x08048469 :    push   ecx
   0x0804846a :    sub    esp,0x20
   0x0804846d :    mov    ebx,ecx

7           int value = 5;
   0x0804846f :    mov    DWORD PTR [ebp-0xc],0x5

8           char buffer_one[8], buffer_two[8];
9
10          strcpy(buffer_one, "one"); //Put "one" in buffer one
   0x08048476 :    lea    eax,[ebp-0x14]
   0x08048479 :    mov    DWORD PTR [eax],0x656e6f

11          strcpy(buffer_two, "two"); //Put "two" in buffer two
   0x0804847f :    lea    eax,[ebp-0x1c]
   0x08048482 :    mov    DWORD PTR [eax],0x6f7774

12
13          printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
   0x08048488 :    sub    esp,0x4
   0x0804848b :    lea    eax,[ebp-0x1c]
   0x0804848e :    push   eax
   0x0804848f :    lea    eax,[ebp-0x1c]
   0x08048492 :    push   eax
   0x08048493 :    push   0x8048600
   0x08048498 :    call   0x8048310 
   0x0804849d :    add    esp,0x10

14          printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
---Type  to continue, or q  to quit---
   0x080484a0 :    sub    esp,0x4
   0x080484a3 :    lea    eax,[ebp-0x14]
   0x080484a6 :    push   eax
   0x080484a7 :    lea    eax,[ebp-0x14]
   0x080484aa :    push   eax
   0x080484ab :    push   0x8048630
   0x080484b0 :    call   0x8048310 
   0x080484b5 :    add    esp,0x10

15          printf("[BEFORE] value is at %p and is %d (0x%08x)\n", &value, value, value);
   0x080484b8 :    mov    edx,DWORD PTR [ebp-0xc]
   0x080484bb :    mov    eax,DWORD PTR [ebp-0xc]
   0x080484be :    push   edx
   0x080484bf :   push   eax
   0x080484c0 :   lea    eax,[ebp-0xc]
   0x080484c3 :   push   eax
   0x080484c4 :   push   0x8048660
   0x080484c9 :   call   0x8048310 
   0x080484ce :   add    esp,0x10

16
17          printf("\n[STRCPY] copying %d bytes into buffer_two\n\n", strlen(argv[1]));
   0x080484d1 :   mov    eax,DWORD PTR [ebx+0x4]
   0x080484d4 :   add    eax,0x4
   0x080484d7 :   mov    eax,DWORD PTR [eax]
   0x080484d9 :   sub    esp,0xc
   0x080484dc :   push   eax
   0x080484dd :   call   0x8048340 
   0x080484e2 :   add    esp,0x10
   0x080484e5 :   sub    esp,0x8
   0x080484e8 :   push   eax
   0x080484e9 :   push   0x804868c
   0x080484ee :   call   0x8048310 
   0x080484f3 :   add    esp,0x10

18          strcpy(buffer_two, argv[1]); //copy first command line argument into buffer two.
   0x080484f6 :   mov    eax,DWORD PTR [ebx+0x4]
---Type  to continue, or q  to quit---
   0x080484f9 :   add    eax,0x4
   0x080484fc :   mov    eax,DWORD PTR [eax]
   0x080484fe :   sub    esp,0x8
   0x08048501 :   push   eax
   0x08048502 :   lea    eax,[ebp-0x1c]
   0x08048505 :   push   eax
   0x08048506 :   call   0x8048320 
   0x0804850b :   add    esp,0x10

19
20          printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
   0x0804850e :   sub    esp,0x4
   0x08048511 :   lea    eax,[ebp-0x1c]
   0x08048514 :   push   eax
   0x08048515 :   lea    eax,[ebp-0x1c]
   0x08048518 :   push   eax
   0x08048519 :   push   0x80486bc
   0x0804851e :   call   0x8048310 
   0x08048523 :   add    esp,0x10

21          printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
   0x08048526 :   sub    esp,0x4
   0x08048529 :   lea    eax,[ebp-0x14]
   0x0804852c :   push   eax
   0x0804852d :   lea    eax,[ebp-0x14]
   0x08048530 :   push   eax
   0x08048531 :   push   0x80486ec
   0x08048536 :   call   0x8048310 
   0x0804853b :   add    esp,0x10

22          printf("[AFTER] value is at %p and is %d (0x%08x)\n", &value, value, value);
   0x0804853e :   mov    edx,DWORD PTR [ebp-0xc]
   0x08048541 :   mov    eax,DWORD PTR [ebp-0xc]
   0x08048544 :   push   edx
   0x08048545 :   push   eax
   0x08048546 :   lea    eax,[ebp-0xc]
   0x08048549 :   push   eax
---Type  to continue, or q  to quit---
   0x0804854a :   push   0x804871c
   0x0804854f :   call   0x8048310 
   0x08048554 :   add    esp,0x10

23          
24      }
   0x08048557 :   lea    esp,[ebp-0x8]
   0x0804855a :   pop    ecx
   0x0804855b :   pop    ebx
   0x0804855c :   pop    ebp
   0x0804855d :   lea    esp,[ecx-0x4]
   0x08048560 :   ret    

End of assembler dump.

Now lets take a look at the same program on 64-bit Mint

(gdb) disass /m main
Dump of assembler code for function main:
5	int main(int argc, char *argv[]) {
   0x00000000004005b6 <+0>:	push   rbp
   0x00000000004005b7 <+1>:	mov    rbp,rsp
   0x00000000004005ba <+4>:	sub    rsp,0x30
   0x00000000004005be <+8>:	mov    DWORD PTR [rbp-0x24],edi
   0x00000000004005c1 <+11>:	mov    QWORD PTR [rbp-0x30],rsi

6	
7	  int value = 5;
   0x00000000004005c5 <+15>:	mov    DWORD PTR [rbp-0x4],0x5

8	  char buffer_one[8], buffer_two[8];
9	
10	  strcpy(buffer_one, "one");
   0x00000000004005cc <+22>:	lea    rax,[rbp-0x10]
   0x00000000004005d0 <+26>:	mov    DWORD PTR [rax],0x656e6f

11	  strcpy(buffer_two, "two");
   0x00000000004005d6 <+32>:	lea    rax,[rbp-0x20]
   0x00000000004005da <+36>:	mov    DWORD PTR [rax],0x6f7774

12	
---Type  to continue, or q  to quit---
13	  printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
   0x00000000004005e0 <+42>:	lea    rdx,[rbp-0x20]
   0x00000000004005e4 <+46>:	lea    rax,[rbp-0x20]
   0x00000000004005e8 <+50>:	mov    rsi,rax
   0x00000000004005eb <+53>:	mov    edi,0x400758
   0x00000000004005f0 <+58>:	mov    eax,0x0
   0x00000000004005f5 <+63>:	call   0x400490 <printf@plt>

14	  printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
   0x00000000004005fa <+68>:	lea    rdx,[rbp-0x10]
   0x00000000004005fe <+72>:	lea    rax,[rbp-0x10]
   0x0000000000400602 <+76>:	mov    rsi,rax
   0x0000000000400605 <+79>:	mov    edi,0x400788
   0x000000000040060a <+84>:	mov    eax,0x0
   0x000000000040060f <+89>:	call   0x400490 <printf@plt>

15	  printf("[BEFORE] value is at %p and is %d (0x%08x)\n", &value, value, value);
   0x0000000000400614 <+94>:	mov    ecx,DWORD PTR [rbp-0x4]
   0x0000000000400617 <+97>:	mov    edx,DWORD PTR [rbp-0x4]
   0x000000000040061a <+100>:	lea    rax,[rbp-0x4]
---Type  to continue, or q  to quit---
   0x000000000040061e <+104>:	mov    rsi,rax
   0x0000000000400621 <+107>:	mov    edi,0x4007b8
   0x0000000000400626 <+112>:	mov    eax,0x0
   0x000000000040062b <+117>:	call   0x400490 <printf@plt>

16	
17	  printf("[STRCPY] copying %d bytes into buffer_two\n\n", strlen(argv[1]));
   0x0000000000400630 <+122>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400634 <+126>:	add    rax,0x8
   0x0000000000400638 <+130>:	mov    rax,QWORD PTR [rax]
   0x000000000040063b <+133>:	mov    rdi,rax
   0x000000000040063e <+136>:	call   0x400480 <strlen@plt>
   0x0000000000400643 <+141>:	mov    rsi,rax
   0x0000000000400646 <+144>:	mov    edi,0x4007e8
   0x000000000040064b <+149>:	mov    eax,0x0
   0x0000000000400650 <+154>:	call   0x400490 <printf@plt>

18	  strcpy(buffer_two, argv[1]);
   0x0000000000400655 <+159>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400659 <+163>:	add    rax,0x8
   0x000000000040065d <+167>:	mov    rdx,QWORD PTR [rax]
   0x0000000000400660 <+170>:	lea    rax,[rbp-0x20]
---Type  to continue, or q  to quit---
   0x0000000000400664 <+174>:	mov    rsi,rdx
   0x0000000000400667 <+177>:	mov    rdi,rax
   0x000000000040066a <+180>:	call   0x400470 <strcpy@plt>

19	
20	  printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
   0x000000000040066f <+185>:	lea    rdx,[rbp-0x20]
   0x0000000000400673 <+189>:	lea    rax,[rbp-0x20]
   0x0000000000400677 <+193>:	mov    rsi,rax
   0x000000000040067a <+196>:	mov    edi,0x400818
   0x000000000040067f <+201>:	mov    eax,0x0
   0x0000000000400684 <+206>:	call   0x400490 <printf@plt>

21	  printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
   0x0000000000400689 <+211>:	lea    rdx,[rbp-0x10]
   0x000000000040068d <+215>:	lea    rax,[rbp-0x10]
   0x0000000000400691 <+219>:	mov    rsi,rax
   0x0000000000400694 <+222>:	mov    edi,0x400848
   0x0000000000400699 <+227>:	mov    eax,0x0
   0x000000000040069e <+232>:	call   0x400490 <printf@plt>

---Type  to continue, or q  to quit---
22	  printf("[AFTER] value is at %p and is %d (0x%08x)\n", &value, value, value);
   0x00000000004006a3 <+237>:	mov    ecx,DWORD PTR [rbp-0x4]
   0x00000000004006a6 <+240>:	mov    edx,DWORD PTR [rbp-0x4]
   0x00000000004006a9 <+243>:	lea    rax,[rbp-0x4]
   0x00000000004006ad <+247>:	mov    rsi,rax
   0x00000000004006b0 <+250>:	mov    edi,0x400878
   0x00000000004006b5 <+255>:	mov    eax,0x0
   0x00000000004006ba <+260>:	call   0x400490 <printf@plt>
   0x00000000004006bf <+265>:	mov    eax,0x0

23	
24	
25	
26	}
   0x00000000004006c4 <+270>:	leave  
   0x00000000004006c5 <+271>:	ret    

End of assembler dump.

The first thing that should jump out is the memory addresses.  As expected they are different and the ones in the 64-bit example are 64-bits wide.  We also have a mix of differently named registers.   Notice the r– registers.  That’s because 64-big registers begin with r, but we are also still using some 32-bit registers in our program.  The use of the 32-bit registers works the same as when we used the 16-bit registers in programs previously.

Right now we are concerned with the stack and the buffers we are trying to overflow.  Notice that the 64-bit stack set up is different than the 32-bit stack setup.  There are some pretty significant differences.  For example we are pushing registers on the stack in the initialization for the 32-bit version, but we are not pushing registers on the stack in the 64-bit version.  That’s a very good sign that there is a lot going on there we haven’t covered yet.  I’ll leave going through each piece bit by bit to how much you want to pick it apart.  We will keep taking a look at our program and see what we can logically deduce from the assembly and memory.

Buffers

Lets focus on how the buffers are constructed to see if we can determine anything from the assembly.

8           char buffer_one[8], buffer_two[8];
9
10          strcpy(buffer_one, "one"); //Put "one" in buffer one
   0x08048476 :    lea    eax,[ebp-0x14]
   0x08048479 :    mov    DWORD PTR [eax],0x656e6f

11          strcpy(buffer_two, "two"); //Put "two" in buffer two
   0x0804847f :    lea    eax,[ebp-0x1c]
   0x08048482 :    mov    DWORD PTR [eax],0x6f7774
8	  char buffer_one[8], buffer_two[8];
9	
10	  strcpy(buffer_one, "one");
   0x00000000004005cc <+22>:	lea    rax,[rbp-0x10]
   0x00000000004005d0 <+26>:	mov    DWORD PTR [rax],0x656e6f

11	  strcpy(buffer_two, "two");
   0x00000000004005d6 <+32>:	lea    rax,[rbp-0x20]
   0x00000000004005da <+36>:	mov    DWORD PTR [rax],0x6f7774

At first glance the structure seems very similar but there are things we need to evaluate.  We first load the effective address for the starting point of buffer_one.  In the 32-bit program this is ebp-0x14 for buffer_one.  The 64-bit version we subtract 0x10.  So thats a slight difference, and there’s a slight difference in the size of the move for buffer_two as well, this case the 64-bit version moves by 32 and the 32-bit version moves 28.

The next thing we see is that we are placing a 32-bit value in eax and rax for 32-bit and 64-bit respectively.  That could be where the difference is coming from.  We are completely filling the eax register whereas with rax we are only filling half of it.  We have 32-bits of the register left over.  Thats an idea so lets explore it a little bit.  What happens if we add to our input string to compensate for the difference in register size.

For each buffer we have a difference of 32-bits, i.e. 4 bytes.  We need to do some conversions here to get it right.  Remember that ASCII values consist of 2 hex values which translates to 1 byte of information.  We have two buffers and we need to add 32-bits for each of them which translates to a total of 8 bytes total.

$ ./overflow $(perl -e 'print "A"x26')
[BEFORE] buffer_two is at 0x7ffd4b008780 and contains 'two'
[BEFORE] buffer_one is at 0x7ffd4b008790 and contains 'one'
[BEFORE] value is at 0x7ffd4b00879c and is 5 (0x00000005)
[STRCPY] copying 26 bytes into buffer_two

[AFTER] buffer_two is at 0x7ffd4b008780 and contains 'AAAAAAAAAAAAAAAAAAAAAAAAAA'
[AFTER] buffer_one is at 0x7ffd4b008790 and contains 'AAAAAAAAAA'
[AFTER] value is at 0x7ffd4b00879c and is 5 (0x00000005)

Well that got us into the same amount of overflow into buffer_one as we had in the 32-bit version but it didn’t get us into value yet. If we add another four bytes, that is four more ‘A’ entries then we get our overflow as we had in the 32-bit version.

$ ./overflow $(perl -e 'print "A"x30')
[BEFORE] buffer_two is at 0x7ffc5c9dd130 and contains 'two'
[BEFORE] buffer_one is at 0x7ffc5c9dd140 and contains 'one'
[BEFORE] value is at 0x7ffc5c9dd14c and is 5 (0x00000005)
[STRCPY] copying 30 bytes into buffer_two

[AFTER] buffer_two is at 0x7ffc5c9dd130 and contains 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[AFTER] buffer_one is at 0x7ffc5c9dd140 and contains 'AAAAAAAAAAAAAA'
[AFTER] value is at 0x7ffc5c9dd14c and is 16705 (0x00004141)

What have we learned so far?  We’ve learned that our buffers are allocated on the stack in a different manner.  Our code is the same, each buffer has 8 bytes allocated to it.  That tells us that the amount allocated on the stack for each buffer should be the same for each buffer in the 64-bit version as well.  We see from the output that if we remove three bytes we will stay within buffer_one and not overflow into value.  So why does it take 27 bytes to fill two 8 byte buffers?   We will look at the memory next time and see if we can’t figure out the answer to that question.

Conclusion

We have started taking a look into the differences between the 32-bit and 64-bit architectures from the perspective of the buffer overflow example from HTAE.  I’ve left this at a bit of a cliff hanger because I want to let you think over the structure of the stack a little bit.  We could start digging into documentation and leave the blind analysis aside, but I’m going through this for several reasons.

The first is to work on building intuition.  To build intuition we need to get hands on experience evaluating what we are working with and seeing where our natural thinking leads us astray.  If we are always told the answer right away we lose critical thinking skills.  If we are wrong the first time we will probably remember it the second time as well.

The next reason is to build a logical process for breaking down an exploit into its functional parts.  We need to be able to break down an exploit when there’s no documentation for it.  Practice practice practice.  If we want to become experts on exploits and how they operate we need to look at them in as much detail as possible.  We need to become experts at the assembly of the exploits, and the data structures, and how they are different in different architectures.  To do that we need to look at as much of the code as we possibly can.

The last reason is that we are doing all of this to learn to create exploits in the form of proof of concepts etc.  If we are developing our own exploit nobody will have written how the exploit works on different architectures and in different scenarios.  It will be up to us to figure it out.  We will have to learn the way the exploits operate in various architectures to create mitigations for them as well.

Next time we are going to take a look at both of these programs executing in memory and see what we can determine from there, maybe we will see what the difference is based on the memory structure.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s