基于Frida进行通信数据“解密”

年初接到一个银行代码审计项目,审计内容为由北京某一厂商开发三套系统的Android、iOS客户端部分代码(封装包除外),后台系统的erlang代码部分。其中erlang作为中间平台和银行核心系统进行通信,充当数据转发的角色。

审计得到很多越权类型漏洞,但是在进行漏洞验证的过程中发现程序进行了加密处理。我开始的思路与以往一样——摸清整个加密后写一个Burp插件或者mitm proxy脚本进行数据加解密处理。

但是在花了一天时间仔细分析了一下算法之后发现事情并不简单。

1.Background

年初接到一个银行代码审计项目,审计内容为由北京某一厂商开发三套系统的Android、iOS客户端部分代码(封装包除外),后台系统的erlang代码部分。其中erlang作为中间平台和银行核心系统进行通信,充当数据转发的角色。

审计得到很多越权类型漏洞,但是在进行漏洞验证的过程中发现程序进行了加密处理。我开始的思路与以往一样——摸清整个加密后写一个Burp插件或者mitm proxy脚本进行数据加解密处理。

但是在花了一天时间仔细分析了一下算法之后发现事情并不简单:

(1)加密算法不复杂,就是AES_CBC_128,但是key和IV的生成算法十分复杂;

(2)key和IV值约10分钟更新一次,更新过程采用RSA加密;

(3)加密逻辑封装在第三方库中,代码经过混淆,这在复杂算法逻辑中起到了较好的防护作用,难以分析清楚;

(4)不能花太多时间在分析逻辑、复现逻辑、编写插件上面。于是就在加解密前进行明文数据处理,解密后进行数据显示。

2.LUA & Send Data

通常而言,Android支持的可执行文件是arm程序、第三方库以及dex格式文件。而apk程序由于具有大量用于交互的lua脚本文件,它们无法被安卓系统解释,因此只能通过当前程序的第三方库进行lua脚本解释。

用IDA pro从so文件libluajava.so中可以定位到如下的函数(见图1):

GetMethodId(env, claz, “postAsyn”, “(Lcom/xxxxxx/emp/lua/java/CLEntity;Ljava/lang/String;Ljava/lang/String;III)V”)

图1  定位函数

在jeb中进行全局搜索,可以简单定位到Java函数(见图2)。

图2  定位 Java 函数

eW是个Task,其中的doRun就是关键点(见图3)。

图3  程序运行关键点

NetRequest中的post方法如下,调用了sendPostRequest方法(见图4)。

图4  调用 sendPostRequest 方法

由于CryptoHttpManager是继承了HttpManager的,因此sendPostRequest方法在其中(见图5和图6)。

图5  HttpManager

图6  sendPostRequest

而其中this.a调用的是CryptoHttpManager的方法,又在里面调用了HttpManager的a方法,并将返回内容进行AES解密。也就是说,很可能这个加密也是用的AES(见图7)。

图7AES 解密

调用父类的方法时,还是请求子类的方法进行加密并执行Post动作,然后得到resp之后回到子类的a方法中进行解密(见图8)。

图8加解密流程

3.Hook & Modify

对CryptoHttpManager的a方法(开始请求的方法)进行Hook,以及AESAdapter的decrypt(byte[],byte[],byte[])方法(进行返回解密的方法)。

  // 发送的明文数据
    var cryptoHttpMgr = Java.use("com.xxxxxx.emp.net.CryptoHttpManager");
    var request_method = cryptoHttpMgr.a.overload('java.lang.String','java.lang.Object','java.lang.String','java.lang.String','java.lang.String','java.util.Map','com.xxxxxx.emp.render.EMPThreadPool$Task');
    // 解密的返回数据
    var aesAdaptor = Java.use("com.xxxxxx.emp.security.adapter.AESAdapter");
    var decryptByte = aesAdaptor.decrypt.overload('[B', '[B', '[B');

 

要打印数据,那么就需要用Js把数据发送出来:

request_method.implementation = function(url, param, rsq_method, contenttype,
 accept, headermap, task){
    send("[+] Requesting .... "); // send将固定字符 以及 数据发送出来

    send("- Req Method: " + rsq_method);
    send("- Req URL: " + url);
    send("- Req Params: " + param.toString());

    return request_method.call(this, url, param, rsq_method, contenttype, accept, headermap, task);
};  

decryptByte.implementation = function(content, key, iv){
    send("[+] Decrypting .... ");

    send("- AES key:\\n" + hexdump(b2s(key)));
    send("- AES IV:\\n" + hexdump(b2s(iv)));

    var result = decryptByte.call(this, content, key, iv);

    send("- out:\\n" + hexdump(b2s(result)));

    return result;
};

而Frida接收send函数发送的数据,并打印:

def on_message(message, data):
    if message['type'] == 'send':
        print message['payload']

将输出的内容进行格式化:

