четверг, 11 июля 2024 г.

ebpf map as communication channel

Recently I've done small research to repurpose overvalued ebpf into something useful and even achieved some modest results. It seems that at least you can use ebpf maps in your old-school native drivers without writing single line of code for ebpf progs. You can ask me - c'mon, there are tons of ways to communicate with driver under linux, just to name few:
  • ioctls
  • read from driver/write to driver + possible employ polling
  • file in kernfs
  • futex in shared memory
  • io_uring
  • netlink like auditd or XFRM do

etc

Let's look at typical situation - your IT crew was bitten by violent adherent of the totalitarian sect of rust ebpf Witnesses and as a consequence you now have hundreds of ebpfs (and no single person who know how this pile of spaghetti code works)

Probably now is much better to integrate new drivers with ebpf, right? Considering that ebpf progs have very serious limitations (like you can't read content of linked lists like raw_notifier_head with right locking) it would be very nice to produce output of your EDR drivers directly to ebpf maps

So I made simple POC to show how you can do it. Source of driver & userland test program

Lets dive into gory details and see what other non-obvious advantages can be obtained from such heretical crossbreeding

From userland to kernel

One possible scenario - replace kernel module params. Your userland code creates ebpf map, fills it with params (including binary blobs - for example code for reverse shell, he-he) and then calls driver with ioctl passing file descriptor of ebpf map into driver

 

From kernel to userland

You could replace with ebpf maps lots of files in /sys or /proc. Really - you could have updated in real-time data in structured binary form and avoid tons of text parsers. Just think about it seriously

And finally lets check how to work with ebpf maps in your driver

 

Gathering maps

The first thing is to locate necessary maps. I've implemented 3 way to do this
  1. IOCTL_FROM_FD - by file descriptor returned from bpf_create_map_name. Official way is to call bpf_map_get and it even marked as EXPORT_SYMBOL. However I got
    ERROR: modpost: "bpf_map_get" [/home/redp/lkcd/bmc/bmc.ko] undefined!
    Well, this is not big deal - we can employ good old symbols lookup
  2. IOCTL_BY_ID. All ebpf maps are stored in tree map_idr, so it just calls idr_find
  3. IOCTL_BY_NAME. Traverse all nodes in map_idr tree and find by name

 

CRUD operations

Official way to do this - call functions (sure non-exported) like bpf_map_update_value etc, but they require file descriptor and we can miss it. So we can just skip most of their wrappers logic and call corresponding map->ops methods (don't forget to get necessary RCU lock):
  • for insert/update ops->map_update_elem
  • for read ops->map_lookup_elem (or even ops->map_lookup_and_delete). See how bpf_map_copy_value implements this
  • for delete ops->map_delete_elem, see map_delete_elem

 

Notifications

Surprisingly hardest part was to notify from driver code in userland that ebpf map was updated. Theoretically their file descriptors support polling - see function bpf_map_poll, it calls map->ops->map_poll method if it presents. Unfortunately the only map type implementing it is BPF_MAP_TYPE_RINGBUF

It was very tempting to patch map->ops with copy of original one and just add your own map_poll method. DO'NT DO THAT!
The main threat here is that your driver can be unloaded at arbitrary moments. There is high probability that for rare events (for example, for events that happen a couple of times year) return address to unloaded code will be present in stack of some thread waiting in poll - even if you revert all your patched maps before driver unloading

We should use polling on some other file descriptors - in my case there is only ebpf map so I decided to use FD of driver. For many maps driver could create bunch of files somewhere in kernfs (for example with kobject_uevent) and return them to userland

Комментариев нет:

Отправить комментарий