Remote Android Debugging — Margin Research
Remote Android Debugging

Remote Android Debugging

Martin Wennberg
by Martin Wennberg
Jan 14, 2022

Covid just made GDB obsolete, long live Frida!

Imagine a world where you don’t have to press 500 keys just to get to the same state as you were in the last run and where you actually can introspect the runtime. Frida offers a somewhat clean way to debug complex code consistently with deep introspection in the runtime and with good high level scripting capabilities. Where pwntools fails, Frida fills the gaps.

Debugging using Frida

To debug with Frida, I found it easiest to create a python script which creates the process to debug and then builds the debug script. There are different ways of creating the debug script, in some scenarios it is easier to use python as a meta language for the pre-processing while in most cases you can create a js library for all the standard functionality. One problem/drawback with Frida is the lack of these more high level functions.

Hooking all function overloads

import frida

def call(expand):
    return f"""
    indentation_level += 1;
    var ret = {expand};
    indentation_level -= 1;
    """

def basefun(fun):
    return f"""
    function (...args) {{
    print(`Calling: {fun}(${{[...args].join(", ")}})`);
    var fun = this.{fun};
    {call("fun(...args)")}
    print("{fun} -> " + ret);
    return ret;
    }}
    """
    
def hookall(classpath, fun):
    return f"""
    Java.use("{classpath}").{fun}.overloads.forEach(overload => {{
    overload.implementation = {basefun(fun)}
    }});
    """

We can then easily hook any overloads of a function bar by simply adding hookall("com.foobar.Foo", "bar") to our script. Likewise, we could also get stack traces by simply inserting {stacktrace()} anywhere in our script.

Stacktrace

def stacktrace():
    return f"""{call('Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())')}
    """

The only real fundamental shortcomings of Frida are the communication channels between the device and the debugger. For example, if I wanted to transfer a file from the device file system I would have to use the Frida on_message functionality to communicate using custom protocols (not good).

Dumping files

def on_message(message, data):
    ...
    data = message["payload"]
    if data.startswith("BEGIN"):
        filename = "/".join(data[5:].split("/")[-2:])
        ...
        currfile = open(filename, "wb")
    elif data == "DONE":
        print("[PyI] dumped file %s." % currfile.name)
        currfile.close()
    else:
        currfile.write(bytes(x % 256 for x in data))

def dump_file(filename):
    return f"""
    try {{
        var File = Java.use("java.io.File");
        var FileInputStream = 
        Java.use("java.io.FileInputStream").$new({filename});
        ...
    
        send("BEGIN " + {filename});
        while (true) {{
            var nr = reader.read(buffer);
            ...
            send(buffer);
            ...
        }}
    }} catch (ex) {{
        print(ex);
    }}
    
    send("DONE");
    """

As if Java wasn’t verbose enough already, we are now basically writing code in 3 languages to debug one. A possible solution to this is to create libraries on top of Frida which does all the nasty stuff for you, it is not very often that scenarios like these are encountered though.

Problems with Frida

There are definitely problems with Frida, however, I think they will be fixed with time given that the user base increases. The only real problems I have encountered were the lack of good communication between the device and debugger, a lack of documentation for more complex debug actions and a lack of a stdlib.

Doing this remotely Covid safe?

Trivial! Since adb already works the same over usb as over the internet, all you have to do is to install the frida server on the device and you are up and running! Contrary to intuition, the delay from debugging remotely is almost not noticeable except for actually interacting with the phone itself.

adb tcpip 5555 # while phone is plugged in via USB

adb connect 192.0.2.2 5555

frida-ps -U # it acts like as if the device was plugged in like normal

scrcpy --turn-screen-off # and turn down the brightness

Just in case the screen accidentally gets turned on. You do not want burn-in.

Future development

Using Frida is really easy once you get into it and after having built a codebase for the base functionality, I strongly believe we will start seeing it more in applications ranging from gui debuggers to just average debugging scripts. There is definitely an entry bar and a lot of unnecessary code that can be fixed by some more abstract library, like a gef/pwndbg but for Frida. The improvements that can be made on remote debugging are mainly persistence related, such as turning on WiFi if it is disabled, automagically removing broken proxy setups and everything that otherwise requires physical access.

Figure 1: Remote debugging in action
Figure 1: Remote debugging in action!

Remote Setup

The setup that I managed to throw together consisted of charles, adb, scrcpy, and frida on local, and frida-server, WiFi on remote. It might be helpful to use USB instead of WiFi on the remote device to eliminate some sources of pain. In order to intercept https packages we need to proxy localhost:8888 (or any other port) and set up a reverse proxy with adb over the same port, this can be done by running adb reverse [port number]. Setting the proxy on the device is implementation-dependent, so refer to the specific vendor. You can then navigate to the appropriate site to fetch the cert depending on whether you are using burp / charles / etc. Future improvements on this setup would be to have an isolated WiFi network that transparently proxies everything except adb.

For the person that: doesn’t like docs

Frida Java examples exist at frida examples


Share this article:

arrow-up icon