Fixing timing attacks in Python

So, a discussion came up on python-mentors about how to fix some of the timing attacks that I talked about in my pycon talk, and specifically how to solve them in a general way. I’ve previously tried to do this using multiprocessing- which works pretty well assuming that you’re able to fork without biting the memory bullet and that the return value of the function you’re wrapping is pickleable. If either of those things aren’t true though, you have a problem- and so I tried today to do it with threading. Here’s the result:

#! /usr/bin/env python3

import time
import queue
import threading
import functools

class ConstantTime:
    """Runs for a fixed period of time, then returns no matter what.

    The `timeout` argument specifies, within the limits imposed by the host
    operating system, the amount of time this function will run for.

    The `margin_of_error` argument specifies how long before the timeout to
    begin trying to claw our way back out of the decorated function. It should
    be at least as long as you expect it to take to exit from an arbitrary point 
    in the decorated function.

    Both are specified in seconds.
    """

    def __init__(self, timeout=1, margin_of_error=.1):
        self.timeout = timeout
        self.inner_timeout = timeout - margin_of_error

    def __call__(self, f):
        @functools.wraps(f)
        def inner(*args, **kwargs):
            # this will wait for a fixed period of time before join()ing 
            def sleeper(): time.sleep(self.timeout)
        
            # this pushes the return value back up to us
            q = queue.Queue()
            def wrap(*args, **kwargs):
                q.put_nowait(f(*args, **kwargs))

            # create the thread objects
            timed_t = threading.Thread(target=wrap, args=args, kwargs=kwargs)
            timer_t = threading.Thread(target=sleeper)

            # set the daemon flag on the timed thread so we can exit if we must
            timed_t.daemon = True

            # we start the timed thread first and let the margin of error
            # soak up the difference.
            timed_t.start()
            timer_t.start()
            timed_t.join(timeout=self.inner_timeout)
            timer_t.join()

            # now we try to return, blowing up properly if we don't have a legit
            # return value
            try:
                return q.get_nowait()
            except queue.Empty:
                raise Exception("Exited wrapped function before it completed")

        # return the wrapped function to complete the decoration
        return inner

Works ok, right? Not so fast.

@ConstantTime(timeout=2)
def f(n):
    return pow(65537, 2 >> n)

@ConstantTime(timeout=2)
def g(n):
    while n:
        time.sleep(1.5)
        n -= 1

These two things will behave very differently here; the first, which does not release the GIL, will sit and spin no matter how hard we try to get out of it. The second, which does release the GIL, will exit on time. This effectively means that the code above will leak how long the last GIL-locking operation takes after the buzzer if one happens to be occurring when that happens. Not a good sign- and it goes to show you how hard it can be to engineer these things right.

Advertisement

About geremycondra

I'm a security researcher at the University of Washington, an F/OSS developer, and a terrible speller. I like hard problems, elegant solutions, and seeing how deep the rabbit hole goes.
This entry was posted in Python, Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s