Page MenuHomeFreeBSD

rtld: Validate entry point.
Needs ReviewPublic

Authored by des on Mon, Mar 17, 4:38 PM.

Details

Reviewers
kib
Group Reviewers
Klara
security
Summary
  • When loading an object that has an entry point, ignore the entry point unless it falls within one of the sections from this object.
  • Error out if that section does not have the executable flag set.
  • At the top level, error out if the main object does not have a non-zero entry point.

Sponsored by: Klara, Inc.
Sponsored by: rsync.net

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 62941
Build 59825: arc lint + arc unit

Event Timeline

des requested review of this revision.Mon, Mar 17, 4:38 PM

What is the purpose of this change?
If the main object entry point does not point into executable segment, the process would segfault (or sigbus) automatically.
If a dso has non-zero entry point that does not point into executable segment, there are no consequences.

So why add this?

In D49395#1126219, @kib wrote:

What is the purpose of this change?

You should presumably have some larger context for this one as part of secteam@ (e-mail sent previously, subject "rtld issue" somewhere around 2025/03/12), but (edit: copy now forwarded, realizing post-mortem that kib is not on secteam)

If the main object entry point does not point into executable segment, the process would segfault (or sigbus) automatically.
If a dso has non-zero entry point that does not point into executable segment, there are no consequences.

So why add this?

The threat is an entry point that points into some other object to be loaded (e.g., libc), with the assumption that they can also do some manipulation of the initial stack state in conjunction with a bogus entry point to do things they shouldn't be able to.

In D49395#1126219, @kib wrote:

What is the purpose of this change?

An attacker with write access to a noexec filesystem could craft an Elf binary with no executable sections, ASLR disabled, and an entry point somewhere inside a system library, and pass it as a command-line argument to the linker which would happily load and execute it regardless of the noexec mount. If the attacker is also somehow able to manipulate the initial state of the stack, this escalates to arbitrary code execution. I can't think of a way to do so at the moment, but I prefer not to have to trust that black hats have less imagination than me.

MNT_NOEXEC was never a barrier to prevent a code execution from the files on the mount. It only prevented image activators from parsing files on the volume and using them at execve(2) time.
Then some features like security theater started come in, for instance, I added a check that mmap(PROT_EXEC, fd) does not trivially avoid MNT_NOEXEC protection, see ecc6c515aba41a24668ddd.

On the other hand, we only check MNT_NOEXEC in stock rtld when loading libraries into setuid process. I believe that rtld can load dso from posix shmfd, or can even execute posix shmfd in direct mode. There, the check for MNT_NOEXEC is simply not applicable.

So, if you consider a sealed system where all writeable mounts have NOEXEC option set, I do not think that either patched or stock system would prevent arbitrary code execution from non-privileged local user:

  • a dso or binary can be copied to posix shmfd and code there executed using by direct mode or LD_PRELOAD_FDS
  • dso can be created that does not contain executable segments but either DT_INIT/DT_INITARRAY/DT_PREINITARRAY (constructors) or destructors pointers are relocated in a way similar to what you described as 'attack' on the main binary entry point
  • non-plt ifunc relocations can be used in the similar way
  • ... I stopped trying to enumerate more ways to break the sealing after I realized that ifunc are there.
In D49395#1126370, @kib wrote:

MNT_NOEXEC was never a barrier to prevent a code execution from the files on the mount. It only prevented image activators from parsing files on the volume and using them at execve(2) time.
Then some features like security theater started come in, for instance, I added a check that mmap(PROT_EXEC, fd) does not trivially avoid MNT_NOEXEC protection, see ecc6c515aba41a24668ddd.

On the other hand, we only check MNT_NOEXEC in stock rtld when loading libraries into setuid process. I believe that rtld can load dso from posix shmfd, or can even execute posix shmfd in direct mode. There, the check for MNT_NOEXEC is simply not applicable.

So, if you consider a sealed system where all writeable mounts have NOEXEC option set, I do not think that either patched or stock system would prevent arbitrary code execution from non-privileged local user:

  • a dso or binary can be copied to posix shmfd and code there executed using by direct mode or LD_PRELOAD_FDS
  • dso can be created that does not contain executable segments but either DT_INIT/DT_INITARRAY/DT_PREINITARRAY (constructors) or destructors pointers are relocated in a way similar to what you described as 'attack' on the main binary entry point
  • non-plt ifunc relocations can be used in the similar way
  • ... I stopped trying to enumerate more ways to break the sealing after I realized that ifunc are there.

The RTLD could indeed be hardened, some of which HardenedBSD has already done. LD_PRELOAD and family are disabled by default as is fdlopen(shmfd). I do think that providing the same sanity checks for constructors, deconstructors, ifuncs, object entry, etc. would be a good idea from a correctness standpoint. Perhaps this patch could be enhanced to provide those sanity checks, both at load time and at time of call (example: in case a destructor was validated at load, but an exploit has since changed the function pointer to the destructor).

In D49395#1126370, @kib wrote:

[...] I stopped trying to enumerate more ways to break the sealing after I realized that ifunc are there.

So to summarize, you are rejecting my patch because it only improves rtld a little?