首页
社区
课程
招聘
[原创]某APP收费音频无会员绕过下载过程分析
发表于: 2017-3-27 13:45 23179

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

2017-3-27 13:45
23179
0x00 背景介绍
    在工作后的休闲时间我比较喜欢打开网络电台听一些有声书,大牛实事点评;不知道从什么时候开始网络上突然流行起了付费音频,很多付费音频都是由名家亲自参与制作,质量非常高 很受大众的喜欢;其中以某电台的付费内容最受欢迎,我也购买了好些套音频来跟着名家的脚步学习;名家制作的付费音频在选择的时候不容二说,直接购买就是;但是也不乏其中有很多的标题党 哗众取宠,能不能在不付费的情况下先进行一次试听呢
0x01 分析
    为了方便分析,我特意选择了直接在浏览器中打开电台wap版;随便选择一个付费才能收听的音频,使用Chrome浏览器,调试模式,手机模式如下所示

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

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

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

第一个关键请求:

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"
}

第二个关键请求:

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中获取的;那 这一串参数是怎么得来的呢?

/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设置如下

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

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

F11一步步分析得出结论如下 
解密播放绝对路径:
KEY通过字典o(s, r) 运算由 dg3utf1k6yxdwi09得到 xkt3a41psizxrh9l
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)) 中,经过取字典位移等运算得出密文,
   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"

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

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


OK截至到这里,我们已经将收费音频中的试听音频的逻辑分析清楚了,下面我们看看必须购买才能听的音频是如何使用上诉逻辑获取真实地址的;
由于上面的分析使用的一条收费且可以试听的音频,下面我们看看如何获取其他收费的音频
复习一下:获取收费音频的播放列表的URL是 http://m.xxxx.com/zhubo/主播ID ,我们去主页随便选个收费且不能试听的主播ID试试吧 如下:

于是我们可以直接拿这个ID 来直接拼凑成我们分析的第一条GET语句(返回播放json) :
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值来计算结果

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

0x03 总结
     本篇文章得以完成,完全需要感谢此app在架构过程中产生的逻辑漏洞;(服务器太过于信任客户端和未对收费音频进行认证(音频用户关联性认证)); 对此我要对此app表示感谢!
     好了我们的本次分析某电台收费app之旅就结束了,懂js或者会调用js的同学,相信你们直接写个脚本遍历下载本app的所有付费音频应该不是什么难事吧; :) ,此篇文章我也会一并发送到app官方技术邮箱中,如果相信看到本文的朋友动手实战时会发现文中所诉根本不成立。
由于本人技术有限,文中所写如有错误地方还请大家指正海涵。如果有没看明白的同学可以在帖子中留言或者直接站内信联系我(请不要问我其他高深的问题,因为我连js都不会。。。)
0x04 感谢

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

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



[课程]FART 脱壳王!加量不加价!FART作者讲授!

