Skip to content

CVE-2020-25578 and CVE-2020-25579: Some FreeBSD info leak bugs I found in 2020.

Notifications You must be signed in to change notification settings

farazsth98/freebsd-dirent-info-leak-bugs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

How did I find the bugs?

  1. Randomly decide to audit the filesystems in FreeBSD
  2. Do some research and find that the default filesystem in use is a combination of FFS and UFS
  3. Spend some time auditing ufs_create and find 0 bugs
  4. Go here and look through the commit history for the UFS file system functions
  5. Spot this commit about information being leaked through padding bytes in struct dirent objects allocated on the stack
  6. Analyze the patch and find that the patch to msdosfs_readdir is incomplete. They patched one instance of the bug, but not a second.
  7. Write a PoC to confirm that I can leak 3 bytes from the padding. Then start looking for variants.
  8. Find the variants in mqueuefs, autofs, smbfs, and tmpfs that let me leak a full 8 byte pointer. Write PoC to confirm.

Original bug

As mentioned above, the original bug I found was in msdosfs_readdir while I was analyzing the patch for the commit linked above.

The basic flow for calling readdir on FreeBSD is as follows:

#include <dirent.h>

int main(void) {
    struct dirent *dp;
    DIR *dirp;

    dirp = opendir("./somedir");
    dp = readdir(dirp);
}

Depending on the filesystem that somedir resides in, any one of many *_readdir functions in the FreeBSD kernel might be called.

The patch above adds a function called dirent_terminate which is intended to be called before a struct dirent object is to be returned to userspace (often done using the uiomove function). This function will zero out the padding bytes as well as any remaining bytes in the d_name field of the struct. The definition of struct dirent is here.

Looking at the patch, on line 1562, you can see dirent_terminate being called with the dirbuf variable as the argument. Following this, uiomove is called to copy dirbuf's contents out back to userspace. However, notice that these lines of code are within this if statement's block. The comment above this if statement explains that this branch is only taken if readdir is called on the root of the MSDOS filesystem, so we can simply skip this if statement by calling readdir in any sub-directory past the root of the filesystem.

Further down, we see another call to uiomove on line 1691. However, reading the code closely, you'll see that dirent_terminate isn't called in this instance, which means the padding bytes will remain uninitialized. Unfortunately the d_name field was zeroed out at the start of this function (here), so we can't get a bigger leak.

PoC

First, I didn't have a USB drive, so I had to figure out a way to mount an MSDOS filesystem. The following works:

$ dd if=/dev/zero of=test.img bs=512 count=256000
$ sudo mdconfig -a -t vnode -f test.img
$ sudo newfs_msdos -s 131072000 /dev/md1 # My mdconfig returned md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir

The PoC can be found in original_poc.c. Simply compile with clang and run it from the same directory as the commands above, and you will see the leaked bytes being printed out.

Variants

I started looking for variants of this. I think I just grepped for uiomove\(&.*, which returned around 15-20 results, and I just checked all of them by hand. Unfortunately none of the variants exist in FreeBSD by default (the filesystems have to be manually enabled / compiled into the kernel). The functions with the variants are as follows:

  1. mqfs_readdir
  2. tmpfs_dir_getdotdent
  3. tmpfs_dir_getdotdotdent
  4. smbfs_readvdir
  5. autofs_readdir_one

The bug is the exact same in all of these functions, so I'll just cover mqfs_readdir.

  1. First, a struct dirent entry is allocated on the stack
  2. Next, dirent_terminate is called to zero out the padding + d_name fields of the struct
  3. Finally, vfs_read_dirent is called. This function will call uiomove to copy the struct out to userspace

Everything looks good so far, right? Not necessarily. We have to ensure all the struct's fields are initialized. If you look closely at the code, you'll see that the d_off field is left uninitialized. The type of this field is off_t, which is essentially an int64_t. When the struct is copied out to userspace, we get uninitialized data in this field.

PoC

This same PoC will work for all of the variants, you just have to run it on a different filesystem. For mqueuefs, do the following (needs mqueuefs enabled / compiled into the kernel first):

$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp

The PoC itself can be found in variants_poc.c. Simply compile with clang and run it from the same directory as the commands above. You will see kernel pointers being printed out (presumably one stack pointer and one code section / heap pointer, I didn't check).

About

CVE-2020-25578 and CVE-2020-25579: Some FreeBSD info leak bugs I found in 2020.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages