某APP收费音频无会员绕过下载过程分析

0x00 背景介绍

    在工作后的休闲时间我比较喜欢打开网络电台听一些有声书,大牛实事点评;不知道从什么时候开始网络上突然流行起了付费音频,很多付费音频都是由名家亲自参与制作,质量非常高 很受大众的喜欢;其中以某电台的付费内容最受欢迎,我也购买了好些套音频来跟着名家的脚步学习;名家制作的付费音频在选择的时候不容二说,直接购买就是;但是也不乏其中有很多的标题党 哗众取宠,能不能在不付费的情况下先进行一次试听呢

0x01 分析

    为了方便分析,我特意选择了直接在浏览器中打开电台wap版;随便选择一个付费才能收听的音频,使用Chrome浏览器,调试模式,手机模式如下所示

352285_bvns3uzmwsxddyw.png

音频播放URL:http://m.xxx.com/69149360/sound/32173409

其中直接访问 http://m.xxx.com/69149360 可以显示当前主播的所有音频,也就是说 69149360 为主播ID, 而后面的 32173409 应该为当前的音频ID。

点击播放按钮通过chrome 调试 Network功能标签中,可以看到客户端会向服务端请求音频的真实地址用来播放; 通过分析发现以下两个请求比较关键。

第一个关键请求:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

GET http://m.XXXX.com/mobile/track/pay/32173409?device=pc&uid=&token=&isBackend=false&_=1490364593769

服务端返回:

{

    "ret": 0,

    "msg"null,

    "errorCode"null,

    "trackId": 32173409,

    "uid": 69149360,

    "albumId": 6294413,

    "title""演说之禅 | 耿人健 0309",

    "domain""http://audio.pay.xx.com",

    "duration": 2752,

    "totalLength": 22278931,

    "sampleDuration": 180,

    "sampleLength": 1702877,

    "isAuthorized"false,

    "apiVersion""1.0.0",

    "buyKey""fe4f133ccbf4b22dfa2a1e704ccbbda8",

    "seed": 3669,

    "fileId""38*14*54*34*10*54*56*59*50*65*47*62*53*65*65*59*26*14*3*48*38*50*59*61*47*50*59*47*46*59*20*52*59*56*23*26*15*61*16*55*60*15*12*43*8*43*30*55*30*60*11*39*29*52*53*40*15*7*48*3*53*53*46*2*12*46*43*",

    "ep""ixdsaY59SiQC2v0Mb4wd414PUk0i1ibGSddPKQ7mX3e0nu+O2qjckr8Kga7ahPJmVbQjgHJRfvE0jPb8wQMSjrkPPC9VE6CqX9LAvCdcqUKio+NbmGgY"

}

第二个关键请求:

1

http://audio.pay.xx.com/download/1.0.0/preview/1702877/group1/M01/04/6E/wKgJMljAJmaxaBjBAVPzE83Jfuo884.m4a?sign=7b2b193f95d330f616013cc1ff709b01&buy_key=fe4f133ccbf4b22dfa2a1e704ccbbda8&timestamp=1490364575567020&token=6391&duration=2752

第二个请求直接就是m4a 音频地址了,其中服务器地址是 audio.pay.xx.com ,可以轻易的看出应该是从第一条GET中返回的json中获取的;那 这一串参数是怎么得来的呢?

1

/download/1.0.0/preview/1702877/group1/M01/04/6E/wKgJMljAJmaxaBjBAVPzE83Jfuo884.m4a?sign=7b2b193f95d330f616013cc1ff709b01&buy_key=fe4f133ccbf4b22dfa2a1e704ccbbda8&timestamp=1490364575567020&token=6391&duration=2752

 

0x02 调试

       作为一个优秀且长相帅气的看雪潜水多年的网友,遇到问题想到的第一解决方案就是 调(tiao)试(xi); 对付这种web型的协议密钥就不用祭出倚天剑(OD)和屠龙刀(IDA)了,直接利用chrome自带的调试功能以及Fiddler进行劫持就够了。过程如下:

       从以上两条请求可以分析得出,音频的最终请求参数不是由服务器返回,而是由本地计算得到的;通常在web开发中js往往就承担了计算以及动态控制责任。由于本人对js不是很熟悉,无法快速定位于是使用了一个本办法:删除触发播放按钮的事件来定位,删除一个点击下播放,查看是否能播放,然后再删除一个再点击播放 如此循环;

   在不断删除点击下,最终定位到请求w4a的处理时间在all.js中。打开all.js 发现所有js源码都在同一行,这完全无法分析,而且影响下js断点调试;于是我将js 格式化保存到本地,利用Fiddler 的AutoResponder功能劫持到本地来设置。由于站点使用https 来传输js,Fiddler无法劫持https内容,我们索性将整个请求页面一并劫持并修改源码all.js 去请求非https;Fiddler设置如下

