time to bleed by Joe Damato

technical ramblings from a wanna-be unix dinosaur

5 Things You Don’t Know About User IDs That Will Destroy You

View Comments


*nix user and group IDs are complicated, confusing, and often misused. Look at this code snippet from the popular Ruby project, Starling:

def drop_privileges
  Process.egid = options[:group] if options[:group]
  Process.euid = options[:user] if options[:user]
end

At quick first glance, you might think this code looks OK. But you’d be wrong.

Let’s take a look at 5 things you probably don’t know about user and group IDs that can lead you to your downfall.

  1. The difference between real, effective, and saved IDs
  2. This is always a bit confusing, but without a solid understanding of this concept you are doomed later.

    • Real ID – The real ID is the ID of the process that created the current process. So, let’s say you log in to your box as joe, your shell is then launched with its real ID set to joe. All processes you start from your shell will inherit the real ID joe as their real ID.
    • Effective ID – The effective ID is the ID that the system uses to determine whether a process can take a particular action. There are two popular ways to change your effective ID:
      • su – the su program changes your effective, real, and saved IDs to the ID of the user you are switching to.
      • set ID upon execute (abbreviated setuid) – You can mark a program’s set uid upon execute bit so that the program runs with its effective and saved ID set to the owner of the program (which may not necessarily be you). The real ID will remain untouched. For example, if you have a program:


        rv = getresuid(&ruid, &euid, &suid);

        printf("ruid %d, euid %d, suid %d\n", ruid, euid, suid);

        If you then chown the program as root and chmod +s (which turns on the setuid bit), the program will print:

        ruid 1000, euid 0, suid 0

        when it is run (assuming your user ID is 1000).

    • Saved ID – The saved ID is set to the effective ID when the program starts. This exists so that a program can regain its original effective ID after it drops its effective ID to an unprivileged ID. This use-case can cause problems (as we’ll see soon) if it is not correctly managed.
    • If you start a program as yourself, and it does not have its set ID upon execute bit set, then the program will start running with its real, effective, and saved IDs set to your user ID.
    • If you run a setuid program, your real ID remains unchanged, but your effective and saved IDs are set to the owner of the file.
    • su does the same as running a setuid program, but it also changes your real ID.

  3. Don’t use Process.euid= in Ruby; stay as far away as possible
    • Process.euid= is EXTREMELY platform specific. It might do any of the following:

      • Set just your effective ID
      • Set your effective, real, and saved ID.

      On most recent Linux kernels, Process.euid= changes ONLY the Effective ID. In most cases, this is NOT what you want. Check out this sample Ruby script. What would happen if you ran this script as root?

    • def write_file
        begin
          File.open("/test", "w+") do |f|
            f.write("hello!\n")
            f.close
          end
          puts "wrote test file"
        rescue Errno::EACCES
          puts "could not write test file"
        end
      end
       
      puts "ok, set uid to nobody"
      Process.euid = Etc.getpwnam("nobody").uid
       
      puts "going to try to write to / now…"
       
      write_file
       
      puts "restoring back to root"
       
      Process.euid = 0
       
      puts "now writing file"
       
      write_file

      This might surprise you, but the script regains root‘s ID after it has dropped itself down to nobody.

    • Why does this work?
    • Well as we just said, Process.euid= doesn’t touch the Saved ID, only the Effective ID. As a result, the effective ID can be set back to the saved ID at any time. The only way to avoid this is to call a different Ruby function as we’ll see in #4 below.

  4. Buggy native code running as nobody can execute arbitrary code as root in 8 bytes
    • Imagine a Ruby script much like the one above. The script is run as root to do something special (maybe bind to port 80).
    • The process then drops privileges to nobody.
    • Afterward, your application interacts with buggy native code in the Ruby interpreter, a Ruby extension, or a Ruby gem.
    • If that buggy native code can be “tricked” into executing arbitrary code, a malicious user can elevate the process up from nobody to root in just 8 bytes. Those 8 bytes are: \x31\xdb\x8d\x43\x17\x99\xcd\x80 – which is a binary representation of setuid(0).
    • At this point, a malicious user can execute arbitrary code as the root user

    Let’s take a look at an (abbreviated) code snippet (full here):

    ## we’re using a buggy gem
    require ‘badgem’

    # do some special operations here as the privileged user

    # ok, now let’s (incorrectly) drop to nobody
    Process.euid = Etc.getpwnam("nobody").uid

    # let’s take some user input
    s = MyModule::GetUserInput

    # let’s assume the user is malicious and supplies something like:
    # "\x6a\x17\x58\x31\xdb\xcd\x80\x6a\x0b\x58\x99\x52" +
    # "\x68//sh\x68/bin\x89\xe3\x52\x53\x89\xe1\xcd\x80"
    # as the string.
    # That string is x86_32 linux shellcode for running
    # setuid(0); and execve("/bin/sh", 0, 0) !

    # pass that to a buggy Ruby Gem
    BadGem::bad(s)

    # the user is now sitting in a root shell!!

    This is obviously NOT GOOD.

  5. How to change the real, effective, and saved IDs
  6. In the list below, I’m going to list the functions as syscall - RubyFunction

    • setuid(uid_t uid) - Process::Sys.setuid(integer)
    • This pair of functions always sets the real, effective, and saved user IDs to the value passed in. This is a useful function for permanently dropping privileges, as we’ll see soon. This is a POSIX function. Use this when possible.

    • setresuid(uid_t ruid, uid_t euid, uid_t suid) - Process::Sys.setresuid(rid, eid, sid)
    • This pair of functions allows you to set the real, effective, saved User IDs to arbitrary values, assuming you have a privileged effective ID. Unfortunately, this function is NOT POSIX and is not portable. It does exist on Linux and some BSDs, though.

    • setreuid(uid_t ruid, uid_t eid) - Process::Sys.setreuid(rid, eid)
    • This pair of functions allows you to set the real and effective user IDs to the values passed in. On Linux:

      • A process running with an unprivileged effective ID will only have the ability to set the real ID to the real ID or to the effective ID.
      • A process running with a privileged effective ID will have its saved ID set to the new effective ID if the real or effective IDs are set to a value which was not the previous real ID.

      This is a POSIX function, but has lots of cases with undefined behavior. Be careful.

    • seteuid(uid_t eid) - Process::Sys.seteuid(eid)
    • This pair of functions sets the effective ID of the process but leaves the real and saved IDs unchanged. IMPORTANT: Any process (including those with unprivileged effective IDs) may change their effective ID to their real or saved ID. This is exactly the behavior we saw with the Ruby script in #2 above. This is a POSIX function.

  7. How to correctly and permanently drop privileges
  8. You should use either the:

    • setuid(uid_t uid) - Process::Sys.setuid(integer)
    • or

    • setresuid(uid_t ruid, uid_t euid, uid_t suid) - Process::Sys.setresuid(rid, eid, sid)

    pair of functions to set the real, effective, and saved IDs to the lowest privileged ID possible. On many systems, this is the ID of the user nobody.

    For the truly paranoid, it is recommended to check that dropping privileges was actually successful before continuing. For example:

    require ‘etc’

    def test_drop
      begin
        Process::Sys.setuid(0)
      rescue Errno::EPERM
        true
      else
        false
      end
    end

    uid = Etc.getpwnam("nobody").uid
    Process::Sys.setuid(uid)

    if !test_drop
      puts "Failed!"
      #handle error
    end

