HardenedBSD July 2023 Status Report

There was only one notable change in the src repo in July 2023. But first, a little background info:

A long time ago, I started on a project that makes anonymous remote code injection and PLT/GOT redireciton techniques over the ptrace boundary easy in one little consumable API. The end-goal of the tool is to support injection of shared objects in a completely anonymous manner, and to be able to hijack PLT/GOT entries to point to their counterpart in the newly-injected shared object. I started on this work well over a decade ago (it has roots back to ideas I had in 2003.) The project is aptly named libhijack[0].

One common technique is to rely on shared memory-backed file descriptors, writing the shared object to the shmfd, lseek(shmfd,0,seek_set), then fdlopen(shmfd). This causes the RTLD to load the shared object from anonymous memory. In fact, I've published a PoC[1] that shows this very technique.

I'm currently working on providing shmfd-based anonymous shared object injection support in libhijack, so it can be performed over the ptrace boundary with a simple API call. Imagine something like this on a webserver running nginx:

pid_t pid = get_nginx_pid();
const char *path_to_shared_object = "/lib/libpcap.so.8";
InjectSharedObject(pid, path_to_shared_object);

This would cause the target process to create a new shmfd, write the contents of libpcap.so.8 to it, seek to the beginning, then load it via fdlopen.

I have this mostly implemented, but I'm running into some ptrace oddities.

Where this ties into HardenedBSD's src is: while writing this code, I kept thinking to myself, how would I defend against this kind of technique? Results from fstat(2) aren't helpful as there's no way to detect that we're looking at an anonymous shared memory-backed file descriptor.

However, I noticed that calling fstatfs(2) on the shmfd causes undocumented behavior: fstatfs(2) will return EINVAL. The underlying code for shared memory file descriptors doesn't implement a handler for fstatfs(2), causing the syscall to return EINVAL.

I then added code to the RTLD to check the return value of a call to fstatfs(2) on the file descriptor passed in when RTLD hardening is enabled. If fstatfs(2) fails and sets errno to EINVAL, we prohibit loading the object.

This would force an attacker to fully implement what I call a "remote RTLD": an out-of-process RTLD that loads objects over a boundary (the boundary in this particular case being ptrace.) The attacker would have to force the application to call mmap (which libhijack supports), inject into those new mapping (which libhijack supports), but then perform all the RTLD logic and fixups over the boundary (which libhijack does not support.)

My hope is that one day, libhijack gains that last little bit. That last little bit is the most complex bit. That's why I'm going the shmfd route first: to prove the concept and to flush out a "rough draft" of what's in my head.

HardenedBSD's PaX NOEXEC-inspired strict W^X implementation is effective over the ptrace boundary, further frustrating the concept of a remote RTLD.

[0]: https://github.com/SoldierX/libhijack
[1]: https://git.hardenedbsd.org/shawn.webb/random-code/-/tree/main/memdlopen