In this post, I will be covering the first (of many) stack challenges on the Phoenix VM from exploit education. I will try to beat every challenge without help from other write-ups or papers, but I will be referring to the radare2 documentation for a lot of these challenges.
Phoenix introduces basic memory corruption issues such as buffer overflows, format strings and heap exploitation without any form of modern exploit mitigation systems enabled. It has both 32 bit and 64 bit levels available, for both X86 and ARM systems.
In this post I will be focusing on the amd64 version of the
The following is the source code for the vulnerable binary
Assuming you know a little C, you can see by checking the code above that the file is fairly straight forward. Starting at function main (line 23), the first thing that is created is a struct datatype by the name of locals, that contains 2 members, a character array of 64 bytes and a integer with the
volatile qualifier prefix, as seen below.
In a struct datatype, the member variables are usually right next to one another in memory when declared as seen in the above image. The image shown below is a visual representation of the struct members in memory.
It is also important to note the
volatile keyword in the above integer declaration, as the
volatile keyword has special significance here. You might be thinking… what the fuck does “volatile” even mean in this context?!
The volatile keyword is intended to prevent the compiler from applying any optimizations on objects that can change in ways that cannot be determined by the compiler.
Objects declared as volatile are omitted from optimization because their values can be changed by code outside the scope of current code at any time. The system always reads the current value of a volatile object from the memory location rather than keeping its value in temporary register at the point it is requested, even if a previous instruction asked for a value from the same object. So the simple question is, how can value of a variable change in such a way that compiler cannot predict.https://www.geeksforgeeks.org/understanding-volatile-qualifier-in-c/
I think the “volatile” keyword is basically there to make sure the compiler optimizations don’t move the
changeme variable too far outside the
gets() overflow stack (since its an overflow).
Below you can see the next section of code starting at line 31, sets the
locals.changeme variable to 0, then instantly asks for user input using the
The user-input received by
gets() will be stored in the
locals.buffer variable which contains 64 bytes of space. However, using the
gets() function is considered insecure due to its lack of bounds checking when writing data into a fixed sized variable, like
locals.buffer for instance, so overwriting the buffer is possible in this scenario. You will also notice the
locals struct has the vulnerable buffer, right next to the variable we want to change… see where I am going with this 😀
It looks to me that I should be able to send a large buffer (more than 64 bytes) to the
gets() function, and it should overwrite data going past the
locals.buffer, and into the
locals.changeme address space.
Before taking a look at the disassembly of the
stack-zero binary, let’s run it from the cli to see what is happening from the client point of view.
The program presents a stdin buffer to send whatever data you want to the program which will be stored in
locals.buffer as seen in source code above. This stdin buffer is what I am going to abuse in order to overwrite the
locals.changeme variable in memory.
To get more information about the file, I am going to run the
rabin2 tool against the
stack-zero binary to get a better idea of what features or protections the binary may be using.
The output provided by
rabin2 lists detailed information about the binary including any security mechanisms that may be enabled, plus a lot more including byte alignment, endianness, and even the language the binary was written in. Before each challenge it is a good idea to make sure you check what security mechanisms are enabled for the binary.
Another way to check what security mechanisms the binary may be using is the
checksec command which outputs useful information about the file’s security as seen below.
As you can see in both of the above results, this specific binary is not using any type of security mitigation and should be a fairly straight forward stack overflow.
Next thing to do is open the binary in radare2 by using the shortcut
r2 command, with the
-d flag as seen below.
After loading the
r2 dumps us at the prompt which is shown above in yellow. The above output also displays the PID of the process, the starting location in memory, and the assembly bits.
Next, I want to analyze all the referenced code by issuing the
aaa command within the
r2 prompt as seen below.
aaa command will analyze all symbols and entry points.
The most common radare2 analysis command sequence ishttps://radare.gitbooks.io/radare2book/content/analysis/code_analysis.html
aa, which stands for “analyze all”. That all is referring to all symbols and entry-points. If your binary is stripped you will need to use other commands like
To make sure I am starting at the main function, I use the following command which seeks to the location of
main() in memory.
As you can see, the prompt address changed from
0x004006dd which is the location of the
main() function. To see the visual layout of the program, I use the
V command at the prompt, which should open up a visual viewer as seen below.
Note: You can cycle through the different views using
When using visual mode within
r2, you can see the possible logic paths the program can take depending on the conditionals provided. For instance, in the above code, you can see the conditional logic was split into their own boxes, to show you that one segment of code runs if True, the other will run if False. To exit out of visual mode, you can press the
q key which should bring you back to the
To get a better idea of how the C code compares to the assembly, I created a quick code reference using C code with ASM comments.
While reviewing the above code, it is clear to me that to affect the
je 40061c instruction, I need to overflow the
gets(). To test this overflow, I will create a
rarun2 template using a payload of 65 ‘A’ characters instead of piping data to the binary directly on the command line.
In my solutions directory, I generated the overflow string by echoing python output into a file named
stack-zero-payload.txt with the following command.
Then to create the
rarun2 template, I created another file in my solutions directory named
stack-zero.rr with the following contents.
Note: Ensure to add the rarun2 interpreter instead of bash
rarun2 template is fairly straight forward, as it lists which program to execute, and what data to send as stdin, but these are only a TINY fraction of what this tool can do. Check out the docs here for more information.
With the template created, I can use the template with
r2 by supplying the template location and the
-R option as seen below. This will load the program with the stdin ready to go when executed.
Now with everything ready to go, I am going to open up a visual view of the stack, and set a breakpoint right before the
gets() function overflow. This will allow us to step through the overflow, and visualize exactly what is happening.
Before jumping to
main() to set breakpoints, I need to analyze any symbols and entry points, and load the detected functions with
At this point I jumped into the visual viewer by using the
V command, then cycling to the window I wanted by using the
p key. The following image is the window where I am going to examine the stack while stepping through the program.
In the above visual view, the stack which is outlined in red, only shows 64 bytes by default. Since the buffer we are overwriting is 64 bytes in length, I need to view more of the stack on screen to get a better visual view of the overflow. To do this within
r2, you can press the
: key to open up a command prompt, then use the
e command to set the
stack.size variable to a higher number as seen below.
After setting the new stack size, you can now see 128 bytes in the stack instead of the 64, which makes a big difference as seen below.
The next thing to do here, is to set a breakpoint on the
0x00400609 address, which follows the call to
gets(). At this point during execution, the buffer should have overwritten 1 byte into the address space of the
changeme variable located at
[rbp - local_10h]. This is because the buffer of 64 bytes could not contain 65 bytes, letting 1 spill into the neighboring address, which is occupied by the
db command sets a breakpoint at a specific location, and the
dc command continues execution until it hits a breakpoint.
After pressing the
s key for stepping into the code after the breakpoint, the instruction loaded the
locals.buffer which overwrote the
locals.changeme variable from
0x41 as seen in purple below.
The above image shows 1 byte overflowing into the
locals.changeme variable which is shown within the stack outlined by purple. The
rax register reflects this by displaying the
0x41 which isn’t
0. Since the next instruction is a test, the test will not be equal and the
je instruction will not fire which allows us to pass the challenge. More importantly, you know exactly HOW you passed the challenge.
Allowing the execution to continue, you can see we received the output we wanted to pass the challenge.
This challenge is a good start to learning how assembly works, and how the stack is used for multiple things, including storing struct variable data which can easily overwrite its neighbors memory if insecure functions are being used, such as