使用Frida和BurpSuite测试Android客户端

在做Android客户端客户端测试的时候,如果客户端和服务器传输的数据经过加密处理,传统的中间人攻击面对加密数据毫无办法,除非在抓包工具中将加密数据解密出来,然后再修改明文数据,最后把修改的数据再加密回去。但是这一过程特别繁琐和复杂,面前的文章介绍过了通过xposed hook来解决这一问题,具体原理就是获取加密前的数据,配置好代理(代理配置为burp的地址和端口)发往中转服务器(中转负责将修改的数据返回到hook函数中),这样就绕开了加密的限制,使中间人攻击得以正常进行。

1 Frida拦截函数

整个测试方案的第一步就是需要获取未加密的数据,这个过程需要利用frida来拦截目标函数,获取参数内容。frida中内置的java api可以实现这个功能。承接之前Xposed学习-插件开发(二)这篇文章,class为MainActivity,method为encryptDataWithKey,methon的参数为两个String类型,以下示例代码可以将加密钱的数据打印出来,然后调用原函数继续走正常流程。

# -*- coding:utf-8 -*-
import sys
import frida

package_name = "com.flysands.mockapp"

def get_messages_from_js(message, data):
    print(message)

def hook_log_on_command():
    hook_code = """
    Java.perform(function () {
        console.log("[*] Hook data encrypt function");
        var hclass = Java.use("com.flysands.mockapp.EncryptUtil");
        hclass.encryptDataWithKey.implementation = function (a, b) {
            console.log("Hook Start...");
            console.log(arguments[0]);
            return this.encryptDataWithKey(a, b);
        }
    });
    """
    return hook_code

def main():
    process = frida.get_device_manager().enumerate_devices()[-1].attach(
        package_name)
    script = process.create_script(hook_log_on_command())
    script.on('message', get_messages_from_js)
    script.load()
    sys.stdin.read()

if __name__ == '__main__':
    main()

如果要实现抓包并与burp交互的话。需要一个转发服务器,将post body的内容作为response的body返回,除此之外还需要将我们在JavaScript中拦截的数据,经过burp代理发送到转发服务器。

2 数据发送和修改

要将数据经过代理发送到目标服务器,有以下三个可行方案,经过分析选择了第三个方案,第一个方案貌似不支持,没有frida开放这样的接口,第二个方案理论可行,但是太繁琐,第三个方案利用frida的python和JavaScript的message交互机制可以很简单实现数据发送的功能:

  1. 直接在JavaScript中通过http请求将数据发送到中转服务器
  2. 在JavaScript代码中用Java.use等api进行反射,像在xposed插件中实现的代码一样新建一个线程,并在新线程通过http请求将数据发送到中转服务器,主线程等待返回结果
  3. 利用JavaScript的rpc机制将控制权交给python,由python代码负责将数据发送到中转服务器,也由python代码将结果发送给JavaScript代码

具体的实现原理如下图所示:

frida-burp.png

图1  frida-burp原理

3 中转服务器

使用java代码实现一个简单的http server,将获取的request body的内容作为response的body返回。

package com.xwapi.test;

import com.alibaba.fastjson.JSON;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

/**
 * Created by chenxuesong on 19/12/14.
 */
public class ApiTestForwardHandler implements HttpHandler {

    @Override
    public void handle(HttpExchange httpExchange) throws IOException {
        String response = "hello world";
        httpExchange.sendResponseHeaders(200, 0);
        InputStream is = httpExchange.getRequestBody();
        String url = httpExchange.getRequestURI().toString();
        byte[] bytes = new byte[is.available()];
        is.read(bytes);
        String str = new String(bytes);
        System.out.println("read post body " + str);
        try {
            Map maps = (Map) JSON.parse(str);
            for (Object map : maps.entrySet()) {
                System.out
                    .println("key: " + ((Map.Entry) map).getKey() + "     value: " + ((Map.Entry) map).getValue());
            }
            response = JSON.toJSONString(maps);
        } catch (Exception e) {
            e.printStackTrace();
        }
        OutputStream os = httpExchange.getResponseBody();
        os.write(response.getBytes());
        os.close();

    }
}

4 python和js代码

最终的完整代码如下:

    # -*- coding:utf-8 -*-

import sys

import requests

import frida

package_name = "com.flysands.mockapp"

g_script = None
# 将数据原样返回的服务器的值
FORWARD_SERVER_IP = '192.168.50.254'
FORWARD_SERVER_PORT = 8088
# proxies里面的配置为burp的地址
PROXIES = {"http": "http://127.0.0.1:8080"}


def get_messages_from_js(message, data):
    print(message)
    if message['type'] == 'send':
        if message['payload']['from'] == 'jscode':
            req = requests.post(
                'http://%s:%d/apiforward' % (FORWARD_SERVER_IP,
                                             FORWARD_SERVER_PORT),
                proxies=PROXIES,
                headers={'content-type': 'text/plain'},
                data=message['payload']['payload'])
            g_script.post({'type': 'python_send', 'payload': req.content})


def hook_log_on_command():
    hook_code = """
    Java.perform(function () {
      console.log("[*] Hook data encrypt function");
      var hclass = Java.use("com.flysands.mockapp.EncryptUtil");
      hclass.encryptDataWithKey.implementation = function (a, b) {
        console.log("Hook Start...");
        console.log(arguments[0]);
        send({'from': 'jscode', 'payload': arguments[0]})
        var op = recv('python_send', function(value) { // callback function
            console.log("Rec from forward server content: " + value.payload)
            a =  value.payload
        });
        op.wait();
        return this.encryptDataWithKey(a, b);
    }

    });
    """
    return hook_code


def main():
    process = frida.get_device_manager().enumerate_devices()[-1].attach(
        package_name)
    script = process.create_script(hook_log_on_command())
    script.on('message', get_messages_from_js)
    global g_script
    g_script = script
    script.load()
    sys.stdin.read()


if __name__ == '__main__':
    main()