This blog post is a tutorial to tamper with gRPC traffic in Android applications.

Under the hood, gRPC uses HTTP/2 to transfer messages. However, at the time of writing, all the popular intercepting proxies used for pentesting do not support intercepting and editing gRPC. So we have to come up with a quick and dirty way to tamper with gRPC traffic in an Android applications.

For those who are new to gRPC, check out the following useful articles to have an idea of the terminology and how gRPC works:

This tutorial uses the example app from gRPC official website for the demo. The app’s code can be found at:

Intercepting using mitmproxy

gRPC has 4 types of service methods, each of them has a different messaging mechanism:

  1. Unary RPC
  2. Client-streaming RPC
  3. Server-streaming RPC
  4. Bidirectional streaming RPC

For unary RPC, it can simply be intercepted using mitmproxy.

However, mitmproxy does not have a built-in gRPC message editing feature. So we have developed a mitmproxy extension to allow editing of gRPC messages. Most of the source code is taken from the mitmproxy’s gRPC viewer itself. The extension can be found at: https://gist.github.com/Hacktivate-TH/cd1345dfe772d8c58741313adf8872ca. Currently, the extension only supports uncompressed gRPC messages.

To use the extension, launch mitmproxy with option -s grpc-edit.py to load the extension, intercept the request or response you want to modify, press enter to open the details view, and enter the command grpc.edit @focus into mitmproxy’s command prompt.

Launching plugin

A CLI text editor in your machine will be opened. After making changes, save and close the text editor. The changes will be reflected into mitmproxy.

Launching plugin

This also works for client-streaming and server-streaming RPCs, except for certain circumstances which are explained below.

Problem with intercepting streaming RPCs

There are some scenarios in which using HTTP proxy to intercept streaming RPCs might affect an application’s behavior or, in the worst case, breaks the application functionalities, such as:

For example, in the RouteGuide application, the RouteSummary calculates and returns the total time used for delivery all the messages in the request steam. If the RPC call is proxied, due to the way HTTP proxies work, it will buffer the incoming gRPC message until it receives an HTTP/2 END_STREAM frame. Therefore, the time interval between each message that the server receives will differ from the client.

Time differs when being proxied

For an application in which the client and the server take their turn to send a message alternately, once being proxied, similar to the previous scenario, the request will be buffered. As a result, the order of the messages the server and the client receive will be inaccurate.

Message order without proxy

Message order with proxy

If the application communicates in a “ping-pong” pattern using a bidirectional streaming RPC, in which the server gets a request, then sends back a response, then the client sends another request using data it receives from the previous response, and so on. Once the requests are proxied, they will be buffered indefinitely since the client will never send the END_STREAM frame because it is waiting for the response from the server.

Message gets buffered indefinitely

If the application you’re working with cannot be proxied, the fallback approach is using Frida to tamper with gRPC messages.

Tampering gRPC message using Frida

An example Frida script for tampering with the gRPC message of RouteChat RPC can be found at: https://gist.github.com/Hacktivate-TH/cbda12fbf58e224b3cd7dc0bb35f9215.

In the first part of the script, we hook the function calls to io.grpc.stub.ClientCalls$CallToStreamObserverAdapter.onNext() which will be called upon sending message to bi-directional streaming RPC. The argument of the function call is the object to be serialized into protobuf and sent as a gRPC message. We can use getDeclaredMethods() to find out what setter methods are available to allow us to modify the data before it is sent to the server.

var streamObserver = Java.use("io.grpc.stub.ClientCalls$CallToStreamObserverAdapter");
  streamObserver.onNext.implementation = function(obj) {
    console.log("hooked!!");

    var objClassName = obj.$className;
    //inspect the type of argument

    console.log("Class name:" + objClassName);

    //cast the argument into the correct type

    var obj2 = Java.cast(obj, Java.use(objClassName));
    console.log("Object: " + obj2);

    //list available setter methods

    console.log("Available methods of " + objClassName + ":\n" + Java.use(objClassName).class.getDeclaredMethods

For example, the RouteNote object has setLocation method that allows us to modify the longitude and latitude of the message.

Available methods of object RouteNote

The rest of the script is the modifying of the RouteNote message and the location by creating a new Point object and assign it to the RouteNote object.

    var point = Java.use("io.grpc.examples.routeguide.Point").$new();
    console.log("Available methods of io.grpc.examples.routeguide.Point");
    console.log(Java.use('io.grpc.examples.routeguide.Point').class.getDeclaredMethods());
    point.setLongitude(2);
    point.setLatitude(2);
    console.log(point);
    
    obj2.setLocation(point);
    obj2.setMessage("modified message");
    this.onNext(obj2)

This is the end of the blog post. The way for intercepting gRPC messages we show here might not be perfect, but it does work as a workaround until one of our commonly used tools fully supports gRPC. We hope you find this post useful. Enjoy hacking!