Conclusion

*nix user and group ID management is confusing, difficult, and extremely error prone. It is a difficult system with many nuances, gotchas, and caveats. It is no wonder so many people make mistakes when trying to write secure code. The major things to keep in mind from this article are:

  • Avoid Process.euid= at all costs.
  • Drop privileges as soon as possible in your application.
  • Drop those privileges permanently.
  • Ensure that privileges were correctly dropped.
  • Carefully read and re-read man pages when using the functions listed above.

Written by Joe Damato

April 13th, 2009 at 12:06 pm

  • Ryan

    Without dog piling onto the whys and wherefores of this, thank you for writing it. It's clean, it's concise, and it's saved me a lot of headache and pain. I've actually used what was laid down here to improve code around my organization, and if I ever wind up in the same bar as you then your first pint is on me.

  • Bibble,

    What you said is true but irrelevant. People aren't writing insecure code intentionally, they're writing it because they don't know how these things work!

    The solution isn't to rant and rave about idiot engineers, it's to design systems and libraries that are easy to understand and do the right thing, and make up the difference by educating people.

    That's exactly what Joe is doing.

  • James

    Stevens was a great author and teacher, but UNIX systems have evolved since then and his books do not apply to many current UNIX systems. To better understand UIDs and security, I recommend Chen's paper "SetUID Demystified" from USENIX Security 2002. It can be found at http://www.eecs.berkeley.edu/~.... Matt Bishop's Writing Safe SetUID Programs at http://nob.cs.ucdavis.edu/bish... is another essential reference.

  • "setuid demystified" is flawed

    Actually, it turns out that the "setuid demystified" paper was seriously flawed, which is why those guys wrote a followup paper titled "revising setuid demystified" [USENIX ;login 2008], see: http://www.eecs.berkeley.edu/~...

    They also made the code that supposedly safely manipulates identity available here: http://code.google.com/p/chang...

  • Good coverage, although you didn't make any mention of the added file-system permissions that Linux uses.

    setfsuid(2) etc., credentials(7) and also capabilities(7) are useful manual pages for further exploration for Linux developers.

blog comments powered by Disqus