This is the bug of the year.

It's well established that if Alice forwards an SSH agent to Bob, Bob can use the SSH agent protocol to make Alice open DLLs, because there's an agent protocol command (SSH_AGENTC_ADD_SMARTCARD_KEY) that OpenSSH implements with dlopen: when you ask the agent to access a smart card, OpenSSH dlopen()'s the library corresponding to the `id` of the device. This is a Jann Horn bug from 2016, and OpenSSH fixed it by whitelisting DLLs to /usr/lib and directories like it.

The Qualys bug builds on Horn's bug. When OpenSSH dlopen()'s the library, it then tries to look up a PKCS#11 entry point function, and, when it doesn't find it, it dlclose()'s the library and returns an error.

The issue is that most of the libraries in system library paths were never intended to be opened maliciously, and so they do all sorts of stuff in their constructors and destructors (any function marked `__attribute__((constructor))` or `destructor` is called by dlopen and dlclose respectively). In particular, they register callbacks and signal handlers. Most of these libraries are never expected to dlclose at all, so they tend not to be great about cleaning up. Better still, if you randomly load oddball libraries into random programs, some of them crash, generating SIGBUS and SIGSEGV.

So you've got a classic UAF situation here: (1) force Alice to load a library that registers a SIGBUS handler; it won't be a PKCS#11 handler so it'll get immediately dlclose()'d, but won't clean up the handler. (2) Load another library, which will take over the program text address the signal handler points to. (3) Finally, load a library that SIGBUS's. If you manage to get a controlled jump swapped into place in step (2), you win.

If you're thinking "it's pretty unlikely you're going to be able to line up a controlled jump at exactly the address previously registered as a signal handler", you're right, but there's another quirk of dlclose() they take advantage of: there's an ELF flag, NODELETE, that instructs the linker not to unmap a library when it's unloaded, and a bunch of standard libraries set it, so you can use those libraries to groom the address space.

Finally, because some runtimes require executable stacks, there are standard libraries with an ELF flag that instructs the process to make the stack executable. If you load one of these libraries, and you have a controlled jump, you can write shellcode into the stack like it's 1998.

To figure out the right sequence of steps, they basically recapitulated the original ROP gadget research idea: they swept all the standard Ubuntu libraries with a fuzzer to find combinations of loads that produced controlled jumps (ie, that died trying to execute stack addresses).

A working exploit loads a pattern of "smartcards" that looks like this (all in /usr/lib):

syslinux/modules/efi64/gfxboot.c32 (execstack)
pulse-15.0+dfsg1/modules/module-remap-sink.so (groom)
x86_64-linux-gnu/libgnatcoll_postgres.so.1 (SIGBUS handler)
pulse-15.0+dfsg1/modules/module-http-protocol-unix.so (groom)
x86_64-linux-gnu/sane/libsane-hp.so.1.0.32 (groom)
libreoffice/program/libindex_data.so (groom)
x86_64-linux-gnu/gstreamer-1.0/libgstaudiorate.so (groom)
libreoffice/program/libscriptframe.so (groom)
x86_64-linux-gnu/libisccc-9.16.15-Ubuntu.so (groom)
x86_64-linux-gnu/libxkbregistry.so.0.0.0 (groom)
debug/.build-id/15/c0bee6bcb06fbf381d0e0e6c52f71e1d1bd694.debug (SIGBUS)

The paper goes on to classify like 4 more patterns whereby you can get unexpected control transfers by dlopen() and immediately dlclosing() libraries. The kicker:

we noticed that one shared library's constructor function
(which can be invoked by a remote attacker via an ssh-agent forwarding)
starts a server thread that listens on a TCP port, and we discovered a
remotely exploitable vulnerability (a heap-based buffer overflow) in
this server's implementation.