Tracing API calls in Burp with Frida
A few weeks ago I was performing a security test on a mobile banking application. The application was using a framework that provided additional obfuscation and encryption on top of the TLS connection it used to communicate with the remote server. I used Frida to intercept and dump the plaintext requests/responses before the encryption took place. I wanted to modify intercepted API calls and see how the remote server responded, but this required me to modify the Frida script each time.
Burp is really my tool of choice for (web) backend testing, and a mobile backend should not be that much different. I wanted to make Burp work together with Frida, and intercept/modify API calls in Burp. I did not go as far as creating a Burp plugin, but using a small script we can already get started with intercepting API calls. This spared me the trouble of modifying the frida-trace handler script each time I wanted to modify an API parameter.
Intercepting API requests with Burp does not require a lot of work:
- Set up a Burp listener (e.g. listen port 26080) redirecting traffic to an echo server (e.g. port 27080) in invisible proxy mode
- Have an echo server listening on port 27080 (which just echoes back the request)
- Use Frida to synchronously send an HTTP request to the Burp listener with as payload the API call data
Burp will receive the API request sent by Frida. The user can then modify the call in transit in Burp, which then forwards the data to the echo server. The echo server will simply reflect the (modified) request in the response, which is received back by the Frida code.
Set up Burp listener
Let’s start with setting up a Burp listener. Here, I’ve used listener port 26080 in invisible proxy mode:
Python tracer code
I opted to extend (or rather monkey patch) the existing frida-trace code, in order to have the same flexibility of the tracing tool.
The following Python code extends the frida-trace code to work together with a server you forward the API call to. It does this by sending an HTTP request to the local Burp listener we created. You could also add metadata in the HTTP headers about the API call, or specify a URL path to distinguish between different API calls, which will be visible in Burp.
The current code might be version dependent: I don’t know what will change in future versions, though it currently works with Frida 8.2.2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
from frida import tracer
import requests
BURP_HOST = "localhost"
BURP_PORT = 26080
def frida_process_message(self, message, data, ui):
handled = False
if message['type'] == 'input':
handled = True
elif message['type'] == 'send':
stanza = message['payload']
if stanza['from'] == '/http':
req = requests.request('FRIDA', 'http://%s:%d/' % (BURP_HOST, BURP_PORT), headers={'content-type':'text/plain'}, data=stanza['payload'])
self._script.post({ 'type': 'input', 'payload': req.content })
handled = True
if not handled:
self.__process_message(message, data, ui)
tracer.Tracer.__process_message = tracer.Tracer._process_message
tracer.Tracer._process_message = frida_process_message
if __name__ == '__main__':
print("[x] To intercept in Burp, set up an invisible proxy listening on port %d, forwarding to the echo server." % BURP_PORT)
tracer.main()
|
Trace handler script
The following example could be used to intercept read()
calls in the onLeave
function. This should be easily adaptable for your own usecase.
We only need to forward/intercept the arguments that we want to tamper with.
I’ve implemented this by using the Frida send()
API. This call is received by the Python code, which then responds back to the handler script. The script waits synchronously for a response (while we are performing modifications in Burp), and then executes the callback function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
onEnter: function (log, args, state) {
log("read(" + "fd=" + args[0]+ ", buf=" + args[1]+ ", count=" + args[2] + ")");
state.buf = args[1]
},
onLeave: function (log, retval, state) {
send({from: '/http', payload: Memory.readUtf8String(state.buf)})
var op = recv('input', function(value) { // callback function
log("Forwarding mitm'ed content: " + value.payload)
Memory.writeUtf8String(state.buf, value.payload)
});
op.wait();
}
}
|
Echo server
The following script is a small implementation of an echo server that responds to “FRIDA” requests and echoes back the request payload. This could be extended as well with your own logic to modify requests on-the-fly (although you could just as well implement that directly in your Frida scripts).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
ECHO_PORT = 27080
class RequestHandler(BaseHTTPRequestHandler):
def do_FRIDA(self):
request_path = self.path
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
self.send_response(200)
self.end_headers()
self.wfile.write(self.rfile.read(length))
def main():
print('Listening on localhost:%d' % ECHO_PORT)
server = HTTPServer(('', ECHO_PORT), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
print("[x] Starting echo server on port %d" % ECHO_PORT)
main()
|
Demo
Here’s the code in action:
Recent Comments