352285_7ud56sc1ayqpj84.png

all.js格式化之后变得特别清晰,于是乎我们先稍微看一遍all.js 看看哪里可能是解密的关键地方。干逆向这么久,觉得逆向的过程就是一个猜想和推到猜想的过程;以我C语言功力来理解js 在有可以的地方下断点(关于Chrome JS 调试请看这里: http://www.jb51.net/article/58570.htm)

352285_c7psggnpycrdhfb.png

352285_qpybtw6g0hm379f.png

 

点击播放,最终发现在 success: function(t) { 这个地方停下来

352285_himn572wsxqie24.png

F11一步步分析得出结论如下 

解密播放绝对路径:

KEY通过字典o(s, r) 运算由 dg3utf1k6yxdwi09得到 xkt3a41psizxrh9l

1

t = "dg3utf1k6yxdwi09",e = [19, 1, 4, 7, 30, 14, 28, 8, 24, 17, 6, 35, 34, 16, 9, 10, 13, 22, 32, 29, 31, 21, 18, 3, 2, 23, 25, 27, 11, 20, 5, 15, 12, 0, 33, 26]function o(t, e) {        for (var i = [], o = 0; o < t.length; o++) {            for (var n = 0,            n = "a" <= t[o] && "z" >= t[o] ? t[o].charCodeAt(0) - 97 : t[o] - 0 + 26, a = 0; 36 > a; a++) if (e[a] == n) {                n = a;                break            }            i[o] = 25 < n ? n - 26 : String.fromCharCode(n + 97)        }        return i.join("")    }

再将KEY和第一条GET得到的json内容中的ep一起传入 i(p, e(t.ep)) 中,经过取字典位移等运算得出密文,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

   function e(t) {

        if (!t) return "";

        var e, i, o, n, a, t = t.toString(),

        s = [ - 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];

        for (n = t.length, o = 0, a = ""; o < n;) {

            do e = s[255 & t.charCodeAt(o++)];

            while (o < n && -1 == e);

            if ( - 1 == e) break;

            do i = s[255 & t.charCodeAt(o++)];

            while (o < n && -1 == i);

            if ( - 1 == i) break;

            a += String.fromCharCode(e << 2 | (48 & i) >> 4);

            do {

                if (e = 255 & t.charCodeAt(o++), 61 == e) return a;

                e = s[e]

            while ( o < n && - 1 == e );

            if ( - 1 == e) break;

            a += String.fromCharCode((15 & i) << 4 | (60 & e) >> 2);

            do {

                if (i = 255 & t.charCodeAt(o++), 61 == i) return a;

                i = s[i]

            while ( o < n && - 1 == i );

            if ( - 1 == i) break;

            a += String.fromCharCode((3 & e) << 6 | i)

        }

        return a

    }

function i(t, e) {

        for (var i, o = [], n = 0, a = "", s = 0; 256 > s; s++) o[s] = s;

        for (s = 0; 256 > s; s++) n = (n + o[s] + t.charCodeAt(s % t.length)) % 256,

        i = o[s],

        o[s] = o[n],

        o[n] = i;

        for (var r = n = s = 0; r < e.length; r++) s = (s + 1) % 256,

        n = (n + o[s]) % 256,

        i = o[s],

        o[s] = o[n],

        o[n] = i,

        a += String.fromCharCode(e.charCodeAt(r) ^ o[(o[s] + o[n]) % 256]);

        return a

    }

使用json返回的_randomSeed通过算法得到字符串A "oRIBQLWKzamwPSuh\C3:1cdMDTYvJVeH_q97fjX58p4nFsgxyr20t.NObi6-GEAlUk/Z"

1

2

3

4

5

6

7

8

9

10

11

12

13

cg_hun: function() {

            this._cgStr = "";

            var t = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\:._-1234567890",

            e = t.length,

            i = 0;

            for (i = 0; i < e; i++) {

                var o = this.ran(),

                n = o * t.length,

                a = parseInt(n);

                this._cgStr += t.charAt(a),

                t = t.split(t.charAt(a)).join("")

            }

        },

再使用json中返回的fileId 字段重新排列字符串A 得到最终结果 preview/1702877/group1/M01/04/6E/wKgJMljAJmaxaBjBAVPzE83Jfuo884.m4a

1

2

3

4

5

6

7

cg_fun: function(t) {

            var t = t.split("*"),

            e = "",

            i = 0;

            for (i = 0; i < t.length - 1; i++) e += this._cgStr.charAt(t[i]);

            return e

        },

 

有了m4a的绝对路径结果已经很清晰了; 播放地址拼凑方式为:domain+"download"+apiVersion+解密后的绝对路径+?+sign+buy_key+timestamp+token+duration

352285_3g9wj618k86dkez.png

 

OK截至到这里,我们已经将收费音频中的试听音频的逻辑分析清楚了,下面我们看看必须购买才能听的音频是如何使用上诉逻辑获取真实地址的;

由于上面的分析使用的一条收费且可以试听的音频,下面我们看看如何获取其他收费的音频

复习一下:获取收费音频的播放列表的URL是 http://m.xxxx.com/zhubo/主播ID ,我们去主页随便选个收费且不能试听的主播ID试试吧 如下:

352285_gv7gn633d02dyfc.png

于是我们可以直接拿这个ID 来直接拼凑成我们分析的第一条GET语句(返回播放json) :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

GET http://m.xxxx.com/mobile/track/pay/33236908?device=pc&uid=&token=&isBackend=false&_=1490366894901

返回:

{

    ret: 0,

    msg: null,

    errorCode: null,

    trackId: 33236908,

    uid: 26457553,

    albumId: 6222157,

    title: "24. 记忆宫殿法之“配图定位法”",

    domain: "http://audio.pay.xx.com",

    duration: 672,

    totalLength: 5447895,

    sampleDuration: 180,

    sampleLength: 1505660,

    isAuthorized: false,

    apiVersion: "1.0.0",

    buyKey: "fe4f133ccbf4b22dfa2a1e704ccbbda8",

    seed: 6381,

    fileId: "48*49*34*23*60*34*58*25*38*31*1*31*13*13*1*25*54*49*37*24*48*38*25*14*1*31*25*1*2*25*43*19*25*58*59*54*65*9*38*16*27*66*47*4*58*58*50*11*35*11*26*14*54*38*21*49*48*57*36*2*62*13*21*18*10*2*0*",

    ep: "ixdsaY59SiQC2v0Mb4wd414PUk0i1ibGSddPKQ7mX3e0nriK36iKm74I2vaK1alhX+8mhnoCf/Bg2KP7wlBEjbcPMCpXGqCqX9LAvCdRrEOip+Jdm2oR"

}

看到如此熟悉的结果相信大家知道应该怎么做了吧?懂js的同学可以直接还原 all.js的算法,我这里就偷个懒用了浏览器来直接替换json值来计算结果

352285_3mh9rj9ryr0xqu4.png

直接在解密函数头,双击t.ep中的值进行修改;关键值为:ep, fileId,seed, fileId,duration;由函数直接计算的结果为:

1

http://audio.pay.xx.com/download/1.0.0/preview/1505660/group1/M05/04/B8/wKgJN1jRD2CwwIAVAFMg19rph74369.m4a?sign=7273c31c689628328ae876e12b41eaf8&buy_key=fe4f133ccbf4b22dfa2a1e704ccbbda8&timestamp=1490369220604020&token=8200&duration=672

352285_txh4v371g7dcyv2.png

0x03 总结

     本篇文章得以完成,完全需要感谢此app在架构过程中产生的逻辑漏洞;(服务器太过于信任客户端和未对收费音频进行认证(音频用户关联性认证)); 对此我要对此app表示感谢!

     好了我们的本次分析某电台收费app之旅就结束了,懂js或者会调用js的同学,相信你们直接写个脚本遍历下载本app的所有付费音频应该不是什么难事吧; :) ,此篇文章我也会一并发送到app官方技术邮箱中,如果相信看到本文的朋友动手实战时会发现文中所诉根本不成立。

由于本人技术有限,文中所写如有错误地方还请大家指正海涵。如果有没看明白的同学可以在帖子中留言或者直接站内信联系我(请不要问我其他高深的问题,因为我连js都不会。。。)

0x04 感谢

     在 VPP Security Lab 小组中论漏洞挖掘能力我不及@ggggwwww,论漏洞分析能力又不及@少仲。如此菜的人在VPP中得以生存下来是源于两位的无私分享,谢谢你们!

感谢看雪平台上所有无私奉献的大牛,没有你们的文章,估计我的技术应该还处于村口放牛的水平!谢谢!

转载于:https://my.oschina.net/u/998410/blog/1120358

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值