Talos Vulnerability Report

TALOS-2019-0887

E2fsprogs quotaio_tree.c report_tree() code execution vulnerability

September 24, 2019
CVE Number

CVE-2019-5094

Summary

An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.

Tested Versions

E2fsprogs 1.43.3 - 1.45.3

Product URLs

http://e2fsprogs.sourceforge.net/

CVSSv3 Score

7.5 - CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-787: Out-of-bounds Write

Details

E2fsprogs is a set of programs for interacting with ext2, ext3, and ext4 filesystems, and is considered essential software for Linux and Unix-like operating systems. As such, it ships by default on most Linux distributions.

The quota feature of ext4 filesystems are hidden inodes on an ext4 partition that record usage statistics and can implement quotas on the file system. This feature has been included on e2fsprogs since version 1.42, according to the release notes. They need to be enabled separately with the --enable-quota flag until about version 1.43.0, at which point quota support was enabled by default.

On mount of an ext4 partition, the system will measure the usage and limits of the disk and subsequently update the quota inodes on disk if there have been any updates. In order to do this, e2fsprogs will start by scanning all the quota inodes with qtree_scan_dquots (as they are stored in a qtree data structure):

/* Structure for one loaded quota */
struct dquot {
    struct dquot *dq_next;  /* Pointer to next dquot in the list */
    qid_t dq_id;        /* ID dquot belongs to */
    int dq_flags;       /* Some flags for utils */
    struct quota_handle *dq_h;  /* Handle of quotafile for this dquot */
    struct util_dqblk dq_dqb;   /* Parsed data of dquot */
};

int qtree_scan_dquots(struct quota_handle *h,
          int (*process_dquot) (struct dquot *, void *),
          void *data) {
    char *bitmap;
    struct v2_mem_dqinfo *v2info = &h->qh_info.u.v2_mdqi;
    struct qtree_mem_dqinfo *info = &v2info->dqi_qtree;
    struct dquot *dquot = get_empty_dquot();                               // [1]

    if (!dquot)
        return -1;

    dquot->dq_h = h;             
    if (ext2fs_get_memzero((info->dqi_blocks + 7) >> 3, &bitmap)) {        // [2]
        ext2fs_free_mem(&dquot);
        return -1;
    }
    v2info->dqi_used_entries = report_tree(dquot, QT_TREEOFF, 0, 
                               bitmap, process_dquot, data);               // [3]
    v2info->dqi_data_blocks = find_set_bits(bitmap, info->dqi_blocks);
    ext2fs_free_mem(&bitmap);
    ext2fs_free_mem(&dquot);
    return 0;
}

