2017-12-19
■[セキュリティ][FFmpeg] AbemaTVの仕様とHLSの暗号化の弱さ
AbemaTVの仕様について気になったので調べてみた (研究目的です念の為)。
AbemaTVはPCへの動画配信において、配信プロトコルにHLSを使用しているようだ。HLSはMPEG-DASHと異なりDRMが使えず (厳密にはMac環境のFairplayなどの例外もあるが) 、AbemaTVでは鍵の生成に若干工夫を行ってるのみのようだ。
$ curl https://api.abema.io/v1/channels {"channels":[{"id":"abema-news","name":"AbemaNewsチャンネル","playback":{"hls":"https://linear-abematv.akamaized.net/channel/abema-news/playlist.m3u8"}},{"id":"abema-special","name":"AbemaSPECIALチャンネル","playback":{"hls":"https://linear-abematv.akamaized.net/channel/abema-special/playlist.m3u8"}},(後略)
次に画質一覧をダウンロード。
$ curl https://linear-abematv.akamaized.net/channel/abema-news/playlist.m3u8 #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=184000 180/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000 240/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=900000 360/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1400000 480/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2200000 720/playlist.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4200000 1080/playlist.m3u8
$ curl https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:951004 #EXT-X-DISCONTINUITY-SEQUENCE:16994 #EXT-X-KEY:METHOD=AES-128,URI="abematv-license://XXXXXXXXXXXXXXXXXXXXXX",IV=0x000000000000000000000000000000 #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts #EXTINF:5.005000, https://linear-abematv.akamaized.net/tsnews/abema-news/h264/1080/xxxxxxxxxxxxxxxxxxxxxx.ts
さて、映像はAES-128方式で暗号化されているようだ。暗号の鍵には初期化ベクトル(IV)とURIが指定されているが、URIに使われているabematv-licenseスキーマとは何だろう。仕組みは良く分からないが、Chromeの通信ログを見ると、スキーマの後ろの部分 (XXXXXXXXXXXXXXXXXXXXXX) と何処かにあるトークンを使って、とあるURLにアクセスしているようだ。
トークンはローカルストレージにあるものと同じようなので、Chromeのコンソールからwindow.localStorage["abm_mediaToken"]と打つと手に入る。このトークンとスキーマの後ろの部分を使って、ライセンスキーの種を手に入れる。
$ curl https://license.abema.io/abematv-hls?t=トークン --data '{"lt":"スキーマの後ろの部分","kv":"wd","kg":166}' {"cid":"abema-news","k":"XXXXXXXXXXXXXXXXXXXXXXX"}
さて、どうやってライセンスキーの種 (k) をキーに変換するのだろう? 調べた所、遅延ロードされた以下のJavascriptがこの変換を処理しているようだ。
若干難読化されているけれども、肝心の部分はそのままだし、コードインジェクションもし放題なので割と何とかなる。
キー計算の表面部分のロジックはこんな感じ。
function _0x569113(cid, uid, k){ var _k = k.substring(0,k.length-1); var c = k.charAt(k.length-1); return c=='5'?_0x1e2ccc(cid, uid, _k): c=='4'?_0xa25b8f(cid, uid, _k): _0x2782e2(cid, uid, _k); } var _0x5ee3af=_0x569113(cid, window.localStorage["abm_userId"], k)
キー計算の中心部分は解読していないけれど、alert(_0x5ee3af);をインジェクションしてコードを実行するだけでキーが手に入る。
手に入ったキーは、バイナリ化してkey.binとして保存しておく。あとはそのキーを使って再生するだけ。
$ wget -N https://linear-abematv.akamaized.net/channel/abema-news/1080/playlist.m3u8 \ && sed -i 's/URI=.*\,/URI=\"key.bin\",/g' playlist.m3u8 \ && ffplay playlist.m3u8 -protocol_whitelist file,http,https,tcp,tls,crypto
- 213 https://www.google.co.jp/
- 29 https://www.google.com/
- 23 http://hatenablog.com/k/keywordblog/URI
- 6 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwjF__iMjrTYAhUJgrwKHVEWBKAQFggnMAA&url=http://d.hatena.ne.jp/nazodane/20171219/1513672025&usg=AOvVaw2OfB19oIyqRO9T_BVRt20-
- 6 https://www.google.com.tw/
- 4 http://search.yahoo.co.jp/
- 4 https://www.google.com.hk/
- 3 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=8&ved=0ahUKEwjt3O2IrN_YAhWBwrwKHTY0AUAQFghXMAc&url=http://d.hatena.ne.jp/nazodane/20171219/1513672025&usg=AOvVaw2OfB19oIyqRO9T_BVRt20-
- 3 https://www.google.co.kr/
- 2 https://translate.googleusercontent.com/translate_p?act=url&hl=zh-CN&ie=UTF8&prev=_t&sl=auto&tl=en&u=http://d.hatena.ne.jp/nazodane/20171219/1513672025&depth=1&rurl=translate.google.co.jp&sp=nmt4&usg=ALkJrhgAAAAAWmyCD-IbozxKSQz7kknwXGM_cxhKSddE