Android hooking HTTPS traffic using Frida

Frank picture Frank · Oct 12, 2017 · Viewed 7.5k times · Source

I'm trying to learn Frida to hook into various application. Specifically I'm trying to hook into Android applications, I'm using the Appmon project. That project has an HTTPS.js script that hooks into the getInputStream and getOutputStream from the HttpUrlConnection class. The HTTPS.js script successfully hooks the methods, but when HTTPS traffic is sent the data that i see is encrypted.

According to Android documentation on HTTPUrlConnection if the URL.openconnection() method receives a HTTPS url it will return a HTTPSUrlConnection object.

Android documentation for HttpsURLConnection states it is an abstract class that extends HttpURLConnection.

public abstract class HttpsURLConnection extends HttpURLConnection

I searched the android source code and found HttpsURLConnection is abstract and is extended by DelegatingHttpsURLConnection which is also abstract. DelegatingHttpsURLConnection is extended by HttpsURLConnectionImpl

I've hooked into

com.android.okhttp.internal.huc.HttpsURLConnectionImpl

which is successful but the data is still encrypted. Here is the code

var HttpsURLConnection = Java.use("com.android.okhttp.internal.huc.HttpsURLConnectionImpl");

HttpsURLConnection.getInputStream.overloads[0].implementation = function() {
try {
  methodURL = "";
responseHeaders = "";
responseBody = "";
var Connection = this;
var stream = stream = this.getInputStream.overloads[0].apply(this, arguments);

var requestURL = Connection.getURL().toString();
var requestMethod = Connection.getRequestMethod();
var requestProperties
methodURL = requestMethod + " " + requestURL;
if (Connection.getHeaderFields) {
  var Keys = Connection.getHeaderFields().keySet().toArray();
  var Values = Connection.getHeaderFields().values().toArray();
  responseHeaders = "";
  for (var key in Keys) {
    if (Keys[key] && Keys[key] !== null && Values[key]) {
      responseHeaders += Keys[key] + ": " + Values[key].toString().replace(/\[/gi, "").replace(/\]/gi, "") + "\n";
    } else if (Values[key]) {
      responseHeaders += Values[key].toString().replace(/\[/gi, "").replace(/\]/gi, "") + "\n";
    }
  }
}
var retval;
if (stream) {
  var baos = ByteArrayOutputStream.$new();
  var buffer = -1;

  var BufferedReaderStream = BufferedReader.$new(InputStreamReader.$new(stream));
  while ((buffer =stream.read()) != -1){
      baos.write(buffer);
      responseBody += String.fromCharCode(buffer);
   }
BufferedReaderStream.close();
baos.flush();
retval = ByteArrayInputStream.$new(baos.toByteArray());

}
/*   --- Payload Header --- */


var send_data = {};
send_data.time = new Date();
send_data.txnType = 'HTTPS';
send_data.lib = 'com.android.okhttp.internal.huc.HttpsURLConnectionImpl';
send_data.method = 'getInputStream';
send_data.artifact = [];
/*   --- Payload Body --- */
var data = {};
data.name = "Request/Response";
data.value = methodURL + "\n" + requestHeaders + "\n" + requestBody + "\n\n" + responseHeaders + "\n" + responseBody;
data.argSeq = 0;
send_data.artifact.push(data);
send(JSON.stringify(send_data));

if(retval)
    return retval;
return stream;
} catch (e) {
  this.getInputStream.overloads[0].apply(this, arguments);
}
}

BTW I am planning on submitting a pull request once i get this working.

Android documentation has an example on how to create an HTTP request:

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

Reading this tells me that just calling the getInputStream() method should return the clear text stream, but it doesnt appear to be doing so.

Question: How can i get the clear text data from the HTTPS traffic? I can see the headers, just not the actual data

Update 10/13

I'm wondering if the data I'm seeing isn't encrypted, just encoded. Here is a snipping of the data i receive back:

