0x00 前言
在做移动安全的app渗透或者说移动app的漏洞挖掘时,往往会碰到一种情况:好不容易绕过了app的反抓包机制,通过burp抓到了app传输的数据包,这时想对这部分数据做一些爆破、篡改之类的测试,却发现关键数据进行了加密处理,那么这时就不得不首先解决一下数据解密截取的问题。
0x01 环境搭建
(一)抓包环境
首先是抓包环境,需要针对app的反抓包机制做一些绕过,这不是本篇文章讨论重点,因此在另一篇文章中做介绍。
(二)frida框架
其次是我们做hook操作,需要依赖一些hook框架,来帮助我们更好的完成操作,这里我选用的是frida框架:简单安装教程如下:
1)首先安装python3环境,之后使用pip工具安装frida框架:
pip3 install frida(默认安装最新版)
当然可以指定版本安装:
pip3 install frida==14.2.18 frida-tools==9.2.4
!!注意frida与frida-tools的版本对应关系,可以去github查找
2)再安装frida-server到手机(如果是模拟器注意安装包的选取),下载后解压传输安装赋权启用即可
adb push xxxfdserverxxx /data/local/tmp/fs14218
cd /data/local/tmp
chmod 777 fs14218
./fs14218
//启动
之后另起一个cmd窗口,输入命令查看手机上的信息:
frida-ps -Uai
如果能看到如下一类信息,即安装成功。
(三).反编译工具 jadx-gui
直接下载打开即可食用
0x03 apk逆向&&明文抓取
(零)思路介绍
首先配置好代理,抓包看一下:
可以看到,app传输的数据都经过了一些未知的复杂加密处理,sign字段看过去类似是经过摘要算法得到的签名值之类,那么这里如果我们想要对data、timestamp等字段做一个测试,显然我们是不能直接修改的(可以更改一下看看效果)
既然这里不是一些简单的base64编码之类的处理,不能采取直接解码,又一眼看不出这里的加密方式(密码算法逆向),那么此时我们的思路还有什么呢?
可以想象一下数据的传输过程:
明文数据-->app调用加密算法进行加密--->app调用摘要算法计算消息摘要--->app发送请求,传输加密数据---->........
一眼看过去,可操作的攻击思路是不是就清晰了?
1.可以对加密算法进行逆向,完全掌控加密过程
2.对明文数据在进行加密之前进行截取,并做更改
这篇文章就介绍第二种攻击思路:
对app进行反编译,找到明文传输路径,通过hook方式在加密操作之前,对app的明文数据进行篡改操作,这样的篡改操作在加密与摘要之前,在app的验证机制看来完全合法。接下来实操尝试一下:
(一)反编译及寻找可疑方法调用栈
打开jadx-gui,将得到的app安装包丢进去反编译一下(有壳的先砸壳处理一下):
如何寻找可疑方法呢?这里介绍一种最常用的简单方法:
回头看一下我们抓到的包:
可以看到一些关键字,那么我们就直接在反编译的代码中去检索这些关键字,大概率就可以找到关键位置(如果源码做了混淆,就得采用一些其他办法,这里不做讨论):搜索一下sign关键字:
有所发现,一一进行查看即可,有经验的话,可能可以猜到一些特征:sign是网络请求中的字段等等...随意联想。
经过检查,找到最具可疑性的目标:
出现了请求包中出现的sign、_ver字段,以及可疑的DATA,继续跟进一下:
原来,DATA就是我们的data,hahah~~~
回头追踪sign字段:发现与e.c.a.i.c.a()方法相关,跟过去看一下
阅读代码,找到源头,原来sign值就是将传入的数据+一定的密钥通过encrypt()方法处理得到的,那么这里就可以作为我们的切入点,hook验证一下看看我们的猜测对不对,数据是不是真的经过了这里:
脚本如下:
//原理:APP应用程序在处理数据、提交数据时,通常会将数据存放于集合中,而HashMap又是其中较为常用的。因此,可以通过Hook HashMap的put方法来定位关键代码所在的位置。
// 使用Java.perform包装代码块以确保在正确的线程和类加载器上下文中执行
Java.perform(function () {
// 获取 java.util.HashMap 类引用
var HashMap = Java.use('java.util.HashMap');
// Hook HashMap 的 put 方法
HashMap.put.implementation = function (key, value) {
// 将 Java String 转换为 JavaScript 字符串
var keyStr = key ? key.toString() : '';
// // 如果 key 是 "data" 或 "sign"
if (keyStr === 'data' || keyStr === 'sign') {
// 打印 key 和 value
console.log('Key:', keyStr);
console.log('Value:', value);
// 打印调用栈
console.log('Call Stack:', Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()));
}
// console.log('Key:', keyStr);
// console.log('Value:', value);
// 调用原始的 put 方法实现,并返回结果
return this.put.call(this, key, value);
};
});
使用frida注入脚本:
首先在手机开启frida服务器:
./fs14218
之后执行命令,注入脚本:
frida -U -f 包名 -l hook脚本 --no-pause
如果此时app在前台运行,可以选择-UF模式
frida -UF -l hook脚本
刷新一下app试试:
果然,看到了相应的数据,证明我们的猜测无误,同时脚本也打印出了调用栈。
根据分析调用栈,寻找源头:(找与app包名相关的东西):找到:
e.c.a.i.d.a()方法,追踪到e.c.a.i.c.a()方法,和前面我们的分析吻合。那么这里我们就找到了精准位置
将该方法复制为一段frida代码片段,hook一下看看:
let c = Java.use("e.c.a.i.c");
c["a"].overload('java.lang.String').implementation = function (str) {
console.log(`c.a is called: str=${str}`);
let result = this["a"](str);
console.log(`c.a result=${result}`);
return result;
};
改成简单的hook脚本:1.js
Java.perform(function () {
let c = Java.use("e.c.a.i.c");
c["a"].overload('java.lang.String').implementation = function (str) {
console.log(`c.a is called: str=${str}`);
let result = this["a"](str);
console.log(`c.a result=${result}`);
return result;
};
});
注入一下看看:方法如上
截取到了想要的数据。切入点无误,开始操作,抓取明文:
(二)编写脚本,抓取明文(被动调用)
前面我们找到了精准位置如下:
因此这里就编写对应的hook脚本与python脚本,提取控制转发明文即可达成我们的目的:
首先简单的只打印看一下:
Java.perform(function () {
var c = Java.use("e.c.a.i.c");
var EncryptUtil = Java.use("com.qq.lib.EncryptUtil");
c.a.overload('java.lang.String').implementation = function(str){
console.log("\n请求加密前明文:\n",str);
return this.a(str);
}
EncryptUtil.decrypt.implementation = function(str,str2){
console.log("\n响应解密后明文:\n",this.decrypt(str,str2));
return this.decrypt(str,str2);
}
});
看到,明文已经抓取成功了,接下来就是需要丰富一下脚本功能,把请求与响应转发到我们的burp工具去,方便我们进行改包测试(这里我利用python来实现转发请求,调用hook脚本等功能)
python脚本如下(构建镜像服务器):
import argparse
from threading import Thread
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
import requests
import frida
# 定义端口号
ECHO_PORT = 28080
BURP_PORT = 8080
# 创建一个请求处理器类,继承自 BaseHTTPRequestHandler
class RequestHandler(BaseHTTPRequestHandler):
# 定义处理请求的方法
def do_REQUEST(self):
content_length = int(self.headers.get('content-length', 0))
self.send_response(200)
self.end_headers()
self.wfile.write(self.rfile.read(content_length))
# 处理响应的方法与处理请求的方法相同
do_RESPONSE = do_REQUEST
def echo_server_thread():
print('start echo server at port {}'.format(ECHO_PORT))
server = HTTPServer(('', ECHO_PORT), RequestHandler)
server.serve_forever()
t = Thread(target=echo_server_thread)
t.daemon = True
t.start()
# 添加命令行参数解析
parser = argparse.ArgumentParser(description="Frida Python script with command line arguments.")
parser.add_argument("process_name", help="The process name you want to attach to.")
parser.add_argument("js_file", help="The path to the Frida JS script.")
args = parser.parse_args()
# 通过 USB 设备附加到指定进程
session = frida.get_usb_device().attach(args.process_name)
# 加载 Frida JS 脚本
with open(args.js_file, "r", encoding='utf-8') as f:
js_code = f.read()
script = session.create_script(js_code)
# 定义处理来自 Frida 脚本的消息的函数
def on_message(message, data):
if message['type'] == 'send':
payload = message['payload']
_type, data = payload['type'], payload['data']
# print(message)
if _type == 'REQ':
data = str(data)
r = requests.request('REQUEST', 'http://127.0.0.1:{}/'.format(ECHO_PORT),
proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)},
data=data.encode('utf-8'))
script.post({'type': 'NEW_REQ', 'payload': r.text})
elif _type == 'RESP':
r = requests.request('RESPONSE', 'http://127.0.0.1:{}/'.format(ECHO_PORT),
proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)},
data=data.encode('utf-8'))
script.post({'type': 'NEW_RESP', 'payload': r.text})
# 为 Frida 脚本设置消息处理函数
script.on('message', on_message)
script.load()
# 使主线程保持运行,等待用户输入
sys.stdin.read()
hook脚本如下:
// 使用 Java.perform 函数确保 Frida 在 Java 虚拟机中运行此代码
Java.perform(function () {
// 获取 "e.c.a.i.c" 类
var c = Java.use("e.c.a.i.c");
// 获取 "com.qq.lib.EncryptUtil" 类
var EncryptUtil = Java.use("com.qq.lib.EncryptUtil");
// 定义一个变量来存储从 Python 接收到的新字符串
var newStr;
// Hook "e.c.a.i.c" 类中的 "a" 方法,参数类型为 "java.lang.String"
c.a.overload('java.lang.String').implementation = function (str) {
// 将原始参数转换为 JSON 字符串并将其发送给 Python
send({ type: 'REQ', data: JSON.stringify(str) });
// 等待来自 Python 的新参数
var newArgs = recv('NEW_REQ', function (data) {
// 将从 Python 接收到的 JSON 字符串转换为 JavaScript 对象
newStr = JSON.parse(data.payload);
});
// 等待 recv 函数处理完数据
newArgs.wait();
// 使用新参数调用原始方法并返回结果
return this.a(newStr);
}
// 定义一个变量来存储从 Python 接收到的新明文
var newPlaintext;
// Hook "com.qq.lib.EncryptUtil" 类中的 "decrypt" 方法
EncryptUtil.decrypt.implementation = function (str, str2) {
// 使用原始参数调用原始解密方法并将结果存储在 "plaintext" 变量中
var plaintext = this.decrypt(str, str2);
// 将解密后的明文转换为 JSON 字符串并将其发送给 Python
send({ type: 'RESP', data: JSON.stringify(plaintext) });
// 等待来自 Python 的新明文
var newResult = recv('NEW_RESP', function (data) {
// 将从 Python 接收到的 JSON 字符串转换为 JavaScript 对象
newPlaintext = JSON.parse(data.payload);
});
// 等待 recv 函数处理完数据
newResult.wait();
// 返回新的明文
return newPlaintext;
}
});
记得在burp监听相应的端口:测试效果
启动python脚本:python3 main.py org.tdyoa.mcdfmv(包名为org.tdyoa.mcdfmv) hook.js
burp上接收到python转发过来的响应包,我们进行修改试试:以用户名为例:
原用户名:
改包到新用户名:xiaoheilaoshi123
已经成功篡改,可以开始愉快的测试啦!!!
(三)进阶-主动调用
用了前面的方法,有的小伙伴肯定就要说了,每次测试,我都要去点app,操作手机,好麻烦啊,算了算了。别急,咱们这就解决一下
前面我们知道,我们是通过反编译找到关键函数点,然后用frida进行hook打印寻找调用栈,那么不妨拓展一下省力思路:
能不能找到关键方法,然后脚本直接调用方法,进行hook,这样我们就只需要把app开着放在一边,直接去运行脚本,就可以完成操作?
显然是可以的(就以同一个app中data字段的加密解密方法为例):
编写一下脚本如下(找到该方法相关的类名、密钥等信息,利用python提供的frida、flask库实现frida的功能):
#rpchook.py
import frida
import sys
from flask import Flask, request, jsonify
app = Flask(__name__)
device = frida.get_usb_device()
session = device.attach("org.tdyoa.mcdfmv") # 使用你的APP包名替换
@app.route('/decrypt', methods=['POST'])
def decrypt():
encrypted_data = request.json['encrypted_data']
decrypt_key = request.json['decrypt_key']
script = session.create_script("""
Java.perform(function() {
var EncryptUtil = Java.use('com.qq.lib.EncryptUtil');
var encrypted_data = '%s';
var decrypt_key = '%s';
var decrypted_data = EncryptUtil.decrypt(encrypted_data, decrypt_key);
send(decrypted_data);
});
""" % (encrypted_data, decrypt_key))
decrypted_data = None
def on_message(message, data):
nonlocal decrypted_data
if message['type'] == 'send':
decrypted_data = message['payload']
script.on('message', on_message)
script.load()
script.unload()
return jsonify({'decrypted_data': decrypted_data})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
#testjiemi.py
import requests
url = "http://localhost:5000/decrypt"
headers = {"Content-Type": "application/json"}
data = {
"encrypted_data": "8039C68F5AAE16B87FED41779E66ADC704ACC42028F9D422F97BFAB7D3288972139060B21A850DA4DB0107F6D2165076DCB064BA6C4796B61C4E2AF0F3B02947F41F6D7E6454F6FD06117A714093FDD71A46A017A490E9E51B5D73A403E34F5BFF83A1F2963935B12B04E960BD31540F8773AFB6A6B916FE66531B0D8891B1A0F1B9A0EA4DE7976FC01E3DE4DACEB8BA07D2086F845D2191A06A20626B9C1906B1A7AC02275C9DB8AF88E2D5254F9C7588104E4668C1229365850FA0BB6C2B55CB2DE8D3186C6F5A6DD5E3C1EE4EB8D50E02C7DA6C388D4A0D94C31D180B882E2C05CD15D13F226BD0966BE78FC2942FD23862E92D2A5AC3CF3C818D13C20D37680C3E4B7F42101C84EB795289945107EBBB539B27704A083D3D55B2815FD848F359C9ACE6986E1B751F1041CD9B1DBCF35591DAA4C74974F6A9CD57B4082C15A0FB8FD77A5F9F17B7F80F008D5D18D5D0C4D7D842E5BA5BABDDCB52C7A1E4860ED439F4A97FFBDDFE6BDF7BA82CCA479113A1A2FF59A91E0A4C0304CCB1CF0B64D7307F98C810088D49CB4AF9D06C674B3F8C38DAF82D5D1ABE3D3F08CD29763C7233B55960512D3D16FD4412537872EC6BE1E219B145B6F0116BDCC48C811DCE60CAF080198067138DB7BFC7D3C216B9FACF3D5DCF5734E38A6C99EEF23327CF188089820183E4F739022CB64ABF433A61F1B71F4C00B926E81D52486BF70FFA9A20EAB9381C63143A414391DD9D4A1B49701027F7F7BDCF4A6A108B8212FC463063B6DC42C086292F7A6F9B675805E4064327F8FE0ECDCF8FBCC407858A540FC4D47AA9028CC00C8DB9EB41BB6266187EEEBABB5138C1E83EE25C88A83EB20B0EAD4A7B09D45B9D4DEB44D1887C017B10F985021B2231A5F69053468EC0A45B6411561780D85C12E6E59597846FC4014F2ACB28E17175F952A64EAB5E3C1AA520A3821D6340627EA83448F1505B29D51097577A37F32B2F68D3B3BFC2018B8F5AADC41E20D1D9DA4383D4AEC7BBC6F86AE3602EAE99774EF695532B52E05824E025250F43A70137F56B6B6EBE7B8604FF785039D1F38E1CC10D49DA238F11C50B9BBE9E30FE785A5BFC49E28D3A3025A207A82BEC973E3760915A827428E3BA11B9433D95640EB3FA51394A02D543622F1B815A47C918453A2E6A917B00730F89E28DE31DF9184DAFFEC5737789122AA5A35A7ACA32A289031626CCC59251F890A213C893DBC0B17CAA3EDA18A9B365CA3DD3AC1E49445C4B67F63106BEED0A3346F50E18B7FAAE0600908F15E494C28140E2437282481C0687D31D938514D93CA4FF32F274823A4075982517DB18C8F9ED081452ECA3E35BBA8DE95E77612E3DD8449389B67DE4D631605079B609024CE7F03AB83F7BAAD7FF9DE5DCF6255FF78219FF724F0DBEFA29B4506375FC6383828638AB77B19FA0A0057EF8A84CD09CFB28C239CD11042D52D370FD0FC6C2F284BF4287A5735802FBEDF666601EED5EB9029E1C2448E6393E44536FEC7D178E8BD3843DA4E0B6137C590AD23310F52CD3B8F5AF93081EB3C5D01003AC9D272FECC40C5808EBFC9533421C84C8C5B1C372BF9146107F6B7946B8ECC3D0773AA1211F953B2773E18C06C57949163BADA100A4BC0A3E6A7555914DA70C2537C1EB709C7C54F8BE3C8D2617381BCDEDB9790B1E673EC7B73BBE9AA2B45139EEFC17AC6416215D5B02219968B4DF9311AD8EF3992910932A3B7B72CA9BA5AC66BDDF45DB272D469D08607E4B12201C07BF0EB8FBDA7F41D343ADB8217E289D730EECCA4D84027C9A5AD4619C9326B452AEC88F2C6B1F0A004C7B45594CA5888C022E4975C9E7D337519336D0607AD32BCEE34557DE7374EECC55717AC09139155D7932B19189C48AF9D90C157805B2B97145BE7348637DBA0F3C6A7F91EAEE489FB309CC60041F83EAD4845AC1F2F4EFE06326575602A4715D6E4A296459D5FFB466A5089EC45FEC0158302388A96D725228855A3D85CAC933DBCE2153209069EEF2DE1DA4780A7616492295D81397D581589E13FEAE061D57CCC6D427131E8A7FDB9AEB454F2F7555E598C056A46662E9E0BF96A1B9AA5E09423E6653710A16177AB658F2E14BA9C83639BB64DFF4E4F1CA21143FD2E01B62CAF69D6BC559BAD2B76749919AE6F1648C0E9608B39746A13CFA42F47C9AC52A2198438CFA4283D2B6CFF961D69C40F6DF36DCE6F8DCED811E0197427BDD7F135050A49D004C262D1AECA4417F25A823D95FA921C491853783F56914ABDA14384163640DDC7FA36C2C302A1EF396DF846C3CC8A130583B7C6826771948D60029AA14CCE816D84B113B1E418A2361AA0A4E64BFD5F6DC1EDF9DCC548BC6F7A329D3FABDE9CF1AA6692DF2F67A53D5E951804F769BE31CEA7681F63C8AADCD629460564D31F43843125A35B7CE4EDF8A6D79E15363CF74F955AD6029DD6C0B1EBF8031AC52B502DA90CACF9AEB5FEAD5A19D85216C12F03160CB33F015E461CD2AA6CCFCD68865E53930B544C5364E00A7821B8DE76116D0BD4A3EE231801C0B44C12CC11376BE0437962FD43C677D70D9C39AC2A479550A5A8DFD1C5678964FE3C207AE124AD3D0B406573A72DB2CBDA5421647C3D625D1C29A58395421A186A7BD0B48AA8CD1FD5CCF73DEE9719ACC7837A47B9B0CC436ACAB936872F72F1E65C36FA02CB0B218F1B1D37B552713627F4FE400882185DA042CBD7EB79E90AF628692141704D3B72948536CF62C427477370B65B18D43C9B2D02D6E376F0ABB59916B6C7FBFC3CBD7018BB7F4BE1F91D381A1E059B1D0953FC23DF55A648D50AD4BC44903BCD68E8D6E61090C8DF4A06532DEC45FF9F69CED82B4909563AF336AA339F1684A6AE7D6DBBCD32A897A99049BEBFF8847357DE40A437BE4E49DCF41D2DE7F9804CECE81B7E19F291480F1D0279506020D7960F9563C731F5C23BEC2FE3FC754A8F7D4F6E9C5703A149703ACFCC67068083739EF1E646977FF53F7C11333BEFA0EF7C824D697AA62774BAB7B616ECD2B99A4D3F3D56D01C09F374869B2577E4BACC91539DDE51EB484EEDBF5DFF84E302510E9AB39FB9269511CB8D7788EB0567C5A897F7FDA5D815201DB4CD0C5DEA6836A6C5BC62424640A9869B743D499A865C65D1EF9F9651722D38002DADC41025C1C63B6E3E1022F085A92882E1DC4F23FC9A9A2BF2609E6DF54AFB36F6F136909C7",
"decrypt_key": "PT0dPVxYXglbDARZXwlfXwwMDApYDgReWV4JXl4ICAtfWAxcJD4WCD8SFRYCFQ=="
}
response = requests.post(url, json=data, headers=headers)
print(response.json())
运行效果如下:
好啦,本篇文章讨论就到这里啦,小伙伴们开始愉快的玩耍吧!!!