Decrypting AES-128 Encrypted HTTP Live Streaming

I came across an interestingly encrypted HLS m3u8 playlist the other day. This was on a site that you could only access once – after which, you lose access to the stream.

The usual browser extensions failed to extract the media, but I was able to get the .m3u8 playlist file which was logged in the network requests. This started with:

$ cat 42424242-audio_eng\=6969696-video_eng\=2525252.m3u8
#EXTM3U
#EXT-X-VERSION:3
## Created with Unified Streaming Platform(version=1.9.6)
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-TARGETDURATION:10
#USP-X-TIMESTAMP-MAP:MPEGTS=900000,LOCAL=1970-01-01T00:00:00Z
#EXT-X-KEY:METHOD=AES-128,URI="//goose.vzaar.com/keys",IV=0x2684CF680EC26F480A76A4687952630A
#EXTINF:8, no desc
42424242-audio_eng=6969696-video_eng=2525252-1.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b=
#EXTINF:8, no desc
42424242-audio_eng=6969696-video_eng=2525252-2.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b=
...
Note: Values have been changed for privacy

Interesting, I can’t seem to find the key file. Visiting goose.vzaar.com/keys yields a 404.

Let’s dump the entire playlist files first, to investigate later. Because the usual tools (e.g. ffmpeg, m3u8-downloader) didn’t play nice with this AES-128 key, I used wget to manually dump the files.

for i in {1..9}; do wget "https://base.url/42424242-audio_eng=6969696-video_eng=2525252-$i.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b="; done;
for i in {10..99}; do wget "https://base.url/42424242-audio_eng=6969696-video_eng=2525252-$i.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b="; done;
for i in {100..500}; do wget "https://base.url/42424242-audio_eng=6969696-video_eng=2525252-$i.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b="; done;

As they are encrypted, these .ts files cannot play and cannot be recognized by ffmpeg. Running ffmpeg results in:

$ ffmpeg -i "https://base.url/42424242.ism/42424242-audio_eng=6969696-video_eng=2525252.m3u8?ts=5785785785&s=PkShg6W0RqySBrzPClEb32vHVxTBqOck" -c copy -bsf:a aac_adtstoasc output.mp4
[hls @ 0x7fffdee17340] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7fffdee17340] Skip ('## Created with Unified Streaming Platform(version=1.9.6)')
[hls @ 0x7fffdee17340] Skip ('#EXT-X-INDEPENDENT-SEGMENTS')
[hls @ 0x7fffdee17340] Skip ('#USP-X-TIMESTAMP-MAP:MPEGTS=900000,LOCAL=1970-01-01T00:00:00Z')
[hls @ 0x7fffdee17340] Opening 'https://goose.vzaar.com/keys' for reading
[https @ 0x7fffdf15d440] HTTP error 404 Not Found
[hls @ 0x7fffdee17340] Unable to open key file https://goose.vzaar.com/keys
[hls @ 0x7fffdee17340] Opening 'crypto+https://base.url/42424242.ism/42424242-audio_eng=6969696-video_eng=2525252-1.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b=' for reading
[hls @ 0x7fffdee17340] Error when loading first segment 'https://base.url/42424242.ism/42424242-audio_eng=6969696-video_eng=2525252-1.ts?ts=5785785785&s=fIMGNwHCf2VAdGE89HDtO88fUgblP5b='
https://base.url/42424242.ism/42424242-audio_eng=6969696-video_eng=2525252.m3u8?ts=5785785785&s=PkShg6W0RqySBrzPClEb32vHVxTBqOck: Invalid data found when processing input

The problem with having these kinds of encryption is the key must have been transmitted to the player as well, else the player would be unable to decrypt the files. A simple search for “goose.vzaar.com” in the logged network requests on the browser’s developer console shows one single request to that domain, yielding a .ism file (42424242.ism in the above example). The content of this file is 16 bytes, which is 128 bits. This is our AES key.

I convert the bytes into bits with

$ hexdump -e '16/1 "%02x"' 42424242.ism
gyz2jeqzhl29mtm52s2qt2ra9b5w5uky

Now, we can try to decrypt the files using the above Key and IV from the m3u8. Using openssl,

$ openssl enc -aes-128-cbc -nosalt -d -in 42424242-audio_eng\=6969696-video_eng\=2525252-1.ts -K 'gyz2jeqzhl29mtm52s2qt2ra9b5w5uky' -iv '2684CF680EC26F480A76A4687952630A'
AWAW 0G@ 0'AW!"G@!0P~!7wA gM@<-@@@P b`h＀EH, #x264 - core 163 r3060 5db6aa6 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.vG!ideolan.org/x264.html - options: cabac=1 ref=1 deblock=1:0:0 analyse=0x1:0x111 me=hex subme=2 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzoG!ne=21,11 fast_pskip=1 chroma_qp_offset=0 threads=34 lookahead_threads=8 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=G!1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=1 keyint=50 keyint_min=5 scenecut=40 intra_refresh=0 rc_lookahead=10 rc=abr mbtree=1 bitrate=7000 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=G!69 qpstep=4 ip_ratio=1.40 aq=1:1.00f8/HA2[;v4E?nXҴ9 ]W+-t..............

And there we have it! The mp4 header, showing that the key and IV works. Now all we need to do is join all the encrypted .ts files together and decrypt that, giving us the full .mp4 video file.

$ cat *.ts > all.ts
$ openssl enc -aes-128-cbc -nosalt -d -in all.ts -K 'gyz2jeqzhl29mtm52s2qt2ra9b5w5uky' -iv '2684CF680EC26F480A76A4687952630A' > decrypted.mp4