There's an empty dquot structure initialized at [1] followed by a bitmap malloc at [2] whose size is derived from info->dqi_blocks. After this, the program will then gather the usage information with report_tree at [3]. It's important to know note that the info->dqi_blocks field is not set, as it is read from disk. The quota inode is opened (which is discovered from the partition's superblock) and then reads the first 8 bytes from the given inode block disk here:

#0  0x00007f5a4435e2c0 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00005604480124e1 in unix_read_blk64 (channel=0x56044986a640, block=<optimized out>, count=0x1, buf=<optimized out>) at unix_io.c:881
#2  0x0000560447ffd675 in load_buffer (file=file@entry=0x560449886b40, dontfill=dontfill@entry=0x0) at fileio.c:223
#3  0x0000560447ffdb3a in ext2fs_file_read (file=file@entry=0x560449886b40, buf=buf@entry=0x7ffe7d86bd6c, wanted=wanted@entry=0x8, got=got@entry=0x7ffe7d86bd3c) at fileio.c:305
#4  0x0000560447fec27e in quota_read_nomount (qf=<optimized out>, offset=<optimized out>, buf=0x7ffe7d86bd6c, size=0x8) at quotaio.c:201
#5  0x0000560447fecc11 in quota_file_open (qctx=qctx@entry=0x5604498753c0, h=h@entry=0x7ffe7d86be10, qf_ino=0x20, qf_ino@entry=0x0, qtype=PRJQUOTA, fmt=0x4, fmt@entry=0xffffffff, flags=flags@entry=0x0) at quotaio.c:274
#6  0x0000560447fe9df4 in quota_compare_and_update (qctx=0x5604498753c0, qtype=<optimized out>, usage_inconsistent=0x7ffe7d86c080) at mkquota.c:660
#7  0x0000560447fc7948 in main (argc=<optimized out>, argv=<optimized out>) at unix.c:1906

Soon after, the program will begin to read the rest of the needed info to parse the quota file:

/*
 * Copy dqinfo from disk to memory
 */
static inline void v2_disk2memdqinfo(struct util_dqinfo *m, struct v2_disk_dqinfo *d)
{
    m->dqi_bgrace = ext2fs_le32_to_cpu(d->dqi_bgrace);
    m->dqi_igrace = ext2fs_le32_to_cpu(d->dqi_igrace);
    m->u.v2_mdqi.dqi_flags = ext2fs_le32_to_cpu(d->dqi_flags) & V2_DQF_MASK;
    m->u.v2_mdqi.dqi_qtree.dqi_blocks = ext2fs_le32_to_cpu(d->dqi_blocks);     // [1]
    m->u.v2_mdqi.dqi_qtree.dqi_free_blk = ext2fs_le32_to_cpu(d->dqi_free_blk);
    m->u.v2_mdqi.dqi_qtree.dqi_free_entry = ext2fs_le32_to_cpu(d->dqi_free_entry);
}

As shown above, the (struct *v2_disk_dqinfo)d->dqi_blocks field is taken from offset 0x14 of the quota inode block (as we've already read 8 bytes). The report_tree function is what gathers the usage info of the partition for the quota:

static int report_tree(struct dquot *dquot, 
                       unsigned int blk, 
                       int depth,
                       char *bitmap,
                       int (*process_dquot) (struct dquot *, void *),
                       void *data){
    int entries = 0, i;
    dqbuf_t buf = getdqbuf();        // malloc(0x400)
    __le32 *ref = (__le32 *) buf;

    if (!buf)
        return 0;

    read_blk(dquot->dq_h, blk, buf);                                                          // [1]
    if (depth == QT_TREEDEPTH - 1) {  //  => (4-1)
        for (i = 0; i < QT_BLKSIZE >> 2; i++) { // QT_BLKSIZE => 0x400
            blk = ext2fs_le32_to_cpu(ref[i]);                                                 // [2]
            check_reference(dquot->dq_h, blk); 
            if (blk && !get_bit(bitmap, blk)){                                                // [3]
                entries += report_block(dquot, blk, bitmap, process_dquot, data);             // [4]
            }
        }
    } else {
        for (i = 0; i < QT_BLKSIZE >> 2; i++) { // QT_BLKSIZE => 0x400 
            blk = ext2fs_le32_to_cpu(ref[i]);                                                 // [2]
            if (blk) {
                check_reference(dquot->dq_h, blk);
                entries += report_tree(dquot, blk, depth + 1, bitmap, process_dquot, data);   // [5]
            }
        }
    }
    freedqbuf(buf);
    return entries;
}

At [1], the program reads a block (first iteration is block 1) out of memory and then proceeds to recursively call report_tree [5] again on each block id read at [2], continuously walking a tree of quota inodes until a max depth of 4 is reached, at which point the program does reporting and information gathering from non-quota inodes [4]. In order to save itself some processing time, a bitmap is also provided, only blocks that haven't been seen before are marked, utilizing the bitmap that is provided by the calling function qtree_scan_dquots. For the bit checking functionality, get_bit is used [3]:

/*
 * Scan all dquots in file and call callback on each
 */
#define set_bit(bmp, ind) ((bmp)[(ind) >> 3] |= (1 << ((ind) & 7)))
#define get_bit(bmp, ind) ((bmp)[(ind) >> 3] & (1 << ((ind) & 7)))

The vulnerability can now be seen since the parameters that are passed to set_bit and get_bit are get_bit(bitmap, blk). As stated once again, bitmap is a heap allocation with size of our choosing; the blk parameter is an arbitrary four-byte value read from a block of our choosing. Thus, using the get_bit macro an out-of-bounds 1-byte read will occur, returning a single bit. If this bit is not set, we enter the report_block function at [4]:

static int report_block(struct dquot *dquot, unsigned int blk, char *bitmap,
            int (*process_dquot) (struct dquot *, void *),
            void *data)
{
    struct qtree_mem_dqinfo *info = &dquot->dq_h->qh_info.u.v2_mdqi.dqi_qtree;
    dqbuf_t buf = getdqbuf();  // 1024 size chunk
    struct qt_disk_dqdbheader *dh;
    char *ddata;
    int entries, i;

    if (!buf)
        return 0;

    set_bit(bitmap, blk);                            // [1]
    read_blk(dquot->dq_h, blk, buf);
    dh = (struct qt_disk_dqdbheader *)buf;
    ddata = buf + sizeof(struct qt_disk_dqdbheader);
    entries = ext2fs_le16_to_cpu(dh->dqdh_entries);  

    for (i = 0; i < qtree_dqstr_in_blk(info); i++, ddata += info->dqi_entry_size)
        if (!qtree_entry_unused(info, ddata)) { // just checks for a set bit.
            dquot->dq_dqb.u.v2_mdqb.dqb_off = (blk << QT_BLKSIZE_BITS) +
                                              sizeof(struct qt_disk_dqdbheader) +
                                              (i * ->dqi_entry_size);
            info->dqi_ops->disk2mem_dqblk(dquot, ddata);    
            if (process_dquot(dquot, data) < 0)  // scan_dquots_callback 
                break;
        }
    freedqbuf(buf);
    return entries;
}

While most of the code above doesn't really do that much besides gather statistics, we see the set_bit macro at [1] that will set the same bit that we read before in order to enter this function, resulting in an out-of-bounds 1-bit write on the heap.
Making this vulnerability more powerful is the fact that due to the recursive and looping nature of report_tree a de facto arbitrary amount of non-contiguous bits within [0x0,(0xFFFFFFFF >> 3)] bytes after the bitmap chunk can be set to 0x1.

Crash Information

=================================================================
==18247==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000137aa at pc 0x0000006ea20c bp 0x7fffffffbc10 sp 0x7fffffffbc08
READ of size 1 at 0x6020000137aa thread T0
[Attaching after Thread 0x7ffff7fc5800 (LWP 18247) fork to child process 18283]
[New inferior 2 (process 18283)]
[Detaching after fork from parent process 18247]
[Inferior 1 (process 18247) detached]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 18283 is executing new program: /usr/lib/llvm-3.8/bin/llvm-symbolizer
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    #0 0x6ea20b in report_tree e2fsprogs/lib/support/quotaio_tree.c:611:16
    #1 0x6ea68a in report_tree e2fsprogs/lib/support/quotaio_tree.c:620:16
    #2 0x6ea68a in report_tree e2fsprogs/lib/support/quotaio_tree.c:620:16
    #3 0x6ea68a in report_tree e2fsprogs/lib/support/quotaio_tree.c:620:16
    #4 0x6e978f in qtree_scan_dquots e2fsprogs/lib/support/quotaio_tree.c:658:29
    #5 0x6dff54 in v2_scan_dquots e2fsprogs/lib/support/quotaio_v2.c:273:9
    #6 0x6bb730 in quota_compare_and_update e2fsprogs/lib/support/mkquota.c:671:8
    [...]

Address 0x6020000137aa is a wild pointer.
SUMMARY: AddressSanitizer: heap-buffer-overflow e2fsprogs/lib/support/quotaio_tree.c:611:16 in report_tree
Shadow bytes around the buggy address:
  0x0c047fffa6a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa6b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa6c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa6d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa6e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c047fffa6f0: fa fa fa fa fa[fa]fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa710: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa720: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa730: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffa740: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==18247==ABORTING
[Inferior 2 (process 18283) exited normally]

*******************************************************************************************

-------------------------------------------------------------------------[ registers ]----
$rax   : 0x000060201b381ab2 -> 0x0000000000000000
$rbx   : 0x00007ffe34c01300 -> 0x0000000045e0360e
$rcx   : 0x0000602000001200 -> 0x0200000000000003
$rdx   : 0x000060201b381ab2 -> 0x0000000000000000
$rsp   : 0x00007ffe34c012c0 -> 0x0000000041b58ab3
$rbp   : 0x00007ffe34c014f0 -> 0x00007ffe34c01730 
$rsi   : 0x000060201b381ab2 -> 0x0000000000000000
$rdi   : 0x00007ffe34bfeaff -> 0x007ffe34bff3b000
$rip   : 0x00000000006ea213 -> <report_tree+1811> movsx ecx, BYTE PTR [rax]
$r8    : 0x00007f97532d6801 -> 0x1000007f97532d68
$r9    : 0x00000000000000a9
$r10   : 0x0000000000000073
$r11   : 0x0000000000000001
$r12   : 0x0000000000000001
$r13   : 0x0000100046978428 -> 0xf2f2f200f1f1f1f1
$r14   : 0x0000000000000001
$r15   : 0x0000000000000001
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
-----------------------------------------------------------------------------[ stack ]----
0x00007ffe34c012c0|+0x00: 0x0000000041b58ab3    <-$rsp
0x00007ffe34c012c8|+0x08: 0x00000000009bce01 -> "1 32 8 7 buf:600"
0x00007ffe34c012d0|+0x10: 0x00000000006e9b00 -> <report_tree+0> push rbp
0x00007ffe34c012d8|+0x18: 0x0000100046978428 -> 0xf2f2f200f1f1f1f1
0x00007ffe34c012e0|+0x20: 0x0000619000019f80 -> 0x00000001d9c03f14 -> 0x0000000000000000
0x00007ffe34c012e8|+0x28: 0x0000000000000001
0x00007ffe34c012f0|+0x30: 0x00007ffe34c01410 -> 0x0000608000000c30 -> 0x00007ffe34c01e20 -> 0x0000000400000001 -> 0x0000000000000000
0x00007ffe34c012f8|+0x38: 0x000000000080370a -> 0x00000000a083c748 -> 0x0000000000000000
-------------------------------------------------------------------------[ boop ]----
     0x6ea1f8 <report_tree+1784> cmp    al, cl
     0x6ea1fa <report_tree+1786> jl     0x6ea20c <report_tree+1804>
     0x6ea200 <report_tree+1792> mov    rdi, QWORD PTR [rbx+0xb0]
     0x6ea207 <report_tree+1799> call   0x4c3020 <__asan::__asan_report_load1(__sanitizer::uptr)>
     0x6ea20c <report_tree+1804> mov    rax, QWORD PTR [rbx+0xb0]
->  0x6ea213 <report_tree+1811> movsx  ecx, BYTE PTR [rax]
     0x6ea216 <report_tree+1814> mov    edx, DWORD PTR [rbx+0x1cc]
     0x6ea21c <report_tree+1820> and    edx, 0x7
     0x6ea21f <report_tree+1823> cmp    edx, 0x1f
     0x6ea222 <report_tree+1826> setbe  sil
     0x6ea226 <report_tree+1830> test   sil, 0x1
-----------------------------------------------------------------------------[ trace ]----
#0  0x00000000006ea213 in report_tree (dquot=0x608000000c20, blk=0xd9c03f14, depth=0x3, bitmap=0x6020000012d0 " ", process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_tree.c:611
#1  0x00000000006ea68b in report_tree (dquot=0x608000000c20, blk=0x8, depth=0x2, bitmap=0x6020000012d0 " ", process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_tree.c:620
#2  0x00000000006ea68b in report_tree (dquot=0x608000000c20, blk=0x7, depth=0x1, bitmap=0x6020000012d0 " ", process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_tree.c:620
#3  0x00000000006ea68b in report_tree (dquot=0x608000000c20, blk=0x6, depth=0x0, bitmap=0x6020000012d0 " ", process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_tree.c:620
#4  0x00000000006e9790 in qtree_scan_dquots (h=0x7ffe34c01e20, process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_tree.c:658
#5  0x00000000006dff55 in v2_scan_dquots (h=0x7ffe34c01e20, process_dquot=0x6bbe80 <scan_dquots_callback>, data=0x7ffe34c01ec0) at quotaio_v2.c:273
#6  0x00000000006bb731 in quota_compare_and_update (qctx=0x6060000008c0, qtype=GRPQUOTA, usage_inconsistent=0x7ffe34c022f0) at mkquota.c:671
------------------------------------------------------------------------------------------
<(^_^)>

Timeline

2019-08-28 - Initial Contact
2019-08-30 - Vendor Disclosure
2019-09-16 - Vendor Patched
2019-09-24 - Public Release

Credit

Discovered by Lilith [^_^] of Cisco Talos.