This challenge takes our overwrite further and performs a Buffer Overflow. Let's look at the challenge description:
We need to overwrite the return pointer. First, let's understand what the return pointer does.
Something about this doesn't seem right. We shouldn't be allowed to modify the return pointer without restriction. This is due to a vulnerability in the program known as a buffer overflow or stack smashing. To understand the vulnerability, we must dive into GDB.
I will skip right to the disassembly of main
; however, I recommend following the standard procedure of running info functions
first to see what else might be valuable.
We take a close look at the fgets
call, which gets our input:
0x000000000040159e <+222>: mov rdx,QWORD PTR [rip+0x2acb] # 0x404070 <stdin@GLIBC_2.2.5> 0x00000000004015a5 <+229>: lea rax,[rbp-0x20] 0x00000000004015a9 <+233>: mov esi,0x32 0x00000000004015ae <+238>: mov rdi,rax 0x00000000004015b1 <+241>: call 0x401070 <fgets@plt>
Here, we notice something super interesting. We start writing our data at rbp-0x20
(please ensure you know why!), but we can write 0x32
bytes. This means we're allowed to write more bytes than space allows! This is the buffer overflow vulnerability. We can write more data than the space allotted, meaning we can overwrite critical information for the binary.
So, what's there to overwrite? To answer this, we must understand how function calls work.
Binaries start at a defined function called _start
. This is called the binary's entry point. We often ignore this entry point in favor of main
, which we typically consider the entry point. _start
calls main
, so this isn't a horrible assumption to make. When the call
function is used, two vital things happen:
call
statement.Pushing the return pointer is essential to understanding the layout of our current stack frame. Once this happens, any method with a stack frame will have the following header:
4014c0: 55 push rbp 4014c1: 48 89 e5 mov rbp,rsp 4014c4: 48 83 ec 20 sub rsp,0x20
The value in the sub
statement changes based on the method. This is the size of the stack frame created. We also see before that, rbp
is pushed on the stack. We noticed from the first two challenges that all stack variables are referenced relative to rbp
, meaning rbp
's location is very important. This makes our stack frame:
| --------- stack space --------- | rbp | return address |
The inverse of this happens at the end of the method:
4015ff: b8 00 00 00 00 mov eax,0x0 401604: c9 leave 401605: c3 ret
rax
serves as the return register, meaning the calling function will look at rax
after the function call to check for returns. leave
is a keyword for mov rsp, rbp; pop rbp
, which inverts the operations done at the top of the method. Then we have ret
.
ret
takes whatever is at the top of the stack at the time and goes there. This is a logical operation since the setup and tear-down of stack frames leave the return address on the top of the stack as expected. However, if we stack smash, we can change this address and force the instruction pointer to go there. It's important to note that ret does not validate that the value wasn't changed; it blindly goes there.
Therefore, if we can overwrite all the data leading up to the return pointer and then overwrite the return pointer, we can hijack execution and go elsewhere. This is what buffer overflow vulnerabilities are all about. They aren't much different than the variable overwrite but are more dangerous.
Our payload will look similar to the payload from the last exercise. The first thing we need is our padding, or the distance from where we start writing until the return pointer. The visualization gives us that hint, but let's use the Assembly to confirm our guess.
Here is the fgets
call we're interested in:
0x000000000040159e <+222>: mov rdx,QWORD PTR [rip+0x2acb] # 0x404070 <stdin@GLIBC_2.2.5> 0x00000000004015a5 <+229>: lea rax,[rbp-0x20] 0x00000000004015a9 <+233>: mov esi,0x32 0x00000000004015ae <+238>: mov rdi,rax 0x00000000004015b1 <+241>: call 0x401070 <fgets@plt>
From this, we gather that we start writing at rbp-0x20
. Now, where is the return pointer? Using our stack frame visualization, we see it's the next block after rbp
. Since rbp
is 8 bytes long, the return pointer is at rbp+0x8
. This makes our padding . Now, we need to figure out where to go. We're instructed to go to win()
, which has an address in memory. There are three ways to figure that out:
readelf
: This displays information about ELF (Linux executable) files. The -s
flag provides section headers. Therefore, we can use readelf -s chall | grep win
to filter for the win()
function.gdb
: We can use info functions win
inside gdb
to filter for the win()
function. Or, if we don't want to go inside GDB, use echo "info functions win" | gdb chall
.objdump
: This provides the disassembly. We can filter for the win()
function here, too, using objdump -d chall | grep win
.We see all of these yield the same address 0x4014aa
:
We now need to pack the address in 64-bit little-endian format. If you're using pwntools
, use p64(0x4014aa)
to pack it. Otherwise, we can pack by hand: \xaa\x14\x40\x00\x00\x00\x00\x00
. Finally, we put the payload together and send it to the binary:
win()
, the instruction pointer will pull whatever's on top of the stack for the return pointer. Since we jumped from main()
, which doesn't have a stack frame below it, there's no return pointer for win()
to use. The instruction pointer ends up taking a random value and jumping there, then recognizes there's no valid instruction to execute, and crashes.