On Linux Anti-Debugging

A friend came up with a very interesting problem today. He’s writing this Linux application and wanted to make it untraceable (at least for ptrace-based tracers, such as strace and the likes). So he was employing this very popular trick that was first documented, at least as far I’m concerned, by Silvio Cesare:

void anti_ptrace(void) __attribute__ ((constructor));
void anti_ptrace(void)
{
    if(getenv("LD_PRELOAD"))
        while(1);

    if(ptrace(PTRACE_TRACEME, 0, 0, 0) < 0)
        while(1);
}

So, what this does is use a constructor function (one that gets executed before main() ) to make the process enable tracing on itself. The idea here is that if some other process is already attached (used ptrace() with PTRACE_ATTACH) to this process, the ptrace() call will fail and it will fall into an infinite loop, preventing the program to even really start executing. The LD_PRELOAD check is an addendum against the usermode hook of ptrace().

Pretty neat trick. The problem, though, is that the signal handling gets all messed up. Enabling ptrace() implies that the only signal that will behave as normal is SIGKILL, everything else will just pause the process. Thus, anything relying on signals, such as a simple CTRL+C, will fail miserably.

One of the first ideas that came to my mind was to “turn off” the tracing as soon as the verification finished. Since we don’t really need the tracing functionality, we just disable it after passing the “if” clause and we are fine. Beautiful, huh? Well, not quite. From what I’ve seen, it seems that it’s IMPOSSIBLE to turn it off after using PTRACE_TRACEME. No, PTRACE_DETACH’ing on itself won’t work. Don’t ask me why stuff works this way, I just work here.

So I moved on to trying to find a different, non-ptrace-based way of checking for ptrace attachments. I was thinking of something simple, such as an equivalent to the BeingDebugged flag on Windows’s PEB. Then again, no luck. I couldn’t see anything I could use outside kernel land.

Now I’d like to take the opportunity to make a simple and rather unsettling observation: Silvio’s article is a bit over 11 YEARS OLD now. And yet this seems to be the anti-debugging technique everyone relies on ever since. Why aren’t we moving forward on this area? I understand that researching anti-debugging on a platform that runs 99% on open source software and is hardly a malware target isn’t very exciting. But even though, I find it sadly peculiar that the community hasn’t displayed barely any interest whatsoever on the topic in the span of a decade. The best discussion by far that I have found on the subject (and I only found it as I was writing this) is this one: http://www.woodmann.com/forum/archive/index.php/t-10109.html. They propose a solution for this problem by having another process intercepting the traced process’s signals and passing them back and forth for correct handling.

Anyway, so, after some thinking, I had an idea. In a nutshell: we fire up another process to try attaching to the original one. If it fails, it’s a sign that someone else already attached, so we can invoke whichever aborting/sabotaging measures we want. Else if we are to continue, we detach from the process, effectively restoring all the signal handling stuff back to normal. In the meanwhile, the original process is waiting for the checking process to finish, so we never execute an instruction of the actual program.

To finish it up, this is how the final PoC code looks like:

#include <unistd.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/wait.h>

void anti_ptrace(void) __attribute__ ((constructor));

void anti_ptrace(void)
{
    pid_t child;

    if(getenv("LD_PRELOAD"))
        while(1);

    child = fork();
    if (child)
        wait(NULL);
    else {
        pid_t parent = getppid();

        if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
            while(1);

        sleep(1);
        ptrace(PTRACE_DETACH, parent, 0, 0);
        exit(0);
    }

}

int main()
{
    sleep(10);
    printf("done!\n");
}

One might have noticed the “sleep(1)” in the code above. The “explanation” for that is that it seems that Linux needs a little time between attaching and detaching from a process so that the signal restoring thing works as it should. Well, what can I say? Guess we all need some magic every now and then :P