收藏
免费 1
支持
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  CCkicker   +1.00 2017/05/08
最新回复 (34)
雪    币: 138
活跃值: (460)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
有些sb了 打着app的幌子写了一篇web相关的文章。。。
2017-3-27 13:49
0
雪    币: 1
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
大神膜拜
2017-3-27 13:49
0
雪    币: 55
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
大神膜拜
2017-3-27 13:50
0
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
5
chrome自带格式化js的功能,就在左下角的“{}”处。
2017-3-27 13:55
0
雪    币: 6976
活跃值: (1372)
能力值: ( LV11,RANK:180 )
在线值:
发帖
回帖
粉丝
6
现状web上的内容越来越多. 楼主这篇文字很及时!!
2017-3-27 13:56
0
雪    币: 138
活跃值: (460)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
malokch chrome自带格式化js的功能,就在左下角的“{}”处。
GET 新技能! Thanks!
2017-3-27 13:58
0
雪    币: 219
活跃值: (52)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
8
前排学习
2017-3-27 14:09
0
雪    币: 18
活跃值: (561)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
少仲 前排学习[em_13]
2017-3-27 14:13
0
雪    币: 370
活跃值: (1176)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
10
少仲 前排学习[em_13]
VPP Security Lab,少仲哥厉害啊 !
2017-3-27 14:48
0
雪    币: 219
活跃值: (52)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
ThomasKing VPP Security Lab,少仲哥厉害啊 !
向tk老师学习
2017-3-27 15:29
0
雪    币: 6779
活跃值: (1119)
能力值: ( LV12,RANK:782 )
在线值:
发帖
回帖
粉丝
12
mark一下,正想试试这个
2017-3-27 16:49
0
雪    币: 245
活跃值: (74)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
好久没看到记忆宫殿了
2017-3-29 18:06
0
雪    币: 3121
活跃值: (1609)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
楼主厉害了,操作起来 感觉好吃力。
2017-3-29 19:20
0
雪    币: 7705
活跃值: (2178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
大神厉害啊,前来学习
2017-3-30 09:01
0
雪    币: 34
活跃值: (864)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
16
你好,非常感谢分享。我按照你的思路走了一下,因为太渣,只走到第一节。我直接把你的“/69149360/sound/32173409”这个复制到地址,找到了和你一样的音频。然后在chrome的开发者工具 Network-media选项就可以看到音频的地址:“http://audio.pay.xmcdn.com/download/1.0.0/preview/1702877/group1/M01/04/6E/wKgJMljAJmaxaBjBAVPzE83Jfuo884.m4a?sign=2c21adabfdf6ccbbd53a9374a688405c&buy_key=fe4f133ccbf4b22dfa2a1e704ccbbda8×tamp=1490925896647901&token=6901&duration=2752” 从这里就可以看到它是有sign,buy_key,timestamp,token,duration这几个变量。所以我不太清楚为什么还要解密?是解密之后绝对地址有不一样吗,但我看你解密的好像是一样的。还有我不理解的是,我们可以点击这个地址打开得到一个音频文件,进度条显示45分钟,不论是下载还是在网页,只能听到3分钟,也就是试听的部分。是它经过加密了吗?那如果是加密的,是不是可以解密这个音频文件?额,我不知道有没有这个技术。。。
2017-3-31 11:18
0
雪    币: 138
活跃值: (460)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
17
lumou 你好,非常感谢分享。我按照你的思路走了一下,因为太渣,只走到第一节。我直接把你的“/69149360/sound/32173409”这个复制到地址,找到了和你一样的音频。然后在chrome的开发者工具 ...
我为什么还需要解密:那是因为直接能得到地址的是免费公开收听的; 而我需要用这个解密方法去解开那些只公开3分钟,或者干脆不公开的地址;
2017-3-31 15:25
0
雪    币: 138
活跃值: (460)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
18
lumou 你好,非常感谢分享。我按照你的思路走了一下,因为太渣,只走到第一节。我直接把你的“/69149360/sound/32173409”这个复制到地址,找到了和你一样的音频。然后在chrome的开发者工具 ...
咦 ,为什么你的回复我能收到通知; 其他人的就不会。。。?
2017-3-31 15:27
0
雪    币: 34
活跃值: (864)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
19
Nermor 我为什么还需要解密:那是因为直接能得到地址的是免费公开收听的; 而我需要用这个解密方法去解开那些只公开3分钟,或者干脆不公开的地址;
那我懂了,就是解密之后sign,buy_key,timestamp,token,duration这些值是不一样的。但是我还没看懂具体怎么做到的。我再看看。
2017-3-31 17:33
0
雪    币: 138
活跃值: (460)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
20
lumou 那我懂了,就是解密之后sign,buy_key,timestamp,token,duration这些值是不一样的。但是我还没看懂具体怎么做到的。我再看看。
还有就是解密播放地址;很感谢你能 认真的看完并动手实践 很难得 :)
2017-4-1 08:48
0
雪    币: 34
活跃值: (864)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
21
Nermor 还有就是解密播放地址;很感谢你能 认真的看完并动手实践 很难得 :) [em_63]
:) 我还在琢磨,虽然还不是很懂,但有学到很多。谢谢你。
2017-4-1 22:02
0
雪    币: 5041
活跃值: (3439)
能力值: ( LV13,RANK:283 )
在线值:
发帖
回帖
粉丝
22
多谢分享
2017-4-2 00:16
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
mark 同样懂js的路过
2017-4-2 02:08
0
雪    币: 138
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
楼主,刚刚全部做了一遍,未购买的已经限制全部2:59了,用购买过的ep,  fileId,seed,,duration修改后计算出的链接是全部的,没有看全部的js,参数里面的buyKey没有修改,但是他计算出的链接里面buyKey居然是正确的 
2017-4-4 21:45
0
雪    币: 5843
活跃值: (2867)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
试着做一下,多谢分享
2017-4-6 00:22
0
游客
登录 | 注册 方可回帖
返回
//