Security incident? Suspected breach? 09 71 18 27 69csirt@synacktiv.com

Surviving the surge of new Linux LPE : Defense in Depth not dead

Written by Romain Huon - 29/05/2026 - in Systems - Download

Thanks to AI-assisted vulnerability research and kernel patch diffing that breaks "responsible disclosure" embargos, it's quite the overwhelming time for defenders. There's been a weekly reveal of new Linux critical vulnerabilities, with full exploit scripts made public days before patchs are widely available.

Yet, most of the exploitation chains that have been recently published can be mitigated by tried-and-true Linux security hardening, giving wary defenders time to patch while N-day attackers try their shiny new ./exploit.sh.

Let's review some of them !

Looking to improve your skills? Discover our trainings sessions! Learn more.

Hardened permissions on setuid binaries

When Crackarmor CVEs dropped on a Friday morning, we feverishly read Qualys' outstanding report  describing the multiple vulnerabilities they found in AppArmor Linux Security Module. We quickly realized two things:
- This was very serious, and being Debian + AppArmor aficionados ourselves, we had to patch many of our own systems, but the patched kernel wasn't available yet in Debian repositories
- We already had a mitigation in place for the main LPE/exploit scenario they wrote, which was using 'su' (and then, in a secondary LPE scenario, 'sudo') to trigger the now-famous "confused deputy problem" and write arbitrary content into /sys/kernel/apparmor/ pseudo-files .

On a default installation of a Debian / Ubuntu system, permissions on su & sudo binaries are like this:

$ ls -l $(which su{,do})
-rwsr-xr-x 1 root root  /usr/bin/su
-rwsr-xr-x 1 root root  /usr/bin/sudo

Which means that any user on the system can try to invoke su or sudo.
Any user, so that means even ntpd, or dnmasq, avahi, saned or even "nobody" users, aka the ones that run network-exposed processes.
Since anyone can exec the (setuid) binary, it's then only up to the program's logic and configuration files to decide what happens.
This of course means that if there is any configuration misshap or logic flaw in these touchy programs, they could be used to do something nasty. There's already been a steady history of CVEs regarding those programs.

Last but not least, with Crackarmor, there's no need for a vulnerable su configuration or binary, since the exploitation path uses su only to become... oneself (e.g you enter your own password).

Now, on our hardened systems, the POSIX permissions for those binaries are more like this:

$ ls -l $(which su{,do})
-rwsr-x--- 1 root admins  /usr/bin/su
-rwsr-x--- 1 root admins  /usr/bin/sudo

which means that if your local user is not already root, or a (distinguished) member of the "admins" group (a non-default one we created), you won't be able to exec su or sudo binaries AT ALL, nor even be able to open a filesystem handle to them (which can be especially useful in hindering filesystem-cache based attacks like the "Dirty" family)

This mitigates Crackarmor's impact a lot, since you first need to be a somewhat privileged/trusted user on these systems to try & gain LPE using the exploitation path Qualys described  (and so chances are, you could already legitimately become root anyway).

As a bonus, even if the sudo configuration was a fully opened (ALL : ALL) NOPASSWD: ALL, it's not exploitable if you can't launch sudo at all...

Of course this is *just a mitigation*: if you don't patch the kernel (and reboot :)), its AppArmor stack remains vulnerable, and for example there's still the possibility that other setuid binaries on your system could be used, instead of su, to trigger the "confused deputy" problem (although our security researchers did not find another one - we also tend to restrict most setuid binaries). 
But this is exactly what "defense in depth" concept is there for: buy defenders time in the asymetric war they fight against attackers, be them 0-day hackers, 1-day criminals, or even friendly pentesters. 

Note that the Copy Fail (CVE-2026-31431) public PoC also used su to LPE and so could be mitigated by this hardening.

Implementing this mitigation

If you like to reproduce this approach, be warned that simply doing a chmod/chgrp on the binaries won't last for long. At the next release of these packages (let's say, for a security fix), Debian's dpkg package manager will reapply the default permissions (taken from the package) and the depth of your defense against exploitation of those binaries will again become very shallow. Directly modifying files shipped by packages is not a good practice anyway, and tools like debsums that check installed packages integrity would complain.

The dpkg-correct way to *permanently* change the local ACLs on a packaged file is to use dpkg-statoverride like this:

dpkg-statoverride --update --add root admins 4750 /usr/bin/su{,do}

This is a very helpful command to know when hardening a dpkg-based OS like Debian or Ubuntu, and is easily wrappable in your playbooks.  

