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的解密方法,省去了复杂耗时的破解过程。

  • 相关阅读:
    windows程序设计第4章Text Output练习(831121)
    约瑟夫问题的递归公式
    哈希(hash)以及C++标准库哈希(std::hash)
    返回顶部的一段代码
    对于使用 UNIKON ALL 中表的顺序
    正则表达式的实际运用
    json.help
    省市区联动
    一个JS时间选择控件
    (转)C# Enum,Int,String的互相转换 枚举转换
  • 原文地址:https://www.cnblogs.com/jiangzhaowei/p/8276801.html
Copyright © 2011-2022 走看看