zoukankan      html  css  js  c++  java
  • YouTube视频签名加密算法的破解

    密码学方法

    多年以前,YouTube的视频源地址是直接encode在页面中的,你甚至可以用一行Perl来下载它们

    直到2012年8月,这个简单的脚本(用在0.0.1版本的You-Get中)仍然可以解析出YouTube视频的源地址。(利用页面的url_encoded_fmt_stream_map中提供的信息)

    大约在9月的时候,YouTube采取了反下载措施。这个所谓的措施就是在视频源地址里加上了一个signature参数,需要单独解析出url_encoded_fmt_stream_map中的url域和sig域后合并(将sig的值直接用&signature=连接到原url的后面即可)。未加signature参数或signature不正确的地址会返回403 Forbidden错误。

    最近几天,YouTube开始部署更进一步的反下载措施了。很多视频(似乎主要是Copyrighted material)的url_encoded_fmt_stream_map中不再有sig域(即原来可直接使用的signature参数),取而代之的是一个看起来较相似的s域(实际上是经过加密的signature),同时多了一个域use_cipher_signature=True。(参见youtube-dl的讨论

    在浏览器中播放某YouTube视频(http://youtu.be/FU8csnZxdPA),抓取到的实际地址是:

    http://r13---sn-5uaeznl7.c.youtube.com/videoplayback?algo
    rithm=throttle-factor&burst=40&clen=11611894&cp=U0hWR1RPU
    LUENONl9MSVdDOmJqRU8tZ1VSNFll&cpn=dLOl8I7n-YcCtPDe&expi
    re=1372445266&factor=1.25&fexp=931311%2C929816%2C923002%2
    C907227%2C930504%2C906397%2C928201%2C929123%2C929915%2C92
    %2C929907%2C929125%2C929127%2C925714%2C929917%2C92991
    %2C931202%2C912512%2C912515%2C912521%2C906838%2C906840%2
    C931913%2C904830%2C919373%2C933701%2C904122%2C932211%2C93
    %2C900816%2C909421%2C912711%2C907228&gir=yes&id=154f1
    cb2767174f0&ip=8.35.201.112&ipbits=8&itag=134&keepalive=y
    es&key=yt1&lmt=1369097054006774&ms=au&mt=1372422395&mv=m&
    newshard=yes&range=1802240-2703359&ratebypass=yes&signatu
    re=4A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.6EBE5DDC5720C
    C6B1927402C84DCBF0E52&source=youtube&sparams=algori
    thm%2Cburst%2Cclen%2Ccp%2Cfactor%2Cgir%2Cid%2Cip%2Cipbits
    %2Citag%2Clmt%2Csource%2Cupn%2Cexpire&sver=3&upn=yYnqjUrC
    Ty8

    抽出URL中的signature参数,为:

    A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C6B1927402C84DCBF0E52

    可以看到,该字符串是用"."连接的两个40位16进制数(总长度40+1+40=81)。由于该视频地址的itag=134,在视频页面的url_encoded_fmt_stream_mapadaptive_fmts中找到itag=134所对应的s,为:

    A4A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C2B1927402C84DCBF0E56E56

    这是一个被加密的signature,长度为42+1+43=86,无法直接用于访问视频源地址。注意:每次HTTP request时YouTube均会根据客户端IP自动生成新的s(和signature),故抓取视频源URL和页面中与之匹配的s必须在同一次request中完成。

    所以,现在我们要做的,就是找到从这个长度86位的字符串s(可直接从视频页面中获得)转换到原来的标准81位字符串sig(真正的YouTube视频签名,可通过抓取浏览器request的真实地址获得)的算法。

    逆向工程破解加密算法本来是一件颇有难度的任务,不过,肉眼观察一下加密前sig和加密后s的模式就很容易发现,其中相同的公共子串部分相当可观。大致可以合理猜测,这里的加密只是最基本的字符串重新排列组合而已(即单一的permutation cipher,可看做是移位式密码(transposition cipher)的一种)。

    因为被加密的字符串本身并不是自然语言,这造成了一定的难度,例如无法通过易位构词法(anagramming)来尝试破解。此外,理论上,将一个长度86的字符串通过移位加密成一个长度81的字符串,可能存在的方式有

    $P^{86}_{81} = frac{86!}{(86-81)!} = 2.0189 imes 10^{128}$

    种。这是一个天文数字。穷举法当然也是行不通的。

    然而,在给出了第一对已知的ssig字符串之后,搜索空间可以被大大地缩减。若能给出更多的ssig字符串对,我们将能够很快地确定唯一可行的移位加密方式。

    尝试分析这个81位的sig字符串:

    A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C6B1927402C84DCBF0E52

    对比加密后的86位s字符串,可得到sig中每一位可能对应的移位方式:

    : sig[0] 可能来自于 => s[0, 2, 10, 40, 59, 69, 74]
    A: sig[1] 可能来自于 => s[1, 3, 14, 24]
    : sig[2] 可能来自于 => s[4, 26, 27, 47, 51, 81, 84]
    F: sig[3] 可能来自于 => s[5, 34, 78]
    : sig[4] 可能来自于 => s[6, 11, 16, 29, 58]
    E: sig[5] 可能来自于 => s[7, 9, 22, 28, 44, 46, 80, 83]
    ...

    作为单一的移位式密码,s中的每一个字符只可能在sig中被使用一次,有了这个限制条件,通过搜索可以剪掉明显不可能的解,从而得到较小的解空间。但是对于81位的字符串sig来说,这个搜索的时间代价仍然是难以承受的。

    所幸,在这里为了破解加密算法,其实完全没有搜索的必要,因为我们可以任意收集到无限多的ssig字符串对,通过它们直接排除掉不合理的对应关系,最终得到唯一的加密方式。(这就叫做算法竞赛和解决现实问题的差异……)

    写了一个简单的Ruby程序用于求解:

    begin
      puts "s:"
      s = gets.chomp
      puts "sig:"
      sig = gets.chomp
    
      dic = {}
      s.chars.each_index do |i|
        dic[s[i]] = [] if dic[s[i]].nil?
        dic[s[i]] << i
      end
    
      sol = [(0...s.length).to_a] * sig.length if sol.nil?
      sig.chars.each_index do |i|
        sol[i] &= dic[sig[i]]
      end
      p sol
    end while sol.flatten.length > sig.length

    每次读入一对ssig,将s中的不同字符分布位置存入一个Hash表,然后根据sig每一位字符所可能对应的在s中的分布位置,对sol数组中的可能解加以过滤,直至得出唯一的解(sol.flatten.length == sig.length)。

    输入第一对ssig,得到一个较大的初始解空间:

    s:
    A4A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C2B1927402C84DCBF0E56E56
    sig:
    A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C6B1927402C84DCBF0E52
    [[0, 2, 10, 40, 59, 69, 74], [1, 3, 14, 24], [4, 26,
    , 47, 51, 81, 84], [5, 34, 78], [6, 11, 16, 29, 58], 
    [7, 9, 22, 28, 44, 46, 80, 83], [8, 12, 17, 21, 23, 25, 
    , 65], [7, 9, 22, 28, 44, 46, 80, 83], [0, 2, 10, 40, 
    , 69, 74], [6, 11, 16, 29, 58], [8, 12, 17, 21, 23, 
    , 33, 65], [13, 52, 61, 68], [1, 3, 14, 24], [15, 18, 
    , 45, 64, 77], [6, 11, 16, 29, 58], [8, 12, 17, 21, 
    , 25, 33, 65], [15, 18, 38, 45, 64, 77], [19, 39, 48, 
    , 75], [20, 35, 37, 41, 60, 73], [8, 12, 17, 21, 23, 
    , 33, 65], [7, 9, 22, 28, 44, 46, 80, 83], [8, 12, 
    , 21, 23, 25, 33, 65], [1, 3, 14, 24], [8, 12, 17, 
    , 23, 25, 33, 65], [4, 26, 27, 47, 51, 81, 84], [4, 
    , 27, 47, 51,81,84],[7,9,22,28,44,46,80,],[6,11,16,29,58],[30,66],[31,36,50,55,,72,76],[32,54,56,57,70,79],[8,12,17,21,,25,33,65],[5,34,78],[20,35,37,41,60,73],[31,36,50,55,62,72,76],[20,35,37,41,60,73],[15,18,38,45,64,77],[19,39,48,49,75],[0,2,,40,59,69,74],[20,35,37,41,60,73],[42],[43,82,85],[7,9,22,28,44,46,80,83],[15,18,,45,64,77],[7,9,22,28,44,46,80,83],[4,,27,47,51,81,84],[19,39,48,49,75],[19,39,,49,75],[31,36,50,55,62,72,76],[4,26,27,,51,81,84],[13,52,61,68],[53,63,67,71],[32,54,56,57,70,79],[31,36,50,55,62,72,76],[32,54,56,57,70,79],[32,54,56,57,70,79],[6,,16,29,58],[0,2,10,40,59,69,74],[20,35,,41,60,73],[13,52,61,68],[31,36,50,55,62,,76],[43,82,85],[15,18,38,45,64,77],[8,,17,21,23,25,33,65],[30,66],[53,63,67,],[13,52,61,68],[0,2,10,40,59,69,74],[32,,56,57,70,79],[53,63,67,71],[31,36,50,55,,72,76],[20,35,37,41,60,73],[0,2,10,40,,69,74],[19,39,48,49,75],[31,36,50,55,62,,76],[15,18,38,45,64,77],[5,34,78],[32,,56,57,70,79],[7,9,22,28,44,46,80,83],[4,26,27,47,51,81,84],[53,63,67,71]]

    用Chrome DevTools抓取另一对ssig,可能解的范围大大缩小了:

    s:
    E5ADB020E576DB28E54EE11690138B15207F6.
    D02D483BF44090A079B60177D17F908ED455D6FD6F
    sig:
    E5ADB020E576DB28E54EE11690138B15207F6.
    D02D483BF44090A079BF0177D17F908ED455D66
    [[0, 2], [1, 3], [4, 26, 27], [5], [6], [7], [8, 17], 
    [9], [10, 59], [11], [12], [13], [14], [15], [16], [8, 
    ], [18], [19], [20], [21, 25], [22], [23], [24], [21, 
    ], [4, 26, 27], [4, 26, 27], [28], [29], [30], [31], 
    [32], [33], [34], [35], [36], [37], [38, 45, 64], [39], 
    [40], [41], [42], [43], [44, 80, 83], [38, 45, 64], 
    [46], [47], [48], [49], [50], [51], [52], [53], [54], 
    [55], [56], [57], [58], [10, 59], [60], [61], [62], 
    [82, 85], [38, 45, 64], [65], [66], [67], [68], [69], 
    [70], [71], [72], [73], [74], [75], [76], [77], [78], 
    [79], [44, 80, 83], [81, 84], [63]]

    继续另一对ssig

    s:
    D3D3B8E64E0C31099E73AB2DB61F4CDF55E9B37FF3.
    AE0A4DC3AAAE8A8DCD6253ACA9B3EC3F09D854EB4EB
    sig:
    D3B8E64E0C31099E73AB2DB61F4CDF55E9B37FF3.
    AE0A4DC3AAAE8A8DCD62B3ACA9B3EC3F09D854E5
    [[0, 2], [1, 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], [42], [43], [44], [45], [46], 
    [47], [48], [49], [50], [51], [52], [53], [54], [55], 
    [56], [57], [58], [59], [60], [61], [62], [82, 85], 
    [64], [65], [66], [67], [68], [69], [70], [71], [72], 
    [73], [74], [75], [76], [77], [78], [79], [80, 83], 
    [81, 84], [63]]

    继续:

    s:
    A038284DCE4E964EB8A51519844254E7C08180.
    E05FA11A60B2520E199005C8D07AAC433433
    sig:
    A038284DCE4E964EB8A51519844254E7C08180.
    E05FA11A60B2530E199005C8D07AAC432
    [[0, 2], [1, 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], [42], [43], [44], [45], [46], 
    [47], [48], [49], [50], [51], [52], [53], [54], [55], 
    [56], [57], [58], [59], [60], [61], [62], [82, 85], 
    [64], [65], [66], [67], [68], [69], [70], [71], [72], 
    [73], [74], [75], [76], [77], [78], [79], [80, 83], 
    [81, 84], [63]]

    结果与上一次相比没有变化,似乎并没有收敛到唯一解。观察发现,每次抓取的字符串ss[0..1]位与s[2..3]位总是相同,s[80..82]位与s[83..85]位总是相同,由此可以合理推测,这个86位字符串s的首2位和末3位是冗余的填充位。

    故最终得到的唯一解应当是:

    sol = [[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], [42], [43], [44], [45], [46], 
    [47], [48], [49], [50], [51], [52], [53], [54], [55], 
    [56], [57], [58], [59], [60], [61], [62], [82], [64], 
    [65], [66], [67], [68], [69], [70], [71], [72], [73], 
    [74], [75], [76], [77], [78], [79], [80], [81], [63]]

    为了验证其正确性,我们抓取另一个YouTube视频的ssig对:

    s:
    C5C046A9B0A7FB13CFAC082C04500D0D47A37A7C8.
    D41117823F5351AA983D8F83B61DE705368368
    sig:
    C046A9B0A7FB13CFAC082C04500D0D47A37A7C8.
    D41117823F5351A8983D8F83B61DE70536A

    s执行sol数组中的移位算法所得到的结果:

    sol.flatten!
     => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 
    , 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 
    , 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 
    , 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 
    , 59, 60, 61, 62, 82, 64, 65, 66, 67, 68, 69, 70, 71, 
    , 73, 74, 75, 76, 77, 78, 79, 80, 81, 63]
    sig = (0...81).map { |i| s[sol[i]] }.join
     => "5C046A9B0A7FB13CFAC082C04500D0D47A37A7C8.
    D41117823F5351A8983D8F83B61DE70536A"

    与抓到的正确sig相吻合。

    至此,我们已经得到了从86位s字符串解密出真实signature的具体方法。把这个移位算法改写成简明的Python实现,就是:

    def decrypt86(s):
        if len(s) == 86:
            return s[2:63] + s[82] + s[64:82] + s[63]
        ...

    这就是youtube-dl中用到的签名解密算法(You-Get直接把它抄过来了)。在前面,我们已经破解了len(s) == 86的情况,针对其他长度s的破解自然也就不是什么难事。(可能会出现的加密签名s长度在82~88之间,每一个长度均对应一个特定的加密算法)

    (请看jwz大神对YouTube此小儿科举动的吐槽……)

    非密码学方法

    事情还没有结束。

    可以看到,YouTube目前对视频签名的加密方式不过是单一的transposition cipher而已,同时明文(sig字符串)和密文(s字符串)的样本可以被用户无限制地任意获得(通过Chrome DevTools或类似的工具抓取),这导致了它对于前面这种已知明文攻击(KPA,Known-plaintext attack)非常脆弱。但是,如果未来YouTube再次改进它的加密方式,采取复合式的密码算法,甚至哪怕是简单的substitution + transposition cipher结合,都可能会使破解的困难程度大大增加。

    这种时候,我们其实还有更好的方法。

    在浏览器中访问视频页面http://www.youtube.com/watch?v=FU8csnZxdPA,在源码中找到类似这样的JSON数据:

    "assets": {"js": "http://s.ytimg.com/yts/jsbin/html5player-vfl_ymO4Z.js",

    http://s.ytimg.com/yts/jsbin/html5player-vfl_ymO4Z.js这个文件中定位相关部分的代码:(阅读这种东西是最考验耐心的时候)

    function ur(a,b,c){for(var d=[],e=0;e<a[K];e++){var 
    g=a[e];if(g.sig||g.s){var h=g.sig||Qo(g.s);g.url=
    Fo(g.url,{signature:h})}g.url&&d[G](tr(g.url,g[J],
    g.quality,g.itag,g.stereo3d))}return or(d,!!b,!!c)}

    可借助JavaScript beautifier之类的工具将其格式之:

    function ur(a, b, c) {
        for (var d = [],e = 0; e < a[K]; e++) {
            var g = a[e];
            if (g.sig || g.s) {
                var h = g.sig || Qo(g.s);
                g.url = Fo(g.url, {signature: h})
            }
            g.url && d[G](tr(g.url, g[J], g.quality, g.itag, g.stereo3d))
        }
        return or(d, !!b, !!c)
    }

    看到var h = g.sig || Qo(g.s)这一句,就能大致猜出Qo()是用来对s解密计算出sig的函数了。同样,找到Qo()的定义:

    function Qo(a){a=a[y]("");a=a.reverse();a=a[he](3);
    var b=a[0];a[0]=a[19%a[K]];a[19]=b;a=a.reverse();
    a=a[he](2);return a[O]("")};

    以及该函数中用到的若干外部定义:

    y="split",
    he="slice",
    K="length",
    O="join",

    整理一下,把它们放到一个独立的ECMAScript脚本中:

    #!/usr/bin/env js
    
    var
        y = "split",
        he = "slice",
        K = "length",
        O = "join";
    
    function Qo(a) {
        a = a[y]("");
        a = a.reverse();
        a = a[he](3);
        var b = a[0];
        a[0] = a[19 % a[K]];
        a[19] = b;
        a = a.reverse();
        a = a[he](2);
        return a[O]("")
    };
    
    arguments.forEach(function(s) {
        print(Qo(s));
    });

    保存为decrypt86.es,用SpiderMonkey在本地执行:

    $ chmod +x decrypt86.es
    $ ./decrypt86.es 4A4A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    > 6EBE5DDC5720C003487C2B1927402C84DCBF0E56E56
    A5F3E1E4317AB31BD81E1A155E39C01F8C8BD48.
    EBE5DDC5720C003487C6B1927402C84DCBF0E52

    如预期的那样,得到了正确的sig。容易证明,该Qo()函数实现的算法:

    function decrypt86(s) {
        s = s.split("");
        s = s.reverse();
        s = s.slice(3);
        var t = s[0];
        s[0] = s[19 % s.length];
        s[19] = t;
        s = s.reverse();
        s = s.slice(2);
        return s.join("")
    }

    与我们前面破解出的86位s签名解密算法:(JavaScript版本)

    function decrypt86(s) {
        return s.substr(2, 61) + s.substr(82, 1) +
            s.substr(64, 18) + s.substr(63, 1)
    }

    是完全等价的。如此,可以通过直接分析页面的JavaScript源码得到视频signature的解密方法,省去了复杂耗时的破解过程。

  • 相关阅读:
    ASP.NET在禁用视图状态的情况下仍然使用ViewState对象【转】
    Atcoder Regular Contest 061 D Card Game for Three(组合数学)
    Solution 「CERC 2016」「洛谷 P3684」机棚障碍
    Solution 「CF 599E」Sandy and Nuts
    Solution 「洛谷 P6021」洪水
    Solution 「ARC 058C」「AT 1975」Iroha and Haiku
    Solution 「POI 2011」「洛谷 P3527」METMeteors
    Solution 「CF 1023F」Mobile Phone Network
    Solution 「SP 6779」GSS7
    Solution 「LOCAL」大括号树
  • 原文地址:https://www.cnblogs.com/jiangzhaowei/p/8276801.html
Copyright © 2011-2022 走看看