Its man page even says it all: setuid binaries are a prime target for a bit of ACL hardening. Remove their setuid bit if you can do without (like ping in Debian 13) or make them only executable by a certain group.

 

Kernel modules allow-listing

By default, Linux ships hundred of "optional" kernel modules and will happily load them dynamically whenever a program makes a syscall that requires it.

This is user-friendly, but also quite dangerous. Vulnerabilities have often been found inside niche, lesser-known (so lesser-audited) kernel modules.

Moreover, with the advent of unprivileged user namespaces being now enabled by default on major distros, it's easy for unprivileged users to create a namespace inside which they can call privileged syscalls, and thus access vulnerable kernel code paths that where previously reachable only to the 'real' root user.

And sometimes you don't even need that, if the vulnerabilities lie in code paths callable from unprivileged processes.

In the last batch of Linux kernel LPE exploits, many do make syscalls to vulnerable kernel modules that you might not *really* need on your system:

  • Copy Fail creates AF_ALG sockets to exploit algif_aead. This module enable programs to offload to the kernel some crypto operations on their behalf, but it seems most software that uses it can graciously fall back to other mechanisms.
  • Dirty Frag relies in vulnerabilities found inside esp4 and esp6 modules that implement IPSec protocol. So unless your system is a VPN gateway using IPSec, you probably don't need them loaded. One additional vulnerabilty (used in case unprivileged user namespace creation is not available) lies in AF_RXRPC sockets provided by an rpxrpc module used for the "Andrew File Protocol (AFS)" which is a kind of distributed filesystem alternative to NFS, likely not used on most systems (although apparently Ubuntu loads this module by default).

The official mitigations all suggest to block the affected kernel modules, which works.

But if you want to be one step ahead of the next attacks, it would be preferable to adopt an holistic, minimalistic  "allowlist" approach and only allow the kernel to load the specific modules your system has a known, legitimate reason to use.

It's easier said than done, though.

One way is to compile custom kernels, enabling in the config only the modules you really want, and disabling the lazy loading of all others (replacing all those 'm's with 'n's). This is a very sound approach but not doable for everyone, since you then give up onyour distro's prebuilt, binary-packaged kernels and will have to resolve compilation issues and apply security patches yourself. Each different typology of your systems would also need a different kernel build, e.g. some with IPsec enabled, some without...

A lighter way to achieve kernel modules allow-listing is to leverage the /proc/sys/kernel/modules_disabled sysctl.
Once root writes a '1' to this runtime configuration knob, the kernel will be locked down to currently loaded modules, and any new module's modprobe request will be denied.

Note that the only way to allow further modules loading will then be to reboot the system entirely: once the lockdown is set up, there's no runtime way to switch it off. 

Implementing this mitigation

So the idea here is to let your system boot and load all its really required modules as each service starts, then at some point to run

 echo 1 > /proc/sys/kernel/modules_disabled

to lock everything down. This can be implemented by a simple systemd unit that runs last in the bootchain, or even called by a systemd timer that runs after X minutes of uptime.

[Unit]
Description=Kernel modules final lockdown
After=default.target # adjust if needed

[Service]
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo 1 > /proc/sys/kernel/modules_disabled'

[Install]
WantedBy=multi-user.target

This is just a draft, don't forget to run shh to harden the unit  ;)

The caveat is of course that any program that relies on modules that were not loaded at the time you set up the lockdown will fail. The error code when the automatic modprobe  call fails will then be typically some flavour of "file not found" (and not "permission denied"), so it can be misleading when debugging. So it's not totally easy-mode either, and you might have to do some trial & error, and maybe pre-emptively load some known-useful modules at boot by listing them in /etc/modules-load.d/.

Anyway, if you manage to do it, congratulations: you've switched from a "default-allow" to a "default-block" Linux kernel module configuration, that should buy your blue team some time against exploits, and without having to build kernels yourself.

A smoother, finer-grained approach could very well be inspired by what Cloudflare described in their blog article about how they handled Copy Fail. They used their in-house eBPF tooling to hook socket_bind syscalls and allow AF_ALG socket creation only to programs that had already done it before.

To widen this approach, one could write an eBPF LSM hook for __request_module() and call_modprobe() kernel functions that would run some control logic, like checking an allowlist of modules and/or caller processes, and then allow or deny on-the-fly the new module insertion.

There's already been some discussions about this subject around Linux security mailing lists and forums, so an unified approach may very well emerge soon.