A note on kROP chains for newer linux kernel versions
A (detailed) note on prepare_kernel_cred
changes in newer linux kernel versions. Additionally, a bypass to the introduced hardening update is described.
TL;DR
The changes introduced to
kernel/cred.c
starting from kernel version 6.2 (as well as LTS kernel versions) prevent callingprepare_kernel_cred
with aNULL
value for (easily) gaining root privileges.Instead, the address of
init_task
must be extracted or calculated (based on the kernel base address) and passed as an argument. The kROP chain must now callprepare_kernel_cred(&init_task)
to retrieve acred
struct with root privileges.
Background
While working with newer linux kernels and creating a kernel exploitation CTF challenge, I came across an unusual error when executing my kROP chain: Calling prepare_kernel_cred(0)
didn’t return a cred
struct as expected, but instead returned NULL. This stopped my kROP chain in-place by causing a kernel panic and resulting in a unrecoverable crash.
After searching extensively for hours on how kROP chains for newer linux look like, I came up mostly empty and the few blog posts/writeups that I did find, wouldn’t really explain what changes are required and why these needed to be done in the first place.
So finally, I chose to do the sensible thing at look at the actual linux kernel source code itself.
Why I didn’t do this at first? I really don’t know.
Nonetheless, after spending some more hours reading source code and debugging, I finally found my long-awaited answers to What changed and Why.
But now enough with the backstory, let’s jump in!
Short summary of previous kROP chains
After gaining control of RSP
register (specifically the address rsp
points to), gaining root privileges is fairly straight forward.
You can either overwrite modprobe_path
to your exploit script, overwrite the cred
struct of your current process or finally, directly call commit_creds(prepare_kernel_cred(0))
. We will exclusely look at the later as many other resources exist for the former 2 kROP techniques.
In older versions, the chain would look something like this:
- set
RDI
to0
-> apop rdi ; ret
gadget followed by0x0
- get a
cred
struct from the kernel ->prepare_kernel_cred(0)
- Note that passing
0
will give us the credentials of the first task executed by the kernel, which runs as root
- Note that passing
- move the cred struct from
RAX
toRDI
-> amov rax, rdi ; ret
gadget - replace the credentials of our current process ->
commit_creds
- return to userland (and bypass KPTI if necessary) ->
swapgs ; iretq
orswapgs_restore_regs_and_return_to_usermode
Now that we got the basics down, let’s get to the actual interesting part.
Changes to kernel functions
The changes to our important kernel function can be traced down to the following mail thread. This states the following:
A common exploit pattern for ROP attacks is to abuse prepare_kernel_cred() in order to construct escalated privileges[1]. Instead of providing a short-hand argument (NULL) to the “daemon” argument to indicate using init_cred as the base cred, require that “daemon” is always set to an actual task. Replace all existing callers that were passing NULL with &init_task.
…
Additionally, we can see the changes made to the kernel/cred.c
source code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
if (WARN_ON_ONCE(!daemon))
return NULL;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
old = get_task_cred(daemon);
*new = *old;
// ...
return new;
}
Whereas older kernel versions looked like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
validate_creds(old);
*new = *old;
// ...
return new;
}
What’s the difference?
Before, prepare_kernel_cred
would return the credentials struct for &init_cred
(Lines 12-15). This would essentially give us root privileges.
Now, however, the function will now just fail and return NULL (Lines 6 & 7).
Bypassing hardening measures
So with this information in hand, how can we modify our kROP chain to bypass this hardening measure? It’s quite simple actually.
We can simply replicate the functionality of the old prepare_kernel_cred in our kROP chain:
- Get the address of
init_task
init_task
isn’t exposed through/proc/kallsyms
but can be retrieved with gdb:1 2
gdb> p &init_task $2 = (struct task_struct *) 0xffffffff8200a900 <init_task>
Note that this requires access to debug symbols of the kernel. If you don’t have access to these symbols, you can also break on prepare_kernel_cred (
b *prepare_kernel_cred
) and get the address from the first call (remember how init_task is the first task executed by the kernel?)- If kASLR is enabled, you can calculate the offset from the base address of the kernel. The address of
init_task
should have a static offset. This might not be the case for kernels withFG-KASLR
.
- move the address into
RDI
-> this will require apop rdi ; ret
gadget followed by the address retrieved previously - call
prepare_kernel_cred(&init_task)
This bypass will require full control of the RDI
register as simply setting RDI
to 0
won’t suffice anymore. Additionally, at least a partial (if not arbitrary) read primitive is needed to get the address of init_task
.
This bypass might seem trivial - and it is. However, getting to this point took some time (at least for me).
Sidetrack: Finding useful gadgets
Work in Progess…
Conclusion
The implemented change to kernel/cred.c
and the prepare_kernel_cred
function might prevent some attacks where partial / arbitrary read primitives aren’t avaiable.
However, simply reproducing the original functionality in the kROP chain is already enough to bypass this measure.
So starting from linux 6.2, commit_creds(prepare_kernel_cred(&init_task))
is the new commit_creds(prepare_kernel_cred(0))
:D