SUID Linux: Shadow SUID for Privilege Persistence: Part 2

Click our What is SUID? Shadow Suid For Privilege Persistence: Part 1 for a deeper dive.

In our previous post, What is SUID? Shadow Suid For Privilege Persistence: Part 1,  we explained what setuid is and how Shadow SUID provides the same privilege persistence capability. In this post, we’ll take a deep dive into the technical details of how this privilege persistence works and how it came about.

How Commands Are Executed

To understand how it works we need to first understand what actually happens when we execute a command on Linux.

Screenshot of Linux code execution

When we make an execve() syscall, the kernel reads the first 128 characters of the file. It then iterates over the registered binary-format-handlers to determine which handler should be used. That way, when we execute a file that begins with a #! shebang, the kernel knows it is a script, and the binfmt_script handler is used to find the relevant interpreter (as indicated after the shebang). Similarly, when the file begins with x7fELF, the kernel knows it is a regular Linux binary, and the binfmt_elf handler is used to load the binary into the elf interpreter.

Handling Unknown File Types

In the early days of Linux, when you wanted to add a new file type, you had to write a new kernel module, to register a new handler and direct it to the relevant interpreter to execute the file. Since Linux version 2.1.43 (June 1997), this whole procedure became much easier when a new feature called binfmt_misc was introduced, which allowed the addition of new file types. The binfmt_misc docs tell us:

This Kernel feature allows you to invoke almost (for restrictions see below) every program by simply typing its name in the shell. This includes for example compiled Java(TM), Python or Emacs programs.

To achieve this you must tell binfmt_misc which interpreter has to be invoked with which binary. Binfmt_misc recognises the binary-type by matching some bytes at the beginning of the file with a magic byte sequence (masking out specified bits) you have supplied. Binfmt_misc can also recognise a filename extension aka .com or .exe.

The “special flags” feature, which allows privilege persistence, did not appear until August 2004, in Linux 2.6.8. Digging through the LKML (Linux Kernel Mailing List), I came across this in the changelog :

One can use binfmt_misc to execute untrusted code (interpreter) with elevated privileges. One could argue that all binfmt_misc interpreters are trusted, because only root can register them. But that’s a change from the traditional behavior of binfmt_misc (and binfmt_script).

So it seems that back in the day it was already known that this might be an issue; however, it wasn’t deemed serious enough for a proposed patch, as further discussion in LKML indicated.

This feature should be used with care, since the interpreter will have root permissions when running a setuid binary owned by root….Only root can register an interpreter with binfmt_misc. The feature is documented and the administrator is advised to handle it with care.

It is amazing to think how the security world has changed since then! Back in 2004 a notice to sysadmin in the documentation would probably be enough. I bet that such a way of thinking wouldn’t pass muster today!

Let’s Try It Out!

Let’s see a little demonstration of how this works. Suppose I have a python script, which doesn’t have a shebang header:

Image of Python script no shebang

When I try to execute it, I get an error:

Screenshot of error without shebang

That’s because I didn’t tell the operating system which program is responsible for interpreting the script. By default, bin/sh is used as the interpreter, and the error is actually a result of the print command, which means something different to the shell and requires a file name as an argument.

So now lets add a new binfmt_misc for “.py” files, and mention python as the interpreter:

Screenshot of binfmt_misc python interpreter

Now when executing the python script it works!

Image of working python script without shebang

To see how this useful feature leads to a vulnerability, we need to go back to the documentation and look at the “C – credentials” flag section:

Currently, the behavior of binfmt_misc is to calculate the credentials and security token of the new process according to the interpreter. When this flag is included, these attributes are calculated according to the binary. It also implies the O flag. This feature should be used with care as the interpreter will run with root permissions when a setuid binary owned by root is run with binfmt_misc.

Let’s (Pretend To) Get Malicious!

To see how this might be exploited by attackers, I’ve created a new python script, made root the owner and given it setuid permissions, something that shouldn’t matter normally for a python script.

