原文链接:https://github.com/m2kar/m2kar.github.io/issues/21
一、概述
针对某麦网局部演唱会门票仅能在 app 渠道抢票的问题,本文钻研了 APK 的抢票接口并编写了抢票工具。本文介绍的程序为环境搭建、抓包、trace 剖析、接口参数获取、rpc 调用实现,以及最终的性能实现。通过浏览本文,你将学到反抓包技术破解、Frida hook、jadx apk 逆向技术,并能对淘系 APP 的运行逻辑有所理解。本文仅用于学习交换,严禁用于非法用处。
关键词:frida, damai.cn, Android 逆向
先放胜利截图:
二、缘起
疫情完结的 2023 年 5 月,大家对出去玩都有点疯狂,歌手们也扎堆开演唱会。演唱会虽多,票却一点也不好抢,抢五月天的门票难度不亚于买五一的高铁票。所以想尝试找一些脚本来辅助抢票,之前常常用 selenium 和 request 做一些小爬虫来搞定自动化的工作,所以在 MakiNaruto/Automatic_ticket_purchase 的根底上改了改,实现抢票性能。然而某麦网切实太刁滑了,改完爬虫才发现简直所有的热门演唱会只容许在 app 购买,所以就须要利用 APP 实现接口自动化。
本着能省事则省事的准则,笔者在文章[[Android] 基于 Airtest 实现某麦网 app 主动抢票程序](https://github.com/m2kar/m2kar.github.io/issues/20) 中用自动化测试技术实现了抢票程序,然而速度太慢,简直不能用。果然捷径往往不好走,因而持续尝试剖析某麦网 apk 的 api 接口。
三、环境搭建
本文所需的破解环境如下:
- windows 10
- cn.damai apk 版本 8.5.4 (2023-04-26)
- bluestacks 5.11.56.1003 p64
- adb 31.0.2
- Root Checker 6.5.3
- wireshark 4.0.5
- frida 16.0.19
- jadx-gui 1.4.7
3.1 bluestacks 环境搭建
目前,Android 模拟器竞品很多,抉择 Bluestacks 5 是因为它能和 windows 的 hyper- v 完满兼容,root 过程也绝对简略。首先,须要 root Bluestacks 环境,下载安装 Bluestacks,而后运行 Bluestacks Multi-instance Manager,发现默认装置的版本为 Android Pie 64bit 版本,即 Android 9.0。
敞开 bluestack 后关上 bluestacks 配置文件,文件位于目录:%programdata%\BlueStacks_nxt\bluestacks.conf
在配置文件中查找 root 关键词,对应值批改为 1,共两处。
bst.feature.rooting="1"
bst.instance.Pie64.enable_root_access="1"
启动 bluestack 模拟器,装置 Root Checker APP,点击验证 root,即可发现 root 已胜利。
3.2 adb 调试
bluestack 设置 - 高级中关上 Adb 调试,并记录下端口。
接下来,关上主机命令行,运行命令 adb connect localhost:6652,端口号批改为上一步的端口号,即可连贯。再运行 adb devices,如有对应设施则连贯胜利。进入 adb shell,执行 su 进入 root 权限,命令行标识由 $ 变为 #,即示意 adb 进入 root 权限胜利。
3.3 frida 环境搭建
frida 是赫赫有名的动态分析的 hook 神器,用它能够间接拜访批改二进制的内存、函数和对象,十分不便。它对于 Android 的反对也是很完满。frida 的运行采纳 C / S 架构,客户端为电脑端的开发环境,服务器端为 Android,均需对应部署搭建。
firda 客户端基于 python3 开发,因而首先须要配置好 python3 的运行环境,而后执行 pip install frida-tools 即可实现装置,运行 frida –version 可验证 frida 版本。
(py3) PS E:\TEMP\damai> frida --version
16.0.19
接下来,是在 Android 模拟器中运行 frida-server。这样能够让 Frida 通过 ADB/USB 调试与咱们的 Android 模拟器连贯。
最新的 frida-server 能够从 https://github.com/frida/frida/releases 下载。请留神下载与设施匹配的架构。如果您的模拟器是 x86_64,请下载相应版本的 frida-server。本文应用的版本为 frida-server-16.0.18-android-x86_64.xz。
将下载后的.xz 文件解压,将 frida-server 传入 Android 模拟器,命令如下。
adb push frida-server /data/local/tmp/
应用 adb root 以 root 模式重新启动 ADB,并通过 adb shell 从新进入 shell 的拜访。进入 shell 后,进入咱们搁置 frida-server 的目录并为其授予执行权限:
cd /data/local/tmp/
chmod +x frida-server
执行:./frida-server,运行 frida-server,并放弃本 shell 窗口开启,胜利的截图如下。
接下来,测试是否连贯胜利。在 window 端运行 frida-ps 命令:
看到一堆相熟的 Android 过程,咱们就连贯胜利啦。同时,frida 也反对依赖端口的近程连贯模式,在某些场景下更加灵便。能够通过端口转发的形式实现此性能。
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
27042 是用于与 frida-server 通信的默认端口号, 之后的每个端口对应每个注入的过程,查看 27042 端口可检测 Frida 是否存在。
四、抓包
4.1 抓包及 https 解密办法
习用的 Android 抓包伎俩是用 fiddler/burpsuite/mitmproxy 搭建代理服务器,设置 Android 代理服务器并用中间人劫持的形式获取 http 协定流量的内容。如需对 https 流量解密,还须要在安卓上装置 https 根证书。Android9.0 当前的版本对用户自定义根证书有了一些限度,抓包不再那么简略,但这难不倒技术大神们,大家能够能够参考以下几篇文章:
- 从原理到实战,全面总结 Android HTTPS 抓包
- Android 高版本 HTTPS 抓包解决方案
上述的抓包形式只能抓到 http 协定层以上的流量,这次咱们来点不一样的,用【tcpdump+frida+wireshark】实现 Android 的全流量抓包,能实现 https 解密。
4.1.1 tcpdump
本文基于 termux 装置应用 tcpdump。首先,装置 termux apk。关上 termux 运行:
挂载存储
termux-setup-storage
## 会弹出受权框,点容许
ls ~/storage/
## 如果呈现 dcim, downloads 等目录,即示意胜利
装置 tcpdump
pkg install root-repo
pkg install tcpdump
pkg install tsu
运行抓包
sudo tcpdump -i any -s 0 -w ~/storage/downloads/capture.pcap
上面是 tcpdump 执行胜利的截图。
之后就能够把 downloads 目录下的抓包文件拷贝到电脑上,用 wireshark 关上做进一步剖析。
4.1.2 解密 https 流量
Wireshark 解密 https 流量的办法和原理介绍有很多,可参考以下文章,本文不再赘述。
- https://unit42.paloaltonetworks.com/wireshark-tutorial-decryp…
- https://zhuanlan.zhihu.com/p/36669377
wireshark 解密技术的重点在于拿到客户端通信的密钥日志文件(ssl key log),像上面这种。
在 Android 中实现抓取 ssl key log 须要 hook 零碎的 SSL 相干函数,能够用 frida 实现。首先,将上面的 hook 代码保留为 sslkeyfilelog.js。
// sslkeyfilelog.js
function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {console.log("start----")
function keyLogger(ssl, line) {console.log(new NativePointer(line).readCString());
}
const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']);
Interceptor.attach(SSL_CTX_new, {onLeave: function(retval) {const ssl = new NativePointer(retval);
const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
}
});
}
startTLSKeyLogger(Module.findExportByName('libssl.so', 'SSL_CTX_new'),
Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
)
而后,用 frida 加载运行 hook 代码。
frida -U -l .\sslkeyfilelog.js -f cn.damai
最初,抓包完结后将失去的 key 保留到 sslkey.txt,格局是上面这样的,不要掺杂别的。
CLIENT_RANDOM 557e6dc49faec93dddd41d8c55d3a0084c44031f14d66f68e3b7fb53d3f9586d 886de4677511305bfeaee5ffb072652cbfba626af1465d09dc1f29103fd947c997f6f28962189ee809944887413d8a20
CLIENT_RANDOM e66fb5d6735f0b803426fa88c3692e8b9a1f4dca37956187b22de11f1797e875 65a07797c144ecc86026a44bbc85b5c57873218ce5684dc22d4d4ee9b754eb1961a0789e2086601f5b0441c35d76c448
在运行 Frida Hook 获取 sslkey 的同时,运行 tcpdump 抓包。抓包中顺次测试获取详情页、抉择价位、提交订单等操作,并对应记录下执行操作的工夫,不便后续剖析。抓包实现后,用 wireshark 关上 tcpdump 抓包取得的 pcap 文件,在 wireshark 首选项 -protocols-TLS 中,设置 (Pre)-Master-Secret log filename 为上述 sslkey.txt。
参考:https://www.52pojie.cn/thread-1405917-1-1.html
4.2 流量剖析
在此铺垫一下,通过后期对某麦网 PC 端和挪动端 H5 的剖析,某麦网购票的工作流程大略为:
- 取得详情:接口为 mtop.alibaba.damai.detail.getdetail。基于某上演的 id(itemId)取得上演的详细信息,包含详情、场次、票档 (SkuId) 价位及状态信息,
- 构建订单:接口为 mtop.trade.order.build.h5。发送 上演 id+ 数量 + 票档 id(itemId_count_skuId),失去提交订单所需的表单信息,包含观众、收货地址等。
- 提交订单:接口为 mtop.trade.order.create.h5。对上一步构建订单失去的表单参数作出批改后,发送给服务器,失去最初的订单提交后果和领取信息。
首先,用过滤器 http && tcp.dstport==443,失去向服务器发送的 https 包,如下图:
能够看到大量向服务器申请的数据包,但其中有很多烦扰的图片申请,因为批改过滤器把图片过滤一下。过滤器:http && tcp.dstport==443 and !(http.request.uri contains “.webp” or http.request.uri contains “.jpg” or http.request.uri contains “.png”)
依据之前记录的操作的工夫,以及对网页版的剖析后果,笔者留神到了下图的这条流量。
而后咱们右键抉择这条流量包,点击追踪 http 流,能够看到对应的响应包。
响应包里有些中文应用了 UTF- 8 编码,能够点击右下角的 Show data as,抉择 UTF-8,便能够失常显示。此时能够点击另存为,保留为 txt 文件,不便后续剖析。
订单构建的申请包中外围的数据局部为图中青色圈进去的局部,应用 URL 解码后为:
{"buyNow":"true","buyParam":"716435462268_2_5005943905715","exParams":"{"atomSplit":"1","channel":"damai_app","coVersion":"2.0","coupon":"true","seatInfo":"","umpChannel":"10001","websiteLanguage":"zh_CN_#Hans"}"}
buyParam 为最外围的局部,拼接形式为上演 id+ 数量 + 票档 id。其余局部只需照抄。申请包中还蕴含大量的各种加密参数、ID,而破解实现主动购票脚本的要害就在于如何通过代码的形式拿到这些加密参数。订单构建的响应包为订单提交表单的各项参数,用于生成“确认订单”的表单。
依照同样的形式能够找到订单提交包,订单提交包的 API 门路为 /gw/mtop.trade.order.create。
其中青色圈进去的局部为 data 发送的外围数据,对数据用 URL 解码后为:
{"feature":"{"gzip":"true"}","params":"H4sIAAAAAAA.................AAWk3NKAAA\n"}
看起来像是把原始数据用 gzip 压缩后又应用了 base64 编码,应用 python 尝试解码:
import base64
import gzip
import json
# 解码后变为 python dict
decode_data=base64.b64decode(params_str.replace("\n",""))
decompressed_data=gzip.decompress(decode_data).decode("utf-8")
params=json.loads(decompressed_data)
with open("reverse\order.create-params.json","w") as f:
json.dump(params,f,indent=2)
解码胜利,失去 order.create-params.json,存起来前面应用。
解码后发现 order.create 发送的 data 参数和 order.build 申请返回的后果很类似,减少了一些用户对表单操作的记录。
order.create 申请的 header 中的各种加密参数和 order.build 统一。order.create 申请的返回后果中蕴含了订单创立是否胜利的后果以及领取链接。
五、trace 剖析
通过后面对流量的剖析,咱们曾经晓得客户端向服务器发送的外围数据和加密参数,外围数据的拼接绝对简略,但加密参数怎么取得还比拟艰难。因而,上面要开始剖析加密参数的生成办法。本章节次要采纳 frida trace 动态分析和 jadx 动态剖析相结合的形式,旨在找到加密参数生成的外围函数和输入输出数据的格局。
依据文章 (app 安卓逆向 x -sign,x-sgext,x_mini_wua,x_umt 加密参数解析),其中数据包的加密参数和本文的某麦网很相似,而且提到了 mtopsdk.security.InnerSignImpl 生成的加密函数,本文也参考了这篇文章的思路进行剖析。
5.1 跟踪 InnerSignImpl
运行 frida-trace -U -j “InnerSignImpl!*” 大麦,执行选座提交订单的操作,发现的确有后果输入:
(py3) PS E:\TEMP\damai> frida-trace -U -j "*InnerSignImpl*!*" 大麦
Instrumenting...
InnerSignImpl$1.$init: Loaded handler at "E:\TEMP\damai\__handlers__\mtopsdk.security.InnerSignImpl_1\_init.js"
.... 此处省略...
InnerSignImpl.init: Loaded handler at "E:\TEMP\damai\__handlers__\mtopsdk.security.InnerSignImpl\init.js"
Started tracing 27 functions. Press Ctrl+C to stop.
/* TID 0x144f */
6725 ms InnerSignImpl.getUnifiedSign("<instance: java.util.HashMap>", "<instance: java.util.HashMap>", "23781390", null, true)
6726 ms | InnerSignImpl.convertInnerBaseStrMap("<instance: java.util.Map, $className: java.util.HashMap>", "23781390", true)
6726 ms | <= "<instance: java.util.Map, $className: java.util.HashMap>"
6727 ms | InnerSignImpl.getMiddleTierEnv()
6727 ms | <= 0
6737 ms <= "<instance: java.util.HashMap>"
点击发送申请时,调用了 InnerSignImpl.getUnifiedSign 函数。然而输出参数和数据参数均为 HashMap 类型,后果中未显示具体内容。从后果输入中猜想 frida-trace 是通过对须要 hook 的函数在__handlers__下生成 js 文件,并调用 js 文件进行 hook 操作的,因而笔者批改了“handlers\mtopsdk.security.InnerSignImpl\getUnifiedSign.js”,使其能正确输入 HashMap 类型。
// __handlers__\mtopsdk.security.InnerSignImpl\getUnifiedSign.js
{onEnter(log, args, state) {
// 减少了 HashMap2Str 函数,将 HashMap 类型转换为字符串
function HashMap2Str(params_hm) {var HashMap=Java.use('java.util.HashMap');
var args_map=Java.cast(params_hm,HashMap);
return args_map.toString();};
// 当调用函数时,输入函数参数
log(`InnerSignImpl.getUnifiedSign(${HashMap2Str(args[0])},${HashMap2Str(args[1])},${args[2]},${args[3]})`);
}, onLeave(log, retval, state) {function HashMap2Str(params_hm) {var HashMap=Java.use('java.util.HashMap');
var args_map=Java.cast(params_hm,HashMap);
return args_map.toString();};
if (retval !== undefined) {
// 当函数运行完结时,输入函数后果
log(`<= ${HashMap2Str(retval)}`);
} }}
再次运行 frida-trace,输入的后果曾经能够看到具体内容了:
(py3) PS E:\TEMP\damai> frida-trace -U -j "*InnerSignImpl*!*" 大麦
......
Started tracing 27 functions. Press Ctrl+C to stop.
/* TID 0x15ab */
2653 ms InnerSignImpl.getUnifiedSign({data={"itemId":"719193771661","performId":"211232892","skuParamListJson":"[{"count":1,"price":36000,"priceId":"251592963"}]","dmChannel":"*@damai_android_*","channel_from":"damai_market","appType":"1","osType":"2","calculateTag":"0_0_0_0","source":"10101","version":"6000168"}, deviceId=null, sid=13abe677c5076a4fa3382afc38a96a04, uid=2215803849550, x-features=27, appKey=23781390, api=mtop.damai.item.calcticketprice, utdid=ZF3KUN8khtQDAIlImefp4RYz, ttid=10005890@damai_android_8.5.4, t=1684828096, v=2.0},{pageId=, pageName=},23781390,null)
2654 ms | InnerSignImpl.convertInnerBaseStrMap("<instance: java.util.Map, $className: java.util.HashMap>", "23781390", true)
2655 ms | <= "<instance: java.util.Map, $className: java.util.HashMap>"
2655 ms | InnerSignImpl.getMiddleTierEnv()
2655 ms | <= 0
2662 ms <= {x-sgext=JA2qmBOxRVDxFRzca3r9BZibqJqvn7uerZOriayYu4mpnKCeoJiunKGZu5qqyfmaqJqhmvqYr5n8zPyJqImpmbvLrImomqidu5m7m7uYu5u7mLuYu5u7m7ubqYmtiaiJqImoiaiJqImoiaiJu8+7iaCf/cypnruaqJqomruau5j8y7uau4mgiaiJqInf6fDIu5o=, x-umt=+D0B/05LPEvOgwKIQ1x+SeV5wNE6NzOo, x-mini-wua=atASnVJw3vGX1Tw3Y/zDaVZkDUbLxOxtlUmgDOnIjMTBcMPMqQJLpnxoOWEL53Fq/OPcQZiMpDXWNvDz8UQkI5mtkZvIcDN1oxZnuH0M22LHKar4rnO/xm4LtAiniKgYtfgMGK3stXuCmvtE4raIhROimslSk7hCkxaL/DYuLzBLYwXmNyr9UZi1g, x-sign=azG34N002xAAK0H9KwNr3txWFMxzW0H7ROfkLQK+Db7ueJHktR/yP/0TcdPFzoYf36zd9lJYMsHCmYX3EcoFnJPMk2pxu0H7QbtB+0}
能够看到返回后果中蕴含了 x-sgext,x-umt,x-mini-wua,x-sign 等加密参数。至此,后面的一大堆剖析也算有了小的播种。但比照流量剖析后果中的发送参数,还是缺失了很多参数。上面咱们持续跟踪,找出剩下的参数。
5.2 跟踪 mtopsdk
调研发现淘系的 apk 都蕴含 mtopsdk,猜测会不会有公开的官网文档形容 mtopsdk 的应用办法,因而咱们就找到了【阿里云 mtopsdk Android 接入文档】。其中介绍了申请构建的流程为,笔者重点关注了申请构建和发送的局部:
// 3. 申请构建
// 3.1 生成 MtopRequest 实例
MtopRequest request = new MtopRequest();
// 3.2 生成 MtopBuilder 实例
MtopBuilder builder = instance.build(MtopRequest request, String ttid);
// 4. 申请发送
// 4.2 异步调用
ApiID apiId = builder.addListener(new MyListener).asyncRequest();
因而咱们无妨大胆一些,间接跟踪所有对 mtopsdk 中函数的调用。
(py3) PS E:\TEMP\damai> frida-trace -U -j "*mtopsdk*!*" 大麦
咱们依照阿里的官网文档介绍的流程,对应能够找到在输入的 trace 中找到一些要害的日志。
# MtopRequest 初始化
3249 ms MtopRequest.$init()
3249 ms MtopRequest.setApiName("mtop.trade.order.build")
3249 ms MtopRequest.setVersion("4.0")
3249 ms MtopRequest.setNeedSession(true)
3249 ms MtopRequest.setNeedEcode(true)
3249 ms MtopRequest.setData("{"buyNow":"true","buyParam":"7191937661_1_51826442779","exParams":"{\"atomSplit\":\"1\",\"channel\":\"damai_app\",\"coVersion\":\"2.0\",\"coupon\":\"true\",\"seatInfo\":\"\",\"umpChannel\":\"10001\",\"websiteLanguage\":\"zh_CN_#Hans\"}"}")
# MtopBuilder 初始化
3251 ms MtopBuilder.$init("<instance: mtopsdk.mtop.intf.Mtop>", "<instance: mtopsdk.mtop.domain.MtopRequest>", null)
# MtopBuilder 发送异步申请
3268 ms MtopBuilder.asyncRequest()
# 参数构建
3301 ms | | | InnerProtocolParamBuilderImpl.buildParams("<instance: mtopsdk.framework.domain.MtopContext>")
3391 ms | | | <= "<instance: java.util.Map, $className: java.util.HashMap>",{wua=CofS_+7HCuvRCdz1EN8ICI6A4ZBCJwgY1hi+Bsivjcijs8GggmUxLQQUVTEQ5mYYtPuV7R2QNG5JEONIJRfmzjxFXMrs9AHdepIuqoJJJAyewWALprRnjIAu75t47Tm/RU9xRi7IEo9w0P2aCquLzf7uhiO8JEDSRK/ZdVhURBbof7reFtzEBoYYeIPgnwz7CL3kRlbyqyJcYKxO7ZmmVq1PtMXF2HGJqRSDjdv9l4mySJljIQzBmpX393L6eO1ZQVG1fpp6RaCRcFF+UgfjJXaeMFziHzfQF7KfUQZIeAJV/4GyVEE2f55RwPluOTuQubXQnq+qu41a0V5oyEOFXMoQRYFZzLOv3CjwkiIXsqJFeIHc=, x-sgext=JA0VLKcO8e9Fqqhj38VJuiwkHCUbIA8jGCwUNh0mDzYdIxQhFCcVJxskDyUedk0lHCUVJU4nGyZIc0g2HDYdJg90GDYcJRwiDyYPJA8kDyQPJA8kDyQPJA8nDyQPJQ8lDyUPJQ8lDyUPJQ82STYPLRlwSiUcNhwlHCUcNhw2HnFNNhw2Dy0PJQ8lD1JvfU42HA==, nq=WIFI, data={"buyNow":"true","buyParam":"719193771661_1_5182956442779","exParams":"{"atomSplit":"1","channel":"damai_app","coVersion":"2.0","coupon":"true","seatInfo":"","umpChannel":"10001","websiteLanguage":"zh_CN_#Hans"}"}, pv=6.3, sign=azG34N002xAAKiYA2sv237H04abW2iYKIxaD3GVPak+JifYV0u6VzpriFiKiP+HuuF26BzWpVTClaOIGdjtibfQ99JomGiYKJhomCi, deviceId=null, sid=13abe677c5076a4fa3382afc38a96a04, uid=2215803849550, x-features=27, x-app-conf-v=0, x-mini-wua=a3gSvx5K5/NRy/W8+fDouCSQ6VSmMK3awHwo5X+IayY7JL5SwHtiL0soynSAvCobk01qRQ2fQcTvZWakhmhA9xlNOKdwvxdA5nZ4Tno2asO5e7EvSMj6yqVYAXZZUBjZPUOBw3vpH8L2GUq9Gi6MTszU57a58+hJE2BCGTVsxhRonDw1Nnxp74Ffm, appKey=23781390, api=mtop.trade.order.build, umt=+D0B/05LPEvOgwKIQ1x+SeV5wNE6NzOo, f-refer=mtop, utdid=ZF3KUN8khtQDAIlImefp4RYz, netType=WIFI, x-app-ver=8.5.4, x-c-traceid=ZF3KUN8khtQDAIlImefp4RYz1684829318230001316498, ttid=10005890@damai_android_8.5.4, t=1684829318, v=4.0, user-agent=MTOPSDK/3.1.1.7 (Android;9;samsung;SM-S908E)}
笔者留神到了 InnerProtocolParamBuilderImpl.buildParams 函数的输入后果齐全笼罩了须要的各类加密参数,其输出类型是 MtopContext。从 jadx 逆向的 apk 代码中能够找到 MtopContext 类,即蕴含 Mtop 生命周期的各个类的一个容器。
public class MtopContext {
public ApiID apiId;
public String baseUrl;
public MtopBuilder mtopBuilder;
public Mtop mtopInstance;
public MtopListener mtopListener;
public MtopRequest mtopRequest;
public MtopResponse mtopResponse;
public Request networkRequest;
public Response networkResponse;
public MtopNetworkProp property = new MtopNetworkProp();
public Map<String, String> protocolParams;
public Map<String, String> queryParams;
public ResponseSource responseSource;
public String seqNo;
@NonNull
public MtopStatistics stats;
}
所以当初的问题变为如何可能构建进去 MtopContext,而后调用 buildParams 函数生成各类加密参数。
5.3 业务模块与 mtopsdk 的交互
过后看着 mtopsdk 的调用过程,感觉很简单。然而猜测从用户点击操作 -> 业务代码 ->mtopsdk 的数据流,以及模块间高内聚低耦合的准则,所以猜测模块间的调用不会很简单,所以笔者就想剖析业务代码与 mtopsdk 的调用逻辑。所以就想跟踪次要业务代码的 trace。所以笔者持续跟踪 trace,运行 frida-trace -U -j “cn.damai!*” 大麦,以剖析 cn.damai 包的调用过程,在其中发现了 NcovSkuFragment.buyNow() 函数,看起来是和购买严密相干的函数。又找到 DMBaseMtopRequest 类。
然而在这里有点卡住了,因为只找到了构建 MtopRequest,并未在 cn.damai 的 trace 日志中并未发现其余对 mtop 的调用。而后笔者又尝试搜寻和 api(order.build)相干的代码,找到了:
然而并没有多大用处。而后,作者又读了大量的源代码,终于定位到了 com.taobao.tao.remotebusiness.MtopBussiness 这个要害类。
笔者本认为 com.taobao 结尾的代码不是那么重要,所以最开始把这个类齐全疏忽了。但通过对源码的浏览,发现这个类是 motpsdk 中 MtopBuilder 类的子类,次要负责管理业务代码和 Mtopsdk 的交互。
因而咱们持续通过 trace 跟踪 MtopBussiness 类。运行 frida-trace -U -j “!buyNow” -j “com.taobao.tao.remotebusiness.MtopBusiness!” -j “MtopContext!” -j “mtopsdk.mtop.intf.MtopBuilder!” 大麦
当初业务代码和 mtopsdk 的交互就很清晰了,红色的局部是业务代码的函数,绿色的局部是 mtopsdk 的函数。
5.4 hook 失去接口参数
通过以上对 trace 的剖析,曾经晓得了具体执行的操作,因而咱们能够应用 frida 编写 js 代码,间接调用 APK 中的类,实现性能调用。先展现一个简略的示例,用于构建一个自定义的 MtopRequest 类:
// new_request.js
Java.perform(function () {const MtopRequest = Java.use("mtopsdk.mtop.domain.MtopRequest");
let myMtopRequest = MtopRequest.$new();
myMtopRequest.setApiName("mtop.trade.order.build");
//item_id + count + ski_id 716435462268_1_5005943905715
myMtopRequest.setData("{"buyNow":"true","buyParam":"716435462268_1_5005943905715","exParams":"{\"atomSplit\":\"1\",\"channel\":\"damai_app\",\"coVersion\":\"2.0\",\"coupon\":\"true\",\"seatInfo\":\"\",\"umpChannel\":\"10001\",\"websiteLanguage\":\"zh_CN_#Hans\"}"}")
myMtopRequest.setNeedEcode(true);
myMtopRequest.setNeedSession(true);
myMtopRequest.setVersion("4.0");
console.log(`${myMtopRequest}`)
});
再应用运行命令 frida -U -l .\reverse\new_request.js 大麦,以在某麦 Apk 中执行 js hook 代码。运行之后即可输入笔者本人构建的 MtopRequest 实例。(frida 真的很微妙!)
有了下面的后果,上面持续欠缺这个示例,增加 MtopBussiness 的构建过程和输入过程。
// 引入 Java 中的类
const MtopBusiness = Java.use("com.taobao.tao.remotebusiness.MtopBusiness");
const MtopBuilder = Java.use("mtopsdk.mtop.intf.MtopBuilder");
// let RemoteBusiness = Java.use("com.taobao.tao.remotebusiness.RemoteBusiness");
const MethodEnum = Java.use("mtopsdk.mtop.domain.MethodEnum");
const MtopListenerProxyFactory = Java.use("com.taobao.tao.remotebusiness.listener.MtopListenerProxyFactory");
const System = Java.use('java.lang.System');
const ApiID = Java.use("mtopsdk.mtop.common.ApiID");
const MtopStatistics = Java.use("mtopsdk.mtop.util.MtopStatistics");
const InnerProtocolParamBuilderImpl = Java.use('mtopsdk.mtop.protocol.builder.impl.InnerProtocolParamBuilderImpl');
// create MtopBusiness
let myMtopBusiness = MtopBusiness.build(myMtopRequest);
myMtopBusiness.useWua();
myMtopBusiness.reqMethod(MethodEnum.POST.value);
myMtopBusiness.setCustomDomain("mtop.damai.cn");
myMtopBusiness.setBizId(24);
myMtopBusiness.setErrorNotifyAfterCache(true);
myMtopBusiness.reqStartTime = System.currentTimeMillis();
myMtopBusiness.isCancelled = false;
myMtopBusiness.isCached = false;
myMtopBusiness.clazz = null;
myMtopBusiness.requestType = 0;
myMtopBusiness.requestContext = null;
myMtopBusiness.mtopCommitStatData(false);
myMtopBusiness.sendStartTime = System.currentTimeMillis();
let createListenerProxy = myMtopBusiness.$super.createListenerProxy(myMtopBusiness.$super.listener.value);
let createMtopContext = myMtopBusiness.createMtopContext(createListenerProxy);
let myMtopStatistics = MtopStatistics.$new(null, null); // 创立一个空的统计类
createMtopContext.stats.value = myMtopStatistics;
myMtopBusiness.$super.mtopContext.value = createMtopContext;
createMtopContext.apiId.value = ApiID.$new(null, createMtopContext);
let myMtopContext = createMtopContext;
myMtopContext.mtopRequest.value = myMtopRequest;
let myInnerProtocolParamBuilderImpl = InnerProtocolParamBuilderImpl.$new();
let res = myInnerProtocolParamBuilderImpl.buildParams(myMtopContext);
console.log(`myInnerProtocolParamBuilderImpl.buildParams => ${HashMap2Str(res)}`)
再次执行 frida -U -l .\reverse\new_request.js 大麦,输入后果如下图,此时已能依据笔者任意构建的申请 data 输入其余加密参数:
对于 order.create 的原理相似,此处不再赘述。
六、其余
6.1 补充阐明
通过 frida 调用 Apk 中的 Java 类有时候会呈现找不到类的状况,起因可能是 classloader 没有正确加载。能够在 js 代码前的最后面加上上面的代码,指定正确的 classloader,即可解决该问题。
Java.perform(function () {
//get real classloader
//from http://www.lixiaopeng.top/article/63.html
var application = Java.use("android.app.Application");
var classloader;
application.attach.overload('android.content.Context')
.implementation = function (context) {var result = this.attach(context); // run attach as it is
classloader = context.getClassLoader(); // get real classloader
Java.classFactory.loader = classloader;
return result;
}
});
6.2 frida hook
通过 frida 操纵 Java 类的性能切实过于弱小,平安人员能够执行以下操作:
- 打印函数输入输出。通过 hook 函数,以实现打印函数的输入输出后果。
操作代码能够在 jadx 右键菜单能够很不便的生成。
- 批改已有的类和函数。
- 定义新类和新函数。
- 被动生成类的实例或调用函数。
- RPC 调用。通过 RPC 调用提供 python 编程接口。
6.3 rpc 调用
前文提到 frida 的一个个性是能够通过 rpc 调用提供 python 编程接口。一个简略的示例:
import frida
def on_message(message, data):
if message["type"] == "send":
print("[*] {0}".format(message["payload"]))
else:
print(message)
# hook 代码
jscode = """
rpc.exports = {testrpc: function (a, b) {return a + b;},
}; """
def start_hook():
# 开始 hook
process = frida.get_usb_device().attach("大麦")
script = process.create_script(jscode)
script.on("message", on_message)
script.load()
return script
script = start_hook()
# 调用 hook 代码
print(script.exports.testrpc(1, 2))
# >>> 输入
# 3
frida 应用 rpc 的办法也很简略,仅需应用 rpc.exports,将对应的函数裸露进去,就能被 python 调用。残缺的代码就是将上一章的代码封装为函数,并通过 rpc 对外提供接口,就能够了。为防止侵权,本文不贴出残缺利用代码。代码封装实现后测了一下,均匀一次调用的工夫为 0.024 秒,齐全能够达到抢票的要求。
本文残缺的记录了笔者对于 Apk 与服务器交互 API 的解析过程,包含环境搭建、抓包、trace 剖析、hook、rpc 调用。本文对于淘系 Apk 的剖析能够提供较多参考。本文算是笔者第一次深刻且胜利的用动静 + 动态剖析联合的形式,借助神器 frida+jadx,胜利破解 Apk,因而本文的记录也较为粗疏的记录了作者的思考过程,能够给老手提供参考。