某麦App抢票接口参数分析
本文中所有内容仅供研究与学习使用,禁止用于任何商业用途和非法用途,否则后果自负!!!
0x00 环境
平台:Android
App版本:8.9.5
工具:
抓包:Postern + charles
动态静态分析:jadx、frida
分析的目标很简单,就是绕过手动操作,通过call api生成订单
0x01 信息搜集
查壳
分析apk之前我们先进行查壳
- GDA检测出来加了几维的壳,那就先脱壳
脱壳
使用frida-dexdump 脱壳
- 脱壳比较顺利也没有报错
脱壳后可以看到非常多的dex,这里我们可以通过关键词定位的方式来定位我们要分析的目标dex。
例如:
经过后面分析之后发现,我们这里把多个dex文件打包起来进行分析其实会更方便点,虽然可能会有些错误不过影响不大。网上有很多现成的工具直接使用就行。
抓包
抓包操作这里不是重点,之前的文章中也介绍过了不再详述
- 总之通过抓包,我们抓到了购买的时候发送的数据包
- 这里我们进行了多次抓包,对数据包进行对比。分析出来哪些是变化值哪些是固定值
这里标记出来的是一些变化的值
- 那么接下来我们主要从这里入手开始分析
0x02 开始分析
定位变化值
x-sign:这个参数是一个sign签名
- 找到了7个相关位置,简单看一下发现获取到
x-sign
之后又对值进行了Hash。这与我们抓包观察到的数据也是一致的
跟进去get
看一下函数调用
在这里我们可以看到这里是一个参数构建函数,最后参数都放进了
hashMap
中那么我们是不是可以直接hook这个函数通过它的返回值拿到值呢
jadx有一个快速生成脚本的一个功能:
写脚本尝试:
Java.perform(function () {
let OpenProtocolParamBuilderImpl = Java.use("mtopsdk.mtop.protocol.builder.impl.OpenProtocolParamBuilderImpl");
OpenProtocolParamBuilderImpl["buildParams"].implementation = function (mtopContext) {
console.log('buildParams is called' + ', ' + 'mtopContext: ' + mtopContext);
let ret = this.buildParams(mtopContext);
console.log('buildParams ret value is ' + ret);
return ret;
};
});
执行脚本:
因为这个App有壳所以这里建议使用attach的方式执行脚本
frida -UF -l hook_buildParams.js
- 不过比较遗憾,这个函数没有触发。也就是说这个函数没有执行
- 有很多可能,这里我们也不确定那就继续搜索
x-sign
换个地方试试
- 这个
getUnifiedSign
也很熟悉,刚才我们hook的buildParams
里面就有使用这个函数unifiedSign.get("x-sign")
Hook getUnifiedSign
:
Java.perform(function () {
let InnerSignImpl = Java.use("mtopsdk.security.InnerSignImpl");
InnerSignImpl["getUnifiedSign"].implementation = function (hashMap, hashMap2, str, str2, z, str3) {
console.log('getUnifiedSign is called' + ', ' + 'hashMap: ' + hashMap + ', ' + 'hashMap2: ' + hashMap2 + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2 + ', ' + 'z: ' + z + ', ' + 'str3: ' + str3);
let ret = this.getUnifiedSign(hashMap, hashMap2, str, str2, z, str3);
console.log('getUnifiedSign ret value is ' + ret);
return ret;
};
});
执行脚本:
hashMap: {data={"targetId":"815654333941","operateType":"0","targetType":"7","comboChannel":"1"}, deviceId=Aq7GTyJEy6mKSsdz5-dOn6yhwGimHQ0kUHRlpYQVSLmB, sid=18a85b191457dea730dbda9e34df95fe, uid=2218498098205, x-features=27, appKey=23781390, api=mtop.damai.wireless.tpp.follow.relation.update, mtopBusiness=true, utdid=ZslABvF89NgDAH/0FqQ8kh7h, extdata=openappkey=DEFAULT_AUTH, ttid=10005894@damai_android_8.9.5, t=1724598045, v=1.0}, hashMap2: {pageId=, pageName=}, str: 23781390, str2: null, z: false, str3: r_105
- 检查了一下这里获取到的值都是固定值,我们需要的变化值并不在这里
- 那怎么办呢,既然执行到这个函数了,那么我们就再分析一下跟它同一个类中的兄弟函数是干嘛的
- 看了一下类中的函数还是挺多的,那我们trace一下这个类。触发一下这个类中所有函数的返回值先看看
这里使用r0trace.js
脚本:肉丝大佬写的这个脚本trace的结果还是比较全面的
在执行trace脚本的同时可以开启抓包,直接在trace的结果中搜索抓包抓到的数据
r0trace使用
- 我们这里直接选择hook目标类:把结果保存下来方便查看
frida -UF -l r0tracer.js -o damai.log
- 这里我们直接搜索到了
x-sign
- 另外,这里的调用栈中调用了一个
InnerProtocolParamBuilderImpl.buildParams
跟我们刚才hook的不是同一个类中的,再尝试hook一下
Hook InnerProtocolParamBuilderImpl.buildParams
:
Java.perform(function () {
let OpenProtocolParamBuilderImpl = Java.use("mtopsdk.mtop.protocol.builder.impl.InnerProtocolParamBuilderImpl");
OpenProtocolParamBuilderImpl["buildParams"].implementation = function (mtopContext) {
console.log(`OpenProtocolParamBuilderImpl.buildParams is called: mtopContext=${mtopContext}`);
let result = this["buildParams"](mtopContext);
console.log(`OpenProtocolParamBuilderImpl.buildParams result=${JSON.stringify(result)}`);
return result;
};
});
只需要把刚才hook
buildParams
的类名换掉就ok这个函数我们在jadx中查看的时候不显示函数内容
按照绿字提示修改jadx设置,或者使用GDA打开都可以看到参数内容
点击保存会自动重新反编译
执行脚本
- 这里结果是一个
HashMap
,那么我们需要修改一下脚本把hashMap
转成字符串
修改hook脚本:打印hashMap
Java.perform(function () {
function HashMap2Str(params_hm) {
var HashMap = Java.use('java.util.HashMap');
var args_map = Java.cast(params_hm, HashMap);
return args_map.toString();
};
let OpenProtocolParamBuilderImpl = Java.use("mtopsdk.mtop.protocol.builder.impl.InnerProtocolParamBuilderImpl");
OpenProtocolParamBuilderImpl["buildParams"].implementation = function (mtopContext) {
console.log(`OpenProtocolParamBuilderImpl.buildParams is called: mtopContext=${mtopContext}`);
let result = this["buildParams"](mtopContext);
console.log(`OpenProtocolParamBuilderImpl.buildParams result=${JSON.stringify(result)} => ${HashMap2Str(result)}`);
return result;
};
});
执行脚本:
{nq=WIFI, data={"targetId":"815654333941","operateType":"0","targetType":"7","comboChannel":"1"},
pv=6.2,
sign=ab25b00090358208d69d564e0a26ab820e3bdf18cda74fff74, deviceId=Aq7GTyJEy6mKSsdz5-dOn6yhwGimHQ0kUHRlpYQVSLmB, sid=18a85b191457dea730dbda9e34df95fe,
uid=2218498098205, x-features=27, x-app-conf-v=0,
x-mini-wua=awwQ4Pe34jLFY0Qh9zi5DMECd/E2Ae/G39Cfwbbk8p4bkS73Gu0Dv8ws09qDpKTZUJ+LDdyFTDWqFh0GDfVb8HqhinnALN50oxZl7+WM9PsrRRMmi5ZEaHZEhZiqEmzA3LuByo+DqQmjO5047Rz4hn2AlpqppWG4DGEW0Zn1RCwCsAHr6SVuKAoojIMUQdPQpJxE=,
appKey=23781390, api=mtop.damai.wireless.tpp.follow.relation.update, umt=ILYBZK5LPM7yPQKRhw+L8hTria6wCx7V, f-refer=mtop, utdid=ZslABvF89NgDAH/0FqQ8kh7h, netType=WIFI, x-app-ver=8.9.5,
x-c-traceid=ZslABvF89NgDAH/0FqQ8kh7h1724600765619003819818, ttid=10005894@damai_android_8.9.5, t=1724600765, v=1.0, x-falco-id=null,
user-agent=MTOPSDK/3.1.1.7
(Android;11;Google;Pixel 5)}
- 这次返回结果中就有我们需要的变化值了:
sign
,x-mini-wua
,x-c-traceid
等加密参数都在
小结:
第一次我们hook的是OpenProtocolParamBuilderImpl.buildParams
,但是这个函数没有触发。根据函数命名可知这是一个开放协议生成器实现
第二次我们hook了getUnifiedSign
,从它的参数hashMap
中获取了一些协议头的固定值。然后trace它所在的类在调用栈中找到了InnerProtocolParamBuilderImpl.buildParams
内部协议参数生成器实现,通过对其再次进行hook获取到了所有的加密参数
trace分析
通过之前的分析我们可以通过hook
public Map<String, String> buildParams(MtopContext mtopContext)
这个函数通过返回值拿到动态生成的参数。但是,主动调用buildParams
的话,需要传入它的参数mtopContext
参数分析 - mtopContext
在jadx中双击参数跟进去查看一下参数的原型
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 int requestTotalLength;
public ResponseSource responseSource;
public String seqNo;
@NonNull
public MtopStatistics stats;
public String getNetRequestHeadersLog() {
if (this.networkRequest == null) {
return "";
}
return ", headerFields=" + this.networkRequest.headers;
}
}
所以现在的问题变为如何能够构建出来MtopContext
,然后主动调用buildParams
函数生成各类加密参数
PS:使用r0trace脚本hook的时候,手机上点击购买会提示抢购人数过多被拦截。但是使用frida-trace去hook的时候则不会,购买流程可以正常执行。猜测有frida检测,所以接下来使用frida-trace跟踪。
跟踪 mtopsdk
我们在jadx中跟踪的过程中发现,流程中基本都是用的mtopsdk
中的函数。经过调查发现mtopsdk
是淘系apk通用的,在网上可以找到一些资料,如果有专有云客户账号权限的话可以直接去阿里云查看接入文档
请求构建和发送部分如下:
// 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();
trace mtopsdk
跟踪所有对mtopsdk
中函数的调用,将其保存在文件中方便分析
frida-trace -U -j "*mtopsdk*!*" 大麦 | tee trace_mtopsdk01.log
在手机上进行一些购票之类的操作(操作一遍手动购票的流程,然后分析trace到的结果)
输出的结果比较多,用编辑器打开查看里面跟请求相关的点:MtopRequest
,我们着重找跟订单(trade.order)相关的,其它的geteway
、getdetail
之类的暂时先不关注
mtop.damai.trade.order.build
// MtopRequest初始化
/* TID 0x5018 */
20210 ms MtopRequest.$init()
20211 ms MtopRequest.setApiName("mtop.damai.trade.order.build")
20211 ms MtopRequest.setVersion("1.0")
20211 ms MtopRequest.setNeedSession(true)
20211 ms MtopRequest.setNeedEcode(true)
20211 ms MtopRequest.setData("{\"buyNow\":\"true\",\"buyParam\":\"820154603312_1_5694585682054\",\"exParams\":\"{\\\"UMPCHANNEL_DM\\\":\\\"10001\\\",\\\"UMPCHANNEL_TPP\\\":\\\"50053\\\",\\\"atomSplit\\\":\\\"1\\\",\\\"channel\\\":\\\"damai_app\\\",\\\"coVersion\\\":\\\"2.0\\\",\\\"coupon\\\":\\\"true\\\",\\\"seatInfo\\\":\\\"\\\",\\\"signKey\\\":\\\"clh+ZnVUUQhqTF98TFtuenBbemp3XVoLYkZOfU5KY315WH5neFtaCHA7IxMqMBMEBjUcCgA5PGs=\\\",\\\"subChannel\\\":\\\"\\\",\\\"umpChannel\\\":\\\"10001\\\",\\\"websiteLanguage\\\":\\\"zh_CN_#Hans\\\"}\"}")
// MtopBuilder初始化
20212 ms MtopBuilder.$init("<instance: mtopsdk.mtop.intf.Mtop>", "<instance: mtopsdk.mtop.domain.MtopRequest>", null)
// MtopBuilder发送异步请求
20220 ms MtopBuilder.asyncRequest()
// 参数构建
/* TID 0x5391 */
20290 ms | | | | InnerProtocolParamBuilderImpl.buildParams("<instance: mtopsdk.framework.domain.MtopContext>")
// 返回值
20315 ms | | | | | | <= "<instance: java.util.Map, $className: java.util.HashMap>"
- 我们在前面已经分析到了
InnerProtocolParamBuilderImpl.buildParams
的返回值完全覆盖了我们需要的各类加密参数
业务模块与mtopsdk的交互过程
在源码中定位到了com.taobao.tao.remotebusiness.MtopBussiness
这个关键类
- 这个类是
mtopsdk
中MtopBuilder
的子类,阅读这个类中的源码可以发现其主要负责管理业务代码和mtopsdk
的交互
继续通过trace跟踪MtopBussiness
类
frida-trace -U -j "*!*buyNow*" -j "com.taobao.tao.remotebusiness.MtopBusiness!*" -j "*MtopContext!*" -j "*mtopsdk.mtop.intf.MtopBuilder!*" 大麦
现在业务代码和mtopsdk
的交互就很清晰了,绿色部分是业务代码的函数,黄色部分是mtopsdk
的函数
主动调用接口获取参数
构建自定义的MtopRequest
通过以上的trace分析,已经知道了具体执行的操作。那么接下来我们就可以编写frida代码,直接调用类对象构建自定义的MtopRequest
类
Java.perform(function () {
const MtopRequest = Java.use("mtopsdk.mtop.domain.MtopRequest")
let myMtopRequest = MtopRequest.$new()
myMtopRequest.setApiName("mtop.damai.trade.order.build")
myMtopRequest.setData("{\"buyNow\":\"true\",\"buyParam\":\"793545597100_1_5585824998963\",\"exParams\":\"{\\\"UMPCHANNEL_DM\\\":\\\"10001\\\",\\\"UMPCHANNEL_TPP\\\":\\\"50053\\\",\\\"atomSplit\\\":\\\"1\\\",\\\"channel\\\":\\\"damai_app\\\",\\\"coVersion\\\":\\\"2.0\\\",\\\"coupon\\\":\\\"true\\\",\\\"seatInfo\\\":\\\"[{\\\\\\\"seatId\\\\\\\":\\\\\\\"2376864283\\\\\\\",\\\\\\\"standId\\\\\\\":\\\\\\\"19734679\\\\\\\"}]\\\",\\\"signKey\\\":\\\"clh+ZnVUUQhqTF98TFthcXNfe2t0VF4JY0ROfU5KY3xzU31reFhdCXA7IxMqMBMEBjUcCgA5PGs=\\\",\\\"umpChannel\\\":\\\"10001\\\",\\\"websiteLanguage\\\":\\\"zh_CN_#Hans\\\"}\"}")
myMtopRequest.setNeedEcode(true)
myMtopRequest.setNeedSession(true)
myMtopRequest.setVersion("1.0")
console.log(`${myMtopRequest}`)
})
- 成功构建了自己的
MtopRequest
实例
继续完善,添加MtopBussiness
的构建过程和输出
Java.perform(function () {
const MtopRequest = Java.use("mtopsdk.mtop.domain.MtopRequest")
let myMtopRequest = MtopRequest.$new()
myMtopRequest.setApiName("mtop.damai.trade.order.build")
myMtopRequest.setVersion("1.0")
myMtopRequest.setNeedSession(true)
myMtopRequest.setNeedEcode(true)
myMtopRequest.setData("{\"buyNow\":\"true\",\"buyParam\":\"793545597100_1_5585824998963\",\"exParams\":\"{\\\"UMPCHANNEL_DM\\\":\\\"10001\\\",\\\"UMPCHANNEL_TPP\\\":\\\"50053\\\",\\\"atomSplit\\\":\\\"1\\\",\\\"channel\\\":\\\"damai_app\\\",\\\"coVersion\\\":\\\"2.0\\\",\\\"coupon\\\":\\\"true\\\",\\\"seatInfo\\\":\\\"[{\\\\\\\"seatId\\\\\\\":\\\\\\\"2376864283\\\\\\\",\\\\\\\"standId\\\\\\\":\\\\\\\"19734679\\\\\\\"}]\\\",\\\"signKey\\\":\\\"clh+ZnVUUQhqTF98TFthcXNfe2t0VF4JY0ROfU5KY3xzU31reFhdCXA7IxMqMBMEBjUcCgA5PGs=\\\",\\\"umpChannel\\\":\\\"10001\\\",\\\"websiteLanguage\\\":\\\"zh_CN_#Hans\\\"}\"}")
console.log(`${myMtopRequest}`)
function HashMap2Str(params_hm) {
var HashMap = Java.use('java.util.HashMap');
var args_map = Java.cast(params_hm, HashMap);
return args_map.toString();
};
// 引入Java中的类
const MtopBusiness = Java.use("com.taobao.tao.remotebusiness.MtopBusiness")
const MtopBuilder = Java.use("mtopsdk.mtop.intf.MtopBuilder")
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
/*
@Deprecated
public static MtopBusiness build(MtopRequest mtopRequest) {
return build(Mtop.instance(null), mtopRequest, (String) null);
}
*/
let myMtopBusiness = MtopBusiness.build(myMtopRequest)
myMtopBusiness.useWua()
myMtopBusiness.reqMethod(MethodEnum.POST.value) // mark
myMtopBusiness.setCustomDomain("mtop.damai.cn")
myMtopBusiness.setBizId(24)
myMtopBusiness.setErrorNotifyAfterCache(true)
// MtopBusiness.addListener("<instance: mtopsdk.mtop.common.MtopListener, $className: com.taobao.android.ultron.datamodel.imp.DMRequester$Response>")
myMtopBusiness.startRequest()
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)}`)
})
这段代码较为复杂,回头解释一下吧
Create MtopBusiness:按照上图中标绿色的部分创建
执行结果
总的来说这段代码我们做了几件事情,最终成功的拿到了需要的加密参数和动态参数
- 构建自己的MtopRequest实例
- 将实例转为MtopContext,之后作为buildParams()的参数使用
- 调用buildParams()并打印了一下结果
补充
突然发现上图中的x-c-traceid=null
,在网上的帖子中找到了生成这个值的一段代码,不过不对。
回头再分析这个值吧,这个值其实是一个组合值
utdid + 时间戳 + 一个七位数的值(不确定是不是随机值) + 127115(在我的测试设备上是这个数字不确定换设备是否会改变)
原因:
发现爬虫的协议中不发送这个值也不会报下图这种错误
function getExtraInfo(mtopContext, res) {
// Import Java class
const ActivityThread = Java.use("android.app.ActivityThread")
const SecurityGuardManager = Java.use("com.alibaba.wireless.security.open.SecurityGuardManager")
const IFCComponent = Java.use("com.alibaba.wireless.security.open.middletier.fc.IFCComponent")
// Get context
const application = ActivityThread.currentApplication()
const context = application.getApplicationContext()
// Get x-bx-version
let iFCComponent = SecurityGuardManager.getInstance(context, "").getInterface(IFCComponent.class)
iFCComponent = Java.cast(iFCComponent, IFCComponent)
if (iFCComponent !== null) {
res.put('x-bx-version', iFCComponent.getFCPluginVersion().toString())
}
// Get x-client-traceid
const mtopInstance = mtopContext.mtopInstance.value
const mtopStatistics = mtopContext.stats.value
const mtopConfig = mtopInstance.getMtopConfig()
res.put('x-client-traceid', `${res.get('utdid').toString()}${res.get('t').toString()}${(mtopStatistics.intSeqNo.value % 1000).toString().padStart(4, '0')}`)
}
执行脚本:frida -UF -l .\getMtopContext.js
接下来还有几个问题:
我们需要把
order.create
也构建了,因为通过分析和查找资料。我们发现order.create
才是最终的购买动作需要写爬虫把包拼出来,让请求通过服务器的校验。简单尝试了一下构建的包还是有点问题
再之后,可以正常返回之后还需要过滑块验证码
暂时先这样,后续再补充
- 标题: 某麦App抢票接口参数分析
- 作者: xiaoeryu
- 创建于 : 2024-09-09 19:51:19
- 更新于 : 2024-09-09 20:42:12
- 链接: https://github.com/xiaoeryu/2024/09/09/某麦App抢票接口参数分析/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。