A screenshot of evil python script

Let’s see what happens when we execute it.

Image of executing an evil python script

Even though the python interpreter isn’t a setuid, we were able to force it to become a setuid, only by the fact that the interpreted file has setuid permissions. And just like the last time, we didn’t need to mention the interpreter with a shebang.

Let’s see if we can use one of the local setuid binaries as the “script” and execute our own benign file as the interpreter.

First let’s find a victim suid program, which will be our “interpreted file”, it has to have a unique signature among all executables in the system; otherwise, it is possible that our interpreter will be executed by a program we didn’t intend, and possibly not even a suid. Therefore, we must make sure that in the first 128 bytes of the file we have our own unique signature.

I made a little script to find all unique-header setuids:

A screenshot of locating unique suids

OK, lots of files! I’ve randomly picked ping, but any other suid file will do just fine. First, we need to extract its initial 128 first bytes, and use those to create the new binfmt_misc:

Screenshot of create binfmt_misc interpreter

If we had any problems with the parameters we would receive an error; otherwise, we can tell it worked. We can examine the newly created file on /proc/sys/fs/binfmt_misc/.ping to verify the registered rule.

You might also notice that I used the name “.ping” with a dot at the beginning. In Linux, files prefixed with a dot are hidden and won’t be output by a regular invocation of ls. Of course, this is useful to hide the existence of the binfmt_misc rule we just created.

This is how it would look without the dot prefix, on a regular ls:

A screenshot of showing all files

And here’s how it looks (or doesn’t!) with the dot prefix:

Image of ls not showing hidden files

While doing this research I had to create such rules over and over again, so obviously I wrote a script to automate that, which you can download from here.

Now we can try it all together:

A screenshot of using Shadow SUID

As you can see, we’ve executed the ping command as an unprivileged user, and our own, non-suid program got executed as root, just like it was a suid itself!

A Problem for Defenders

As a bonus for attackers, this vulnerability comes with its own built-in protection: it is extremely difficult to detect. If we delete the interpreter, we get the following error when trying to use ping:

A screenshot of Problem for Defenders

While the reason for the above error is the missing interpreter, the operating system blames it all on the setuid file. Most users would probably reinstall the operating system before figuring out the true cause.

Staying Out of Sight

But this also leads to a problem for attackers. What about the original suid? What should I do if I – or my victim – need to use the original ping? Seems like it’s unreachable now and any attempt to execute the original suid leads to executing our root-shell.

It would be natural to think we could call the original suid from inside our interpreter, but that won’t work. If we try to re-execute ping from within the interpreter, we will enter an endless loop: execution of ping will cause the interpreter to load, which will try to execute ping, which in its turn will execute the interpreter… and so on recursively.

I found two possible workarounds. We could execute the original program using ld.so, which lets you execute programs using the elf interpreter. Even though this is the simpler of the two methods, I didn’t like it too much, since you have a lot of cleaning up to do (as an attacker). This method means ld.so becomes the first argument, and deleting it doesn’t look too good when the sysadmin inspects what’s going on via the ps command.

It turns out there’s a better way, which is basically disabling the rule by writing 0 to the rule file, executing the original command again and turning the rule back on by writing 1 to the rule file.

Let’s try it out ( code):

A screenshot of Toggling Shadow SUID

Nice!

Obviously instead of simply running the “id” command I could have ran any other command, and in real scenarios it is more likely that an attacker would make it run bash or meterpreter to facilitate exploitation of the target system.

Summary

It seems that a small kernel feature change that likely seemed an edge case security issue in 2004 turns out to be a serious vulnerability when viewed in light of today’s threatscape. Combined with how difficult it is to detect a shadow suid with classic sysadmin tools, this is an issue that should concern anyone running a Linux system.

While searching for any other mentions of malicious use of this technique, I was only able to find this one example, so hats off Toffan.

All the source codes for the examples are available in the SentinelOne github.