function hexdump(buffer, blockSize) {
    blockSize = blockSize || 16;
    var lines = [];
    var hex = "0123456789ABCDEF";
    for (var b = 0; b < buffer.length; b += blockSize) {
        var block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
        var addr = ("0000" + b.toString(16)).slice(-4);
        var codes = block.split('').map(function(ch) {
            var code = ch.charCodeAt(0);
            return " " + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
        }).join("");
        codes += "   ".repeat(blockSize - block.length);
        var chars = block.replace(/[^\x20-\x7E]/g, '.'); // 不可打印字符
        if (chars.charAt(chars.length - 1) == '\\'){ // 打印\这种符号的时候,在Python中直接把后面的字符转义了,所以要修正
            chars += '\\';
        }
        chars += " ".repeat(blockSize - block.length);
        lines.push(addr + " " + codes + "  " + chars);
    }
    return lines.join("\\n");
}
function b2s(array) {
    var result = "";
    for (var i = 0; i < array.length; i++) {
        result += String.fromCharCode(modulus(array[i], 256));
    }
    return result;
}
function modulus(x, n) {
    return ((x % n) + n) % n;
}

输出效果如图9所示。

图9打印日志

但是我要篡改数据包,篡改就需要Burpsuite,此时想到的是(见图10):

图10数据包篡改流程示意

而我们也不能发送到真正的Server,就只是一个简单的Server请求数据返回即可:

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser

ECHO_PORT = 2205

class RequestHandler(BaseHTTPRequestHandler):

    def do_POST(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()

数据可以被打印出来,就表明数据已经到了Python层,因此用requests库进行发送即可。

发送数据时需要针对数据内容才能进行处理,其他请求不进行处理,于是从以下几个方面进行限制:

  • 请求特征 —— id=
  • 请求长度 —— 必定是大于14的
  • 传输标志 —— Req Params

然后需要用框架中的post将内容回传:

def on_message(message, data):
    if message['type'] == 'send':
        print(message['payload'])
        payload = ''
        if len(message['payload']) > 14 and 'id=' in message['payload'] and 'Req Params' in message['payload']:
            payload = message['payload'][14:]
            r = requests.post("http://" + BURP_HOST + ":" + str(BURP_PORT), data = payload, proxies = proxies);
            if r.status_code == 200:
                script.post({"type": "modify", "payload": r.text})
    else:
        print(message)

同时要通过同步方式回传数据的获取(不能异步):

       var op = recv('modify', function onMessage(modMessage) { // 接收回传回来的type为modify的内容
            send("- Fix Params: " + modMessage['payload']); // 打印修改后的值
            param = modMessage['payload']; // 覆盖原值
        });
        op.wait(); // 同步等待

解密后的内容篡改也是一样的思路,在return之前进行值修改。

4.Repeating

但是大多数时候还需要数据包重放 。可喜的是这个并没有密钥一次失效之说。打标签是比较简单的实现方式:

(1)发送一个Burp请求。

(2)等待返回。

  • 如果有asd(随手写的)-> 进入(3);
  • 如果没有asd -> 跳过循环 -> 调用程序方法(循环跳出的会执行多一次call)。

(3)去除asd标签。

(4)调用程序方法。

(5)加上asd标签,回到1。

while(1){
    send("[*] Repeating .... ");
    send("- Req params: " + param); // 1
    var op = recv('mod_req', function onMessage(modMessage) { //2
        send("- Fix Params: " + modMessage['payload']);
        param = modMessage['payload'];
    });
    op.wait();
    
    if (param.indexOf('asd') < 0) break; // 2.2
    //2.1
    param = param.replace(/asd/,""); // 3
    request_method.call(this, url, param, rsq_method, contenttype, accept, headermap, task); // 4
    param = param + 'asd'; // 5
}

python中的信息处理代码如下:

def on_message(message, data):
    global theflag
    global vericode
    if message['type'] == 'send':
        print(message['payload'].decode('utf-8'))
        payload = ''
        typestr = 'mod_req'
        if len(message['payload']) > 14 and 'Req params:' in message['payload'] and 'id=' in message['payload']:
            payload = message['payload'][14:]
            r = requests.post("http://" + BURP_HOST + ":" + str(BURP_PORT), data = payload, proxies = proxies);
            if r.status_code == 200:
                script.post({"type": typestr, "payload": r.text})
    else:
        print(message)

还有另外一个思路,关键是要注意返回的数据只是函数中的其中一个参数,其他参数还要进行初始化或者设置全局变量等操作:

(1)利用SimpleHTTPServer监听一个端口用于接收Burp的请求。

(2)用异步消息接收被post过来的消息(数据是端口监听的数据)。

(3)接收到数据后进行函数调用即可。

5.成果检验

启动服务器以及Burpsuite,运行程序并进行附加,明文数据进入burpsuite进行修改,返回到程序中进行加密前数据替换,界面显示不同的结果(完整的解密数据在terminal显示)(见图11):

图11成功解密通信数据

6.经验总结

使用Hook进行加密前修改明文数据后,在一些银行的Android以及iOS系统测试中进行了应用,在PC端测试中也可以进行应用。

总的来说,这个方法的优点在于完全不需要考虑证书问题(无论是单向校验还是双向校验),不需要在意修改是否超时,也不需要在意一些与业务无关数据的生成。

缺点在于编码问题是一个很难解决的问题,而返回数据包存在大量中文,因此返回数据包修改后就无法使用。另外运行太慢,数据量过大进行密钥交换时,会出现卡死的现象。

Spread the word. Share this post!

Meet The Author

Leave Comment