codersnotes

Being Sneaky in C June 11th, 2015

A bleeding heart

I entered the 2014 Underhanded C Contest last year. Didn't win, but hey-ho what can you do. Still, I thought I'd post my entry and a write-up of it, in case anyone found it interesting.

The goal this year was to write some code for a Twitter-like service. I'll spare the full details; if you want them go check the original contest post.

Your function is supposed to take a 140-character text message, and based on its contents decide whether to secretly log it out to a file or not. The user who posted the message is not supposed to be able to know they're being logged.

The key goal here is that you have to write a function which is supposed to make a secret yes/no decision, and not leak that decision out to the user who it affects. Got it? It's very important that you don't let the user know the result of the decision. It's a secret decision. Secret. Sshhh.

The contest challenge is that you, as a vigilante programmer fighting for the common man, wants to actually leak that logging decision out to the user even though your boss instructed you not to. So your code needs to look perfectly innocent even though it's doing something underhanded.

If you want to play along, you can download my complete entry source code below.
Spoilers continue ahead.


So here's the core function I wrote that makes the decision on whether to log a post or not. A "piu" is the codename of a message on this messaging service (a la "tweet"). (edit: I added a little more context for it)

void encrypt(char *dest, size_t size)
{
    while (size--)
        *dest++ ^= 0x69;
}

void secure_dump(piu *entry, FILE *file)
{
    // Encrypt the log so that no-one can track it.
    encrypt((char *)entry, sizeof(piu));

    fwrite(entry, 1, sizeof(piu), file);
}

void surveil(const piu *entry)
{
    // Make a copy so we can modify it.
    // We then work only on the copy, to make _sure_ we don't
    // leak any surveillance information.
    piu *copy = (piu *)malloc(sizeof(piu));
    *copy = *entry;

    // See if it matches any requests.
    int req = should_surveil(copy);
    if (req != -1)
    {
        // Save it off to the secure logfile.
        FILE *fp = requests_to_scan[req].write_here;
        secure_dump(copy, fp);
    }

    free(copy);
}

The decision being made here is the req variable - if req is -1, we return without logging the message. Otherwise we log it. But most importantly, I don't return req anywhere, so the decision is local to this function.

And yet somehow, the user who posted the message can be made aware of that decision.

How is this possible? Well this is the beauty of it - it isn't, at least according to the source code as read by the auditor.

Generally in the Underhanded C contest, people try to find creative ways to insert sneaky macro tricks into their functions. Clever little ways to drop a bug in there, but obscure it so it's incredibly unobvious. This lets you inject a bug into a function, but have it hard to spot just by looking at the source.

My entry doesn't do that. You can audit the surveil function until the end of time and you won't find the leak in there, because there isn't one. You'll see that the function even takes a "const" parameter, to guarantee that there's no way to modify the incoming message, even by accident.

Instead I got my inspiration from the "hilarious" real-world HeartBleed bug that was announced last year.

The key to this entry is that I call malloc and make a temporary copy of the message. The justification for making a copy is that the secure_dump function needs to work on a copy, because it needs to encrypt the message as it writes it out to the logfile.

malloc and free form a side-channel that isn't visible in the source code. It's a way of passing data around memory without it being written explicitly in the program. You can write some bytes into malloc'd memory, then free it and watch as the next guy to call malloc suddenly finds a delightful little surprise in what he'd thought was empty memory.

So what's the actual bug here? The real killer here is there's only actually one bug anywhere in the entire source code. (disclaimer: there may be other accidental bugs in the code, I'm only human after all, but there's only one deliberate bug)

And here it is:

typedef struct {
    char buffer[140*4];
    time_t stamp;
    int count;
} filter_data;

Did you spot it? No? What really, you didn't spot it? :-)
OK it's pretty subtle. Here's the fixed version of that function:

typedef struct {
    char buffer[140*4+1];
    time_t stamp;
    int count;
} filter_data;

That's right, there's an off-by-one error in a completely unrelated struct in a completely unrelated source file.

The only reason this is in any way a problem is that memory that this filter_data structure happens to be working with came from the output of malloc, and as such just happens to contain the old contents of the previous call to malloc. Which in our case, means you get a copy of either an encrypted message or an unencrypted message. Unencrypted messages here tend to end in zero, encrypted ones don't.

This single byte error causes the timestamp variable to be corrupted, and so the user can tell if he's being watched by just looking at the low byte of his last-activity timestamp.

This doesn't happen necessarily on all systems. It's dependent on how their malloc works. But it's really, really common. This is not simply a theoretical attack, but can in fact be used for significant real-world damage, as HeartBleed demonstrated.

Once you have the ability to access re-used memory, there are many possible attacks that can be chosen. HeartBleed was able to simply read the entire re-used block out and dump it across a socket, which is a very powerful capability. For this contest, in order to simplify the code and avoid drawing attention, a buffer corruption of the timestamp is a simple and effective means.

It also has the property that the message itself remains untouched; the flag to signal whether you're being surveilled isn't even in the message! You'd have to perhaps navigate to the PiuPiu user's page to see it there.

And lastly, my favorite little salt-in-the-wound here: If you try to debug it using Visual Studio, the bug vanishes! Running a program (even a release-build one) from inside the debugger enables the CRT debug heap, which automatically clears data upon free.

In C/C++, you can use bugs in one part of a program to cause trouble in another. That's pretty darn underhanded.

Written by Richard Mitton,

software engineer and travelling wizard.

Follow me on twitter: http://twitter.com/grumpygiant