HardenedBSD July 2023 Status Report

Skip to first unread message

Shawn Webb

Aug 7, 2023, 11:03:26 AM8/7/23
to HardenedBSD Users
Hey all,

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


Shawn Webb
Cofounder / Security Engineer

Reply all
Reply to author
0 new messages