Underhanded Python: detecting the debugger

There used to be an annual competition among C programmers called the Underhanded C contest, with the aim of inventing creative ways to write code that appears to do one thing but is actually doing something very different (and theoretically malicious, though it’s a “white hat” contest).

I recently got to thinking about whether you can do this kind of thing in Python. The original format of the contest doesn’t really work in Python: it’s all too easy to write Python code that doesn’t do what it seems, to the point where there’s probably no challenge. But in a dynamic language, there are lots of interesting things that aren’t realistic in a language like C.

For example, can you detect and interfere with the debugger? Can you hide your malicious behaviour when a debugger is attached to look at it? It turns out you can.

Debugging in Python

It’s pretty clear that you can’t implement a Python debugger wholly in Python without any support from the Python runtime. Python code will only run when something calls it, and your debugger code wouldn’t have any way to impose itself upon the code being debugged.

However, Python tries to make things as flexible as possible by implementing a minimal amount of support in the Python runtime and having the rest of the debugger built on top of that in Python. The key tool that makes this work is something I mentioned in an earlier article about coverage testing: sys.settrace.

The way settrace works is that you can register a hook function with it that will be called on some conditions (moving to a new line of code, entering a function scope etc.). This hook is an ordinary Python function that can do whatever you want. The implementation of settrace is built into the Python runtime, but that’s all the special support you need.

How to behave differently when being debugged

Let’s keep things simple. Let’s suppose we want to write a simple function that adds two numbers, and prints "I'm malicious" if it’s called when the debugger isn’t around to see it. Something like:

def add(a, b):
    if not in_debugger():
        print("I'm malicious")

    return a + b

It’s pretty simple, we just have to check whether the settrace hook is set:

import sys

def add(a, b):
    if sys.gettrace() is None:
        print("I'm malicious")

    return a + b

This works pretty well. If you run it in pdb, you’ll see that the message is not printed. One quirk (for better or worse) is that if you do a continue from PDB it will not detect the debugger, because continue disables the debug hook entirely (until it’s reinserted with set_trace() or breakpoint() or whatever). Depending on the requirements for our malicious program this might or might not be a problem.

This isn’t specific to PDB, either. It should work with any debugger for cpython (and may work for other Python implementations, I haven’t checked).

Of course, this is a toy example. The most obvious problem is that if you step into the function you’ll immediately see the malicious code. I have a few ideas about this, which I plan to return to later.

Leave a Reply

Your email address will not be published. Required fields are marked *