More Android Anti-Debugging Fun

In my last blog post, I talked about tampering with the virtual method tables of certain JDWP-related classes in ART. By sprinkling an app with little anti-JDWP tricks, developers can effectively sabotage JVM-level debugging, causing much anger and grief to reverse engineers. There's a catch however: Android is built on Linux, and thus inherits the ptrace system call, which provides tracing and debugging facilities on the native layer. Many powerful tools, including gdb, strace, jtrace and Frida, are built on top of those facilities (some of those tools even offer introspection, providing a convenient backdoor for interacting with the Java VM). Anti-reversing schemes therefore always incorporate some form of anti-ptrace madness. 

Many old-school Linux anti-debugging tricks, such as monitoring the proc filesystem and detecting breakpoints in memory, work perfectly fine on Android. Another technique often used in malware and commercial products is self-debugging. This method exploits the fact that only one debugger can attach to a process at any one time. Let's investigate how this works.

PTRACE_TRACEME Please!

On Linux, the ptrace() system call is used to observe and control the execution of another process (the "tracee"), and examine and change the tracee's memory and registers. It is the primary means of implementing breakpoint debugging and system call tracing. A straigthforward way of using the ptrace system call for anti-debugging is forking a single child, and then calling ptrace(parent_pid) to attach to the parent.

void anti_debug() {

    child_pid = fork();

    if (child_pid == 0)
    {
        int ppid = getppid();
        int status;

        if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
        {
            waitpid(ppid, &status, 0);

            ptrace(PTRACE_CONT, ppid, NULL, NULL);

            while (waitpid(ppid, &status, 0)) {

                if (WIFSTOPPED(status)) {
                    ptrace(PTRACE_CONT, ppid, NULL, NULL);
                } else {
                    // Process has exited for some reason
                    _exit(0);
                }
            }
        }
    }
}

If implemented as above, the child will keep tracing the parent process until the parent exits, causing future attempts to attach a debugger to the parent to fail. We can verify this by compiling the code into a JNI function and packing it into an app we run on the device.

Assuming we have done that, let's now step into the shoes of the reverse engineer attempting to debug the app. The first sign that something funky is going on is that ps returns not one, but two processes with the same command line:

root@android:/ # ps | grep -i anti
u0_a151   18190 201   1535844 54908 ffffffff b6e0f124 S sg.vantagepoint.antidebug
u0_a151   18224 18190 1495180 35824 c019a3ac b6e0ee5c S sg.vantagepoint.antidebug

