年初接到一个银行代码审计项目,审计内容为由北京某一厂商开发三套系统的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端测试中也可以进行应用。
总的来说,这个方法的优点在于完全不需要考虑证书问题(无论是单向校验还是双向校验),不需要在意修改是否超时,也不需要在意一些与业务无关数据的生成。
缺点在于编码问题是一个很难解决的问题,而返回数据包存在大量中文,因此返回数据包修改后就无法使用。另外运行太慢,数据量过大进行密钥交换时,会出现卡死的现象。