某麦App抢票接口参数分析

xiaoeryu Lv5

本文中所有内容仅供研究与学习使用,禁止用于任何商业用途和非法用途,否则后果自负!!!

0x00 环境

平台:Android

App版本:8.9.5

工具:

抓包:Postern + charles

动态静态分析:jadx、frida

分析的目标很简单,就是绕过手动操作,通过call api生成订单

0x01 信息搜集

查壳

分析apk之前我们先进行查壳

  • GDA检测出来加了几维的壳,那就先脱壳

脱壳

使用frida-dexdump 脱壳

  • 脱壳比较顺利也没有报错

脱壳后可以看到非常多的dex,这里我们可以通过关键词定位的方式来定位我们要分析的目标dex。

例如:

经过后面分析之后发现,我们这里把多个dex文件打包起来进行分析其实会更方便点,虽然可能会有些错误不过影响不大。网上有很多现成的工具直接使用就行。

抓包

抓包操作这里不是重点,之前的文章中也介绍过了不再详述

  • 总之通过抓包,我们抓到了购买的时候发送的数据包
  • 这里我们进行了多次抓包,对数据包进行对比。分析出来哪些是变化值哪些是固定值
这里标记出来的是一些变化的值
  • 那么接下来我们主要从这里入手开始分析

0x02 开始分析

定位变化值

  1. 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)相关的,其它的getewaygetdetail之类的暂时先不关注

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这个关键类

  • 这个类是mtopsdkMtopBuilder的子类,阅读这个类中的源码可以发现其主要负责管理业务代码和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:按照上图中标绿色的部分创建

执行结果

总的来说这段代码我们做了几件事情,最终成功的拿到了需要的加密参数和动态参数

  1. 构建自己的MtopRequest实例
  2. 将实例转为MtopContext,之后作为buildParams()的参数使用
  3. 调用buildParams()并打印了一下结果

补充

突然发现上图中的x-c-traceid=null,在网上的帖子中找到了生成这个值的一段代码,不过不对。

回头再分析这个值吧,这个值其实是一个组合值

utdid + 时间戳 + 一个七位数的值(不确定是不是随机值) + 127115(在我的测试设备上是这个数字不确定换设备是否会改变)

原因:

  1. 发现爬虫的协议中不发送这个值也不会报下图这种错误

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

接下来还有几个问题:

  1. 我们需要把order.create也构建了,因为通过分析和查找资料。我们发现order.create才是最终的购买动作

  2. 需要写爬虫把包拼出来,让请求通过服务器的校验。简单尝试了一下构建的包还是有点问题

  3. 再之后,可以正常返回之后还需要过滑块验证码

暂时先这样,后续再补充

  • 标题: 某麦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 进行许可。
评论