Android apps hardly fork() for legitimate reasons (correct me in the comments if I'm wrong), so this should already ring some alarm bells. Attempting to attach to the parent process with gdbserver confirms the suspicion:

root@android:/ # ./gdbserver --attach localhost:12345 18190
warning: process 18190 is already traced by process 18224
Cannot attach to lwp 18190: Operation not permitted (1)
Exiting

Still wearing reverse engineering boots, what is the first thing we can try here? Get rid of that pesky child!

root@android:/ # kill -9 18224

With the offspring gone (let's face it: This child was a bit too attached to its parent for its own good*), the parent should be open for a new relationship. Let's attempt to attach gdbserver again:

root@android:/ # ./gdbserver --attach localhost:12345 18190  Attached; pid = 18190
Listening on port 12345

Well that, was easy - to the point that most honest reverse engineers would feel ashamed for succeeding that way! In the real world, you don't get away that easily though, because the ptrace call is usually combined with more or less intricate forms of integrity monitoring. This can be done in a variety of ways - the developer's imagination is the only limit. Common methods include:

  • Forking multiple processes that trace one another;
  • Keeping track of running processes to make sure the children stay alive;
  • Monitoring values in the /proc filesystem, such as TracerPID in /proc/pid/status.

Let's look at a simple improvement we can make to the above method. After the initial fork(), we launch an extra thread in the parent that continually monitors the status of the child. Depending on whether the app has been built in debug or release mode (according to the android:debuggable flag in the Manifest), the child process is expected to behave in one of the following ways:

1. In release mode, the call to ptrace fails and the child exits immediately with a segmentation fault (exit code 11).

2. In debug mode, the call to ptrace works and the child is expected to run indefinitely. As a consequence, a call to waitpid(child_pid) should never return - if it does, something is fishy and we kill the whole process group.

The complete JNI implementation is below. Feel free to use it in your own project by adding  JNIEXPORT (...)_antidebug() as a native method.

#include <jni.h>
#include <string>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>

static int child_pid;

void *monitor_pid(void *) {

    int status;

    waitpid(child_pid, &status, 0);

    /* Child status should never change. */

    _exit(0); // Commit seppuku

}

void anti_debug() {

    child_pid = fork();

    if (child_pid == 0)
    {
        int ppid = getppid();
        int status;

        if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
        {
            waitpid(ppid, &status, 0);

            ptrace(PTRACE_CONT, ppid, NULL, NULL);

            while (waitpid(ppid, &status, 0)) {

                if (WIFSTOPPED(status)) {
                    ptrace(PTRACE_CONT, ppid, NULL, NULL);
                } else {
                    // Process has exited
                    _exit(0);
                }
            }
        }

    } else {
        pthread_t t;

        /* Start the monitoring thread */

        pthread_create(&t, NULL, monitor_pid, (void *)NULL);
    }
}
extern "C"

JNIEXPORT void JNICALL
Java_sg_vantagepoint_antidebug_MainActivity_antidebug(
        JNIEnv *env,
        jobject /* this */) {

        anti_debug();
}

Again, we pack this into an Android app to see if it works. Just as before, two processes show up when running the debug build of the app.

root@android:/ # ps | grep -i anti-debug
u0_a152   20267 201   1552508 56796 ffffffff b6e0f124 S sg.vantagepoint.anti-debug
u0_a152   20301 20267 1495192 33980 c019a3ac b6e0ee5c S sg.vantagepoint.anti-debug

However, if we now terminate the child process, the parent exits as well:

root@android:/ # kill -9 20301                               
root@android:/ # ./gdbserver --attach localhost:12345 20267   
gdbserver: unable to open /proc file '/proc/20267/status'
Cannot attach to lwp 20267: No such file or directory (2)
Exiting

To bypass this, it is necessary to modify the behavior of the app slightly (the easiest way is to patch the call to _exit with NOPs, or hooking the function _exit in libc.so). At this point, we have entered the proverbial "arms race": It is always possible to implement more "elaborate" forms of this defense, all of which can ultimately be defeated by reverse engineers willing to put in the effort. An interesting Android variant is described in my HITB 2016 paper, and there's surely a whole lot of case studies to be found in malware research (let me know in the comments if you know of any good examples).

If you want to have a shot at cracking this defense (along with a few others), try UnCrackable App for Android Level 2, and consider contributing your solution to the OWASP Mobile Security Testing Guide.

Bypassing Native Anti-Debugging

There is no generic way of bypassing anti-debugging tricks: It depends on the particular mechanism(s) used to prevent or detect debugging, as well as other defenses in the overall protection scheme. For example, if there are no integrity checks, or you have already deactivated them, patching the app might be the easiest way. In other cases, using Xposed, Frida or kernel modules could be more appropriate. Possible methods include:

1. Patching out the anti-debugging functionality. Disable the unwanted behaviour by simply overwriting it with NOP instructions. Note that more complex patches might be required if the anti-debugging mechanism is well thought-out.
2. Using Frida or Xposed to hook native APIs like ptrace() and fork(), or hooking the relevant system calls using a Kernel module.

We're documenting these and other techniques in the OWASP Mobile Testing Guide - plus, you'll also find many other debugging tricks there:

About this Article

This article is part of the Mobile Reverse Engineering Unleashed series. Click the blue label on the top of this page to list orther articles in this series.

About the OWASP Mobile Security Testing Guide

I wrote this how-to for the OWASP Mobile Security Testing Guide (MSTG), a manual for testing the security of mobile apps. The MSTG is an open source effort and we welcome contributions and feedback. To discuss and contribute, join the OWASP Mobile Security Project Slack Channel. You can sign up here:

http://owasp.herokuapp.com/

Also, check out the mobile crackmes we developed for the guide!

About the Author

Bernhard Mueller is a full-stack hacker, security researcher, and winner of BlackHat's Pwnie Award.

* Intentionally cringeworthy