Introducing 0xB33SM0K3R: The Ultimate eBPF Bypass Technique
TL;DR: Your eBPF tools are safe. This is tongue-in-cheek post about a general exploit technique applied to the eBPF subsystem, parodying clout chasing tweeters and overwrought corporate blogs. It could be useful for rootkits, but is not a vulnerability in and of itself.
What is it, and why should I care?
This is a technique you can use after gaining kernelspace execution to effectively silence the eBPF subsystem, without it being obvious from the outside that the subsystem has been silenced. Probes and programs will still be registered, and statistics on kprobes will still be correctly accounted. From the outside, it will appear all is well. However, no output will be generated from the eBPF programs, so tools (such as EDRs) that send data to userspace from eBPF generate no telemetry. Without telemetry, the tool is effectively bypassed.
In practice this isn’t much of a problem. If you are using eBPF based monitoring tools, a complete lack of telemetry is a glaring red flag and should be investigated. It’s also only applicable once an adversary has gained kernelspace execution, meaning you’ve missed their initial access and privilege escalation. It would take quite a bit of work to weaponize this idea into something that could actually evade detection in the real world.
I mean 0xB33SM0K3R IS A FULLY FUD EDR BYPASS THAT HACKS ANYTHING IN THE WORLD INCLUDING CROWDSTRIKE AND ALSO EVERY BIG CYBER COMPANY.
it’s just removing write protection, you just remove the WP bit, it’s just that
The actual technique we’re going to use is a pretty simple and well known post-exploitation technique. First you disable write protection from kernel text, then you modify the kernel function to do what you want instead of what it does. Wild. We’re gonna do this on x86, but the same concept applies to other architectures, just modify it for however the architecture you’re targeting handles write protection for kernel text.
Once an adversary has kernelspace execution, it’s basically game over. The
kernel tries its best to protect itself from accidental damage, but there’s not
much you can do once someone has arbitrary execution. If they’ve gotten that
far, they are the kernel. Throw the whole thing out and start over. If you’re
concerned about this attack, don’t be! By the time an adversary uses this you’ve
already fucked up been compromised and should be focusing on your resume
Incident Response.
Manually Clearing the WP Bit from CR0
Take a look at this here blog by elastic.co for a great example of what we’re talking about. They provide a classic example of kernelspace code you can use (for example, through an LKM or in a rootkit) to globally disable write protection. With global write protection disabled, we can arbitrarily modify kernel functions.
/**
* Disable write protection (clear WP bit in CR0)
*/
static inline void disable_write_protection(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0_forced(cr0);
}
Their write_cr0_forced
function just inlines and avoids compiler optimizations
for a mov %0, %%cr0
instruction. The clear_bit
function is a helper in the
kernel that boils down to an &=
instruction that wipes out a specific bit. On
x86, read_cr0
also inlines to a mov %%cr0, %0
instruction. Expand all the
helpers and what we have here is just…
static inline void disable_write_protection(void)
{
unsigned long cr0;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(1 << 16);
asm volatile("mov %0, %%cr0" : : "r"(cr0) : "memory");
}
Groovy. We run this, and suddenly protected kernel functions become ours to
modify. The linked blog also tells you how to re-enable write protection so the
kernel can keep functioning as normal and an adversary can cover their tracks.
I’m not going to cover that, because who cares about making sure the kernel
we’re exploiting remains stable? (also just change the &=
to an |=
and
remove the negation to set the bit again it’s not that hard)
Lazily Clearing Write Protection (but my hypervisor!)
Unfortunately, this technique has not always worked for me in the past. Sometimes your hypervisor will intercept attempts to modify CR0 and handle it in a way that gives the desired effect without allowing a guest to exploit the host. Sometimes your hypervisor will do this unsuccessfully, and you can’t write kernel text even with the bit removed. Boo.
Fortunately, we can just use the kernel’s built in page permission management functions to get what we want anyway. If we know the address of the function we want to change (and we really should, if you don’t have that handy go fetch it and come back) we can set the page it’s on to read-write.
The relevant function is set_memory_rw
:
int set_memory_rw(unsigned long addr, int numpages)
{
return change_page_attr_set(&addr, numpages, __pgprot(_PAGE_RW), 0);
}
With this and the address of the function we want to modify, a straightforward call gets us what we want.
set_memory_rw((unsigned long)target_function, 1);
This won’t disable write protection globally, but for this super awesome FUD bypass exploit we only care about modifying a few functions. Unprotecting one page at a time is super good enough.
Getting to the Front Page of HackerNews er Maximizing Engagement on Twitter wait Impressing Skids in Your Discord Silencing eBPF Tools with 0xB33SM0K3R by Patching eBPF Output Helpers
Now we can actually do the exploit. It’s super simple. eBPF programs typically
use helpers like
perf_event_output
or
bpf_ringbuf_submit
to send data back to userspace. If we nop out these functions in the kernel, the
programs will still run, do all their logic, modify all their maps, and then
submit nothing to userspace! The data shall be processed, but never submitted.
The EDR has been silenced! FUD!
Without the eBPF userspace outputs, tools will silently fail and you won’t know why. Except for the fact that being completely silent is unusual. And also developers of EDRs and eBPF based infrastructure monitoring tools have already thought of this. And it will be super obvious that a machine has gone dead. Almost as though thinking of this stuff is the full time job of the people that develop these systems.
Anyway! The function we’re going to patch here is perf_event_output
. This is a
pretty standard way to send output to userspace, but it’s falling out of favor
for some newer stuff like ringbuf_submit
. Did I say that already? The concept
is the same for both.
Our super awesome FUD bypass exploit code:
set_memory_rw((unsigned long)perf_event_output, 1);
unsigned char *code = (unsigned char *)perf_event_output;
// set the address to xor rax, rax; ret
code[0] = 0x48; // REX.W prefix (we're using all 64 bits baby)
code[1] = 0x31; // XOR r/m64, r64 (get ready to wipe out that register)
code[2] = 0xC0; // ModRM byte for RAX, RAX (your rax is now zero)
code[3] = 0xC3; // RET instruction (return 0)
Yes that’s right. We’re overwriting perf_event_output
from its usual code to a
dastardly return 0;
. Nobody will ever be safe again (they never were).
Okay wait how do I weaponize this?
If you have to ask I’m not telling you. Please like and subscribe.
Is this a joke?
Yes. In all seriousness, it’s a real technique that could be used in a rootkit to selectively modify functions. But it’s using legitimate kernel functions and requires pre-existing kernelspace execution, so calling it an exploit is a ooh big stretch.
A clever adversary might use this in an existing rootkit to selectively disable eBPF output during certain activities, re-enabling them later to fly under the radar. A cleverer adversary is already doing this.
Please do not take my tone seriously. People shouldn’t be fired when a company gets breached. And if you Participated In Employment and wrote a hyped up blog you are not bad and should not feel bad. If you have published research you haven’t fully understood the implications of, you are not bad and should not feel bad. Lord knows I’ve done both. Just check your ego at the door, and be receptive to new information.
It gets a little frustrating when research, defensive or offensive, is published in bad faith or in ways that muddy the waters. Then we all have to burn cycles explaining to people why the big thing they’re worried about has a marketing budget disproportionate to its risk. Or, more realistically, writing a useless detection for the damn thing anyway. A detection that will never actually go off, to calm down someone who read a tweet they didn’t understand, published by a researcher who wasn’t actually doing what they thought they were doing.
All of this was written by me except for the parts that weren’t. Those are AI. Any mistakes are also AI.
Friday out.