【保姆级教学】某金融app FRIDA hook加解密算法+jsrpc=乱杀
0x01 APP渗透测试
因为是经过授权的测试,所以拿到的这个包是没有加固的。
加固的话,也是有对策的,可以使用脱壳机脱壳,使用hluda绕过frida检测。
1.1 绕过root检测
1.打开app,发现提示:”系统已root,不允许操作“,然后软件进行退出,无法进行后续操作。
所以需要绕过root检测。
2.打开magisk,在设置中选择“隐藏magisk”,然后输入随机包名,根据提示进行重启
● 安装shamiko:https://github.com/LSPosed/LSPosed.github.io/releases
笑脸为启用状态。
● magisk设置——配置排除列表——勾选app(此处的操作是为app隐藏root,及没有通过root启动app)
需要关闭“遵循排除列表”
3.然后重启,便可以打开app了。
1.2 抓包(burp抓安卓app包)
电脑地址:192.168.1.39
手机端设置:
将手机wifi连接到与电脑同一网段下,然后修改wifi代理:
电脑burp设置:
注意:抓取https的包需要安装证书,需要注意的是,安卓7.0以下,系统信任用户安装的证书,而安卓7.0以上,系统不会信任用户安装的证书,导致无法抓取https的包,建议使用magisk+Move Certificates将证书移动到系统路径下。
- 然后开始抓包,还好app没有各种检测,能顺利抓包~
通过抓包,发现请求包和返回包都被加密了,但是通过观察多个请求包会发现请求体的内容都是以“#01开头”,而这正是突破口,我们可以通过反编译代码,全局搜索“#01”,来寻找加密的地方。
1.3 APP反编译分析
1.使用burp对app进行抓包,发现报文被加密,但是有个相同的特点——报文都是以"#01"开头,于是我们在jadx-gui中全局搜索(ctrl+shift+F)“#01”
2.在搜索结果中可以看到关键字“encrypteDataWithSM“(中文意思为:使用SM方式加密数据),然后双击点进去查看详细代码。
3.通过上图,可以看到encrypt与decrypt,通过分析代码推断此处为加密和解密函数,为了确定是加解密函数,我们对这两个方法进行hook,使用objection也可以。
0x02 Frida HOOK
2.1 编写hook代码
import frida, sys
jscode ="""
Java.perform(function () {
<!-- 获取CryptoUtil这个类的对象 -->
var CryptoUtil = Java.use('com.xxxx.security.CryptoUtil');
<!-- 调用encryptDataWithSM方法,因为这个方法的参数有三个,所以在 function 中有三个参数,至于为什么没有类型修饰,因为 JavaScript 是弱语言 -->
CryptoUtil.encryptDataWithSM.implementation = function (app,dataStr,keyStr) {
console.log("Hook Start..."); //
console.log("app:",app); //打印app参数
console.log("dataStr",dataStr); //打印dataStr参数
console.log("keyStr",keyStr); //打印keyStr参数
var data = this.encryptDataWithSM(app,dataStr,keyStr);//使用this.encryptDataWithSM()再次调用原函数并把原本的参数传递给这个encryptDataWithSM()函数,然后通过return原封不动的返回。
return data;
}
});
"""
# 这下面的代码的作用就是为了执行上面 JS 代码,输出 JS 中 send() 方法中的内容
def message(message, data):
if message["type"] == 'send':
print("<li> {0}".format(message['payload']))
else:
print(message)
# 获取这个 APK 的进程,可以使用frda-ps -Ua获取
process = frida.get_usb_device().attach('xxxxxx银行')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()
手机端运行frida,然后pycharm直接运行代码,操作手机,查看控制台输出。
从控制台可以看到encryptDataWithSM()函数被成功hook,而dataStr也是我们想要的报文。
2.2 hook 加解密函数
我们修改代码,只打印dataStr并同时hook解密函数。
import frida, sys
jscode ="""
Java.perform(function () {
var CryptoUtil = Java.use('com.xxxx.security.CryptoUtil');
CryptoUtil.encryptDataWithSM.implementation = function (app,dataStr,keyStr) {
console.log("Hook Start...");
console.log("encrycptDataStr",dataStr);
var data = this.encryptDataWithSM(app,dataStr,keyStr);
return data;
}
CryptoUtil.decryptDataWithSM.implementation = function (app,dataStr,keyStr) {
console.log("Hook Start....");
console.log("decrycptDataStr:",dataStr)
var data = this.decryptDataWithSM(app,dataStr,keyStr);
return data;
}
});
"""
def message(message, data):
if message["type"] == 'send':
print("<li> {0}".format(message['payload']))
else:
print(message)
process = frida.get_usb_device().attach('XXXXX银行')
script= process.create_script(jscode)
script.on("message", message)
script.load()
sys.stdin.read()
我们看到encryptDataWithSM加密前的数据以及decryptDataWithSM解密前的数据被打印出来。现在我们是数据都拿到了,但是怎么篡改数据那,那么我们就要rpc了。
0x03 编写转发脚本
感谢pyth0n大佬的项目
3.1 修改hook代码
修改hook脚本,将hook出来的数据,发送到burp便于篡改数据,篡改后的请求包发送到服务端,服务端处理后,返回响应包。
666.js
Java.perform(function () {
var CryptoUtil = Java.use('com.xxxx.security.CryptoUtil');
CryptoUtil.encryptDataWithSM.implementation = function(app,dataStr,keyStr) {
console.log("Hook Start...");
console.log("encrycptDataStr",dataStr,keyStr);
var data;
send({from:"/http",payload:dataStr,api_path:"request"}); //将dataStr发送到burpTracer.py
<!-- 接收burp篡改后返回的数据 ↓-->
var op = recv("input",function(value){
data = value.payload
});
op.wait();
console.log("经过burp修改后的参数:" +data);
var ret = this.encryptDataWithSM(app,data,keyStr);
return ret;
}
//hook解密函数
CryptoUtil.decryptDataWithSM.implementation = function (app,dataStr,keyStr) {
var getVal = this.decryptDataWithSM(app, dataStr, keyStr);
send({ from: "/http", payload: getVal, api_path: "response" });
var op = recv('input',function (value) {
console.log(value.payload);
});
op.wait();
return getVal;
}
});
3.2 burpTracer.py,直接使用
from codecs import ignore_errors
import frida
import requests
import time
import sys
import os
import socket
import argparse
from log import *
print ('''\033[1;31m \n
_____ _ _ ___ _ ____ _
| ___| __(_) __| | __ _ |_ _|_ __ | |_ ___ _ __ / ___|___ _ __ | |_
| |_ | '__| |/ _` |/ _` | | || '_ \| __/ _ \ '__| | / _ \ '_ \| __|
| _|| | | | (_| | (_| | | || | | | || __/ | | |__| __/ |_) | |_
|_| |_| |_|\__,_|\__,_| |___|_| |_|\__\___|_| \____\___| .__/ \__|
#pyth0n |_|
Intercept Api in Android Application
''')
print ("\033[1;34m<li>___author___: @Pyth0n\033[1;37m")
print ("\033[1;34m<li>___version___: 1.0\033[1;37m")
print ("")
BURP_HOST = "127.0.0.1"
BURP_PORT = 26080
def check_platform():
try:
platforms = {
'linux' : 'Linux',
'linux1' : 'Linux',
'linux2' : 'Linux',
'darwin' : 'OS X',
'win32' : 'Windows'
}
if sys.platform not in platforms:
sys.exit(logger.error("[x_x] Your platform currently does not support."))
except Exception as e:
logger.error("[x_x] Something went wrong, please check your error message.\n Message - {0}".format(e))
def check_ps_for_win32():
try:
if sys.platform == "win32":
PROCESSNAME = "iTunes.exe"
for proc in psutil.process_iter():
try:
if proc.name() == PROCESSNAME:
return True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as e:
pass
return sys.exit(logger.error("[x_x] Please install iTunes on MicrosoftStore or run iTunes frist."))
except Exception as e:
logger.error("[x_x] Something went wrong, please check your error message.\n Message - {0}".format(e))
def check_echo_server():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1',27080))
if result == 0:
logger.info("<li> Connect to echoServer successfully.")
else:
sock.close()
sys.exit(logger.error("[x_x] Please start echoServer."))
def run():
#check platform support
check_platform()
#check process iTunes for Win32s
#check_ps_for_win32()
#check python version
if sys.version_info < (3, 0):
logger.error("[x_x] iOS hook requires Python 3.x")
sys.exit(0)
else:
handle_del_log()
main()
def handle_del_log():
try:
pwd = os.getcwd()
path = pwd + '/errors.log'
file_stats = os.stat(path)
if (file_stats.st_size > 1024000000): #delete errors.log if file size > 1024 MB
os.remove(path)
else:
return True
except Exception as e:
logger.error("[x_x] Something went wrong when clear error log. Please clear error log manual.\n [Error Message] - {0}".format(e))
def main():
def frida_process_message(message, data):
handled = False
if message['type'] == 'input':
handled = True
elif message['type'] == 'send':
body = message['payload']
API_PATH = body['api_path']
if body['from'] == '/http':
try:
# 把数据发给 本地burp 监听的 26080端口
req = requests.request('FRIDA', 'http://%s:%d/%s' % (BURP_HOST, BURP_PORT, API_PATH), headers={'content-type':'application/json'}, data=body['payload'].encode('utf-8'))
script.post({ 'type': 'input', 'payload': req.text }) # 把修改后的数据传输回给js
handled = True
except requests.exceptions.RequestException as e:
logger.error("[x_x] Connection refused, please check configurage on BurpSute.\n [Error Message] - {0}".format(e))
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--package")
parser.add_argument("-n", "--name")
parser.add_argument("-s", "--script", help='custom handler script')
parser.add_argument("-r", "--remote",help="远程主机")
args, leftovers = parser.parse_known_args()
try:
#Spawning application with default script
if args.package is not None and args.script is None:
#check echoServer
check_echo_server()
#
logger.info('<li> Spawning: ' + args.package)
logger.info('<li> Script: ' + 'handlers.js')
time.sleep(2)
device = frida.get_usb_device()
pid = device.spawn(args.package)
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("handlers.js") as f:
script = session.create_script(f.read())
script.on("message", frida_process_message)
script.load()
input()
#Attaching default script to application
if args.name is not None and args.script is None:
#check echoServer
check_echo_server()
#
logger.info('<li> Attaching: ' + args.name)
logger.info('<li> Script: ' + 'handlers.js')
time.sleep(2)
process = frida.get_usb_device().attach(args.name)
with open("handlers.js") as f:
script = process.create_script(f.read())
script.on("message", frida_process_message)
script.load()
input()
# Spawing application with custom script
if args.package is not None and args.script is not None:
#check echoServer
check_echo_server()
#
if os.path.isfile(args.script):
logger.info('<li> Spawning: ' + args.package)
logger.info('<li> Script: ' + args.script)
time.sleep(2)
device = frida.get_remote_device()
pid = device.spawn(args.package)
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open(args.script,encoding='utf-8',errors='ignore') as f:
script = session.create_script(f.read())
script.on("message", frida_process_message)
script.load()
input()
else:
logger.error('[?] Script not found!')
#Attaching default script to application
if args.name is not None and args.script is not None:
#check echoServer
check_echo_server()
#
logger.info('<li> Attaching: ' + args.name)
logger.info('<li> Script: ' + args.script)
time.sleep(2)
process = frida.get_usb_device().attach(args.name)
with open(args.script,encoding='utf-8',errors='ignore') as f:
script = process.create_script(f.read())
script.on("message", frida_process_message)
script.load()
input()
if args.remote is not None and args.script is not None:
#check echoServer
check_echo_server()
#
logger.info('<li> Attaching: ' + args.remote)
logger.info('<li> Script: ' + args.script)
time.sleep(2)
process = frida.get_remote_device().attach(args.remote)
with open(args.script,encoding='utf-8',errors='ignore') as f:
script = process.create_script(f.read())
script.on("message", frida_process_message)
script.load()
input()
#EXCEPTION FOR FRIDA
except frida.ServerNotRunningError:
logger.error("Frida server is not running.")
except frida.TimedOutError:
logger.error("Timed out while waiting for device to appear.")
except frida.TransportError:
logger.error("[x_x] The application may crash or lose connection.")
#EXCEPTION FOR OPTIONPARSING
#EXCEPTION FOR SYSTEM
except Exception as e:
logger.error("[x_x] Something went wrong, please check your error message.\n Message - {0}".format(e))
except KeyboardInterrupt:
logger.info("Bye bro!!")
# sys.exit(0)
if __name__ == '__main__':
run()
3.3echoServer.py 直接使用
from http.server import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from log import *
print ('''\033[1;31m \n
_ _____
| | / ____|
___ ___| |__ ___| (___ ___ _ ____ _____ _ __
/ _ \/ __| '_ \ / _ \\___ \ / _ \ '__\ \ / / _ \ '__|
| __/ (__| | | | (_) |___) | __/ | \ V / __/ |
\___|\___|_| |_|\___/_____/ \___|_| \_/ \___|_|
''')
print ("\033[1;34m<li>___author___: @Pyth0n\033[1;37m")
print ("\033[1;34m<li>___version___: 1.0\033[1;37m")
print ("")
ECHO_PORT = 27080
class RequestHandler(BaseHTTPRequestHandler):
def do_FRIDA(self):
request_path = self.path
request_headers = self.headers
content_length = request_headers.get('content-length')
# length = int(content_length[0]) if content_length else 0
length = int(content_length) if content_length else 0
self.send_response(200)
self.end_headers()
self.wfile.write(self.rfile.read(length))
def main():
try:
logger.info('<li> Listening on 127.0.0.1:%d' % ECHO_PORT)
server = HTTPServer(('', ECHO_PORT), RequestHandler)
server.serve_forever()
except KeyboardInterrupt:
logger.info("Stop echoServer!!")
if __name__ == "__main__":
logger.info('<li> Starting echoServer on port %d' % ECHO_PORT)
main()
3.4 使用
● burp的proxyoptions导入burpsuite_configuration_proxy.json,
burpsuite_configuration_proxy.json
{
"proxy":{
"request_listeners":[
{
"certificate_mode":"per_host",
"custom_tls_protocols":[],
"enable_http2":true,
"listen_mode":"loopback_only",
"listener_port":8080,
"running":true,
"use_custom_tls_protocols":false
},
{
"certificate_mode":"per_host",
"custom_tls_protocols":[
"SSLv3",
"TLSv1",
"TLSv1.1",
"TLSv1.2",
"TLSv1.3"
],
"enable_http2":true,
"listen_mode":"specific_address",
"listen_specific_address":"127.0.0.1",
"listener_port":26080,
"redirect_to_host":"127.0.0.1",
"redirect_to_port":27080,
"running":true,
"support_invisible_proxying":true,
"use_custom_tls_protocols":false
}
]
}
}
● 手机上开启frida-server
● 运行echoServer.py
● 运行burpTracer.py
adb forward tcp:27042 tcp:27042 #转发端口
burpTracer.py -s 666.js -p com.xxx.xxx.bank
此时手机会自己打开app,如果burp打开监听,也会收到数据包,便可以愉快的改包测试了。.
下面是流程原理:
0x04 乱杀
敏感信息泄露
- 登录时,输入账号然后抓包,将抓到的请求包放行。
- 从返回包中,可以看到
接下来就是各种乱杀:
自评TCV:3
评论88次
我现在的办法是下载安卓源码,定制xi统,去掉root特征,自定义fingerprint特征。
不行,这个方法的弊端就是只能测逻辑类、敏感泄露类的漏洞,不能重放
感谢楼主分享,还从未使用过shamiko的小白打算先学xi下工具的使用~ :P
一直很想学app渗透测试,学xi了。
金融的多因子认证确实做了很多。。
学到了,思路清晰
发自内心的膜拜!!!
吊,一直想学xi下怎么自动转发包
技术精湛,文笔也精彩,妥妥的学xi好文,赞!
这也太强了吧,这个思路很好用呀
Adminxe师傅太强了!
转发部分学到了 新技能get
写的不错,值得学xi
不错,我们银行现在就在用类似的方法测试
如果是自己安全人员找开发要解密工具就行了,或者三方的加解密都有解密机
在 repeater 中可以修改数据包吗
不行,这个方法的弊端就是只能测逻辑类、敏感泄露类的漏洞,不能重放
重放的话用app本来的数据包不就好了,应该只有加密没有会话签名吧
完整的流程很清晰啊,一直到解密,学到了
不错,我们银行现在就在用类似的方法测试
师傅太强了,学xi了
试了一些企业版加固的app,还是绕不过root检测,这种有方法吗
能用frida的话直接hook掉root检测部分,按理说mgisk能过掉很大一部分了
有没有可以重放数据包的思路,这个一个个包不能重放挺难受的
有绕过FRIDA检测方法吗
可以试试葫芦娃的hluda,如果不行,可以hook dlopen,看看启动的时候是哪个so里面检测了root,然后干掉他,还有一步到位的就是修改xi统源码,去掉root特征。
太强了,话说ios下面的hook加密有办法吗
ios下面也是一样的方法,只不过写法不一样