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.
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:
When I try to execute it, I get an error:
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
So now lets add a new
binfmt_misc for “.py” files, and mention python as the interpreter:
Now when executing the python script it works!
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.
Let’s see what happens when we execute it.
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:
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
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
And here’s how it looks (or doesn’t!) with the dot prefix:
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:
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
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
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):
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.
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.
All the source codes for the examples are available in the SentinelOne github.
Read more about Linux Security
Reversing Malware on macOS
Endpoint Protection Platform Free Demo