\\u001f\x8b\\b\\u0000\\u0000\\u0000\\u0000\\u0000\\u0004\\u0000\xed\xbd\\u0007`\\u001cI\x96%&/m\xca{\x7fJ\xf5J\xd7\xe0t\xa1\\b\x80`\\u0013$\xd8\x90@\\u0010\xec\xc1\x88\xcd\xe6\x92\xec\\u001diG#)\xab*\x81\xcaeVe]f\\u0016@\xcc\xed\x9d\xbc\xf7\xde{\xef\xbd\xf7\xde{\xef\xbd\xf7\xba;\x9dN\'\xf7\xdf\xff?\\\\fd\\u0001l\xf6\xceJ\xda\xc9\x9e!\x80\xaa\xc8\\u001f?~|\\u001f?\\"\\u001e\xff\\u001e\xef\\u0016ez\x99\xd7MQ-?\xfbhw\xbc\xf3Q\x9a/\xa7\xd5\xacX^

The response header indicates that its gzip encoded

Content-Encoding: gzip
Content-Length: 438
Content-Type: text/xml; charset=utf-8

I wonder if its related to this stackoverflow answer, although the outgoing request does have the "Accept-Encoding: gzip" header. I tried adding a call to GZIPInputStream, but the application doesnt like the response

Update 2 So I am able to get it to capture the data, it is a problem with gzip. The problem I'm running into now is that the application on the Android device expcts a GZIPed input stream. To display the data within Frida i have to run it through GZIPInputStream, not sure yet how to compress it again to send to the app. I tried breifly with GZIPOutputStream but that didnt work. Here is my updated code.

 HttpURLConnection.getInputStream.overloads[0].implementation = function() {    
try {
  methodURL = "";
responseHeaders = "";
responseBody = "";
var Connection = this;
if("gzip" == Connection.getContentEncoding())
{
    var stream = InputStreamReader.$new(GZIPInputStream.$new(this.getInputStream.apply(this, arguments)));
}   
else
{
    var stream = InputStreamReader.$new(this.getInputStream.apply(this, arguments));
}
}

var requestURL = Connection.getURL().toString();
var requestMethod = Connection.getRequestMethod();
var requestProperties
methodURL = requestMethod + " " + requestURL;
if (Connection.getHeaderFields) {
  var Keys = Connection.getHeaderFields().keySet().toArray();
  var Values = Connection.getHeaderFields().values().toArray();
  responseHeaders = "";
  for (var key in Keys) {
    if (Keys[key] && Keys[key] !== null && Values[key]) {
      responseHeaders += Keys[key] + ": " + Values[key].toString().replace(/\[/gi, "").replace(/\]/gi, "") + "\n";
    } else if (Values[key]) {
      responseHeaders += Values[key].toString().replace(/\[/gi, "").replace(/\]/gi, "") + "\n";
    }
  }
}
var retval;
if (stream) {
  var baos = ByteArrayOutputStream.$new();
  var buffer = -1;

  var BufferedReaderStream = BufferedReader.$new(stream);
  while ((buffer =stream.read()) != -1){
      baos.write(buffer);
      responseBody += String.fromCharCode(buffer);
   }
BufferedReaderStream.close();
baos.flush();
if("gzip" == Connection.getContentEncoding())
{
    retval = GZIPOutputStream.$new(ByteArrayInputStream.$new(baos.toByteArray()));
}
else
{
    retval = ByteArrayInputStream.$new(baos.toByteArray());
}


}
/*   --- Payload Header --- */


var send_data = {};
send_data.time = new Date();
send_data.txnType = 'HTTP';
send_data.lib = 'com.android.okhttp.internal.http.HttpURLConnectionImpl';
send_data.method = 'getInputStream';
send_data.artifact = [];
/*   --- Payload Body --- */
var data = {};
data.name = "Request/Response";
data.value = methodURL + "\n" + requestHeaders + "\n" + requestBody + "\n\n" + responseHeaders + "\n" + responseBody;
data.argSeq = 0;
send_data.artifact.push(data);
send(JSON.stringify(send_data));

if(retval)
    return retval;
return stream;
} catch (e) {
  this.getInputStream.overloads[0].apply(this, arguments);
}
}

Answer